summaryrefslogtreecommitdiffstats
path: root/devtools/client/webconsole
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /devtools/client/webconsole
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/webconsole')
-rw-r--r--devtools/client/webconsole/README.md69
-rw-r--r--devtools/client/webconsole/actions/autocomplete.js376
-rw-r--r--devtools/client/webconsole/actions/filters.js59
-rw-r--r--devtools/client/webconsole/actions/history.js90
-rw-r--r--devtools/client/webconsole/actions/index.js21
-rw-r--r--devtools/client/webconsole/actions/input.js501
-rw-r--r--devtools/client/webconsole/actions/messages.js179
-rw-r--r--devtools/client/webconsole/actions/moz.build17
-rw-r--r--devtools/client/webconsole/actions/notifications.js48
-rw-r--r--devtools/client/webconsole/actions/object.js71
-rw-r--r--devtools/client/webconsole/actions/toolbox.js49
-rw-r--r--devtools/client/webconsole/actions/ui.js256
-rw-r--r--devtools/client/webconsole/browser-console-manager.js186
-rw-r--r--devtools/client/webconsole/browser-console.js137
-rw-r--r--devtools/client/webconsole/components/App.css523
-rw-r--r--devtools/client/webconsole/components/App.js513
-rw-r--r--devtools/client/webconsole/components/FilterBar/ConsoleSettings.js194
-rw-r--r--devtools/client/webconsole/components/FilterBar/FilterBar.js441
-rw-r--r--devtools/client/webconsole/components/FilterBar/FilterButton.js37
-rw-r--r--devtools/client/webconsole/components/FilterBar/FilterCheckbox.js31
-rw-r--r--devtools/client/webconsole/components/FilterBar/moz.build11
-rw-r--r--devtools/client/webconsole/components/Input/ConfirmDialog.js197
-rw-r--r--devtools/client/webconsole/components/Input/EagerEvaluation.css101
-rw-r--r--devtools/client/webconsole/components/Input/EagerEvaluation.js147
-rw-r--r--devtools/client/webconsole/components/Input/EditorToolbar.js162
-rw-r--r--devtools/client/webconsole/components/Input/EvaluationContextSelector.css33
-rw-r--r--devtools/client/webconsole/components/Input/EvaluationContextSelector.js290
-rw-r--r--devtools/client/webconsole/components/Input/EvaluationNotification.css32
-rw-r--r--devtools/client/webconsole/components/Input/EvaluationNotification.js63
-rw-r--r--devtools/client/webconsole/components/Input/JSTerm.js1605
-rw-r--r--devtools/client/webconsole/components/Input/ReverseSearchInput.css125
-rw-r--r--devtools/client/webconsole/components/Input/ReverseSearchInput.js285
-rw-r--r--devtools/client/webconsole/components/Input/moz.build14
-rw-r--r--devtools/client/webconsole/components/Output/CollapseButton.js33
-rw-r--r--devtools/client/webconsole/components/Output/ConsoleOutput.js381
-rw-r--r--devtools/client/webconsole/components/Output/ConsoleTable.js272
-rw-r--r--devtools/client/webconsole/components/Output/GripMessageBody.js114
-rw-r--r--devtools/client/webconsole/components/Output/LazyMessageList.js393
-rw-r--r--devtools/client/webconsole/components/Output/Message.js482
-rw-r--r--devtools/client/webconsole/components/Output/MessageContainer.js131
-rw-r--r--devtools/client/webconsole/components/Output/MessageIcon.js78
-rw-r--r--devtools/client/webconsole/components/Output/MessageIndent.js47
-rw-r--r--devtools/client/webconsole/components/Output/MessageRepeat.js35
-rw-r--r--devtools/client/webconsole/components/Output/message-types/CSSWarning.js173
-rw-r--r--devtools/client/webconsole/components/Output/message-types/ConsoleApiCall.js221
-rw-r--r--devtools/client/webconsole/components/Output/message-types/ConsoleCommand.js105
-rw-r--r--devtools/client/webconsole/components/Output/message-types/DefaultRenderer.js15
-rw-r--r--devtools/client/webconsole/components/Output/message-types/EvaluationResult.js124
-rw-r--r--devtools/client/webconsole/components/Output/message-types/JSTracerTrace.js174
-rw-r--r--devtools/client/webconsole/components/Output/message-types/NavigationMarker.js62
-rw-r--r--devtools/client/webconsole/components/Output/message-types/NetworkEventMessage.js243
-rw-r--r--devtools/client/webconsole/components/Output/message-types/PageError.js130
-rw-r--r--devtools/client/webconsole/components/Output/message-types/SimpleTable.js130
-rw-r--r--devtools/client/webconsole/components/Output/message-types/WarningGroup.js80
-rw-r--r--devtools/client/webconsole/components/Output/message-types/moz.build18
-rw-r--r--devtools/client/webconsole/components/Output/moz.build21
-rw-r--r--devtools/client/webconsole/components/SideBar.js128
-rw-r--r--devtools/client/webconsole/components/moz.build15
-rw-r--r--devtools/client/webconsole/constants.js231
-rw-r--r--devtools/client/webconsole/enhancers/actor-releaser.js56
-rw-r--r--devtools/client/webconsole/enhancers/batching.js34
-rw-r--r--devtools/client/webconsole/enhancers/css-error-reporting.js43
-rw-r--r--devtools/client/webconsole/enhancers/message-cache-clearing.js41
-rw-r--r--devtools/client/webconsole/enhancers/moz.build11
-rw-r--r--devtools/client/webconsole/index.html138
-rw-r--r--devtools/client/webconsole/middleware/event-telemetry.js129
-rw-r--r--devtools/client/webconsole/middleware/history-persistence.js71
-rw-r--r--devtools/client/webconsole/middleware/moz.build10
-rw-r--r--devtools/client/webconsole/middleware/performance-marker.js27
-rw-r--r--devtools/client/webconsole/moz.build44
-rw-r--r--devtools/client/webconsole/panel.js105
-rw-r--r--devtools/client/webconsole/reducers/autocomplete.js181
-rw-r--r--devtools/client/webconsole/reducers/filters.js32
-rw-r--r--devtools/client/webconsole/reducers/history.js244
-rw-r--r--devtools/client/webconsole/reducers/index.js34
-rw-r--r--devtools/client/webconsole/reducers/messages.js1723
-rw-r--r--devtools/client/webconsole/reducers/moz.build15
-rw-r--r--devtools/client/webconsole/reducers/notifications.js58
-rw-r--r--devtools/client/webconsole/reducers/prefs.js48
-rw-r--r--devtools/client/webconsole/reducers/ui.js137
-rw-r--r--devtools/client/webconsole/selectors/autocomplete.js13
-rw-r--r--devtools/client/webconsole/selectors/filters.js10
-rw-r--r--devtools/client/webconsole/selectors/history.js95
-rw-r--r--devtools/client/webconsole/selectors/messages.js93
-rw-r--r--devtools/client/webconsole/selectors/moz.build14
-rw-r--r--devtools/client/webconsole/selectors/notifications.js12
-rw-r--r--devtools/client/webconsole/selectors/prefs.js17
-rw-r--r--devtools/client/webconsole/selectors/ui.js13
-rw-r--r--devtools/client/webconsole/service-container.js72
-rw-r--r--devtools/client/webconsole/store.js160
-rw-r--r--devtools/client/webconsole/test/README.md61
-rw-r--r--devtools/client/webconsole/test/browser/_browser_console.toml117
-rw-r--r--devtools/client/webconsole/test/browser/_jsterm.toml283
-rw-r--r--devtools/client/webconsole/test/browser/_webconsole.toml827
-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.js138
-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.js109
-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.js47
-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.js66
-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.js132
-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.js136
-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.js92
-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.js84
-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_commands.js67
-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.js391
-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_evaluation_notification_original_variable_mapping_warning.js75
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_file_load_save_keyboard_shortcut.js91
-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.js74
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_helper_dollar_dollar.js84
-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_helpers_no_override.js143
-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.js130
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_fixed_header.js70
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_selector.js145
-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_jsterm_trace_command.js217
-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.js101
-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.js122
-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.js147
-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.js504
-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.js184
-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.js144
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_context_menu_store_as_global.js124
-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_too_many_reports.js50
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_csp_violation.js145
-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.js146
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_custom_formatters.js205
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_custom_formatters_errors.js200
-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.js78
-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.js142
-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.js134
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_keyboard_accessibility.js258
-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.js188
-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.js158
-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_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.js70
-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.js169
-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.js831
-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_object_inspector_wrapped_primitive_value.js63
-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.js327
-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.js55
-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.js371
-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.js144
-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.js62
-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.js24
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_worklet_console.js34
-rw-r--r--devtools/client/webconsole/test/browser/browser_webconsole_worklet_error.js22
-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.js1916
-rw-r--r--devtools/client/webconsole/test/browser/shared-head.js517
-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.html128
-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.html31
-rw-r--r--devtools/client/webconsole/test/browser/test-console-worklet.html41
-rw-r--r--devtools/client/webconsole/test/browser/test-console.html36
-rw-r--r--devtools/client/webconsole/test/browser/test-csp-many-errors.html24
-rw-r--r--devtools/client/webconsole/test/browser/test-csp-many-errors.html^headers^1
-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-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
-rw-r--r--devtools/client/webconsole/test/chrome/chrome.toml6
-rw-r--r--devtools/client/webconsole/test/chrome/head.js14
-rw-r--r--devtools/client/webconsole/test/chrome/test_render_perf.html240
-rw-r--r--devtools/client/webconsole/test/node/.eslintrc.js7
-rw-r--r--devtools/client/webconsole/test/node/babel.config.js24
-rw-r--r--devtools/client/webconsole/test/node/components/console-api-call.log-messages.test.js60
-rw-r--r--devtools/client/webconsole/test/node/components/console-api-call.test.js695
-rw-r--r--devtools/client/webconsole/test/node/components/console-output.test.js52
-rw-r--r--devtools/client/webconsole/test/node/components/css-warning.test.js125
-rw-r--r--devtools/client/webconsole/test/node/components/eager-evaluation.test.js154
-rw-r--r--devtools/client/webconsole/test/node/components/evaluation-result.test.js505
-rw-r--r--devtools/client/webconsole/test/node/components/filter-bar.test.js226
-rw-r--r--devtools/client/webconsole/test/node/components/filter-button.test.js43
-rw-r--r--devtools/client/webconsole/test/node/components/filter-checkbox.test.js39
-rw-r--r--devtools/client/webconsole/test/node/components/message-container.test.js70
-rw-r--r--devtools/client/webconsole/test/node/components/message-icon.test.js70
-rw-r--r--devtools/client/webconsole/test/node/components/message-location.test.js72
-rw-r--r--devtools/client/webconsole/test/node/components/message-repeat.test.js20
-rw-r--r--devtools/client/webconsole/test/node/components/message-types-aria.test.js59
-rw-r--r--devtools/client/webconsole/test/node/components/network-event-message.test.js154
-rw-r--r--devtools/client/webconsole/test/node/components/page-error.test.js661
-rw-r--r--devtools/client/webconsole/test/node/components/warning-group.test.js107
-rw-r--r--devtools/client/webconsole/test/node/components/webconsole-wrapper.test.js148
-rw-r--r--devtools/client/webconsole/test/node/fixtures/DevToolsUtils.js8
-rw-r--r--devtools/client/webconsole/test/node/fixtures/WebConsoleUtils.js17
-rw-r--r--devtools/client/webconsole/test/node/fixtures/async-storage.js13
-rw-r--r--devtools/client/webconsole/test/node/fixtures/serviceContainer.js27
-rw-r--r--devtools/client/webconsole/test/node/fixtures/stubs/browser_dummy.js11
-rw-r--r--devtools/client/webconsole/test/node/fixtures/stubs/consoleApi.js1489
-rw-r--r--devtools/client/webconsole/test/node/fixtures/stubs/cssMessage.js87
-rw-r--r--devtools/client/webconsole/test/node/fixtures/stubs/evaluationResult.js1431
-rw-r--r--devtools/client/webconsole/test/node/fixtures/stubs/index.js37
-rw-r--r--devtools/client/webconsole/test/node/fixtures/stubs/networkEvent.js269
-rw-r--r--devtools/client/webconsole/test/node/fixtures/stubs/pageError.js2482
-rw-r--r--devtools/client/webconsole/test/node/fixtures/stubs/platformMessage.js56
-rw-r--r--devtools/client/webconsole/test/node/fixtures/stubs/stubs.toml15
-rw-r--r--devtools/client/webconsole/test/node/helpers.js188
-rw-r--r--devtools/client/webconsole/test/node/jest-setup.js54
-rw-r--r--devtools/client/webconsole/test/node/jest.config.js23
-rw-r--r--devtools/client/webconsole/test/node/middleware/debounce.test.js62
-rw-r--r--devtools/client/webconsole/test/node/package.json29
-rw-r--r--devtools/client/webconsole/test/node/store/filters.test.js345
-rw-r--r--devtools/client/webconsole/test/node/store/hidden-messages.test.js199
-rw-r--r--devtools/client/webconsole/test/node/store/messages.test.js1305
-rw-r--r--devtools/client/webconsole/test/node/store/network-messages.test.js133
-rw-r--r--devtools/client/webconsole/test/node/store/private-messages.test.js252
-rw-r--r--devtools/client/webconsole/test/node/store/release-actors.test.js203
-rw-r--r--devtools/client/webconsole/test/node/store/search.test.js115
-rw-r--r--devtools/client/webconsole/test/node/store/ui.test.js119
-rw-r--r--devtools/client/webconsole/test/node/utils/areMessagesSimilar.test.js50
-rw-r--r--devtools/client/webconsole/test/node/yarn.lock6057
-rw-r--r--devtools/client/webconsole/test/xpcshell/.eslintrc.js6
-rw-r--r--devtools/client/webconsole/test/xpcshell/test_webconsole_l10n.js31
-rw-r--r--devtools/client/webconsole/test/xpcshell/xpcshell.toml6
-rw-r--r--devtools/client/webconsole/test/yarn.lock2602
-rw-r--r--devtools/client/webconsole/types.js90
-rw-r--r--devtools/client/webconsole/utils.js53
-rw-r--r--devtools/client/webconsole/utils/clipboard.js57
-rw-r--r--devtools/client/webconsole/utils/context-menu.js368
-rw-r--r--devtools/client/webconsole/utils/id-generator.js15
-rw-r--r--devtools/client/webconsole/utils/l10n.js70
-rw-r--r--devtools/client/webconsole/utils/messages.js1028
-rw-r--r--devtools/client/webconsole/utils/moz.build14
-rw-r--r--devtools/client/webconsole/utils/object-inspector.js158
-rw-r--r--devtools/client/webconsole/utils/prefs.js46
-rw-r--r--devtools/client/webconsole/webconsole-ui.js746
-rw-r--r--devtools/client/webconsole/webconsole-wrapper.js486
-rw-r--r--devtools/client/webconsole/webconsole.js469
720 files changed, 84131 insertions, 0 deletions
diff --git a/devtools/client/webconsole/README.md b/devtools/client/webconsole/README.md
new file mode 100644
index 0000000000..5aadfda231
--- /dev/null
+++ b/devtools/client/webconsole/README.md
@@ -0,0 +1,69 @@
+# WebConsole
+
+The WebConsole (webconsole) shows you all the console API calls made by scripts and alerts
+you when javascript errors are thrown by a script.
+It can also display network logs, and you can evaluate expressions using the console
+input, a.k.a. JsTerm. You can read [more](https://firefox-source-docs.mozilla.org/devtools-user/web_console/) about it
+to learn all the features and how to use the tool.
+
+## Run WebConsole
+
+If you want to build the WebConsole inside of the DevTools toolbox (Firefox Devtools Panels),
+follow the [simple Firefox build](https://firefox-source-docs.mozilla.org/devtools/getting-started/build.html)
+documentation. Start your compiled firefox and open the Firefox developer tool, you can
+then see the WebConsole tab.
+
+## Code Structure
+
+Top level files are used to launch the WebConsole inside of the DevTools toolbox.
+The main files used to run the WebConsole are:
+
+* `main.js` called by devtools toolbox to launch the WebConsole panel.
+* `index.html` panel UI and launch scripts.
+
+### UI
+
+The WebConsole UI is built using [React](https://firefox-source-docs.mozilla.org/devtools/frontend/react.html)
+components (in `components/`).
+
+The React application is rendered from `webconsole-wrapper.js`.
+It contains 4 top components:
+* **ConsoleOutput** (in `ConsoleOutput.js`) is the component where messages are rendered.
+* **FilterBar** (in `FilterBar.js`) is the component for the filter bars (filter input and toggle buttons).
+* **SideBar** (in `SideBar.js`) is the component that render the sidebar where objects can be placed in.
+* **JsTerm** (in `JsTerm.js`) is the component that render the console input.
+
+We prefer stateless component (defined by function) instead of stateful component
+(defined by class) unless the component has to maintain its internal state.
+
+### State
+
+Besides the UI, the WebConsole manages the app state via [Redux].
+The following locations define the app state:
+
+* `src/constants.js` constants used across the tool including action and event names.
+* `src/actions/` for all actions that change the state.
+* `src/reducers/` for all reducers that change the state.
+* `src/selectors/` functions that return a formatted version of parts of the app state.
+
+The redux state is a plain javascript object with the following properties:
+```js
+{
+ // State of the filter input and toggle buttons
+ filters,
+ // State of the input history
+ history,
+ // Console messages data and state (hidden, expanded, groups, …)
+ messages,
+ // State of notifications displayed on the output (e.g. self-XSS warning message)
+ notifications,
+ // Preferences (persist message, message limit, …)
+ prefs,
+ // Interface state (filter bar visible, sidebar visible, …)
+ ui,
+}
+```
+
+### Tests
+
+[See test/README.md](test/README.md)
diff --git a/devtools/client/webconsole/actions/autocomplete.js b/devtools/client/webconsole/actions/autocomplete.js
new file mode 100644
index 0000000000..e2d08351ec
--- /dev/null
+++ b/devtools/client/webconsole/actions/autocomplete.js
@@ -0,0 +1,376 @@
+/* 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 {
+ AUTOCOMPLETE_CLEAR,
+ AUTOCOMPLETE_DATA_RECEIVE,
+ AUTOCOMPLETE_PENDING_REQUEST,
+ AUTOCOMPLETE_RETRIEVE_FROM_CACHE,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+const {
+ analyzeInputString,
+ shouldInputBeAutocompleted,
+} = require("resource://devtools/shared/webconsole/analyze-input-string.js");
+
+loader.lazyRequireGetter(
+ this,
+ "getSelectedTarget",
+ "resource://devtools/shared/commands/target/selectors/targets.js",
+ true
+);
+
+/**
+ * Update the data used for the autocomplete popup in the console input (JsTerm).
+ *
+ * @param {Boolean} force: True to force a call to the server (as opposed to retrieve
+ * from the cache).
+ * @param {Array<String>} getterPath: Array representing the getter access (i.e.
+ * `a.b.c.d.` is described as ['a', 'b', 'c', 'd'] ).
+ * @param {Array<String>} expressionVars: Array of the variables defined in the expression.
+ */
+function autocompleteUpdate(force, getterPath, expressionVars) {
+ return async ({ dispatch, getState, webConsoleUI, hud }) => {
+ if (hud.inputHasSelection()) {
+ return dispatch(autocompleteClear());
+ }
+
+ const inputValue = hud.getInputValue();
+ const mappedVars = hud.getMappedVariables() ?? {};
+ const allVars = (expressionVars ?? []).concat(Object.keys(mappedVars));
+ const frameActorId = await hud.getSelectedFrameActorID();
+
+ const cursor = webConsoleUI.getInputCursor();
+
+ const state = getState().autocomplete;
+ const { cache } = state;
+ if (
+ !force &&
+ (!inputValue || /^[a-zA-Z0-9_$]/.test(inputValue.substring(cursor)))
+ ) {
+ return dispatch(autocompleteClear());
+ }
+
+ const rawInput = inputValue.substring(0, cursor);
+ const retrieveFromCache =
+ !force &&
+ cache &&
+ cache.input &&
+ rawInput.startsWith(cache.input) &&
+ /[a-zA-Z0-9]$/.test(rawInput) &&
+ frameActorId === cache.frameActorId;
+
+ if (retrieveFromCache) {
+ return dispatch(autoCompleteDataRetrieveFromCache(rawInput));
+ }
+
+ const authorizedEvaluations = updateAuthorizedEvaluations(
+ state.authorizedEvaluations,
+ getterPath,
+ mappedVars
+ );
+
+ const { input, originalExpression } = await getMappedInput(
+ rawInput,
+ mappedVars,
+ hud
+ );
+
+ return dispatch(
+ autocompleteDataFetch({
+ input,
+ frameActorId,
+ authorizedEvaluations,
+ force,
+ allVars,
+ mappedVars,
+ originalExpression,
+ })
+ );
+ };
+}
+
+/**
+ * Combine or replace authorizedEvaluations with the newly authorized getter path, if any.
+ * @param {Array<Array<String>>} authorizedEvaluations Existing authorized evaluations (may
+ * be updated in place)
+ * @param {Array<String>} getterPath The new getter path
+ * @param {{[String]: String}} mappedVars Map of original to generated variable names.
+ * @returns {Array<Array<String>>} The updated authorized evaluations (the original array,
+ * if it was updated in place) */
+function updateAuthorizedEvaluations(
+ authorizedEvaluations,
+ getterPath,
+ mappedVars
+) {
+ if (!Array.isArray(authorizedEvaluations) || !authorizedEvaluations.length) {
+ authorizedEvaluations = [];
+ }
+
+ if (Array.isArray(getterPath) && getterPath.length) {
+ // We need to check for any previous authorizations. For example, here if getterPath
+ // is ["a", "b", "c", "d"], we want to see if there was any other path that was
+ // authorized in a previous request. For that, we only add the previous
+ // authorizations if the last auth is contained in getterPath. (for the example, we
+ // would keep if it is [["a", "b"]], not if [["b"]] nor [["f", "g"]])
+ const last = authorizedEvaluations[authorizedEvaluations.length - 1];
+
+ const generatedPath = mappedVars[getterPath[0]]?.split(".");
+ if (generatedPath) {
+ getterPath = generatedPath.concat(getterPath.slice(1));
+ }
+
+ const isMappedVariable =
+ generatedPath && getterPath.length === generatedPath.length;
+ const concat = !last || last.every((x, index) => x === getterPath[index]);
+ if (isMappedVariable) {
+ // If the path consists only of an original variable, add all the prefixes of its
+ // mapping. For example, for myVar => a.b.c, authorize a, a.b, and a.b.c. This
+ // ensures we'll only show a prompt for myVar once even if a.b and a.b.c are both
+ // unsafe getters.
+ authorizedEvaluations = generatedPath.map((_, i) =>
+ generatedPath.slice(0, i + 1)
+ );
+ } else if (concat) {
+ authorizedEvaluations.push(getterPath);
+ } else {
+ authorizedEvaluations = [getterPath];
+ }
+ }
+ return authorizedEvaluations;
+}
+
+/**
+ * Apply source mapping to the autocomplete input.
+ * @param {String} rawInput The input to map.
+ * @param {{[String]: String}} mappedVars Map of original to generated variable names.
+ * @param {WebConsole} hud A reference to the webconsole hud.
+ * @returns {String} The source-mapped expression to autocomplete.
+ */
+async function getMappedInput(rawInput, mappedVars, hud) {
+ if (!mappedVars || !Object.keys(mappedVars).length) {
+ return { input: rawInput, originalExpression: undefined };
+ }
+
+ const inputAnalysis = analyzeInputString(rawInput, 500);
+ if (!shouldInputBeAutocompleted(inputAnalysis)) {
+ return { input: rawInput, originalExpression: undefined };
+ }
+
+ const {
+ mainExpression: originalExpression,
+ isPropertyAccess,
+ isElementAccess,
+ lastStatement,
+ } = inputAnalysis;
+
+ // If we're autocompleting a variable name, pass it through unchanged so that we
+ // show original variable names rather than generated ones.
+ // For example, if we have the mapping `myVariable` => `x`, show variables starting
+ // with myVariable rather than x.
+ if (!isPropertyAccess && !isElementAccess) {
+ return { input: lastStatement, originalExpression };
+ }
+
+ let generated =
+ (await hud.getMappedExpression(originalExpression))?.expression ??
+ originalExpression;
+ // Strip off the semicolon if the expression was converted to a statement
+ const trailingSemicolon = /;\s*$/;
+ if (
+ trailingSemicolon.test(generated) &&
+ !trailingSemicolon.test(originalExpression)
+ ) {
+ generated = generated.slice(0, generated.lastIndexOf(";"));
+ }
+
+ const suffix = lastStatement.slice(originalExpression.length);
+ return { input: generated + suffix, originalExpression };
+}
+
+/**
+ * Called when the autocompletion data should be cleared.
+ */
+function autocompleteClear() {
+ return {
+ type: AUTOCOMPLETE_CLEAR,
+ };
+}
+
+/**
+ * Called when the autocompletion data should be retrieved from the cache (i.e.
+ * client-side).
+ *
+ * @param {String} input: The input used to filter the cached data.
+ */
+function autoCompleteDataRetrieveFromCache(input) {
+ return {
+ type: AUTOCOMPLETE_RETRIEVE_FROM_CACHE,
+ input,
+ };
+}
+
+let currentRequestId = 0;
+function generateRequestId() {
+ return currentRequestId++;
+}
+
+/**
+ * Action that fetch autocompletion data from the server.
+ *
+ * @param {Object} Object of the following shape:
+ * - {String} input: the expression that we want to complete.
+ * - {String} frameActorId: The id of the frame we want to autocomplete in.
+ * - {Boolean} force: true if the user forced an autocompletion (with Ctrl+Space).
+ * - {Array} authorizedEvaluations: Array of the properties access which can be
+ * executed by the engine.
+ * Example: [["x", "myGetter"], ["x", "myGetter", "y", "glitter"]]
+ * to retrieve properties of `x.myGetter.` and `x.myGetter.y.glitter`.
+ */
+function autocompleteDataFetch({
+ input,
+ frameActorId,
+ force,
+ authorizedEvaluations,
+ allVars,
+ mappedVars,
+ originalExpression,
+}) {
+ return async ({ dispatch, commands, webConsoleUI, hud }) => {
+ // Retrieve the right WebConsole front that relates either to (by order of priority):
+ // - the currently selected target in the context selector
+ // (contextSelectedTargetFront),
+ // - the currently selected Node in the inspector (selectedNodeActor),
+ // - the currently selected frame in the debugger (when paused) (frameActor),
+ // - the currently selected target in the iframe dropdown
+ // (selectedTargetFront from the TargetCommand)
+ const selectedNodeActorId = webConsoleUI.getSelectedNodeActorID();
+
+ let targetFront = commands.targetCommand.selectedTargetFront;
+ // Note that getSelectedTargetFront will return null if we default to the top level target.
+ const contextSelectorTargetFront = getSelectedTarget(
+ hud.commands.targetCommand.store.getState()
+ );
+ const selectedActorId = selectedNodeActorId || frameActorId;
+ if (contextSelectorTargetFront) {
+ targetFront = contextSelectorTargetFront;
+ } else if (selectedActorId) {
+ const selectedFront = commands.client.getFrontByID(selectedActorId);
+ if (selectedFront) {
+ targetFront = selectedFront.targetFront;
+ }
+ }
+
+ const webconsoleFront = await targetFront.getFront("console");
+
+ const id = generateRequestId();
+ dispatch({ type: AUTOCOMPLETE_PENDING_REQUEST, id });
+
+ webconsoleFront
+ .autocomplete(
+ input,
+ undefined,
+ frameActorId,
+ selectedNodeActorId,
+ authorizedEvaluations,
+ allVars
+ )
+ .then(data => {
+ if (data.isUnsafeGetter && originalExpression !== undefined) {
+ data.getterPath = unmapGetterPath(
+ data.getterPath,
+ originalExpression,
+ mappedVars
+ );
+ }
+ return dispatch(
+ autocompleteDataReceive({
+ id,
+ input,
+ force,
+ frameActorId,
+ data,
+ authorizedEvaluations,
+ })
+ );
+ })
+ .catch(e => {
+ console.error("failed autocomplete", e);
+ dispatch(autocompleteClear());
+ });
+ };
+}
+
+/**
+ * Replace generated variable names in an unsafe getter path with their original
+ * counterparts.
+ * @param {Array<String>} getterPath Array of properties leading up to and including the
+ * unsafe getter.
+ * @param {String} originalExpression The expression that was evaluated, before mapping.
+ * @param {{[String]: String}} mappedVars Map of original to generated variable names.
+ * @returns {Array<String>} An updated getter path containing original variables.
+ */
+function unmapGetterPath(getterPath, originalExpression, mappedVars) {
+ // We know that the original expression is a sequence of property accesses, that only
+ // the first part can be a mapped variable, and that the getter path must start with
+ // its generated path or be a prefix of it.
+
+ // Suppose we have the expression `foo.bar`, which maps to `a.b.c.bar`.
+ // Get the first part of the expression ("foo")
+ const originalVariable = /^[^.[?]*/s.exec(originalExpression)[0].trim();
+ const generatedVariable = mappedVars[originalVariable];
+ if (generatedVariable) {
+ // Get number of properties in "a.b.c"
+ const generatedVariableParts = generatedVariable.split(".");
+ // Replace ["a", "b", "c"] with "foo" in the getter path.
+ // Note that this will also work if the getter path ends inside of the mapped
+ // variable, like ["a", "b"].
+ return [
+ originalVariable,
+ ...getterPath.slice(generatedVariableParts.length),
+ ];
+ }
+ return getterPath;
+}
+
+/**
+ * Called when we receive the autocompletion data from the server.
+ *
+ * @param {Object} Object of the following shape:
+ * - {Integer} id: The autocompletion request id. This will be used in the reducer
+ * to check that we update the state with the last request results.
+ * - {String} input: the expression that we want to complete.
+ * - {String} frameActorId: The id of the frame we want to autocomplete in.
+ * - {Boolean} force: true if the user forced an autocompletion (with Ctrl+Space).
+ * - {Object} data: The actual data returned from the server.
+ * - {Array} authorizedEvaluations: Array of the properties access which can be
+ * executed by the engine.
+ * Example: [["x", "myGetter"], ["x", "myGetter", "y", "glitter"]]
+ * to retrieve properties of `x.myGetter.` and `x.myGetter.y.glitter`.
+ */
+function autocompleteDataReceive({
+ id,
+ input,
+ frameActorId,
+ force,
+ data,
+ authorizedEvaluations,
+}) {
+ return {
+ type: AUTOCOMPLETE_DATA_RECEIVE,
+ id,
+ input,
+ force,
+ frameActorId,
+ data,
+ authorizedEvaluations,
+ };
+}
+
+module.exports = {
+ autocompleteClear,
+ autocompleteUpdate,
+};
diff --git a/devtools/client/webconsole/actions/filters.js b/devtools/client/webconsole/actions/filters.js
new file mode 100644
index 0000000000..108614ee9f
--- /dev/null
+++ b/devtools/client/webconsole/actions/filters.js
@@ -0,0 +1,59 @@
+/* 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 {
+ getAllFilters,
+} = require("resource://devtools/client/webconsole/selectors/filters.js");
+
+const {
+ FILTER_TEXT_SET,
+ FILTER_TOGGLE,
+ FILTERS_CLEAR,
+ PREFS,
+ FILTERS,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+function filterTextSet(text) {
+ return {
+ type: FILTER_TEXT_SET,
+ text,
+ };
+}
+
+function filterToggle(filter) {
+ return ({ dispatch, getState, prefsService }) => {
+ dispatch({
+ type: FILTER_TOGGLE,
+ filter,
+ });
+ const filterState = getAllFilters(getState());
+ prefsService.setBoolPref(
+ PREFS.FILTER[filter.toUpperCase()],
+ filterState[filter]
+ );
+ };
+}
+
+function filtersClear() {
+ return ({ dispatch, getState, prefsService }) => {
+ dispatch({
+ type: FILTERS_CLEAR,
+ });
+
+ const filterState = getAllFilters(getState());
+ for (const filter in filterState) {
+ if (filter !== FILTERS.TEXT) {
+ prefsService.clearUserPref(PREFS.FILTER[filter.toUpperCase()]);
+ }
+ }
+ };
+}
+
+module.exports = {
+ filterTextSet,
+ filterToggle,
+ filtersClear,
+};
diff --git a/devtools/client/webconsole/actions/history.js b/devtools/client/webconsole/actions/history.js
new file mode 100644
index 0000000000..3c55389020
--- /dev/null
+++ b/devtools/client/webconsole/actions/history.js
@@ -0,0 +1,90 @@
+/* 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 {
+ APPEND_TO_HISTORY,
+ CLEAR_HISTORY,
+ HISTORY_LOADED,
+ UPDATE_HISTORY_POSITION,
+ REVERSE_SEARCH_INPUT_CHANGE,
+ REVERSE_SEARCH_BACK,
+ REVERSE_SEARCH_NEXT,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+/**
+ * Append a new value in the history of executed expressions,
+ * or overwrite the most recent entry. The most recent entry may
+ * contain the last edited input value that was not evaluated yet.
+ */
+function appendToHistory(expression) {
+ return {
+ type: APPEND_TO_HISTORY,
+ expression,
+ };
+}
+
+/**
+ * Clear the console history altogether. Note that this will not affect
+ * other consoles that are already opened (since they have their own copy),
+ * but it will reset the array for all newly-opened consoles.
+ */
+function clearHistory() {
+ return {
+ type: CLEAR_HISTORY,
+ };
+}
+
+/**
+ * Fired when the console history from previous Firefox sessions is loaded.
+ */
+function historyLoaded(entries) {
+ return {
+ type: HISTORY_LOADED,
+ entries,
+ };
+}
+
+/**
+ * Update place-holder position in the history list.
+ */
+function updateHistoryPosition(direction, expression) {
+ return {
+ type: UPDATE_HISTORY_POSITION,
+ direction,
+ expression,
+ };
+}
+
+function reverseSearchInputChange(value) {
+ return {
+ type: REVERSE_SEARCH_INPUT_CHANGE,
+ value,
+ };
+}
+
+function showReverseSearchNext({ access } = {}) {
+ return {
+ type: REVERSE_SEARCH_NEXT,
+ access,
+ };
+}
+
+function showReverseSearchBack({ access } = {}) {
+ return {
+ type: REVERSE_SEARCH_BACK,
+ access,
+ };
+}
+
+module.exports = {
+ appendToHistory,
+ clearHistory,
+ historyLoaded,
+ updateHistoryPosition,
+ reverseSearchInputChange,
+ showReverseSearchNext,
+ showReverseSearchBack,
+};
diff --git a/devtools/client/webconsole/actions/index.js b/devtools/client/webconsole/actions/index.js
new file mode 100644
index 0000000000..29aeb937a2
--- /dev/null
+++ b/devtools/client/webconsole/actions/index.js
@@ -0,0 +1,21 @@
+/* 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 actionModules = [
+ require("resource://devtools/client/webconsole/actions/autocomplete.js"),
+ require("resource://devtools/client/webconsole/actions/filters.js"),
+ require("resource://devtools/client/webconsole/actions/input.js"),
+ require("resource://devtools/client/webconsole/actions/messages.js"),
+ require("resource://devtools/client/webconsole/actions/ui.js"),
+ require("resource://devtools/client/webconsole/actions/notifications.js"),
+ require("resource://devtools/client/webconsole/actions/object.js"),
+ require("resource://devtools/client/webconsole/actions/toolbox.js"),
+ require("resource://devtools/client/webconsole/actions/history.js"),
+];
+
+const actions = Object.assign({}, ...actionModules);
+
+module.exports = actions;
diff --git a/devtools/client/webconsole/actions/input.js b/devtools/client/webconsole/actions/input.js
new file mode 100644
index 0000000000..f7dba1cbd8
--- /dev/null
+++ b/devtools/client/webconsole/actions/input.js
@@ -0,0 +1,501 @@
+/* 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 {
+ Utils: WebConsoleUtils,
+} = require("resource://devtools/client/webconsole/utils.js");
+const {
+ EVALUATE_EXPRESSION,
+ SET_TERMINAL_INPUT,
+ SET_TERMINAL_EAGER_RESULT,
+ EDITOR_PRETTY_PRINT,
+ HELP_URL,
+} = require("resource://devtools/client/webconsole/constants.js");
+const {
+ getAllPrefs,
+} = require("resource://devtools/client/webconsole/selectors/prefs.js");
+const ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");
+const l10n = require("resource://devtools/client/webconsole/utils/l10n.js");
+
+loader.lazyServiceGetter(
+ this,
+ "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1",
+ "nsIClipboardHelper"
+);
+loader.lazyRequireGetter(
+ this,
+ "messagesActions",
+ "resource://devtools/client/webconsole/actions/messages.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "historyActions",
+ "resource://devtools/client/webconsole/actions/history.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "ConsoleCommand",
+ "resource://devtools/client/webconsole/types.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "netmonitorBlockingActions",
+ "resource://devtools/client/netmonitor/src/actions/request-blocking.js"
+);
+
+loader.lazyRequireGetter(
+ this,
+ ["saveScreenshot", "captureAndSaveScreenshot"],
+ "resource://devtools/client/shared/screenshot.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "createSimpleTableMessage",
+ "resource://devtools/client/webconsole/utils/messages.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "getSelectedTarget",
+ "resource://devtools/shared/commands/target/selectors/targets.js",
+ true
+);
+
+async function getMappedExpression(hud, expression) {
+ let mapResult;
+ try {
+ mapResult = await hud.getMappedExpression(expression);
+ } catch (e) {
+ console.warn("Error when calling getMappedExpression", e);
+ }
+
+ let mapped = null;
+ if (mapResult) {
+ ({ expression, mapped } = mapResult);
+ }
+ return { expression, mapped };
+}
+
+function evaluateExpression(expression, from = "input") {
+ return async ({ dispatch, toolbox, webConsoleUI, hud, commands }) => {
+ if (!expression) {
+ expression = hud.getInputSelection() || hud.getInputValue();
+ }
+ if (!expression) {
+ return null;
+ }
+
+ // We use the messages action as it's doing additional transformation on the message.
+ const { messages } = dispatch(
+ messagesActions.messagesAdd([
+ new ConsoleCommand({
+ messageText: expression,
+ timeStamp: Date.now(),
+ }),
+ ])
+ );
+ const [consoleCommandMessage] = messages;
+
+ dispatch({
+ type: EVALUATE_EXPRESSION,
+ expression,
+ from,
+ });
+
+ WebConsoleUtils.usageCount++;
+
+ let mapped;
+ ({ expression, mapped } = await getMappedExpression(hud, expression));
+
+ // Even if the evaluation fails,
+ // we still need to pass the error response to onExpressionEvaluated.
+ const onSettled = res => res;
+
+ const response = await commands.scriptCommand
+ .execute(expression, {
+ frameActor: hud.getSelectedFrameActorID(),
+ selectedNodeActor: webConsoleUI.getSelectedNodeActorID(),
+ selectedTargetFront: getSelectedTarget(
+ webConsoleUI.hud.commands.targetCommand.store.getState()
+ ),
+ mapped,
+ // Allow breakpoints to be triggerred and the evaluated source to be shown in debugger UI
+ disableBreaks: false,
+ })
+ .then(onSettled, onSettled);
+
+ const serverConsoleCommandTimestamp = response.startTime;
+
+ // In case of remote debugging, it might happen that the debuggee page does not have
+ // the exact same clock time as the client. This could cause some ordering issues
+ // where the result message is displayed *before* the expression that lead to it.
+ if (
+ serverConsoleCommandTimestamp &&
+ consoleCommandMessage.timeStamp > serverConsoleCommandTimestamp
+ ) {
+ // If we're in such case, we remove the original command message, and add it again,
+ // with the timestamp coming from the server.
+ dispatch(messagesActions.messageRemove(consoleCommandMessage.id));
+ dispatch(
+ messagesActions.messagesAdd([
+ new ConsoleCommand({
+ messageText: expression,
+ timeStamp: serverConsoleCommandTimestamp,
+ }),
+ ])
+ );
+ }
+
+ return dispatch(onExpressionEvaluated(response));
+ };
+}
+
+/**
+ * The JavaScript evaluation response handler.
+ *
+ * @private
+ * @param {Object} response
+ * The message received from the server.
+ */
+function onExpressionEvaluated(response) {
+ return async ({ dispatch, webConsoleUI }) => {
+ if (response.error) {
+ console.error(`Evaluation error`, response.error, ": ", response.message);
+ return;
+ }
+
+ // If the evaluation was a top-level await expression that was rejected, there will
+ // be an uncaught exception reported, so we don't need to do anything.
+ if (response.topLevelAwaitRejected === true) {
+ return;
+ }
+
+ if (!response.helperResult) {
+ webConsoleUI.wrapper.dispatchMessageAdd(response);
+ return;
+ }
+
+ await dispatch(handleHelperResult(response));
+ };
+}
+
+function handleHelperResult(response) {
+ // eslint-disable-next-line complexity
+ return async ({ dispatch, hud, toolbox, webConsoleUI, getState }) => {
+ const { result, helperResult } = response;
+ const helperHasRawOutput = !!helperResult?.rawOutput;
+
+ if (helperResult?.type) {
+ switch (helperResult.type) {
+ case "exception":
+ dispatch(
+ messagesActions.messagesAdd([
+ {
+ message: {
+ level: "error",
+ arguments: [helperResult.message],
+ chromeContext: true,
+ },
+ resourceType: ResourceCommand.TYPES.CONSOLE_MESSAGE,
+ },
+ ])
+ );
+ break;
+ case "clearOutput":
+ dispatch(messagesActions.messagesClear());
+ break;
+ case "clearHistory":
+ dispatch(historyActions.clearHistory());
+ break;
+ case "historyOutput":
+ const history = getState().history.entries || [];
+ const columns = new Map([
+ ["_index", "(index)"],
+ ["expression", "Expressions"],
+ ]);
+ dispatch(
+ messagesActions.messagesAdd([
+ {
+ ...createSimpleTableMessage(
+ columns,
+ history.map((expression, index) => {
+ return { _index: index, expression };
+ })
+ ),
+ },
+ ])
+ );
+ break;
+ case "inspectObject": {
+ const objectActor = helperResult.object;
+ if (hud.toolbox && !helperResult.forceExpandInConsole) {
+ hud.toolbox.inspectObjectActor(objectActor);
+ } else {
+ webConsoleUI.inspectObjectActor(objectActor);
+ }
+ break;
+ }
+ case "help":
+ hud.openLink(HELP_URL);
+ break;
+ case "copyValueToClipboard":
+ clipboardHelper.copyString(helperResult.value);
+ dispatch(
+ messagesActions.messagesAdd([
+ {
+ resourceType: ResourceCommand.TYPES.PLATFORM_MESSAGE,
+ message: l10n.getStr(
+ "webconsole.message.commands.copyValueToClipboard"
+ ),
+ },
+ ])
+ );
+ break;
+ case "screenshotOutput":
+ const { args, value } = helperResult;
+ const targetFront =
+ getSelectedTarget(hud.commands.targetCommand.store.getState()) ||
+ hud.currentTarget;
+ let screenshotMessages;
+
+ // @backward-compat { version 87 } The screenshot-content actor isn't available
+ // in older server.
+ // With an old server, the console actor captures the screenshot when handling
+ // the command, and send it to the client which only needs to save it to a file.
+ // With a new server, the server simply acknowledges the command,
+ // and the client will drive the whole screenshot process (capture and save).
+ if (targetFront.hasActor("screenshotContent")) {
+ screenshotMessages = await captureAndSaveScreenshot(
+ targetFront,
+ webConsoleUI.getPanelWindow(),
+ args
+ );
+ } else {
+ screenshotMessages = await saveScreenshot(
+ webConsoleUI.getPanelWindow(),
+ args,
+ value
+ );
+ }
+
+ if (screenshotMessages && screenshotMessages.length) {
+ dispatch(
+ messagesActions.messagesAdd(
+ screenshotMessages.map(message => ({
+ message: {
+ level: message.level || "log",
+ arguments: [message.text],
+ chromeContext: true,
+ },
+ resourceType: ResourceCommand.TYPES.CONSOLE_MESSAGE,
+ }))
+ )
+ );
+ }
+ break;
+ case "blockURL":
+ const blockURL = helperResult.args.url;
+ // The console actor isn't able to block the request as the console actor runs in the content
+ // process, while the request has to be blocked from the parent process.
+ // Then, calling the Netmonitor action will only update the visual state of the Netmonitor,
+ // but we also have to block the request via the NetworkParentActor.
+ await hud.commands.networkCommand.blockRequestForUrl(blockURL);
+ toolbox
+ .getPanel("netmonitor")
+ ?.panelWin.store.dispatch(
+ netmonitorBlockingActions.addBlockedUrl(blockURL)
+ );
+
+ dispatch(
+ messagesActions.messagesAdd([
+ {
+ resourceType: ResourceCommand.TYPES.PLATFORM_MESSAGE,
+ message: l10n.getFormatStr(
+ "webconsole.message.commands.blockedURL",
+ [blockURL]
+ ),
+ },
+ ])
+ );
+ break;
+ case "unblockURL":
+ const unblockURL = helperResult.args.url;
+ await hud.commands.networkCommand.unblockRequestForUrl(unblockURL);
+ toolbox
+ .getPanel("netmonitor")
+ ?.panelWin.store.dispatch(
+ netmonitorBlockingActions.removeBlockedUrl(unblockURL)
+ );
+
+ dispatch(
+ messagesActions.messagesAdd([
+ {
+ resourceType: ResourceCommand.TYPES.PLATFORM_MESSAGE,
+ message: l10n.getFormatStr(
+ "webconsole.message.commands.unblockedURL",
+ [unblockURL]
+ ),
+ },
+ ])
+ );
+ // early return as we already dispatched necessary messages.
+ return;
+
+ // Sent when using ":command --help or :command --usage"
+ // to help discover command arguments.
+ //
+ // The remote runtime will tell us about the usage as it may
+ // be different from the client one.
+ case "usage":
+ dispatch(
+ messagesActions.messagesAdd([
+ {
+ resourceType: ResourceCommand.TYPES.PLATFORM_MESSAGE,
+ message: helperResult.message,
+ },
+ ])
+ );
+ break;
+
+ case "traceOutput":
+ // Nothing in particular to do.
+ // The JSTRACER_STATE resource will report the start/stop of the profiler.
+ break;
+ }
+ }
+
+ const hasErrorMessage =
+ response.exceptionMessage ||
+ (helperResult && helperResult.type === "error");
+
+ // Hide undefined results coming from helper functions.
+ const hasUndefinedResult =
+ result && typeof result == "object" && result.type == "undefined";
+
+ if (hasErrorMessage || helperHasRawOutput || !hasUndefinedResult) {
+ dispatch(messagesActions.messagesAdd([response]));
+ }
+ };
+}
+
+function focusInput() {
+ return ({ hud }) => {
+ return hud.focusInput();
+ };
+}
+
+function setInputValue(value) {
+ return ({ hud }) => {
+ return hud.setInputValue(value);
+ };
+}
+
+/**
+ * Request an eager evaluation from the server.
+ *
+ * @param {String} expression: The expression to evaluate.
+ * @param {Boolean} force: When true, will request an eager evaluation again, even if
+ * the expression is the same one than the one that was used in
+ * the previous evaluation.
+ */
+function terminalInputChanged(expression, force = false) {
+ return async ({ dispatch, webConsoleUI, hud, commands, getState }) => {
+ const prefs = getAllPrefs(getState());
+ if (!prefs.eagerEvaluation) {
+ return null;
+ }
+
+ const { terminalInput = "" } = getState().history;
+
+ // Only re-evaluate if the expression did change.
+ if (
+ (!terminalInput && !expression) ||
+ (typeof terminalInput === "string" &&
+ typeof expression === "string" &&
+ expression.trim() === terminalInput.trim() &&
+ !force)
+ ) {
+ return null;
+ }
+
+ dispatch({
+ type: SET_TERMINAL_INPUT,
+ expression: expression.trim(),
+ });
+
+ // There's no need to evaluate an empty string.
+ if (!expression || !expression.trim()) {
+ return dispatch({
+ type: SET_TERMINAL_EAGER_RESULT,
+ expression,
+ result: null,
+ });
+ }
+
+ let mapped;
+ ({ expression, mapped } = await getMappedExpression(hud, expression));
+
+ // We don't want to evaluate top-level await expressions (see Bug 1786805)
+ if (mapped?.await) {
+ return dispatch({
+ type: SET_TERMINAL_EAGER_RESULT,
+ expression,
+ result: null,
+ });
+ }
+
+ const response = await commands.scriptCommand.execute(expression, {
+ frameActor: hud.getSelectedFrameActorID(),
+ selectedNodeActor: webConsoleUI.getSelectedNodeActorID(),
+ selectedTargetFront: getSelectedTarget(
+ hud.commands.targetCommand.store.getState()
+ ),
+ mapped,
+ eager: true,
+ });
+
+ return dispatch({
+ type: SET_TERMINAL_EAGER_RESULT,
+ result: getEagerEvaluationResult(response),
+ });
+ };
+}
+
+/**
+ * Refresh the current eager evaluation by requesting a new eager evaluation.
+ */
+function updateInstantEvaluationResultForCurrentExpression() {
+ return ({ getState, dispatch }) =>
+ dispatch(terminalInputChanged(getState().history.terminalInput, true));
+}
+
+function getEagerEvaluationResult(response) {
+ const result = response.exception || response.result;
+ // Don't show syntax errors results to the user.
+ if (result?.isSyntaxError || (result && result.type == "undefined")) {
+ return null;
+ }
+
+ return result;
+}
+
+function prettyPrintEditor() {
+ return {
+ type: EDITOR_PRETTY_PRINT,
+ };
+}
+
+module.exports = {
+ evaluateExpression,
+ focusInput,
+ setInputValue,
+ terminalInputChanged,
+ updateInstantEvaluationResultForCurrentExpression,
+ prettyPrintEditor,
+};
diff --git a/devtools/client/webconsole/actions/messages.js b/devtools/client/webconsole/actions/messages.js
new file mode 100644
index 0000000000..407cfa0121
--- /dev/null
+++ b/devtools/client/webconsole/actions/messages.js
@@ -0,0 +1,179 @@
+/* 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 {
+ prepareMessage,
+ getNaturalOrder,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+const {
+ IdGenerator,
+} = require("resource://devtools/client/webconsole/utils/id-generator.js");
+const {
+ batchActions,
+} = require("resource://devtools/client/shared/redux/middleware/debounce.js");
+
+const {
+ CSS_MESSAGE_ADD_MATCHING_ELEMENTS,
+ MESSAGE_CLOSE,
+ MESSAGE_OPEN,
+ MESSAGE_REMOVE,
+ MESSAGE_TYPE,
+ MESSAGES_ADD,
+ MESSAGES_CLEAR,
+ MESSAGES_DISABLE,
+ NETWORK_MESSAGES_UPDATE,
+ NETWORK_UPDATES_REQUEST,
+ PRIVATE_MESSAGES_CLEAR,
+ TARGET_MESSAGES_REMOVE,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+const defaultIdGenerator = new IdGenerator();
+
+function messagesAdd(packets, idGenerator = null, persistLogs = false) {
+ if (idGenerator == null) {
+ idGenerator = defaultIdGenerator;
+ }
+ const messages = packets.map(packet =>
+ prepareMessage(packet, idGenerator, persistLogs)
+ );
+ // Sort the messages by their timestamps.
+ messages.sort(getNaturalOrder);
+
+ if (!persistLogs) {
+ for (let i = messages.length - 1; i >= 0; i--) {
+ if (messages[i].type === MESSAGE_TYPE.CLEAR) {
+ return batchActions([
+ messagesClear(),
+ {
+ type: MESSAGES_ADD,
+ messages: messages.slice(i),
+ },
+ ]);
+ }
+ }
+ }
+
+ // When this is used for non-cached messages then handle clear message and
+ // split up into batches
+ return {
+ type: MESSAGES_ADD,
+ messages,
+ };
+}
+
+function messagesClear() {
+ return {
+ type: MESSAGES_CLEAR,
+ };
+}
+
+function messagesDisable(ids) {
+ return {
+ type: MESSAGES_DISABLE,
+ ids,
+ };
+}
+
+function privateMessagesClear() {
+ return {
+ type: PRIVATE_MESSAGES_CLEAR,
+ };
+}
+
+function targetMessagesRemove(targetFront) {
+ return {
+ type: TARGET_MESSAGES_REMOVE,
+ targetFront,
+ };
+}
+
+function messageOpen(id) {
+ return {
+ type: MESSAGE_OPEN,
+ id,
+ };
+}
+
+function messageClose(id) {
+ return {
+ type: MESSAGE_CLOSE,
+ id,
+ };
+}
+
+/**
+ * Make a query on the server to get a list of DOM elements matching the given
+ * CSS selectors and store the information in the state.
+ *
+ * @param {Message} message
+ * The CSSWarning message
+ */
+function messageGetMatchingElements(message) {
+ return async ({ dispatch, commands }) => {
+ try {
+ // We need to do the querySelectorAll using the target the message is coming from,
+ // as well as with the window the warning message was emitted from.
+ const selectedTargetFront = message?.targetFront;
+
+ const response = await commands.scriptCommand.execute(
+ `document.querySelectorAll('${message.cssSelectors}')`,
+ {
+ selectedTargetFront,
+ innerWindowID: message.innerWindowID,
+ disableBreaks: true,
+ }
+ );
+ dispatch({
+ type: CSS_MESSAGE_ADD_MATCHING_ELEMENTS,
+ id: message.id,
+ elements: response.result,
+ });
+ } catch (err) {
+ console.error(err);
+ }
+ };
+}
+
+function messageRemove(id) {
+ return {
+ type: MESSAGE_REMOVE,
+ id,
+ };
+}
+
+function networkMessageUpdates(packets, idGenerator = null) {
+ if (idGenerator == null) {
+ idGenerator = defaultIdGenerator;
+ }
+
+ const messages = packets.map(packet => prepareMessage(packet, idGenerator));
+
+ return {
+ type: NETWORK_MESSAGES_UPDATE,
+ messages,
+ };
+}
+
+function networkUpdateRequests(updates) {
+ return {
+ type: NETWORK_UPDATES_REQUEST,
+ updates,
+ };
+}
+
+module.exports = {
+ messagesAdd,
+ messagesClear,
+ messagesDisable,
+ messageOpen,
+ messageClose,
+ messageRemove,
+ messageGetMatchingElements,
+ networkMessageUpdates,
+ networkUpdateRequests,
+ privateMessagesClear,
+ targetMessagesRemove,
+};
diff --git a/devtools/client/webconsole/actions/moz.build b/devtools/client/webconsole/actions/moz.build
new file mode 100644
index 0000000000..5b06311064
--- /dev/null
+++ b/devtools/client/webconsole/actions/moz.build
@@ -0,0 +1,17 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ "autocomplete.js",
+ "filters.js",
+ "history.js",
+ "index.js",
+ "input.js",
+ "messages.js",
+ "notifications.js",
+ "object.js",
+ "toolbox.js",
+ "ui.js",
+)
diff --git a/devtools/client/webconsole/actions/notifications.js b/devtools/client/webconsole/actions/notifications.js
new file mode 100644
index 0000000000..2ffe96985b
--- /dev/null
+++ b/devtools/client/webconsole/actions/notifications.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/. */
+
+"use strict";
+
+const {
+ APPEND_NOTIFICATION,
+ REMOVE_NOTIFICATION,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+/**
+ * Append a notification into JSTerm notification list.
+ */
+function appendNotification(
+ label,
+ value,
+ image,
+ priority,
+ buttons = [],
+ eventCallback
+) {
+ return {
+ type: APPEND_NOTIFICATION,
+ label,
+ value,
+ image,
+ priority,
+ buttons,
+ eventCallback,
+ };
+}
+
+/**
+ * Remove notification with specified value from JSTerm
+ * notification list.
+ */
+function removeNotification(value) {
+ return {
+ type: REMOVE_NOTIFICATION,
+ value,
+ };
+}
+
+module.exports = {
+ appendNotification,
+ removeNotification,
+};
diff --git a/devtools/client/webconsole/actions/object.js b/devtools/client/webconsole/actions/object.js
new file mode 100644
index 0000000000..1fd9df0fc2
--- /dev/null
+++ b/devtools/client/webconsole/actions/object.js
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+loader.lazyServiceGetter(
+ this,
+ "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1",
+ "nsIClipboardHelper"
+);
+
+function storeAsGlobal(actor) {
+ return async ({ commands, hud }) => {
+ const evalString = `{ let i = 0;
+ while (this.hasOwnProperty("temp" + i) && i < 1000) {
+ i++;
+ }
+ this["temp" + i] = _self;
+ "temp" + i;
+ }`;
+
+ const res = await commands.scriptCommand.execute(evalString, {
+ selectedObjectActor: actor,
+ // Prevent any type of breakpoint when evaluating this code
+ disableBreaks: true,
+ // Ensure always overriding "$0" and "_self" console command, even if the page implements a variable with the same name.
+ preferConsoleCommandsOverLocalSymbols: true,
+ });
+
+ // Select the adhoc target in the console.
+ if (hud.toolbox) {
+ const objectFront = commands.client.getFrontByID(actor);
+ if (objectFront) {
+ const targetActorID = objectFront.targetFront?.actorID;
+ if (targetActorID) {
+ hud.toolbox.selectTarget(targetActorID);
+ }
+ }
+ }
+
+ hud.focusInput();
+ hud.setInputValue(res.result);
+ };
+}
+
+function copyMessageObject(actor, variableText) {
+ return async ({ commands }) => {
+ if (actor) {
+ // The Debugger.Object of the OA will be bound to |_self| during evaluation.
+ // See server/actors/webconsole/eval-with-debugger.js `evalWithDebugger`.
+ const res = await commands.scriptCommand.execute("copy(_self)", {
+ selectedObjectActor: actor,
+ // Prevent any type of breakpoint when evaluating this code
+ disableBreaks: true,
+ // ensure always overriding "$0" and "_self" console command, even if the page implements a variable with the same name.
+ preferConsoleCommandsOverLocalSymbols: true,
+ });
+
+ clipboardHelper.copyString(res.helperResult.value);
+ } else {
+ clipboardHelper.copyString(variableText);
+ }
+ };
+}
+
+module.exports = {
+ storeAsGlobal,
+ copyMessageObject,
+};
diff --git a/devtools/client/webconsole/actions/toolbox.js b/devtools/client/webconsole/actions/toolbox.js
new file mode 100644
index 0000000000..65739f2ca5
--- /dev/null
+++ b/devtools/client/webconsole/actions/toolbox.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";
+
+function openNetworkPanel(messageId) {
+ return ({ hud }) => {
+ hud.openNetworkPanel(messageId);
+ };
+}
+
+function resendNetworkRequest(messageId) {
+ return ({ hud }) => {
+ hud.resendNetworkRequest(messageId);
+ };
+}
+
+function highlightDomElement(grip) {
+ return ({ hud }) => {
+ const highlighter = hud.getHighlighter();
+ if (highlighter) {
+ highlighter.highlight(grip);
+ }
+ };
+}
+
+function unHighlightDomElement(grip) {
+ return ({ hud }) => {
+ const highlighter = hud.getHighlighter();
+ if (highlighter) {
+ highlighter.unhighlight(grip);
+ }
+ };
+}
+
+function openNodeInInspector(contentDomReference) {
+ return ({ hud }) => {
+ hud.openNodeInInspector({ contentDomReference });
+ };
+}
+
+module.exports = {
+ highlightDomElement,
+ unHighlightDomElement,
+ openNetworkPanel,
+ resendNetworkRequest,
+ openNodeInInspector,
+};
diff --git a/devtools/client/webconsole/actions/ui.js b/devtools/client/webconsole/actions/ui.js
new file mode 100644
index 0000000000..b3f9d2d578
--- /dev/null
+++ b/devtools/client/webconsole/actions/ui.js
@@ -0,0 +1,256 @@
+/* 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 {
+ getAllPrefs,
+} = require("resource://devtools/client/webconsole/selectors/prefs.js");
+const {
+ getAllUi,
+} = require("resource://devtools/client/webconsole/selectors/ui.js");
+const {
+ getMessage,
+} = require("resource://devtools/client/webconsole/selectors/messages.js");
+
+const {
+ INITIALIZE,
+ PERSIST_TOGGLE,
+ PREFS,
+ REVERSE_SEARCH_INPUT_TOGGLE,
+ SELECT_NETWORK_MESSAGE_TAB,
+ SHOW_OBJECT_IN_SIDEBAR,
+ SIDEBAR_CLOSE,
+ SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE,
+ SHOW_EVALUATION_NOTIFICATION,
+ TIMESTAMPS_TOGGLE,
+ WARNING_GROUPS_TOGGLE,
+ FILTERBAR_DISPLAY_MODE_SET,
+ EDITOR_TOGGLE,
+ EDITOR_SET_WIDTH,
+ EDITOR_ONBOARDING_DISMISS,
+ EAGER_EVALUATION_TOGGLE,
+ AUTOCOMPLETE_TOGGLE,
+ ENABLE_NETWORK_MONITORING,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+function openLink(url, e) {
+ return ({ hud }) => {
+ return hud.openLink(url, e);
+ };
+}
+
+function persistToggle() {
+ return ({ dispatch, getState, prefsService }) => {
+ dispatch({
+ type: PERSIST_TOGGLE,
+ });
+ const uiState = getAllUi(getState());
+ prefsService.setBoolPref(PREFS.UI.PERSIST, uiState.persistLogs);
+ };
+}
+
+function networkMonitoringToggle() {
+ return ({ dispatch, getState, prefsService, webConsoleUI }) => {
+ dispatch({ type: ENABLE_NETWORK_MONITORING });
+ const uiState = getAllUi(getState());
+
+ prefsService.setBoolPref(
+ PREFS.UI.ENABLE_NETWORK_MONITORING,
+ uiState.enableNetworkMonitoring
+ );
+
+ if (uiState.enableNetworkMonitoring) {
+ webConsoleUI.startWatchingNetworkResources();
+ } else {
+ webConsoleUI.stopWatchingNetworkResources();
+ }
+ };
+}
+
+function timestampsToggle() {
+ return ({ dispatch, getState, prefsService }) => {
+ dispatch({
+ type: TIMESTAMPS_TOGGLE,
+ });
+ const uiState = getAllUi(getState());
+ prefsService.setBoolPref(
+ PREFS.UI.MESSAGE_TIMESTAMP,
+ uiState.timestampsVisible
+ );
+ };
+}
+
+function autocompleteToggle() {
+ return ({ dispatch, getState, prefsService }) => {
+ dispatch({
+ type: AUTOCOMPLETE_TOGGLE,
+ });
+ const prefsState = getAllPrefs(getState());
+ prefsService.setBoolPref(
+ PREFS.FEATURES.AUTOCOMPLETE,
+ prefsState.autocomplete
+ );
+ };
+}
+
+function warningGroupsToggle() {
+ return ({ dispatch, getState, prefsService }) => {
+ dispatch({
+ type: WARNING_GROUPS_TOGGLE,
+ });
+ const prefsState = getAllPrefs(getState());
+ prefsService.setBoolPref(
+ PREFS.FEATURES.GROUP_WARNINGS,
+ prefsState.groupWarnings
+ );
+ };
+}
+
+function eagerEvaluationToggle() {
+ return ({ dispatch, getState, prefsService }) => {
+ dispatch({
+ type: EAGER_EVALUATION_TOGGLE,
+ });
+ const prefsState = getAllPrefs(getState());
+ prefsService.setBoolPref(
+ PREFS.FEATURES.EAGER_EVALUATION,
+ prefsState.eagerEvaluation
+ );
+ };
+}
+
+function selectNetworkMessageTab(id) {
+ return {
+ type: SELECT_NETWORK_MESSAGE_TAB,
+ id,
+ };
+}
+
+function initialize() {
+ return {
+ type: INITIALIZE,
+ };
+}
+
+function sidebarClose() {
+ return {
+ type: SIDEBAR_CLOSE,
+ };
+}
+
+function splitConsoleCloseButtonToggle(shouldDisplayButton) {
+ return {
+ type: SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE,
+ shouldDisplayButton,
+ };
+}
+
+function editorToggle() {
+ return ({ dispatch, getState, prefsService }) => {
+ dispatch({
+ type: EDITOR_TOGGLE,
+ });
+ const uiState = getAllUi(getState());
+ prefsService.setBoolPref(PREFS.UI.EDITOR, uiState.editor);
+ };
+}
+
+function editorOnboardingDismiss() {
+ return ({ dispatch, prefsService }) => {
+ dispatch({
+ type: EDITOR_ONBOARDING_DISMISS,
+ });
+ prefsService.setBoolPref(PREFS.UI.EDITOR_ONBOARDING, false);
+ };
+}
+
+function setEditorWidth(width) {
+ return ({ dispatch, prefsService }) => {
+ dispatch({
+ type: EDITOR_SET_WIDTH,
+ width,
+ });
+ prefsService.setIntPref(PREFS.UI.EDITOR_WIDTH, width);
+ };
+}
+
+/**
+ * Dispatches a SHOW_OBJECT_IN_SIDEBAR action, with a grip property corresponding to the
+ * {actor} parameter in the {messageId} message.
+ *
+ * @param {String} actorID: Actor id of the object we want to place in the sidebar.
+ * @param {String} messageId: id of the message containing the {actor} parameter.
+ */
+function showMessageObjectInSidebar(actorID, messageId) {
+ return ({ dispatch, getState }) => {
+ const { parameters } = getMessage(getState(), messageId);
+ if (Array.isArray(parameters)) {
+ for (const parameter of parameters) {
+ if (parameter && parameter.actorID === actorID) {
+ dispatch(showObjectInSidebar(parameter));
+ return;
+ }
+ }
+ }
+ };
+}
+
+function showObjectInSidebar(front) {
+ return {
+ type: SHOW_OBJECT_IN_SIDEBAR,
+ front,
+ };
+}
+
+function reverseSearchInputToggle({ initialValue, access } = {}) {
+ return {
+ type: REVERSE_SEARCH_INPUT_TOGGLE,
+ initialValue,
+ access,
+ };
+}
+
+function filterBarDisplayModeSet(displayMode) {
+ return {
+ type: FILTERBAR_DISPLAY_MODE_SET,
+ displayMode,
+ };
+}
+
+function openSidebar(messageId, rootActorId) {
+ return ({ dispatch }) => {
+ dispatch(showMessageObjectInSidebar(rootActorId, messageId));
+ };
+}
+
+function showEvaluationNotification(notification) {
+ return {
+ type: SHOW_EVALUATION_NOTIFICATION,
+ notification,
+ };
+}
+
+module.exports = {
+ eagerEvaluationToggle,
+ editorOnboardingDismiss,
+ editorToggle,
+ filterBarDisplayModeSet,
+ initialize,
+ persistToggle,
+ reverseSearchInputToggle,
+ selectNetworkMessageTab,
+ setEditorWidth,
+ showMessageObjectInSidebar,
+ showObjectInSidebar,
+ sidebarClose,
+ splitConsoleCloseButtonToggle,
+ timestampsToggle,
+ networkMonitoringToggle,
+ warningGroupsToggle,
+ openLink,
+ openSidebar,
+ autocompleteToggle,
+ showEvaluationNotification,
+};
diff --git a/devtools/client/webconsole/browser-console-manager.js b/devtools/client/webconsole/browser-console-manager.js
new file mode 100644
index 0000000000..b38391f685
--- /dev/null
+++ b/devtools/client/webconsole/browser-console-manager.js
@@ -0,0 +1,186 @@
+/* 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 {
+ CommandsFactory,
+} = require("resource://devtools/shared/commands/commands-factory.js");
+
+loader.lazyRequireGetter(
+ this,
+ "Tools",
+ "resource://devtools/client/definitions.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "l10n",
+ "resource://devtools/client/webconsole/utils/l10n.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "BrowserConsole",
+ "resource://devtools/client/webconsole/browser-console.js"
+);
+
+const BC_WINDOW_FEATURES =
+ "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+class BrowserConsoleManager {
+ constructor() {
+ this._browserConsole = null;
+ this._browserConsoleInitializing = null;
+ this._browerConsoleSessionState = false;
+ }
+
+ storeBrowserConsoleSessionState() {
+ this._browerConsoleSessionState = !!this.getBrowserConsole();
+ }
+
+ getBrowserConsoleSessionState() {
+ return this._browerConsoleSessionState;
+ }
+
+ /**
+ * Open a Browser Console for the current commands context.
+ *
+ * @param nsIDOMWindow iframeWindow
+ * The window where the browser console UI is already loaded.
+ * @return object
+ * A promise object for the opening of the new BrowserConsole instance.
+ */
+ async openBrowserConsole(win) {
+ const hud = new BrowserConsole(this.commands, win, win);
+ this._browserConsole = hud;
+ await hud.init();
+ return hud;
+ }
+
+ /**
+ * Close the opened Browser Console
+ */
+ async closeBrowserConsole() {
+ if (!this._browserConsole) {
+ return;
+ }
+
+ // Ensure destroying the commands,
+ // even if the console throws during cleanup.
+ try {
+ await this._browserConsole.destroy();
+ } catch (e) {
+ console.error(e);
+ }
+ this._browserConsole = null;
+
+ await this.commands.destroy();
+ this.commands = null;
+ }
+
+ /**
+ * Toggle the Browser Console.
+ */
+ async toggleBrowserConsole() {
+ if (this._browserConsole) {
+ return this.closeBrowserConsole();
+ }
+
+ if (this._browserConsoleInitializing) {
+ return this._browserConsoleInitializing;
+ }
+
+ // Temporarily cache the async startup sequence so that if toggleBrowserConsole
+ // gets called again we can return this console instead of opening another one.
+ this._browserConsoleInitializing = (async () => {
+ this.commands = await CommandsFactory.forBrowserConsole();
+ const win = await this.openWindow();
+ const browserConsole = await this.openBrowserConsole(win);
+ return browserConsole;
+ })();
+
+ try {
+ const browserConsole = await this._browserConsoleInitializing;
+ this._browserConsoleInitializing = null;
+ return browserConsole;
+ } catch (e) {
+ // Ensure always clearing this field, even in case of exception,
+ // which may happen when closing during initialization.
+ this._browserConsoleInitializing = null;
+ throw e;
+ }
+ }
+
+ async openWindow() {
+ const win = Services.ww.openWindow(
+ null,
+ Tools.webConsole.url,
+ "_blank",
+ BC_WINDOW_FEATURES,
+ null
+ );
+
+ await new Promise(resolve => {
+ win.addEventListener("DOMContentLoaded", resolve, { once: true });
+ });
+
+ // It's important to declare the unload *after* the initial "DOMContentLoaded",
+ // otherwise, since the window is navigated to Tools.webConsole.url, an unload event
+ // is fired.
+ win.addEventListener("unload", this.closeBrowserConsole.bind(this), {
+ once: true,
+ });
+
+ this.updateWindowTitle(win);
+ return win;
+ }
+
+ /**
+ * Opens or focuses the Browser Console.
+ */
+ openBrowserConsoleOrFocus() {
+ const hud = this.getBrowserConsole();
+ if (hud) {
+ hud.iframeWindow.focus();
+ return Promise.resolve(hud);
+ }
+
+ return this.toggleBrowserConsole();
+ }
+
+ /**
+ * Get the Browser Console instance, if open.
+ *
+ * @return object|null
+ * A BrowserConsole instance or null if the Browser Console is not
+ * open.
+ */
+ getBrowserConsole() {
+ return this._browserConsole;
+ }
+
+ /**
+ * Set the title of the Browser Console window.
+ *
+ * @param {Window} win: The BrowserConsole window
+ */
+ updateWindowTitle(win) {
+ let title;
+ const mode = Services.prefs.getCharPref(
+ "devtools.browsertoolbox.scope",
+ null
+ );
+ if (mode == "everything") {
+ title = l10n.getStr("multiProcessBrowserConsole.title");
+ } else if (mode == "parent-process") {
+ title = l10n.getStr("parentProcessBrowserConsole.title");
+ } else {
+ throw new Error("Unsupported mode: " + mode);
+ }
+
+ win.document.title = title;
+ }
+}
+
+exports.BrowserConsoleManager = new BrowserConsoleManager();
diff --git a/devtools/client/webconsole/browser-console.js b/devtools/client/webconsole/browser-console.js
new file mode 100644
index 0000000000..5d10b19c21
--- /dev/null
+++ b/devtools/client/webconsole/browser-console.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";
+
+const WebConsole = require("resource://devtools/client/webconsole/webconsole.js");
+const { Utils } = require("resource://devtools/client/webconsole/utils.js");
+
+loader.lazyRequireGetter(
+ this,
+ "Telemetry",
+ "resource://devtools/client/shared/telemetry.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "BrowserConsoleManager",
+ "resource://devtools/client/webconsole/browser-console-manager.js",
+ true
+);
+
+/**
+ * A BrowserConsole instance is an interactive console initialized *per commands*
+ * that displays console log data as well as provides an interactive terminal to
+ * manipulate all browser debuggable context and targeted by default at the current
+ * top-level window's document.
+ *
+ * This object only wraps the iframe that holds the Browser Console UI. This is
+ * meant to be an integration point between the Firefox UI and the Browser Console
+ * UI and features.
+ *
+ * This object extends the WebConsole object located in webconsole.js
+ */
+class BrowserConsole extends WebConsole {
+ #bcInitializer = null;
+ #bcDestroyer = null;
+ #telemetry;
+ /*
+ * @constructor
+ * @param object commands
+ * The commands object with all interfaces defined from devtools/shared/commands/
+ * @param nsIDOMWindow iframeWindow
+ * The window where the browser console UI is already loaded.
+ * @param nsIDOMWindow chromeWindow
+ * The window of the browser console owner.
+ */
+ constructor(commands, iframeWindow, chromeWindow) {
+ super(null, commands, iframeWindow, chromeWindow, true);
+
+ this.#telemetry = new Telemetry();
+ }
+
+ /**
+ * Initialize the Browser Console instance.
+ *
+ * @return object
+ * A promise for the initialization.
+ */
+ init() {
+ if (this.#bcInitializer) {
+ return this.#bcInitializer;
+ }
+
+ this.#bcInitializer = (async () => {
+ // Only add the shutdown observer if we've opened a Browser Console window.
+ ShutdownObserver.init();
+
+ this.#telemetry.toolOpened("browserconsole", this);
+
+ await super.init(false);
+
+ // Reports the console as created only after everything is done,
+ // including TargetCommand.startListening.
+ const id = Utils.supportsString(this.hudId);
+ Services.obs.notifyObservers(id, "web-console-created");
+ })();
+ return this.#bcInitializer;
+ }
+
+ /**
+ * Destroy the object.
+ *
+ * @return object
+ * A promise object that is resolved once the Browser Console is closed.
+ */
+ destroy() {
+ if (this.#bcDestroyer) {
+ return this.#bcDestroyer;
+ }
+
+ this.#bcDestroyer = (async () => {
+ this.#telemetry.toolClosed("browserconsole", this);
+
+ this.commands.targetCommand.destroy();
+ await super.destroy();
+ await this.currentTarget.destroy();
+ this.chromeWindow.close();
+ })();
+
+ return this.#bcDestroyer;
+ }
+
+ updateWindowTitle() {
+ BrowserConsoleManager.updateWindowTitle(this.chromeWindow);
+ }
+}
+
+/**
+ * The ShutdownObserver listens for app shutdown and saves the current state
+ * of the Browser Console for session restore.
+ */
+var ShutdownObserver = {
+ _initialized: false,
+
+ init() {
+ if (this._initialized) {
+ return;
+ }
+
+ Services.obs.addObserver(this, "quit-application-granted");
+
+ this._initialized = true;
+ },
+
+ observe(message, topic) {
+ if (topic == "quit-application-granted") {
+ BrowserConsoleManager.storeBrowserConsoleSessionState();
+ this.uninit();
+ }
+ },
+
+ uninit() {
+ Services.obs.removeObserver(this, "quit-application-granted");
+ },
+};
+
+module.exports = BrowserConsole;
diff --git a/devtools/client/webconsole/components/App.css b/devtools/client/webconsole/components/App.css
new file mode 100644
index 0000000000..15dfa06f5e
--- /dev/null
+++ b/devtools/client/webconsole/components/App.css
@@ -0,0 +1,523 @@
+/* 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,
+body {
+ height: 100vh;
+ margin: 0;
+ padding: 0;
+ overflow: hidden;
+}
+
+#app-wrapper {
+ height: 100vh;
+ max-height: 100vh;
+}
+
+.webconsole-output {
+ direction: ltr;
+ overflow: auto;
+ overflow-anchor: none;
+ user-select: text;
+ position: relative;
+ container-name: console-output;
+ container-type: inline-size;
+}
+
+.webconsole-app {
+ --object-inspector-hover-background: transparent;
+ --attachment-margin-block-end: 3px;
+ --primary-toolbar-height: 29px;
+ display: grid;
+ /*
+ * Here's the design we want in in-line mode
+ * +----------------------------------------------+
+ * | [ChromeDebugToolbar] |
+ * +----------------------------------------------+
+ * | Filter bar primary ↔ |
+ * +----------------------------↔ |
+ * | [Filter bar secondary] ↔ |
+ * +----------------------------↔ |
+ * | ↔ |
+ * + +----------------------+ ↔ |
+ * | | | ↔ |
+ * | | Output | ↔ [sidebar] |
+ * | | | ↔ |
+ * | +----------------------+ ↔ |
+ * | | [NotificationBox] | ↔ |
+ * | +----------------------+ ↔ |
+ * | | | ↔ |
+ * | | JSTerm | ↔ |
+ * | | | ↔ |
+ * | +----------------------+ ↔ |
+ * | | [EagerEvaluation] | ↔ |
+ * | +----------------------+ ↔ |
+ * | | [EvalNotification] | ↔ |
+ * | +----------------------+ ↔ |
+ * +----------------------------↔ |
+ * | [Reverse search input] ↔ |
+ * +----------------------------------------------+
+ *
+ * - ↔ are width resizers
+ * - Elements inside brackets may not be visible, so we set
+ * rows/columns to "auto" to make them collapse when the element
+ * they contain is hidden.
+ */
+ grid-template-areas: "chrome-debug-toolbar chrome-debug-toolbar"
+ "filter-toolbar sidebar"
+ "filter-toolbar-secondary sidebar"
+ "output-input sidebar"
+ "eval-notification sidebar"
+ "reverse-search sidebar";
+ grid-template-rows: auto var(--primary-toolbar-height) auto 1fr auto auto;
+ grid-template-columns: minmax(200px, 1fr) minmax(0, auto);
+ max-height: 100vh !important;
+ height: 100vh !important;
+ width: 100vw;
+ overflow: hidden;
+ color: var(--console-output-color);
+ -moz-user-focus: normal;
+}
+
+.chrome-debug-toolbar {
+ grid-column: chrome-debug-toolbar;
+ grid-row: chrome-debug-toolbar;
+}
+
+.webconsole-filteringbar-wrapper {
+ grid-column: filter-toolbar;
+ grid-row: filter-toolbar / filter-toolbar-secondary;
+ grid-template-rows: subgrid;
+}
+
+.webconsole-filterbar-primary {
+ grid-row: filter-toolbar;
+}
+
+/* Only put the filter buttons toolbar on its own row in narrow filterbar layout */
+.narrow .devtools-toolbar.webconsole-filterbar-secondary {
+ grid-row: filter-toolbar-secondary;
+}
+
+.flexible-output-input {
+ display: flex;
+ flex-direction: column;
+ grid-area: output-input;
+ /* Don't take more height than the grid allows to */
+ max-height: 100%;
+ overflow: hidden;
+}
+
+.flexible-output-input .webconsole-output {
+ flex-shrink: 100000;
+ overflow-x: hidden;
+}
+
+.flexible-output-input > .webconsole-output:not(:empty) {
+ min-height: var(--console-row-height);
+}
+
+/* webconsole.css | chrome://devtools/skin/webconsole.css */
+.webconsole-filteringbar-wrapper .devtools-toolbar {
+ padding-inline-end: 0;
+}
+
+.devtools-button.webconsole-console-settings-menu-button {
+ /* adjust outline offset so it's not clipped */
+ --theme-outline-offset: -2px;
+ height: 100%;
+ margin: 0;
+}
+
+.webconsole-console-settings-menu-button::before {
+ background-image: url("chrome://devtools/skin/images/settings.svg");
+}
+
+.webconsole-app .jsterm-input-container {
+ overflow-y: auto;
+ overflow-x: hidden;
+ /* We display the open editor button at the end of the input */
+ display: grid;
+ grid-template-columns: 1fr auto;
+ /* This allows us to not define a column for the CodeMirror container */
+ grid-auto-flow: column;
+ /* This element has tabindex="-1" and can briefly show a focus outline when
+ * clicked, before we move the focus to CodeMirror. */
+ outline: none;
+}
+
+.webconsole-app:not(.jsterm-editor) .jsterm-input-container {
+ direction: ltr;
+ /* Define the border width and padding as variables so that we can keep
+ * border-top-width, padding and min-height in sync. */
+ --jsterm-border-width: 0;
+ --jsterm-padding-top: 0;
+ --jsterm-padding-bottom: 0;
+ min-height: calc(
+ var(--console-row-height) +
+ var(--jsterm-border-width) +
+ var(--jsterm-padding-top) +
+ var(--jsterm-padding-bottom)
+ );
+ padding-top: var(--jsterm-padding-top);
+ padding-bottom: var(--jsterm-padding-bottom);
+ border-top-color: var(--theme-splitter-color);
+ border-top-width: var(--jsterm-border-width);
+ border-top-style: solid;
+}
+
+.webconsole-app .webconsole-output:not(:empty) ~ .jsterm-input-container {
+ --jsterm-border-width: 1px;
+}
+
+.webconsole-app:not(.jsterm-editor, .eager-evaluation) .jsterm-input-container {
+ /* The input should be full-height when eager evaluation is disabled. */
+ flex-grow: 1;
+ --jsterm-padding-top: var(--console-input-extra-padding);
+ --jsterm-padding-bottom: var(--console-input-extra-padding);
+}
+
+.webconsole-app:not(.jsterm-editor).eager-evaluation .jsterm-input-container {
+ --jsterm-padding-top: var(--console-input-extra-padding);
+}
+
+.webconsole-input-openEditorButton {
+ /* adjust outline offset so it's not clipped */
+ --theme-outline-offset: -2px;
+ height: var(--console-row-height);
+ margin: 0;
+ padding-block: 0;
+}
+
+.webconsole-input-buttons {
+ grid-column: -1 / -2;
+ display: flex;
+ align-items: flex-start;
+}
+
+:root:dir(rtl) .webconsole-input-openEditorButton {
+ transform: scaleX(-1);
+}
+
+.webconsole-input-openEditorButton::before {
+ background-image: url("chrome://devtools/skin/images/webconsole/editor.svg");
+}
+
+.webconsole-app .reverse-search {
+ grid-area: reverse-search;
+ /* Those 2 next lines make it so the element isn't impacting the grid column size, but
+ will still take the whole available space. */
+ width: 0;
+ min-width: 100%;
+ /* Let the reverse search buttons wrap to the next line */
+ flex-wrap: wrap;
+ justify-content: end;
+}
+
+.sidebar {
+ display: grid;
+ grid-area: sidebar;
+ grid-template-rows: subgrid;
+ border-inline-start: 1px solid var(--theme-splitter-color);
+ background-color: var(--theme-sidebar-background);
+ width: 200px;
+ min-width: 150px;
+ max-width: 100%;
+}
+
+.sidebar-resizer {
+ grid-row: 1 / -1;
+ grid-column: -1 / -2;
+}
+
+.webconsole-sidebar-toolbar {
+ grid-row: 1 / 2;
+ min-height: 100%;
+ display: flex;
+ justify-content: end;
+ margin: 0;
+ padding: 0;
+}
+
+.sidebar-contents {
+ grid-row: 2 / -1;
+ overflow: auto;
+ direction: ltr;
+}
+
+.webconsole-sidebar-toolbar .sidebar-close-button {
+ margin: 0;
+}
+
+.sidebar-close-button::before {
+ background-image: url("chrome://devtools/skin/images/close.svg");
+}
+
+.sidebar-contents .object-inspector {
+ min-width: 100%;
+}
+
+/** EDITOR MODE */
+.webconsole-app.jsterm-editor {
+ display: grid;
+ /*
+ * Here's the design we want in editor mode
+ * +-----------------------------------------------------------------------+
+ * | [ChromeDebugToolbar] |
+ * +-----------------------------------------------------------------------+
+ * | [Notification Box (self XSS warning)] |
+ * +--------------------------+--------------------------+-----------------+
+ * | Editor Toolbar ↔ Filter bar primary ↔ |
+ * +--------------------------↔--------------------------↔ |
+ * | ↔ [Filter bar secondary] ↔ |
+ * | ↔--------------------------↔ |
+ * | ↔ ↔ |
+ * | Editor ↔ output ↔ [sidebar] |
+ * | ↔ ↔ |
+ * | ↔ ↔ |
+ * | ↔ ↔ |
+ * | ↔ ↔ |
+ * +--------------------------↔ ↔ |
+ * | [Eager evaluation] ↔ ↔ |
+ * +--------------------------↔ ↔ |
+ * | [Eval Notification] ↔ ↔ |
+ * +--------------------------↔ ↔ |
+ * | [Reverse search input] ↔ ↔ |
+ * +-----------------------------------------------------+-----------------+
+ *
+ * - ↔ are width resizers
+ * - Elements inside brackets may not be visible, so we set
+ * rows/columns to "auto" to make them collapse when the element
+ * they contain is hidden.
+ */
+ grid-template-areas: "chrome-debug-toolbar chrome-debug-toolbar chrome-debug-toolbar"
+ "notification notification notification"
+ "editor-toolbar filter-toolbar sidebar"
+ "editor filter-toolbar-secondary sidebar"
+ "editor output sidebar"
+ "eager-evaluation output sidebar"
+ "eval-notification output sidebar"
+ "reverse-search output sidebar";
+ grid-template-rows:
+ auto
+ auto
+ var(--primary-toolbar-height)
+ auto
+ 1fr
+ auto
+ auto
+ auto;
+ grid-template-columns: minmax(150px, auto) minmax(200px, 1fr) minmax(0, auto);
+}
+
+.jsterm-editor .flexible-output-input {
+ /* This allow us to place the div children (jsterm, output, notification) on the grid */
+ display: contents;
+}
+
+.jsterm-editor .webconsole-editor-toolbar {
+ grid-area: editor-toolbar;
+ border-inline-end: 1px solid var(--theme-splitter-color);
+ display: grid;
+ align-items: center;
+ /*
+ * The following elements are going to be present in the toolbar:
+ * - The run button
+ * - The evaluation selector button
+ * - The pretty print button
+ * - A separator
+ * - The history nav
+ * - A separator
+ * - The close button
+ *
+ * +-------------------------------------------+
+ * | ▶︎ Run Top↕ {} | ˄ ˅ 🔍 | ✕ |
+ * +-------------------------------------------+
+ *
+ */
+ grid-template-columns: auto auto 1fr auto auto auto auto auto auto auto;
+ height: unset;
+}
+
+.jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-executeButton {
+ padding-inline: 4px 8px;
+ height: 20px;
+ margin-inline-start: 5px;
+ display: flex;
+ align-items: center;
+}
+
+
+.jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-executeButton::before {
+ content: url("chrome://devtools/skin/images/webconsole/run.svg");
+ height: 16px;
+ width: 16px;
+ -moz-context-properties: fill;
+ fill: currentColor;
+ margin-inline-end: 2px;
+}
+
+.jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-prettyPrintButton {
+ grid-column: -7 / -8;
+}
+
+.jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-prettyPrintSeparator {
+ grid-column: -6 / -7;
+}
+
+.jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-history-prevExpressionButton {
+ grid-column: -5 / -6;
+}
+
+.jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-history-nextExpressionButton {
+ grid-column: -4 / -5;
+}
+
+.jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-reverseSearchButton {
+ grid-column: -3 / -4;
+}
+
+.jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-historyNavSeparator {
+ grid-column: -2 / -3;
+}
+
+.jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-closeButton {
+ grid-column: -1 / -2;
+}
+
+.jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-prettyPrintButton::before {
+ mask-image: url("chrome://devtools/content/debugger/images/prettyPrint.svg");
+ background-size: 16px;
+ background-color: var(--theme-icon-color);
+}
+
+.jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-history-prevExpressionButton::before {
+ background-image: url("chrome://devtools/skin/images/arrowhead-up.svg");
+ background-size: 16px;
+}
+
+.jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-history-nextExpressionButton::before {
+ background-image: url("chrome://devtools/skin/images/arrowhead-down.svg");
+ background-size: 16px;
+}
+
+.jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-reverseSearchButton::before {
+ background-image: url("chrome://devtools/skin/images/webconsole/reverse-search.svg");
+ background-size: 14px;
+}
+
+.jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-closeButton::before {
+ background-image: url("chrome://devtools/skin/images/close.svg");
+}
+
+.jsterm-editor .webconsole-input-openEditorButton {
+ display: none;
+}
+
+.jsterm-editor .webconsole-output {
+ grid-area: output;
+}
+
+.jsterm-editor .jsterm-input-container {
+ grid-area: editor;
+ width: 30vw;
+ /* Don't allow the input to be narrower than the grid-column it's in */
+ min-width: 100%;
+ border-top: none;
+ border-inline-end: 1px solid var(--theme-splitter-color);
+ padding: 0;
+ /* Needed as we might have the onboarding UI displayed */
+ display: flex;
+ flex-direction: column;
+ background-color: var(--theme-sidebar-background);
+}
+
+.jsterm-editor #webconsole-notificationbox {
+ grid-area: notification;
+}
+
+.jsterm-editor .jsterm-input-container > .CodeMirror {
+ flex: 1;
+ padding-inline-start: 0;
+ font-size: var(--theme-code-font-size);
+ line-height: var(--theme-code-line-height);
+ background-image: none;
+}
+
+.jsterm-editor .eager-evaluation-result {
+ grid-area: eager-evaluation;
+ /* The next 2 lines make it so the element isn't impacting the grid column size, but
+ will still take the whole available space. */
+ min-width: 100%;
+ width: 0;
+}
+
+.evaluation-notification {
+ grid-area: eval-notification;
+ /* The next 2 lines make it so the element isn't impacting the grid column size, but
+ will still take the whole available space. */
+ min-width: 100%;
+ width: 0;
+ border: 1px solid;
+ display: flex;
+ padding: 0.5em;
+}
+
+.jsterm-editor .editor-resizer {
+ grid-column: editor;
+ /* We want the splitter to cover the whole column (minus self-xss message) */
+ grid-row: editor / reverse-search;
+}
+
+.editor-onboarding {
+ display: none;
+}
+
+.jsterm-editor .editor-onboarding {
+ display: grid;
+ /**
+ * Here's the design we want:
+ * ┌──────┬────────────────────────┐
+ * │ Icon │ Onboarding text │
+ * ├──────┼────────────────────────┤
+ * │ │ Got it!│
+ * └──────┴────────────────────────┘
+ **/
+ grid-template-columns: 22px 1fr;
+ border-bottom: 1px solid var(--theme-splitter-color);
+ padding: 8px 16px;
+ background-color: var(--theme-body-alternate-emphasized-background);
+ grid-gap: 0 14px;
+ font-family: system-ui, -apple-system, sans-serif;
+ font-size: 12px;
+ line-height: 1.5;
+}
+
+.editor-onboarding-fox {
+ width: 22px;
+ height: 22px;
+ align-self: center;
+}
+
+.jsterm-editor .editor-onboarding p {
+ padding: 0;
+ margin: 0;
+}
+
+.jsterm-editor .editor-onboarding .editor-onboarding-shortcut {
+ font-weight: bold;
+}
+
+.editor-onboarding-dismiss-button {
+ grid-row: 2 / 3;
+ grid-column: 2 / 3;
+ justify-self: end;
+ padding: 2px;
+ background: transparent;
+ border: none;
+ color: var(--theme-link-color);
+ font-family: inherit;
+ cursor: pointer;
+ font-size: inherit;
+}
diff --git a/devtools/client/webconsole/components/App.js b/devtools/client/webconsole/components/App.js
new file mode 100644
index 0000000000..cabef26b1e
--- /dev/null
+++ b/devtools/client/webconsole/components/App.js
@@ -0,0 +1,513 @@
+/* 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 {
+ Component,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+loader.lazyRequireGetter(
+ this,
+ "PropTypes",
+ "resource://devtools/client/shared/vendor/react-prop-types.js"
+);
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/redux/visibility-handler-connect.js");
+
+const actions = require("resource://devtools/client/webconsole/actions/index.js");
+const {
+ FILTERBAR_DISPLAY_MODES,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+// We directly require Components that we know are going to be used right away
+const ConsoleOutput = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/ConsoleOutput.js")
+);
+const FilterBar = createFactory(
+ require("resource://devtools/client/webconsole/components/FilterBar/FilterBar.js")
+);
+const ReverseSearchInput = createFactory(
+ require("resource://devtools/client/webconsole/components/Input/ReverseSearchInput.js")
+);
+const JSTerm = createFactory(
+ require("resource://devtools/client/webconsole/components/Input/JSTerm.js")
+);
+const ConfirmDialog = createFactory(
+ require("resource://devtools/client/webconsole/components/Input/ConfirmDialog.js")
+);
+const EagerEvaluation = createFactory(
+ require("resource://devtools/client/webconsole/components/Input/EagerEvaluation.js")
+);
+const EvaluationNotification = createFactory(
+ require("resource://devtools/client/webconsole/components/Input/EvaluationNotification.js")
+);
+
+// And lazy load the ones that may not be used.
+loader.lazyGetter(this, "SideBar", () =>
+ createFactory(
+ require("resource://devtools/client/webconsole/components/SideBar.js")
+ )
+);
+
+loader.lazyGetter(this, "EditorToolbar", () =>
+ createFactory(
+ require("resource://devtools/client/webconsole/components/Input/EditorToolbar.js")
+ )
+);
+
+loader.lazyGetter(this, "NotificationBox", () =>
+ createFactory(
+ require("resource://devtools/client/shared/components/NotificationBox.js")
+ .NotificationBox
+ )
+);
+loader.lazyRequireGetter(
+ this,
+ ["getNotificationWithValue", "PriorityLevels"],
+ "resource://devtools/client/shared/components/NotificationBox.js",
+ true
+);
+
+loader.lazyGetter(this, "GridElementWidthResizer", () =>
+ createFactory(
+ require("resource://devtools/client/shared/components/splitter/GridElementWidthResizer.js")
+ )
+);
+
+loader.lazyGetter(this, "ChromeDebugToolbar", () =>
+ createFactory(
+ require("resource://devtools/client/framework/components/ChromeDebugToolbar.js")
+ )
+);
+
+const l10n = require("resource://devtools/client/webconsole/utils/l10n.js");
+const {
+ Utils: WebConsoleUtils,
+} = require("resource://devtools/client/webconsole/utils.js");
+
+const SELF_XSS_OK = l10n.getStr("selfxss.okstring");
+const SELF_XSS_MSG = l10n.getFormatStr("selfxss.msg", [SELF_XSS_OK]);
+
+const {
+ getAllNotifications,
+} = require("resource://devtools/client/webconsole/selectors/notifications.js");
+const { div } = dom;
+const isMacOS = Services.appinfo.OS === "Darwin";
+
+/**
+ * Console root Application component.
+ */
+class App extends Component {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ webConsoleUI: PropTypes.object.isRequired,
+ notifications: PropTypes.object,
+ onFirstMeaningfulPaint: PropTypes.func.isRequired,
+ serviceContainer: PropTypes.object.isRequired,
+ closeSplitConsole: PropTypes.func.isRequired,
+ autocomplete: PropTypes.bool,
+ currentReverseSearchEntry: PropTypes.string,
+ reverseSearchInputVisible: PropTypes.bool,
+ reverseSearchInitialValue: PropTypes.string,
+ editorMode: PropTypes.bool,
+ editorWidth: PropTypes.number,
+ inputEnabled: PropTypes.bool,
+ sidebarVisible: PropTypes.bool.isRequired,
+ eagerEvaluationEnabled: PropTypes.bool.isRequired,
+ filterBarDisplayMode: PropTypes.oneOf([
+ ...Object.values(FILTERBAR_DISPLAY_MODES),
+ ]).isRequired,
+ showEvaluationContextSelector: PropTypes.bool,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.onClick = this.onClick.bind(this);
+ this.onPaste = this.onPaste.bind(this);
+ this.onKeyDown = this.onKeyDown.bind(this);
+ this.onBlur = this.onBlur.bind(this);
+ }
+
+ componentDidMount() {
+ window.addEventListener("blur", this.onBlur);
+ }
+
+ onBlur() {
+ this.props.dispatch(actions.autocompleteClear());
+ }
+
+ onKeyDown(event) {
+ const { dispatch, webConsoleUI } = this.props;
+
+ if (
+ (!isMacOS && event.key === "F9") ||
+ (isMacOS && event.key === "r" && event.ctrlKey === true)
+ ) {
+ const initialValue =
+ webConsoleUI.jsterm && webConsoleUI.jsterm.getSelectedText();
+
+ dispatch(
+ actions.reverseSearchInputToggle({ initialValue, access: "keyboard" })
+ );
+ event.stopPropagation();
+ // Prevent Reader Mode to be enabled (See Bug 1682340)
+ event.preventDefault();
+ }
+
+ if (
+ event.key.toLowerCase() === "b" &&
+ ((isMacOS && event.metaKey) || (!isMacOS && event.ctrlKey))
+ ) {
+ event.stopPropagation();
+ event.preventDefault();
+ dispatch(actions.editorToggle());
+ }
+ }
+
+ onClick(event) {
+ const target = event.originalTarget || event.target;
+ const { reverseSearchInputVisible, dispatch, webConsoleUI } = this.props;
+
+ if (
+ reverseSearchInputVisible === true &&
+ !target.closest(".reverse-search")
+ ) {
+ event.preventDefault();
+ event.stopPropagation();
+ dispatch(actions.reverseSearchInputToggle());
+ return;
+ }
+
+ // Do not focus on middle/right-click or 2+ clicks.
+ if (event.detail !== 1 || event.button !== 0) {
+ return;
+ }
+
+ // Do not focus if a link was clicked
+ if (target.closest("a")) {
+ return;
+ }
+
+ // Do not focus if an input field was clicked
+ if (target.closest("input")) {
+ return;
+ }
+
+ // Do not focus if the click happened in the reverse search toolbar.
+ if (target.closest(".reverse-search")) {
+ return;
+ }
+
+ // Do not focus if something other than the output region was clicked
+ // (including e.g. the clear messages button in toolbar)
+ if (!target.closest(".webconsole-app")) {
+ return;
+ }
+
+ // Do not focus if something is selected
+ const selection = webConsoleUI.document.defaultView.getSelection();
+ if (selection && !selection.isCollapsed) {
+ return;
+ }
+
+ if (webConsoleUI?.jsterm) {
+ webConsoleUI.jsterm.focus();
+ }
+ }
+
+ onPaste(event) {
+ const { dispatch, webConsoleUI, notifications } = this.props;
+
+ const { usageCount, CONSOLE_ENTRY_THRESHOLD } = WebConsoleUtils;
+
+ // Bail out if self-xss notification is suppressed.
+ if (
+ webConsoleUI.isBrowserConsole ||
+ usageCount >= CONSOLE_ENTRY_THRESHOLD
+ ) {
+ return;
+ }
+
+ // Stop event propagation, so the clipboard content is *not* inserted.
+ event.preventDefault();
+ event.stopPropagation();
+
+ // Bail out if self-xss notification is already there.
+ if (getNotificationWithValue(notifications, "selfxss-notification")) {
+ return;
+ }
+
+ const input = event.target;
+
+ // Cleanup function if notification is closed by the user.
+ const removeCallback = eventType => {
+ if (eventType == "removed") {
+ input.removeEventListener("keyup", pasteKeyUpHandler);
+ dispatch(actions.removeNotification("selfxss-notification"));
+ }
+ };
+
+ // Create self-xss notification
+ dispatch(
+ actions.appendNotification(
+ SELF_XSS_MSG,
+ "selfxss-notification",
+ null,
+ PriorityLevels.PRIORITY_WARNING_HIGH,
+ null,
+ removeCallback
+ )
+ );
+
+ // Remove notification automatically when the user types "allow pasting".
+ const pasteKeyUpHandler = e => {
+ const { value } = e.target;
+ if (value.includes(SELF_XSS_OK)) {
+ dispatch(actions.removeNotification("selfxss-notification"));
+ input.removeEventListener("keyup", pasteKeyUpHandler);
+ WebConsoleUtils.usageCount = WebConsoleUtils.CONSOLE_ENTRY_THRESHOLD;
+ }
+ };
+
+ input.addEventListener("keyup", pasteKeyUpHandler);
+ }
+
+ renderChromeDebugToolbar() {
+ const { webConsoleUI } = this.props;
+ if (!webConsoleUI.isBrowserConsole) {
+ return null;
+ }
+ return ChromeDebugToolbar({
+ // This should always be true at this point
+ isBrowserConsole: webConsoleUI.isBrowserConsole,
+ });
+ }
+
+ renderFilterBar() {
+ const { closeSplitConsole, filterBarDisplayMode, webConsoleUI } =
+ this.props;
+
+ return FilterBar({
+ key: "filterbar",
+ closeSplitConsole,
+ displayMode: filterBarDisplayMode,
+ webConsoleUI,
+ });
+ }
+
+ renderEditorToolbar() {
+ const {
+ editorMode,
+ dispatch,
+ reverseSearchInputVisible,
+ serviceContainer,
+ webConsoleUI,
+ showEvaluationContextSelector,
+ inputEnabled,
+ } = this.props;
+
+ if (!inputEnabled) {
+ return null;
+ }
+
+ return editorMode
+ ? EditorToolbar({
+ key: "editor-toolbar",
+ editorMode,
+ dispatch,
+ reverseSearchInputVisible,
+ serviceContainer,
+ showEvaluationContextSelector,
+ webConsoleUI,
+ })
+ : null;
+ }
+
+ renderConsoleOutput() {
+ const { onFirstMeaningfulPaint, serviceContainer, editorMode } = this.props;
+
+ return ConsoleOutput({
+ key: "console-output",
+ serviceContainer,
+ onFirstMeaningfulPaint,
+ editorMode,
+ });
+ }
+
+ renderJsTerm() {
+ const {
+ webConsoleUI,
+ serviceContainer,
+ autocomplete,
+ editorMode,
+ editorWidth,
+ inputEnabled,
+ } = this.props;
+
+ return JSTerm({
+ key: "jsterm",
+ webConsoleUI,
+ serviceContainer,
+ onPaste: this.onPaste,
+ autocomplete,
+ editorMode,
+ editorWidth,
+ inputEnabled,
+ });
+ }
+
+ renderEagerEvaluation() {
+ const { eagerEvaluationEnabled, serviceContainer, inputEnabled } =
+ this.props;
+
+ if (!eagerEvaluationEnabled || !inputEnabled) {
+ return null;
+ }
+
+ return EagerEvaluation({ serviceContainer });
+ }
+
+ renderReverseSearch() {
+ const { serviceContainer, reverseSearchInitialValue } = this.props;
+
+ return ReverseSearchInput({
+ key: "reverse-search-input",
+ setInputValue: serviceContainer.setInputValue,
+ focusInput: serviceContainer.focusInput,
+ initialValue: reverseSearchInitialValue,
+ });
+ }
+
+ renderSideBar() {
+ const { serviceContainer, sidebarVisible } = this.props;
+ return sidebarVisible
+ ? SideBar({
+ key: "sidebar",
+ serviceContainer,
+ visible: sidebarVisible,
+ })
+ : null;
+ }
+
+ renderNotificationBox() {
+ const { notifications, editorMode } = this.props;
+
+ return notifications && notifications.size > 0
+ ? NotificationBox({
+ id: "webconsole-notificationbox",
+ key: "notification-box",
+ displayBorderTop: !editorMode,
+ displayBorderBottom: editorMode,
+ wrapping: true,
+ notifications,
+ })
+ : null;
+ }
+
+ renderConfirmDialog() {
+ const { webConsoleUI, serviceContainer } = this.props;
+
+ return ConfirmDialog({
+ webConsoleUI,
+ serviceContainer,
+ key: "confirm-dialog",
+ });
+ }
+
+ renderRootElement(children) {
+ const { editorMode, sidebarVisible, inputEnabled, eagerEvaluationEnabled } =
+ this.props;
+
+ const classNames = ["webconsole-app"];
+ if (sidebarVisible) {
+ classNames.push("sidebar-visible");
+ }
+ if (editorMode && inputEnabled) {
+ classNames.push("jsterm-editor");
+ }
+
+ if (eagerEvaluationEnabled && inputEnabled) {
+ classNames.push("eager-evaluation");
+ }
+
+ return div(
+ {
+ className: classNames.join(" "),
+ onKeyDown: this.onKeyDown,
+ onClick: this.onClick,
+ ref: node => {
+ this.node = node;
+ },
+ },
+ children
+ );
+ }
+
+ render() {
+ const { webConsoleUI, editorMode, dispatch, inputEnabled } = this.props;
+
+ const chromeDebugToolbar = this.renderChromeDebugToolbar();
+ const filterBar = this.renderFilterBar();
+ const editorToolbar = this.renderEditorToolbar();
+ const consoleOutput = this.renderConsoleOutput();
+ const notificationBox = this.renderNotificationBox();
+ const jsterm = this.renderJsTerm();
+ const eager = this.renderEagerEvaluation();
+ const evaluationNotification = EvaluationNotification();
+ const reverseSearch = this.renderReverseSearch();
+ const sidebar = this.renderSideBar();
+ const confirmDialog = this.renderConfirmDialog();
+
+ return this.renderRootElement([
+ chromeDebugToolbar,
+ filterBar,
+ editorToolbar,
+ dom.div(
+ { className: "flexible-output-input", key: "in-out-container" },
+ consoleOutput,
+ notificationBox,
+ jsterm,
+ eager,
+ evaluationNotification
+ ),
+ editorMode && inputEnabled
+ ? GridElementWidthResizer({
+ key: "editor-resizer",
+ enabled: editorMode,
+ position: "end",
+ className: "editor-resizer",
+ getControlledElementNode: () => webConsoleUI.jsterm.node,
+ onResizeEnd: width => dispatch(actions.setEditorWidth(width)),
+ })
+ : null,
+ reverseSearch,
+ sidebar,
+ confirmDialog,
+ ]);
+ }
+}
+
+const mapStateToProps = state => ({
+ notifications: getAllNotifications(state),
+ reverseSearchInputVisible: state.ui.reverseSearchInputVisible,
+ reverseSearchInitialValue: state.ui.reverseSearchInitialValue,
+ editorMode: state.ui.editor,
+ editorWidth: state.ui.editorWidth,
+ sidebarVisible: state.ui.sidebarVisible,
+ filterBarDisplayMode: state.ui.filterBarDisplayMode,
+ eagerEvaluationEnabled: state.prefs.eagerEvaluation,
+ autocomplete: state.prefs.autocomplete,
+ showEvaluationContextSelector: state.ui.showEvaluationContextSelector,
+});
+
+const mapDispatchToProps = dispatch => ({
+ dispatch,
+});
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(App);
diff --git a/devtools/client/webconsole/components/FilterBar/ConsoleSettings.js b/devtools/client/webconsole/components/FilterBar/ConsoleSettings.js
new file mode 100644
index 0000000000..dbff800baa
--- /dev/null
+++ b/devtools/client/webconsole/components/FilterBar/ConsoleSettings.js
@@ -0,0 +1,194 @@
+/* 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";
+
+// React & Redux
+const {
+ Component,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const actions = require("resource://devtools/client/webconsole/actions/index.js");
+const {
+ l10n,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+
+// Additional Components
+const MenuButton = createFactory(
+ require("resource://devtools/client/shared/components/menu/MenuButton.js")
+);
+
+loader.lazyGetter(this, "MenuItem", function () {
+ return createFactory(
+ require("resource://devtools/client/shared/components/menu/MenuItem.js")
+ );
+});
+
+loader.lazyGetter(this, "MenuList", function () {
+ return createFactory(
+ require("resource://devtools/client/shared/components/menu/MenuList.js")
+ );
+});
+
+class ConsoleSettings extends Component {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ eagerEvaluation: PropTypes.bool.isRequired,
+ groupWarnings: PropTypes.bool.isRequired,
+ persistLogs: PropTypes.bool.isRequired,
+ timestampsVisible: PropTypes.bool.isRequired,
+ webConsoleUI: PropTypes.object.isRequired,
+ autocomplete: PropTypes.bool.isRequired,
+ enableNetworkMonitoring: PropTypes.bool.isRequired,
+ };
+ }
+
+ renderMenuItems() {
+ const {
+ dispatch,
+ eagerEvaluation,
+ groupWarnings,
+ persistLogs,
+ timestampsVisible,
+ autocomplete,
+ webConsoleUI,
+ enableNetworkMonitoring,
+ } = this.props;
+
+ const items = [];
+
+ if (
+ !webConsoleUI.isBrowserConsole &&
+ !webConsoleUI.isBrowserToolboxConsole
+ ) {
+ // Persist Logs
+ items.push(
+ MenuItem({
+ key: "webconsole-console-settings-menu-item-persistent-logs",
+ checked: persistLogs,
+ className:
+ "menu-item webconsole-console-settings-menu-item-persistentLogs",
+ label: l10n.getStr(
+ "webconsole.console.settings.menu.item.enablePersistentLogs.label"
+ ),
+ tooltip: l10n.getStr(
+ "webconsole.console.settings.menu.item.enablePersistentLogs.tooltip"
+ ),
+ onClick: () => dispatch(actions.persistToggle()),
+ })
+ );
+ }
+
+ if (webConsoleUI.isBrowserConsole || webConsoleUI.isBrowserToolboxConsole) {
+ // Enable network monitoring
+ items.push(
+ MenuItem({
+ key: "webconsole-console-settings-menu-item-enable-network-monitoring",
+ checked: enableNetworkMonitoring,
+ className:
+ "menu-item webconsole-console-settings-menu-item-enableNetworkMonitoring",
+ label: l10n.getStr("browserconsole.enableNetworkMonitoring.label"),
+ tooltip: l10n.getStr(
+ "browserconsole.enableNetworkMonitoring.tooltip"
+ ),
+ onClick: () => dispatch(actions.networkMonitoringToggle()),
+ })
+ );
+ }
+
+ // Timestamps
+ items.push(
+ MenuItem({
+ key: "webconsole-console-settings-menu-item-timestamps",
+ checked: timestampsVisible,
+ className: "menu-item webconsole-console-settings-menu-item-timestamps",
+ label: l10n.getStr(
+ "webconsole.console.settings.menu.item.timestamps.label"
+ ),
+ tooltip: l10n.getStr(
+ "webconsole.console.settings.menu.item.timestamps.tooltip"
+ ),
+ onClick: () => dispatch(actions.timestampsToggle()),
+ })
+ );
+
+ // Warning Groups
+ items.push(
+ MenuItem({
+ key: "webconsole-console-settings-menu-item-warning-groups",
+ checked: groupWarnings,
+ className:
+ "menu-item webconsole-console-settings-menu-item-warning-groups",
+ label: l10n.getStr(
+ "webconsole.console.settings.menu.item.warningGroups.label"
+ ),
+ tooltip: l10n.getStr(
+ "webconsole.console.settings.menu.item.warningGroups.tooltip"
+ ),
+ onClick: () => dispatch(actions.warningGroupsToggle()),
+ })
+ );
+
+ // autocomplete
+ items.push(
+ MenuItem({
+ key: "webconsole-console-settings-menu-item-autocomplete",
+ checked: autocomplete,
+ className:
+ "menu-item webconsole-console-settings-menu-item-autocomplete",
+ label: l10n.getStr(
+ "webconsole.console.settings.menu.item.autocomplete.label"
+ ),
+ tooltip: l10n.getStr(
+ "webconsole.console.settings.menu.item.autocomplete.tooltip"
+ ),
+ onClick: () => dispatch(actions.autocompleteToggle()),
+ })
+ );
+
+ // Eager Evaluation
+ items.push(
+ MenuItem({
+ key: "webconsole-console-settings-menu-item-eager-evaluation",
+ checked: eagerEvaluation,
+ className:
+ "menu-item webconsole-console-settings-menu-item-eager-evaluation",
+ label: l10n.getStr(
+ "webconsole.console.settings.menu.item.instantEvaluation.label"
+ ),
+ tooltip: l10n.getStr(
+ "webconsole.console.settings.menu.item.instantEvaluation.tooltip"
+ ),
+ onClick: () => dispatch(actions.eagerEvaluationToggle()),
+ })
+ );
+
+ return MenuList({ id: "webconsole-console-settings-menu-list" }, items);
+ }
+
+ render() {
+ const { webConsoleUI } = this.props;
+ const doc = webConsoleUI.document;
+ const { toolbox } = webConsoleUI.wrapper;
+
+ return MenuButton(
+ {
+ menuId: "webconsole-console-settings-menu-button",
+ toolboxDoc: toolbox ? toolbox.doc : doc,
+ className: "devtools-button webconsole-console-settings-menu-button",
+ title: l10n.getStr("webconsole.console.settings.menu.button.tooltip"),
+ },
+ // We pass the children in a function so we don't require the MenuItem and MenuList
+ // components until we need to display them (i.e. when the button is clicked).
+ () => this.renderMenuItems()
+ );
+ }
+}
+
+module.exports = ConsoleSettings;
diff --git a/devtools/client/webconsole/components/FilterBar/FilterBar.js b/devtools/client/webconsole/components/FilterBar/FilterBar.js
new file mode 100644
index 0000000000..fa9ab15e87
--- /dev/null
+++ b/devtools/client/webconsole/components/FilterBar/FilterBar.js
@@ -0,0 +1,441 @@
+/* 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";
+
+// React & Redux
+const {
+ Component,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+// Actions
+const actions = require("resource://devtools/client/webconsole/actions/index.js");
+
+// Selectors
+const {
+ getAllFilters,
+} = require("resource://devtools/client/webconsole/selectors/filters.js");
+const {
+ getFilteredMessagesCount,
+} = require("resource://devtools/client/webconsole/selectors/messages.js");
+const {
+ getAllPrefs,
+} = require("resource://devtools/client/webconsole/selectors/prefs.js");
+const {
+ getAllUi,
+} = require("resource://devtools/client/webconsole/selectors/ui.js");
+
+// Utilities
+const {
+ l10n,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+const { PluralForm } = require("resource://devtools/shared/plural-form.js");
+
+// Constants
+const {
+ FILTERS,
+ FILTERBAR_DISPLAY_MODES,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+// Additional Components
+const FilterButton = require("resource://devtools/client/webconsole/components/FilterBar/FilterButton.js");
+const ConsoleSettings = createFactory(
+ require("resource://devtools/client/webconsole/components/FilterBar/ConsoleSettings.js")
+);
+const SearchBox = createFactory(
+ require("resource://devtools/client/shared/components/SearchBox.js")
+);
+
+loader.lazyRequireGetter(
+ this,
+ "PropTypes",
+ "resource://devtools/client/shared/vendor/react-prop-types.js"
+);
+
+const disabledCssFilterButtonTitle = l10n.getStr(
+ "webconsole.cssFilterButton.inactive.tooltip"
+);
+
+class FilterBar extends Component {
+ static get propTypes() {
+ return {
+ closeButtonVisible: PropTypes.bool,
+ closeSplitConsole: PropTypes.func,
+ dispatch: PropTypes.func.isRequired,
+ displayMode: PropTypes.oneOf([...Object.values(FILTERBAR_DISPLAY_MODES)])
+ .isRequired,
+ enableNetworkMonitoring: PropTypes.bool.isRequired,
+ filter: PropTypes.object.isRequired,
+ filteredMessagesCount: PropTypes.object.isRequired,
+ groupWarnings: PropTypes.bool.isRequired,
+ persistLogs: PropTypes.bool.isRequired,
+ eagerEvaluation: PropTypes.bool.isRequired,
+ timestampsVisible: PropTypes.bool.isRequired,
+ webConsoleUI: PropTypes.object.isRequired,
+ autocomplete: PropTypes.bool.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.renderFiltersConfigBar = this.renderFiltersConfigBar.bind(this);
+ this.maybeUpdateLayout = this.maybeUpdateLayout.bind(this);
+ this.resizeObserver = new ResizeObserver(this.maybeUpdateLayout);
+ }
+
+ componentDidMount() {
+ this.filterInputMinWidth = 150;
+ try {
+ const filterInput = this.wrapperNode.querySelector(".devtools-searchbox");
+ this.filterInputMinWidth = Number(
+ window.getComputedStyle(filterInput)["min-width"].replace("px", "")
+ );
+ } catch (e) {
+ // If the min-width of the filter input isn't set, or is set in a different unit
+ // than px.
+ console.error("min-width of the filter input couldn't be retrieved.", e);
+ }
+
+ this.maybeUpdateLayout();
+ this.resizeObserver.observe(this.wrapperNode);
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ const {
+ closeButtonVisible,
+ displayMode,
+ enableNetworkMonitoring,
+ filter,
+ filteredMessagesCount,
+ groupWarnings,
+ persistLogs,
+ timestampsVisible,
+ eagerEvaluation,
+ autocomplete,
+ } = this.props;
+
+ if (
+ nextProps.closeButtonVisible !== closeButtonVisible ||
+ nextProps.displayMode !== displayMode ||
+ nextProps.enableNetworkMonitoring !== enableNetworkMonitoring ||
+ nextProps.filter !== filter ||
+ nextProps.groupWarnings !== groupWarnings ||
+ nextProps.persistLogs !== persistLogs ||
+ nextProps.timestampsVisible !== timestampsVisible ||
+ nextProps.eagerEvaluation !== eagerEvaluation ||
+ nextProps.autocomplete !== autocomplete
+ ) {
+ return true;
+ }
+
+ if (
+ JSON.stringify(nextProps.filteredMessagesCount) !==
+ JSON.stringify(filteredMessagesCount)
+ ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Update the boolean state that informs where the filter buttons should be rendered.
+ * If the filter buttons are rendered inline with the filter input and the filter
+ * input width is reduced below a threshold, the filter buttons are rendered on a new
+ * row. When the filter buttons are on a separate row and the filter input grows
+ * wide enough to display the filter buttons without dropping below the threshold,
+ * the filter buttons are rendered inline.
+ */
+ maybeUpdateLayout() {
+ const { dispatch, displayMode } = this.props;
+
+ // If we don't have the wrapperNode reference, or if the wrapperNode isn't connected
+ // anymore, we disconnect the resize observer (componentWillUnmount is never called
+ // on this component, so we have to do it here).
+ if (!this.wrapperNode || !this.wrapperNode.isConnected) {
+ this.resizeObserver.disconnect();
+ return;
+ }
+
+ const filterInput = this.wrapperNode.querySelector(".devtools-searchbox");
+ const { width: filterInputWidth } = filterInput.getBoundingClientRect();
+
+ if (displayMode === FILTERBAR_DISPLAY_MODES.WIDE) {
+ if (filterInputWidth <= this.filterInputMinWidth) {
+ dispatch(
+ actions.filterBarDisplayModeSet(FILTERBAR_DISPLAY_MODES.NARROW)
+ );
+ }
+
+ return;
+ }
+
+ if (displayMode === FILTERBAR_DISPLAY_MODES.NARROW) {
+ const filterButtonsToolbar = this.wrapperNode.querySelector(
+ ".webconsole-filterbar-secondary"
+ );
+
+ const buttonMargin = 5;
+ const filterButtonsToolbarWidth = Array.from(
+ filterButtonsToolbar.children
+ ).reduce(
+ (width, el) => width + el.getBoundingClientRect().width + buttonMargin,
+ 0
+ );
+
+ if (
+ filterInputWidth - this.filterInputMinWidth >
+ filterButtonsToolbarWidth
+ ) {
+ dispatch(actions.filterBarDisplayModeSet(FILTERBAR_DISPLAY_MODES.WIDE));
+ }
+ }
+ }
+
+ renderSeparator() {
+ return dom.div({
+ className: "devtools-separator",
+ });
+ }
+
+ renderClearButton() {
+ return dom.button({
+ className: "devtools-button devtools-clear-icon",
+ title: l10n.getStr("webconsole.clearButton.tooltip"),
+ onClick: () => this.props.dispatch(actions.messagesClear()),
+ });
+ }
+
+ renderFiltersConfigBar() {
+ const { dispatch, filter, filteredMessagesCount } = this.props;
+
+ const getLabel = (baseLabel, filterKey) => {
+ const count = filteredMessagesCount[filterKey];
+ if (filter[filterKey] || count === 0) {
+ return baseLabel;
+ }
+ return `${baseLabel} (${count})`;
+ };
+
+ return dom.div(
+ {
+ className: "devtools-toolbar webconsole-filterbar-secondary",
+ key: "config-bar",
+ },
+ FilterButton({
+ active: filter[FILTERS.ERROR],
+ label: getLabel(
+ l10n.getStr("webconsole.errorsFilterButton.label"),
+ FILTERS.ERROR
+ ),
+ filterKey: FILTERS.ERROR,
+ dispatch,
+ }),
+ FilterButton({
+ active: filter[FILTERS.WARN],
+ label: getLabel(
+ l10n.getStr("webconsole.warningsFilterButton.label"),
+ FILTERS.WARN
+ ),
+ filterKey: FILTERS.WARN,
+ dispatch,
+ }),
+ FilterButton({
+ active: filter[FILTERS.LOG],
+ label: getLabel(
+ l10n.getStr("webconsole.logsFilterButton.label"),
+ FILTERS.LOG
+ ),
+ filterKey: FILTERS.LOG,
+ dispatch,
+ }),
+ FilterButton({
+ active: filter[FILTERS.INFO],
+ label: getLabel(
+ l10n.getStr("webconsole.infoFilterButton.label"),
+ FILTERS.INFO
+ ),
+ filterKey: FILTERS.INFO,
+ dispatch,
+ }),
+ FilterButton({
+ active: filter[FILTERS.DEBUG],
+ label: getLabel(
+ l10n.getStr("webconsole.debugFilterButton.label"),
+ FILTERS.DEBUG
+ ),
+ filterKey: FILTERS.DEBUG,
+ dispatch,
+ }),
+ dom.div({
+ className: "devtools-separator",
+ }),
+ FilterButton({
+ active: filter[FILTERS.CSS],
+ title: filter[FILTERS.CSS] ? undefined : disabledCssFilterButtonTitle,
+ label: l10n.getStr("webconsole.cssFilterButton.label"),
+ filterKey: FILTERS.CSS,
+ dispatch,
+ }),
+ FilterButton({
+ active: filter[FILTERS.NETXHR],
+ label: l10n.getStr("webconsole.xhrFilterButton.label"),
+ filterKey: FILTERS.NETXHR,
+ dispatch,
+ }),
+ FilterButton({
+ active: filter[FILTERS.NET],
+ label: l10n.getStr("webconsole.requestsFilterButton.label"),
+ filterKey: FILTERS.NET,
+ dispatch,
+ })
+ );
+ }
+
+ renderSearchBox() {
+ const { dispatch, filteredMessagesCount } = this.props;
+
+ let searchBoxSummary;
+ let searchBoxSummaryTooltip;
+ if (filteredMessagesCount.text > 0) {
+ searchBoxSummary = l10n.getStr("webconsole.filteredMessagesByText.label");
+ searchBoxSummary = PluralForm.get(
+ filteredMessagesCount.text,
+ searchBoxSummary
+ ).replace("#1", filteredMessagesCount.text);
+
+ searchBoxSummaryTooltip = l10n.getStr(
+ "webconsole.filteredMessagesByText.tooltip"
+ );
+ searchBoxSummaryTooltip = PluralForm.get(
+ filteredMessagesCount.text,
+ searchBoxSummaryTooltip
+ ).replace("#1", filteredMessagesCount.text);
+ }
+
+ return SearchBox({
+ type: "filter",
+ placeholder: l10n.getStr("webconsole.filterInput.placeholder"),
+ keyShortcut: l10n.getStr("webconsole.find.key"),
+ onChange: text => dispatch(actions.filterTextSet(text)),
+ summary: searchBoxSummary,
+ summaryTooltip: searchBoxSummaryTooltip,
+ });
+ }
+
+ renderSettingsButton() {
+ const {
+ dispatch,
+ enableNetworkMonitoring,
+ eagerEvaluation,
+ groupWarnings,
+ persistLogs,
+ timestampsVisible,
+ webConsoleUI,
+ autocomplete,
+ } = this.props;
+
+ return ConsoleSettings({
+ dispatch,
+ enableNetworkMonitoring,
+ eagerEvaluation,
+ groupWarnings,
+ persistLogs,
+ timestampsVisible,
+ webConsoleUI,
+ autocomplete,
+ });
+ }
+
+ renderCloseButton() {
+ const { closeSplitConsole } = this.props;
+
+ return dom.div(
+ {
+ className: "devtools-toolbar split-console-close-button-wrapper",
+ key: "wrapper",
+ },
+ dom.button({
+ id: "split-console-close-button",
+ key: "split-console-close-button",
+ className: "devtools-button",
+ title: l10n.getStr("webconsole.closeSplitConsoleButton.tooltip"),
+ onClick: () => {
+ closeSplitConsole();
+ },
+ })
+ );
+ }
+
+ render() {
+ const { closeButtonVisible, displayMode } = this.props;
+
+ const isNarrow = displayMode === FILTERBAR_DISPLAY_MODES.NARROW;
+ const isWide = displayMode === FILTERBAR_DISPLAY_MODES.WIDE;
+
+ const separator = this.renderSeparator();
+ const clearButton = this.renderClearButton();
+ const searchBox = this.renderSearchBox();
+ const filtersConfigBar = this.renderFiltersConfigBar();
+ const settingsButton = this.renderSettingsButton();
+
+ const children = [
+ dom.div(
+ {
+ className:
+ "devtools-toolbar devtools-input-toolbar webconsole-filterbar-primary",
+ key: "primary-bar",
+ },
+ clearButton,
+ separator,
+ searchBox,
+ isWide && separator,
+ isWide && filtersConfigBar,
+ separator,
+ settingsButton
+ ),
+ ];
+
+ if (closeButtonVisible) {
+ children.push(this.renderCloseButton());
+ }
+
+ if (isNarrow) {
+ children.push(filtersConfigBar);
+ }
+
+ return dom.div(
+ {
+ className: `webconsole-filteringbar-wrapper ${displayMode}`,
+ "aria-live": "off",
+ ref: node => {
+ this.wrapperNode = node;
+ },
+ },
+ children
+ );
+ }
+}
+
+function mapStateToProps(state) {
+ const uiState = getAllUi(state);
+ const prefsState = getAllPrefs(state);
+ return {
+ closeButtonVisible: uiState.closeButtonVisible,
+ filter: getAllFilters(state),
+ filteredMessagesCount: getFilteredMessagesCount(state),
+ groupWarnings: prefsState.groupWarnings,
+ persistLogs: uiState.persistLogs,
+ eagerEvaluation: prefsState.eagerEvaluation,
+ timestampsVisible: uiState.timestampsVisible,
+ autocomplete: prefsState.autocomplete,
+ enableNetworkMonitoring: uiState.enableNetworkMonitoring,
+ };
+}
+
+module.exports = connect(mapStateToProps)(FilterBar);
diff --git a/devtools/client/webconsole/components/FilterBar/FilterButton.js b/devtools/client/webconsole/components/FilterBar/FilterButton.js
new file mode 100644
index 0000000000..2a2ad6bf70
--- /dev/null
+++ b/devtools/client/webconsole/components/FilterBar/FilterButton.js
@@ -0,0 +1,37 @@
+/* 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 PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const actions = require("resource://devtools/client/webconsole/actions/index.js");
+
+FilterButton.displayName = "FilterButton";
+
+FilterButton.propTypes = {
+ label: PropTypes.string.isRequired,
+ filterKey: PropTypes.string.isRequired,
+ active: PropTypes.bool.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ title: PropTypes.string,
+};
+
+function FilterButton(props) {
+ const { active, label, filterKey, dispatch, title } = props;
+
+ return dom.button(
+ {
+ "aria-pressed": active === true,
+ className: "devtools-togglebutton",
+ "data-category": filterKey,
+ title,
+ onClick: () => {
+ dispatch(actions.filterToggle(filterKey));
+ },
+ },
+ label
+ );
+}
+
+module.exports = FilterButton;
diff --git a/devtools/client/webconsole/components/FilterBar/FilterCheckbox.js b/devtools/client/webconsole/components/FilterBar/FilterCheckbox.js
new file mode 100644
index 0000000000..f36788e998
--- /dev/null
+++ b/devtools/client/webconsole/components/FilterBar/FilterCheckbox.js
@@ -0,0 +1,31 @@
+/* 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 PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+FilterCheckbox.displayName = "FilterCheckbox";
+
+FilterCheckbox.propTypes = {
+ label: PropTypes.string.isRequired,
+ title: PropTypes.string,
+ checked: PropTypes.bool.isRequired,
+ onChange: PropTypes.func.isRequired,
+};
+
+function FilterCheckbox(props) {
+ const { checked, label, title, onChange } = props;
+ return dom.label(
+ { title, className: "filter-checkbox" },
+ dom.input({
+ type: "checkbox",
+ checked,
+ onChange,
+ }),
+ label
+ );
+}
+
+module.exports = FilterCheckbox;
diff --git a/devtools/client/webconsole/components/FilterBar/moz.build b/devtools/client/webconsole/components/FilterBar/moz.build
new file mode 100644
index 0000000000..46ef681317
--- /dev/null
+++ b/devtools/client/webconsole/components/FilterBar/moz.build
@@ -0,0 +1,11 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ "ConsoleSettings.js",
+ "FilterBar.js",
+ "FilterButton.js",
+ "FilterCheckbox.js",
+)
diff --git a/devtools/client/webconsole/components/Input/ConfirmDialog.js b/devtools/client/webconsole/components/Input/ConfirmDialog.js
new file mode 100644
index 0000000000..799d8d76b1
--- /dev/null
+++ b/devtools/client/webconsole/components/Input/ConfirmDialog.js
@@ -0,0 +1,197 @@
+/* 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";
+
+loader.lazyRequireGetter(
+ this,
+ "PropTypes",
+ "resource://devtools/client/shared/vendor/react-prop-types.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "HTMLTooltip",
+ "resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "createPortal",
+ "resource://devtools/client/shared/vendor/react-dom.js",
+ true
+);
+
+// React & Redux
+const {
+ Component,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const {
+ getAutocompleteState,
+} = require("resource://devtools/client/webconsole/selectors/autocomplete.js");
+const autocompleteActions = require("resource://devtools/client/webconsole/actions/autocomplete.js");
+const {
+ l10n,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+
+const LEARN_MORE_URL = `https://firefox-source-docs.mozilla.org/devtools-user/web_console/invoke_getters_from_autocomplete/`;
+
+class ConfirmDialog extends Component {
+ static get propTypes() {
+ return {
+ // Console object.
+ webConsoleUI: PropTypes.object.isRequired,
+ // Update autocomplete popup state.
+ autocompleteUpdate: PropTypes.func.isRequired,
+ autocompleteClear: PropTypes.func.isRequired,
+ // Data to be displayed in the confirm dialog.
+ getterPath: PropTypes.array,
+ serviceContainer: PropTypes.object.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ const { webConsoleUI } = props;
+ webConsoleUI.confirmDialog = this;
+
+ this.cancel = this.cancel.bind(this);
+ this.confirm = this.confirm.bind(this);
+ this.onLearnMoreClick = this.onLearnMoreClick.bind(this);
+ }
+
+ componentDidMount() {
+ const doc = this.props.webConsoleUI.document;
+ const { toolbox } = this.props.webConsoleUI.wrapper;
+ const tooltipDoc = toolbox ? toolbox.doc : doc;
+ // The popup will be attached to the toolbox document or HUD document in the case
+ // such as the browser console which doesn't have a toolbox.
+ this.tooltip = new HTMLTooltip(tooltipDoc, {
+ className: "invoke-confirm",
+ });
+ }
+
+ componentDidUpdate() {
+ const { getterPath, serviceContainer } = this.props;
+
+ if (getterPath) {
+ this.tooltip.show(serviceContainer.getJsTermTooltipAnchor(), { y: 5 });
+ } else {
+ this.tooltip.hide();
+ this.props.webConsoleUI.jsterm.focus();
+ }
+ }
+
+ componentDidThrow(e) {
+ console.error("Error in ConfirmDialog", e);
+ this.setState(state => ({ ...state, hasError: true }));
+ }
+
+ onLearnMoreClick(e) {
+ this.props.serviceContainer.openLink(LEARN_MORE_URL, e);
+ }
+
+ cancel() {
+ this.tooltip.hide();
+ this.props.autocompleteClear();
+ }
+
+ confirm() {
+ this.tooltip.hide();
+ this.props.autocompleteUpdate(this.props.getterPath);
+ }
+
+ render() {
+ if (
+ (this.state && this.state.hasError) ||
+ !this.props ||
+ !this.props.getterPath
+ ) {
+ return null;
+ }
+
+ const { getterPath } = this.props;
+ const getterName = getterPath.join(".");
+
+ // We deliberately use getStr, and not getFormatStr, because we want getterName to
+ // be wrapped in its own span.
+ const description = l10n.getStr("webconsole.confirmDialog.getter.label");
+ const [descriptionPrefix, descriptionSuffix] = description.split("%S");
+
+ const closeButtonTooltip = l10n.getFormatStr(
+ "webconsole.confirmDialog.getter.closeButton.tooltip",
+ ["Esc"]
+ );
+ const invokeButtonLabel = l10n.getFormatStr(
+ "webconsole.confirmDialog.getter.invokeButtonLabelWithShortcut",
+ ["Tab"]
+ );
+
+ const learnMoreElement = dom.a(
+ {
+ className: "learn-more-link",
+ key: "learn-more-link",
+ title: LEARN_MORE_URL.split("?")[0],
+ onClick: this.onLearnMoreClick,
+ },
+ l10n.getStr("webConsoleMoreInfoLabel")
+ );
+
+ return createPortal(
+ [
+ dom.div(
+ {
+ className: "confirm-label",
+ key: "confirm-label",
+ },
+ dom.p(
+ {},
+ dom.span({}, descriptionPrefix),
+ dom.span({ className: "emphasized" }, getterName),
+ dom.span({}, descriptionSuffix)
+ ),
+ dom.button({
+ className: "devtools-button close-confirm-dialog-button",
+ key: "close-button",
+ title: closeButtonTooltip,
+ onClick: this.cancel,
+ })
+ ),
+ dom.button(
+ {
+ className: "confirm-button",
+ key: "confirm-button",
+ onClick: this.confirm,
+ },
+ invokeButtonLabel
+ ),
+ learnMoreElement,
+ ],
+ this.tooltip.panel
+ );
+ }
+}
+
+// Redux connect
+function mapStateToProps(state) {
+ const autocompleteData = getAutocompleteState(state);
+ return {
+ getterPath: autocompleteData.getterPath,
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ autocompleteUpdate: getterPath =>
+ dispatch(autocompleteActions.autocompleteUpdate(true, getterPath)),
+ autocompleteClear: () => dispatch(autocompleteActions.autocompleteClear()),
+ };
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(ConfirmDialog);
diff --git a/devtools/client/webconsole/components/Input/EagerEvaluation.css b/devtools/client/webconsole/components/Input/EagerEvaluation.css
new file mode 100644
index 0000000000..d4deb2b8bf
--- /dev/null
+++ b/devtools/client/webconsole/components/Input/EagerEvaluation.css
@@ -0,0 +1,101 @@
+/* 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/. */
+
+.eager-evaluation-result {
+ flex: none;
+ font-family: var(--monospace-font-family);
+ font-size: var(--theme-code-font-size);
+ line-height: var(--console-output-line-height);
+}
+
+.theme-light .eager-evaluation-result {
+ --log-icon-color: var(--grey-35);
+}
+
+.theme-dark .eager-evaluation-result {
+ --log-icon-color: var(--grey-55);
+}
+
+.eager-evaluation-result__row {
+ direction: ltr;
+ display: flex;
+ align-items: center;
+ overflow-y: hidden;
+ height: var(--console-row-height);
+ padding: 0 2px;
+}
+
+.eager-evaluation-result__icon {
+ flex: none;
+ width: 14px;
+ height: 14px;
+ margin: 0 8px;
+ background: url(chrome://devtools/skin/images/webconsole/return.svg) no-repeat
+ center;
+ background-size: 12px;
+ -moz-context-properties: fill;
+ fill: var(--log-icon-color);
+}
+
+.eager-evaluation-result__text {
+ /* Override Reps variables to turn eager eval output gray */
+ filter: saturate(0%);
+
+ flex: 1 1 auto;
+ height: 14px;
+ overflow: hidden;
+ /* Use pre rather than nowrap because we want to preserve consecutive spaces,
+ * e.g. if we display "some string" we should not collapse spaces. */
+ white-space: pre;
+}
+
+/* Style the reps result */
+.eager-evaluation-result__text > * {
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.eager-evaluation-result__text * {
+ /* Some Reps elements define white-space:pre-wrap, which lets the text break
+ * to a new line */
+ white-space: inherit !important;
+}
+
+.eager-evaluation-result__text .objectBox-function .param {
+ color: var(--null-color);
+}
+
+/*
+ * Inline mode specifics
+ */
+.webconsole-app:not(.jsterm-editor) .eager-evaluation-result {
+ /* It should fill the remaining height in the output+input area */
+ flex-grow: 1;
+ background-color: var(--console-input-background);
+ /* Reserve a bit of whitespace after the content. */
+ min-height: calc(
+ var(--console-row-height) + var(--console-input-extra-padding)
+ );
+}
+
+/*
+ * Editor mode specifics
+ */
+.webconsole-app.jsterm-editor .eager-evaluation-result {
+ border-top: 1px solid var(--theme-splitter-color);
+ border-inline-end: 1px solid var(--theme-splitter-color);
+ /* Make text smaller when displayed in the sidebar */
+ font-size: 10px;
+ line-height: 14px;
+ background-color: var(--theme-sidebar-background);
+}
+
+.webconsole-app.jsterm-editor .eager-evaluation-result:empty {
+ display: none;
+}
+
+.webconsole-app.jsterm-editor .eager-evaluation-result__row {
+ height: var(--theme-toolbar-height);
+}
diff --git a/devtools/client/webconsole/components/Input/EagerEvaluation.js b/devtools/client/webconsole/components/Input/EagerEvaluation.js
new file mode 100644
index 0000000000..52d5467b45
--- /dev/null
+++ b/devtools/client/webconsole/components/Input/EagerEvaluation.js
@@ -0,0 +1,147 @@
+/* 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 {
+ Component,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const {
+ getTerminalEagerResult,
+} = require("resource://devtools/client/webconsole/selectors/history.js");
+
+const actions = require("resource://devtools/client/webconsole/actions/index.js");
+
+loader.lazyGetter(this, "REPS", function () {
+ return require("resource://devtools/client/shared/components/reps/index.js")
+ .REPS;
+});
+loader.lazyGetter(this, "MODE", function () {
+ return require("resource://devtools/client/shared/components/reps/index.js")
+ .MODE;
+});
+loader.lazyRequireGetter(
+ this,
+ "PropTypes",
+ "resource://devtools/client/shared/vendor/react-prop-types.js"
+);
+
+/**
+ * Show the results of evaluating the current terminal text, if possible.
+ */
+class EagerEvaluation extends Component {
+ static get propTypes() {
+ return {
+ terminalEagerResult: PropTypes.any,
+ hud: PropTypes.object.isRequired,
+ highlightDomElement: PropTypes.func.isRequired,
+ unHighlightDomElement: PropTypes.func.isRequired,
+ };
+ }
+
+ static getDerivedStateFromError(error) {
+ return { hasError: true };
+ }
+
+ componentDidUpdate(prevProps) {
+ const { highlightDomElement, unHighlightDomElement, terminalEagerResult } =
+ this.props;
+
+ if (canHighlightObject(prevProps.terminalEagerResult)) {
+ unHighlightDomElement(prevProps.terminalEagerResult.getGrip());
+ }
+
+ if (canHighlightObject(terminalEagerResult)) {
+ highlightDomElement(terminalEagerResult.getGrip());
+ }
+
+ if (this.state?.hasError) {
+ // If the render function threw at some point, clear the error after 1s so the
+ // component has a chance to render again.
+ // This way, we don't block instant evaluation for the whole session, in case the
+ // input changed in the meantime. If the input didn't change, we'll hit
+ // getDerivatedStateFromError again (and this won't render anything), so it's safe.
+ setTimeout(() => {
+ this.setState({ hasError: false });
+ }, 1000);
+ }
+ }
+
+ componentWillUnmount() {
+ const { unHighlightDomElement, terminalEagerResult } = this.props;
+
+ if (canHighlightObject(terminalEagerResult)) {
+ unHighlightDomElement(terminalEagerResult.getGrip());
+ }
+ }
+
+ renderRepsResult() {
+ const { terminalEagerResult } = this.props;
+
+ const result = terminalEagerResult.getGrip
+ ? terminalEagerResult.getGrip()
+ : terminalEagerResult;
+ const { isError } = result || {};
+
+ return REPS.Rep({
+ key: "rep",
+ object: result,
+ mode: isError ? MODE.SHORT : MODE.LONG,
+ });
+ }
+
+ render() {
+ const hasResult =
+ this.props.terminalEagerResult !== null &&
+ this.props.terminalEagerResult !== undefined &&
+ !this.state?.hasError;
+
+ return dom.div(
+ { className: "eager-evaluation-result", key: "eager-evaluation-result" },
+ hasResult
+ ? dom.span(
+ { className: "eager-evaluation-result__row" },
+ dom.span({
+ className: "eager-evaluation-result__icon",
+ key: "icon",
+ }),
+ dom.span(
+ { className: "eager-evaluation-result__text", key: "text" },
+ this.renderRepsResult()
+ )
+ )
+ : null
+ );
+ }
+}
+
+function canHighlightObject(obj) {
+ const grip = obj?.getGrip && obj.getGrip();
+ return (
+ grip &&
+ (REPS.ElementNode.supportsObject(grip) ||
+ REPS.TextNode.supportsObject(grip)) &&
+ grip.preview.isConnected
+ );
+}
+
+function mapStateToProps(state) {
+ return {
+ terminalEagerResult: getTerminalEagerResult(state),
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ highlightDomElement: grip => dispatch(actions.highlightDomElement(grip)),
+ unHighlightDomElement: grip =>
+ dispatch(actions.unHighlightDomElement(grip)),
+ };
+}
+module.exports = connect(mapStateToProps, mapDispatchToProps)(EagerEvaluation);
diff --git a/devtools/client/webconsole/components/Input/EditorToolbar.js b/devtools/client/webconsole/components/Input/EditorToolbar.js
new file mode 100644
index 0000000000..8fe82421eb
--- /dev/null
+++ b/devtools/client/webconsole/components/Input/EditorToolbar.js
@@ -0,0 +1,162 @@
+/* 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";
+
+// React & Redux
+const {
+ Component,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const EvaluationContextSelector = createFactory(
+ require("resource://devtools/client/webconsole/components/Input/EvaluationContextSelector.js")
+);
+
+const actions = require("resource://devtools/client/webconsole/actions/index.js");
+const {
+ l10n,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+const isMacOS = Services.appinfo.OS === "Darwin";
+
+// Constants used for defining the direction of JSTerm input history navigation.
+const {
+ HISTORY_BACK,
+ HISTORY_FORWARD,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+class EditorToolbar extends Component {
+ static get propTypes() {
+ return {
+ editorMode: PropTypes.bool,
+ dispatch: PropTypes.func.isRequired,
+ reverseSearchInputVisible: PropTypes.bool.isRequired,
+ serviceContainer: PropTypes.object.isRequired,
+ webConsoleUI: PropTypes.object.isRequired,
+ showEvaluationContextSelector: PropTypes.bool,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.onReverseSearchButtonClick =
+ this.onReverseSearchButtonClick.bind(this);
+ }
+
+ onReverseSearchButtonClick(event) {
+ const { dispatch, serviceContainer } = this.props;
+
+ event.stopPropagation();
+ dispatch(
+ actions.reverseSearchInputToggle({
+ initialValue: serviceContainer.getInputSelection(),
+ access: "editor-toolbar-icon",
+ })
+ );
+ }
+
+ renderEvaluationContextSelector() {
+ if (!this.props.showEvaluationContextSelector) {
+ return null;
+ }
+
+ return EvaluationContextSelector({
+ webConsoleUI: this.props.webConsoleUI,
+ });
+ }
+
+ render() {
+ const { editorMode, dispatch, reverseSearchInputVisible, webConsoleUI } =
+ this.props;
+
+ if (!editorMode) {
+ return null;
+ }
+
+ const enterStr = l10n.getStr("webconsole.enterKey");
+
+ return dom.div(
+ {
+ className:
+ "devtools-toolbar devtools-input-toolbar webconsole-editor-toolbar",
+ },
+ dom.button(
+ {
+ className: "devtools-button webconsole-editor-toolbar-executeButton",
+ title: l10n.getFormatStr(
+ "webconsole.editor.toolbar.executeButton.tooltip",
+ [isMacOS ? `Cmd + ${enterStr}` : `Ctrl + ${enterStr}`]
+ ),
+ onClick: () => dispatch(actions.evaluateExpression()),
+ },
+ l10n.getStr("webconsole.editor.toolbar.executeButton.label")
+ ),
+ this.renderEvaluationContextSelector(),
+ dom.button({
+ className:
+ "devtools-button webconsole-editor-toolbar-prettyPrintButton",
+ title: l10n.getStr(
+ "webconsole.editor.toolbar.prettyPrintButton.tooltip"
+ ),
+ onClick: () => dispatch(actions.prettyPrintEditor()),
+ }),
+ dom.div({
+ className:
+ "devtools-separator webconsole-editor-toolbar-prettyPrintSeparator",
+ }),
+ dom.button({
+ className:
+ "devtools-button webconsole-editor-toolbar-history-prevExpressionButton",
+ title: l10n.getStr(
+ "webconsole.editor.toolbar.history.prevExpressionButton.tooltip"
+ ),
+ onClick: () => {
+ webConsoleUI.jsterm.historyPeruse(HISTORY_BACK);
+ },
+ }),
+ dom.button({
+ className:
+ "devtools-button webconsole-editor-toolbar-history-nextExpressionButton",
+ title: l10n.getStr(
+ "webconsole.editor.toolbar.history.nextExpressionButton.tooltip"
+ ),
+ onClick: () => {
+ webConsoleUI.jsterm.historyPeruse(HISTORY_FORWARD);
+ },
+ }),
+ dom.button({
+ className: `devtools-button webconsole-editor-toolbar-reverseSearchButton ${
+ reverseSearchInputVisible ? "checked" : ""
+ }`,
+ title: reverseSearchInputVisible
+ ? l10n.getFormatStr(
+ "webconsole.editor.toolbar.reverseSearchButton.closeReverseSearch.tooltip",
+ ["Esc" + (isMacOS ? " | Ctrl + C" : "")]
+ )
+ : l10n.getFormatStr(
+ "webconsole.editor.toolbar.reverseSearchButton.openReverseSearch.tooltip",
+ [isMacOS ? "Ctrl + R" : "F9"]
+ ),
+ onClick: this.onReverseSearchButtonClick,
+ }),
+ dom.div({
+ className:
+ "devtools-separator webconsole-editor-toolbar-historyNavSeparator",
+ }),
+ dom.button({
+ className: "devtools-button webconsole-editor-toolbar-closeButton",
+ title: l10n.getFormatStr(
+ "webconsole.editor.toolbar.closeButton.tooltip2",
+ [isMacOS ? "Cmd + B" : "Ctrl + B"]
+ ),
+ onClick: () => dispatch(actions.editorToggle()),
+ })
+ );
+ }
+}
+
+module.exports = EditorToolbar;
diff --git a/devtools/client/webconsole/components/Input/EvaluationContextSelector.css b/devtools/client/webconsole/components/Input/EvaluationContextSelector.css
new file mode 100644
index 0000000000..27b244feae
--- /dev/null
+++ b/devtools/client/webconsole/components/Input/EvaluationContextSelector.css
@@ -0,0 +1,33 @@
+/* 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/. */
+
+.webconsole-evaluation-selector-button {
+ padding: 1px 16px 1px 8px !important;
+ margin-top: 2px;
+ background-position-x: right 4px !important;
+ max-width: 150px;
+}
+
+/* This overrides the .devtools-dropdown-button:dir(rtl) rule from toolbars.css */
+html[dir="rtl"] .webconsole-evaluation-selector-button {
+ background-position-x: right 4px !important;
+}
+
+.jsterm-editor .webconsole-editor-toolbar .webconsole-evaluation-selector-button {
+ height: 20px;
+ margin-inline-start: 5px;
+ margin-top: 1px;
+}
+
+.webconsole-evaluation-selector-button.checked.devtools-dropdown-button {
+ background-color: var(--blue-60);
+ color: white;
+ fill: currentColor;
+}
+
+.webconsole-evaluation-selector-button.checked.devtools-dropdown-button:hover,
+.webconsole-evaluation-selector-button.checked.devtools-dropdown-button[aria-expanded="true"] {
+ background-color: var(--blue-70) !important;
+ color: white !important;
+}
diff --git a/devtools/client/webconsole/components/Input/EvaluationContextSelector.js b/devtools/client/webconsole/components/Input/EvaluationContextSelector.js
new file mode 100644
index 0000000000..3842c0e7db
--- /dev/null
+++ b/devtools/client/webconsole/components/Input/EvaluationContextSelector.js
@@ -0,0 +1,290 @@
+/* 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";
+
+// React & Redux
+const {
+ Component,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const targetActions = require("resource://devtools/shared/commands/target/actions/targets.js");
+const webconsoleActions = require("resource://devtools/client/webconsole/actions/index.js");
+
+const {
+ l10n,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+const targetSelectors = require("resource://devtools/shared/commands/target/selectors/targets.js");
+
+loader.lazyGetter(this, "TARGET_TYPES", function () {
+ return require("resource://devtools/shared/commands/target/target-command.js")
+ .TYPES;
+});
+
+// Additional Components
+const MenuButton = createFactory(
+ require("resource://devtools/client/shared/components/menu/MenuButton.js")
+);
+
+loader.lazyGetter(this, "MenuItem", function () {
+ return createFactory(
+ require("resource://devtools/client/shared/components/menu/MenuItem.js")
+ );
+});
+
+loader.lazyGetter(this, "MenuList", function () {
+ return createFactory(
+ require("resource://devtools/client/shared/components/menu/MenuList.js")
+ );
+});
+
+class EvaluationContextSelector extends Component {
+ static get propTypes() {
+ return {
+ selectTarget: PropTypes.func.isRequired,
+ onContextChange: PropTypes.func.isRequired,
+ selectedTarget: PropTypes.object,
+ lastTargetRefresh: PropTypes.number,
+ targets: PropTypes.array,
+ webConsoleUI: PropTypes.object.isRequired,
+ };
+ }
+
+ shouldComponentUpdate(nextProps) {
+ if (this.props.selectedTarget !== nextProps.selectedTarget) {
+ return true;
+ }
+
+ if (this.props.lastTargetRefresh !== nextProps.lastTargetRefresh) {
+ return true;
+ }
+
+ if (this.props.targets.length !== nextProps.targets.length) {
+ return true;
+ }
+
+ for (let i = 0; i < nextProps.targets.length; i++) {
+ const target = this.props.targets[i];
+ const nextTarget = nextProps.targets[i];
+ if (target.url != nextTarget.url || target.name != nextTarget.name) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ componentDidUpdate(prevProps) {
+ if (this.props.selectedTarget !== prevProps.selectedTarget) {
+ this.props.onContextChange();
+ }
+ }
+
+ getIcon(target) {
+ if (target.targetType === TARGET_TYPES.FRAME) {
+ return "chrome://devtools/content/debugger/images/globe-small.svg";
+ }
+
+ if (
+ target.targetType === TARGET_TYPES.WORKER ||
+ target.targetType === TARGET_TYPES.SHARED_WORKER ||
+ target.targetType === TARGET_TYPES.SERVICE_WORKER
+ ) {
+ return "chrome://devtools/content/debugger/images/worker.svg";
+ }
+
+ if (target.targetType === TARGET_TYPES.PROCESS) {
+ return "chrome://devtools/content/debugger/images/window.svg";
+ }
+
+ return null;
+ }
+
+ renderMenuItem(target) {
+ const { selectTarget, selectedTarget } = this.props;
+
+ const label = target.isTopLevel
+ ? l10n.getStr("webconsole.input.selector.top")
+ : target.name;
+
+ return MenuItem({
+ key: `webconsole-evaluation-selector-item-${target.actorID}`,
+ className: "menu-item webconsole-evaluation-selector-item",
+ type: "checkbox",
+ checked: selectedTarget ? selectedTarget == target : target.isTopLevel,
+ label,
+ tooltip: target.url || target.name,
+ icon: this.getIcon(target),
+ onClick: () => selectTarget(target.actorID),
+ });
+ }
+
+ renderMenuItems() {
+ const { targets } = this.props;
+
+ // Let's sort the targets (using "numeric" so Content processes are ordered by PID).
+ const collator = new Intl.Collator("en", { numeric: true });
+ targets.sort((a, b) => collator.compare(a.name, b.name));
+
+ let mainTarget;
+ const sections = {
+ [TARGET_TYPES.FRAME]: [],
+ [TARGET_TYPES.WORKER]: [],
+ [TARGET_TYPES.SHARED_WORKER]: [],
+ [TARGET_TYPES.SERVICE_WORKER]: [],
+ };
+ // When in Browser Toolbox, we want to display the process targets with the frames
+ // in the same process as a group
+ // e.g.
+ // |------------------------------|
+ // | Top |
+ // | -----------------------------|
+ // | (pid 1234) priviledgedabout |
+ // | New Tab |
+ // | -----------------------------|
+ // | (pid 5678) web |
+ // | cnn.com |
+ // | -----------------------------|
+ // | RemoteSettingWorker.js |
+ // |------------------------------|
+ //
+ // This object will be keyed by PID, and each property will be an object with a
+ // `process` property (for the process target item), and a `frames` property (and array
+ // for all the frame target items).
+ const processes = {};
+
+ const { webConsoleUI } = this.props;
+ const handleProcessTargets =
+ webConsoleUI.isBrowserConsole || webConsoleUI.isBrowserToolboxConsole;
+
+ for (const target of targets) {
+ const menuItem = this.renderMenuItem(target);
+
+ if (target.isTopLevel) {
+ mainTarget = menuItem;
+ } else if (target.targetType == TARGET_TYPES.PROCESS) {
+ if (!processes[target.processID]) {
+ processes[target.processID] = { frames: [] };
+ }
+ processes[target.processID].process = menuItem;
+ } else if (
+ target.targetType == TARGET_TYPES.FRAME &&
+ handleProcessTargets &&
+ target.processID
+ ) {
+ // The associated process target might not have been handled yet, so make sure
+ // to create it.
+ if (!processes[target.processID]) {
+ processes[target.processID] = { frames: [] };
+ }
+ processes[target.processID].frames.push(menuItem);
+ } else {
+ sections[target.targetType].push(menuItem);
+ }
+ }
+
+ // Note that while debugging popups, we might have a small period
+ // of time where we don't have any top level target when we reload
+ // the original tab
+ const items = mainTarget ? [mainTarget] : [];
+
+ // Handle PROCESS targets sections first, as we want to display the associated frames
+ // below the process to group them.
+ if (processes) {
+ for (const [pid, { process, frames }] of Object.entries(processes)) {
+ items.push(dom.hr({ role: "menuseparator", key: `${pid}-separator` }));
+ if (process) {
+ items.push(process);
+ }
+ if (frames) {
+ items.push(...frames);
+ }
+ }
+ }
+
+ for (const [targetType, menuItems] of Object.entries(sections)) {
+ if (menuItems.length) {
+ items.push(
+ dom.hr({ role: "menuseparator", key: `${targetType}-separator` }),
+ ...menuItems
+ );
+ }
+ }
+
+ return MenuList(
+ { id: "webconsole-console-evaluation-context-selector-menu-list" },
+ items
+ );
+ }
+
+ getLabel() {
+ const { selectedTarget } = this.props;
+
+ if (!selectedTarget || selectedTarget.isTopLevel) {
+ return l10n.getStr("webconsole.input.selector.top");
+ }
+
+ return selectedTarget.name;
+ }
+
+ render() {
+ const { webConsoleUI, targets, selectedTarget } = this.props;
+
+ // Don't render if there's only one target.
+ // Also bail out if the console is being destroyed (where WebConsoleUI.wrapper gets
+ // nullified).
+ if (targets.length <= 1 || !webConsoleUI.wrapper) {
+ return null;
+ }
+
+ const doc = webConsoleUI.document;
+ const { toolbox } = webConsoleUI.wrapper;
+
+ return MenuButton(
+ {
+ menuId: "webconsole-input-evaluationsButton",
+ toolboxDoc: toolbox ? toolbox.doc : doc,
+ label: this.getLabel(),
+ className:
+ "webconsole-evaluation-selector-button devtools-button devtools-dropdown-button" +
+ (selectedTarget && !selectedTarget.isTopLevel ? " checked" : ""),
+ title: l10n.getStr("webconsole.input.selector.tooltip"),
+ },
+ // We pass the children in a function so we don't require the MenuItem and MenuList
+ // components until we need to display them (i.e. when the button is clicked).
+ () => this.renderMenuItems()
+ );
+ }
+}
+
+const toolboxConnected = connect(
+ state => ({
+ targets: targetSelectors.getToolboxTargets(state),
+ selectedTarget: targetSelectors.getSelectedTarget(state),
+ lastTargetRefresh: targetSelectors.getLastTargetRefresh(state),
+ }),
+ dispatch => ({
+ selectTarget: actorID => dispatch(targetActions.selectTarget(actorID)),
+ }),
+ undefined,
+ { storeKey: "target-store" }
+)(EvaluationContextSelector);
+
+module.exports = connect(
+ state => state,
+ dispatch => ({
+ onContextChange: () => {
+ dispatch(
+ webconsoleActions.updateInstantEvaluationResultForCurrentExpression()
+ );
+ dispatch(webconsoleActions.autocompleteClear());
+ },
+ })
+)(toolboxConnected);
diff --git a/devtools/client/webconsole/components/Input/EvaluationNotification.css b/devtools/client/webconsole/components/Input/EvaluationNotification.css
new file mode 100644
index 0000000000..2e387bda7f
--- /dev/null
+++ b/devtools/client/webconsole/components/Input/EvaluationNotification.css
@@ -0,0 +1,32 @@
+/* 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/. */
+
+.evaluation-notification.warning {
+ color: var(--console-warning-color);
+ border-color: var(--console-warning-border);
+ background-color: var(--console-warning-background)
+}
+
+.evaluation-notification.warning .evaluation-notification__icon {
+ color: var(--theme-icon-warning-color);
+ background-image: url(chrome://devtools/content/debugger/images/sourcemap.svg);
+}
+
+.evaluation-notification__icon {
+ flex: none;
+ align-self: flex-start;
+ width: 16px;
+ height: 16px;
+ margin: var(--console-output-vertical-padding) 5px var(--console-output-vertical-padding) 0;
+ background-image: none;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 16px;
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
+
+.evaluation-notification__text {
+ margin: var(--console-output-vertical-padding) 0;
+}
diff --git a/devtools/client/webconsole/components/Input/EvaluationNotification.js b/devtools/client/webconsole/components/Input/EvaluationNotification.js
new file mode 100644
index 0000000000..3f09fc72cd
--- /dev/null
+++ b/devtools/client/webconsole/components/Input/EvaluationNotification.js
@@ -0,0 +1,63 @@
+/* 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 {
+ Component,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const {
+ getAllUi,
+} = require("resource://devtools/client/webconsole/selectors/ui.js");
+
+const {
+ ORIGINAL_VARIABLE_MAPPING,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+loader.lazyRequireGetter(
+ this,
+ "PropTypes",
+ "resource://devtools/client/shared/vendor/react-prop-types.js"
+);
+
+const l10n = require("resource://devtools/client/webconsole/utils/l10n.js");
+
+/**
+ * Show the results of evaluating the current terminal text, if possible.
+ */
+class EvaluationNotification extends Component {
+ static get propTypes() {
+ return {
+ notification: PropTypes.string,
+ };
+ }
+
+ render() {
+ const { notification } = this.props;
+ if (notification == ORIGINAL_VARIABLE_MAPPING) {
+ return dom.span(
+ { className: "evaluation-notification warning" },
+ dom.span({ className: "evaluation-notification__icon" }),
+ dom.span(
+ { className: "evaluation-notification__text" },
+ l10n.getStr("evaluationNotifcation.noOriginalVariableMapping.msg")
+ )
+ );
+ }
+ return null;
+ }
+}
+
+function mapStateToProps(state) {
+ return {
+ notification: getAllUi(state).notification,
+ };
+}
+
+module.exports = connect(mapStateToProps, null)(EvaluationNotification);
diff --git a/devtools/client/webconsole/components/Input/JSTerm.js b/devtools/client/webconsole/components/Input/JSTerm.js
new file mode 100644
index 0000000000..f00ddd66b0
--- /dev/null
+++ b/devtools/client/webconsole/components/Input/JSTerm.js
@@ -0,0 +1,1605 @@
+/* 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 { debounce } = require("resource://devtools/shared/debounce.js");
+const isMacOS = Services.appinfo.OS === "Darwin";
+
+loader.lazyRequireGetter(this, "Debugger", "Debugger");
+loader.lazyRequireGetter(
+ this,
+ "EventEmitter",
+ "resource://devtools/shared/event-emitter.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "AutocompletePopup",
+ "resource://devtools/client/shared/autocomplete-popup.js"
+);
+
+loader.lazyRequireGetter(
+ this,
+ "PropTypes",
+ "resource://devtools/client/shared/vendor/react-prop-types.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "KeyCodes",
+ "resource://devtools/client/shared/keycodes.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "Editor",
+ "resource://devtools/client/shared/sourceeditor/editor.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "getFocusableElements",
+ "resource://devtools/client/shared/focus.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "l10n",
+ "resource://devtools/client/webconsole/utils/messages.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "saveAs",
+ "resource://devtools/shared/DevToolsUtils.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "beautify",
+ "resource://devtools/shared/jsbeautify/beautify.js"
+);
+
+// React & Redux
+const {
+ Component,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+// History Modules
+const {
+ getHistory,
+ getHistoryValue,
+} = require("resource://devtools/client/webconsole/selectors/history.js");
+const {
+ getAutocompleteState,
+} = require("resource://devtools/client/webconsole/selectors/autocomplete.js");
+const actions = require("resource://devtools/client/webconsole/actions/index.js");
+
+const EvaluationContextSelector = createFactory(
+ require("resource://devtools/client/webconsole/components/Input/EvaluationContextSelector.js")
+);
+
+// Constants used for defining the direction of JSTerm input history navigation.
+const {
+ HISTORY_BACK,
+ HISTORY_FORWARD,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+const JSTERM_CODEMIRROR_ORIGIN = "jsterm";
+
+/**
+ * Create a JSTerminal (a JavaScript command line). This is attached to an
+ * existing HeadsUpDisplay (a Web Console instance). This code is responsible
+ * with handling command line input and code evaluation.
+ */
+class JSTerm extends Component {
+ static get propTypes() {
+ return {
+ // Returns previous or next value from the history
+ // (depending on direction argument).
+ getValueFromHistory: PropTypes.func.isRequired,
+ // History of executed expression (state).
+ history: PropTypes.object.isRequired,
+ // Console object.
+ webConsoleUI: PropTypes.object.isRequired,
+ // Needed for opening context menu
+ serviceContainer: PropTypes.object.isRequired,
+ // Handler for clipboard 'paste' event (also used for 'drop' event, callback).
+ onPaste: PropTypes.func,
+ // Evaluate provided expression.
+ evaluateExpression: PropTypes.func.isRequired,
+ // Update position in the history after executing an expression (action).
+ updateHistoryPosition: PropTypes.func.isRequired,
+ // Update autocomplete popup state.
+ autocompleteUpdate: PropTypes.func.isRequired,
+ autocompleteClear: PropTypes.func.isRequired,
+ // Data to be displayed in the autocomplete popup.
+ autocompleteData: PropTypes.object.isRequired,
+ // Toggle the editor mode.
+ editorToggle: PropTypes.func.isRequired,
+ // Dismiss the editor onboarding UI.
+ editorOnboardingDismiss: PropTypes.func.isRequired,
+ // Set the last JS input value.
+ terminalInputChanged: PropTypes.func.isRequired,
+ // Is the input in editor mode.
+ editorMode: PropTypes.bool,
+ editorWidth: PropTypes.number,
+ editorPrettifiedAt: PropTypes.number,
+ showEditorOnboarding: PropTypes.bool,
+ autocomplete: PropTypes.bool,
+ showEvaluationContextSelector: PropTypes.bool,
+ autocompletePopupPosition: PropTypes.string,
+ inputEnabled: PropTypes.bool,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ const { webConsoleUI } = props;
+
+ this.webConsoleUI = webConsoleUI;
+ this.hudId = this.webConsoleUI.hudId;
+
+ this._onEditorChanges = this._onEditorChanges.bind(this);
+ this._onEditorBeforeChange = this._onEditorBeforeChange.bind(this);
+ this._onEditorKeyHandled = this._onEditorKeyHandled.bind(this);
+ this.onContextMenu = this.onContextMenu.bind(this);
+ this.imperativeUpdate = this.imperativeUpdate.bind(this);
+
+ // We debounce the autocompleteUpdate so we don't send too many requests to the server
+ // as the user is typing.
+ // The delay should be small enough to be unnoticed by the user.
+ this.autocompleteUpdate = debounce(this.props.autocompleteUpdate, 75, this);
+
+ // Updates to the terminal input which can trigger eager evaluations are
+ // similarly debounced.
+ this.terminalInputChanged = debounce(
+ this.props.terminalInputChanged,
+ 75,
+ this
+ );
+
+ // Because the autocomplete has a slight delay (75ms), there can be time where the
+ // codeMirror completion text is out-of-date, which might lead to issue when the user
+ // accept the autocompletion while the update of the completion text is still pending.
+ // In order to account for that, we put any future value of the completion text in
+ // this property.
+ this.pendingCompletionText = null;
+
+ /**
+ * Last input value.
+ * @type string
+ */
+ this.lastInputValue = "";
+
+ this.autocompletePopup = null;
+
+ EventEmitter.decorate(this);
+ webConsoleUI.jsterm = this;
+ }
+
+ componentDidMount() {
+ if (this.props.editorMode) {
+ this.setEditorWidth(this.props.editorWidth);
+ }
+
+ const autocompleteOptions = {
+ onSelect: this.onAutocompleteSelect.bind(this),
+ onClick: this.acceptProposedCompletion.bind(this),
+ listId: "webConsole_autocompletePopupListBox",
+ position: this.props.autocompletePopupPosition,
+ autoSelect: true,
+ useXulWrapper: true,
+ };
+
+ const doc = this.webConsoleUI.document;
+ const { toolbox } = this.webConsoleUI.wrapper;
+ const tooltipDoc = toolbox ? toolbox.doc : doc;
+ // The popup will be attached to the toolbox document or HUD document in the case
+ // such as the browser console which doesn't have a toolbox.
+ this.autocompletePopup = new AutocompletePopup(
+ tooltipDoc,
+ autocompleteOptions
+ );
+
+ if (this.node) {
+ const onArrowUp = () => {
+ let inputUpdated;
+ if (this.autocompletePopup.isOpen) {
+ this.autocompletePopup.selectPreviousItem();
+ return null;
+ }
+
+ if (this.props.editorMode === false && this.canCaretGoPrevious()) {
+ inputUpdated = this.historyPeruse(HISTORY_BACK);
+ }
+
+ return inputUpdated ? null : "CodeMirror.Pass";
+ };
+
+ const onArrowDown = () => {
+ let inputUpdated;
+ if (this.autocompletePopup.isOpen) {
+ this.autocompletePopup.selectNextItem();
+ return null;
+ }
+
+ if (this.props.editorMode === false && this.canCaretGoNext()) {
+ inputUpdated = this.historyPeruse(HISTORY_FORWARD);
+ }
+
+ return inputUpdated ? null : "CodeMirror.Pass";
+ };
+
+ const onArrowLeft = () => {
+ if (this.autocompletePopup.isOpen || this.getAutoCompletionText()) {
+ this.clearCompletion();
+ }
+ return "CodeMirror.Pass";
+ };
+
+ const onArrowRight = () => {
+ // We only want to complete on Right arrow if the completion text is
+ // displayed.
+ if (this.getAutoCompletionText()) {
+ this.acceptProposedCompletion();
+ return null;
+ }
+
+ this.clearCompletion();
+ return "CodeMirror.Pass";
+ };
+
+ const onCtrlCmdEnter = () => {
+ if (this.hasAutocompletionSuggestion()) {
+ return this.acceptProposedCompletion();
+ }
+
+ this._execute();
+ return null;
+ };
+
+ this.editor = new Editor({
+ autofocus: true,
+ enableCodeFolding: this.props.editorMode,
+ lineNumbers: this.props.editorMode,
+ lineWrapping: true,
+ mode: {
+ name: "javascript",
+ globalVars: true,
+ },
+ styleActiveLine: false,
+ tabIndex: "0",
+ viewportMargin: Infinity,
+ disableSearchAddon: true,
+ extraKeys: {
+ Enter: () => {
+ // No need to handle shift + Enter as it's natively handled by CodeMirror.
+
+ const hasSuggestion = this.hasAutocompletionSuggestion();
+ if (
+ !hasSuggestion &&
+ !Debugger.isCompilableUnit(this._getValue())
+ ) {
+ // incomplete statement
+ return "CodeMirror.Pass";
+ }
+
+ if (hasSuggestion) {
+ return this.acceptProposedCompletion();
+ }
+
+ if (!this.props.editorMode) {
+ this._execute();
+ return null;
+ }
+ return "CodeMirror.Pass";
+ },
+
+ "Cmd-Enter": onCtrlCmdEnter,
+ "Ctrl-Enter": onCtrlCmdEnter,
+
+ [Editor.accel("S")]: () => {
+ const value = this._getValue();
+ if (!value) {
+ return null;
+ }
+
+ const date = new Date();
+ const suggestedName =
+ `console-input-${date.getFullYear()}-` +
+ `${date.getMonth() + 1}-${date.getDate()}_${date.getHours()}-` +
+ `${date.getMinutes()}-${date.getSeconds()}.js`;
+ const data = new TextEncoder().encode(value);
+ return saveAs(window, data, suggestedName, [
+ {
+ pattern: "*.js",
+ label: l10n.getStr("webconsole.input.openJavaScriptFileFilter"),
+ },
+ ]);
+ },
+
+ [Editor.accel("O")]: async () => this._openFile(),
+
+ Tab: () => {
+ if (this.hasEmptyInput()) {
+ this.editor.codeMirror.getInputField().blur();
+ return false;
+ }
+
+ if (
+ this.props.autocompleteData &&
+ this.props.autocompleteData.getterPath
+ ) {
+ this.props.autocompleteUpdate(
+ true,
+ this.props.autocompleteData.getterPath
+ );
+ return false;
+ }
+
+ const isSomethingSelected = this.editor.somethingSelected();
+ const hasSuggestion = this.hasAutocompletionSuggestion();
+
+ if (hasSuggestion && !isSomethingSelected) {
+ this.acceptProposedCompletion();
+ return false;
+ }
+
+ if (!isSomethingSelected) {
+ this.insertStringAtCursor("\t");
+ return false;
+ }
+
+ // Something is selected, let the editor handle the indent.
+ return true;
+ },
+
+ "Shift-Tab": () => {
+ if (this.hasEmptyInput()) {
+ this.focusPreviousElement();
+ return false;
+ }
+
+ const hasSuggestion = this.hasAutocompletionSuggestion();
+
+ if (hasSuggestion) {
+ return false;
+ }
+
+ return "CodeMirror.Pass";
+ },
+
+ Up: onArrowUp,
+ "Cmd-Up": onArrowUp,
+
+ Down: onArrowDown,
+ "Cmd-Down": onArrowDown,
+
+ Left: onArrowLeft,
+ "Ctrl-Left": onArrowLeft,
+ "Cmd-Left": onArrowLeft,
+ "Alt-Left": onArrowLeft,
+ // On OSX, Ctrl-A navigates to the beginning of the line.
+ "Ctrl-A": isMacOS ? onArrowLeft : undefined,
+
+ Right: onArrowRight,
+ "Ctrl-Right": onArrowRight,
+ "Cmd-Right": onArrowRight,
+ "Alt-Right": onArrowRight,
+
+ "Ctrl-N": () => {
+ // Control-N differs from down arrow: it ignores autocomplete state.
+ // Note that we preserve the default 'down' navigation within
+ // multiline text.
+ if (
+ Services.appinfo.OS === "Darwin" &&
+ this.props.editorMode === false &&
+ this.canCaretGoNext() &&
+ this.historyPeruse(HISTORY_FORWARD)
+ ) {
+ return null;
+ }
+
+ this.clearCompletion();
+ return "CodeMirror.Pass";
+ },
+
+ "Ctrl-P": () => {
+ // Control-P differs from up arrow: it ignores autocomplete state.
+ // Note that we preserve the default 'up' navigation within
+ // multiline text.
+ if (
+ Services.appinfo.OS === "Darwin" &&
+ this.props.editorMode === false &&
+ this.canCaretGoPrevious() &&
+ this.historyPeruse(HISTORY_BACK)
+ ) {
+ return null;
+ }
+
+ this.clearCompletion();
+ return "CodeMirror.Pass";
+ },
+
+ PageUp: () => {
+ if (this.autocompletePopup.isOpen) {
+ this.autocompletePopup.selectPreviousPageItem();
+ } else {
+ const { outputScroller } = this.webConsoleUI;
+ const { scrollTop, clientHeight } = outputScroller;
+ outputScroller.scrollTop = Math.max(0, scrollTop - clientHeight);
+ }
+
+ return null;
+ },
+
+ PageDown: () => {
+ if (this.autocompletePopup.isOpen) {
+ this.autocompletePopup.selectNextPageItem();
+ } else {
+ const { outputScroller } = this.webConsoleUI;
+ const { scrollTop, scrollHeight, clientHeight } = outputScroller;
+ outputScroller.scrollTop = Math.min(
+ scrollHeight,
+ scrollTop + clientHeight
+ );
+ }
+
+ return null;
+ },
+
+ Home: () => {
+ if (this.autocompletePopup.isOpen) {
+ this.autocompletePopup.selectItemAtIndex(0);
+ return null;
+ }
+
+ if (!this._getValue()) {
+ this.webConsoleUI.outputScroller.scrollTop = 0;
+ return null;
+ }
+
+ if (this.getAutoCompletionText()) {
+ this.clearCompletion();
+ }
+
+ return "CodeMirror.Pass";
+ },
+
+ End: () => {
+ if (this.autocompletePopup.isOpen) {
+ this.autocompletePopup.selectItemAtIndex(
+ this.autocompletePopup.itemCount - 1
+ );
+ return null;
+ }
+
+ if (!this._getValue()) {
+ const { outputScroller } = this.webConsoleUI;
+ outputScroller.scrollTop = outputScroller.scrollHeight;
+ return null;
+ }
+
+ if (this.getAutoCompletionText()) {
+ this.clearCompletion();
+ }
+
+ return "CodeMirror.Pass";
+ },
+
+ "Ctrl-Space": () => {
+ if (!this.autocompletePopup.isOpen) {
+ this.props.autocompleteUpdate(
+ true,
+ null,
+ this._getExpressionVariables()
+ );
+ return null;
+ }
+
+ return "CodeMirror.Pass";
+ },
+
+ Esc: false,
+ // Don't handle Ctrl/Cmd + F so it can be listened by a parent node
+ [Editor.accel("F")]: false,
+ },
+ });
+
+ this.editor.on("changes", this._onEditorChanges);
+ this.editor.on("beforeChange", this._onEditorBeforeChange);
+ this.editor.on("blur", this._onEditorBlur);
+ this.editor.on("keyHandled", this._onEditorKeyHandled);
+
+ this.editor.appendToLocalElement(this.node);
+ const cm = this.editor.codeMirror;
+ cm.on("paste", (_, event) => this.props.onPaste(event));
+ cm.on("drop", (_, event) => this.props.onPaste(event));
+
+ this.node.addEventListener("keydown", event => {
+ if (event.keyCode === KeyCodes.DOM_VK_ESCAPE) {
+ if (this.autocompletePopup.isOpen) {
+ this.clearCompletion();
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ if (
+ this.props.autocompleteData &&
+ this.props.autocompleteData.getterPath
+ ) {
+ this.props.autocompleteClear();
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+ });
+
+ this.resizeObserver = new ResizeObserver(() => {
+ // If we don't have the node reference, or if the node isn't connected
+ // anymore, we disconnect the resize observer (componentWillUnmount is never
+ // called on this component, so we have to do it here).
+ if (!this.node || !this.node.isConnected) {
+ this.resizeObserver.disconnect();
+ return;
+ }
+ // Calling `refresh` will update the cursor position, and all the selection blocks.
+ this.editor.codeMirror.refresh();
+ });
+ this.resizeObserver.observe(this.node);
+
+ // Update the character width needed for the popup offset calculations.
+ this._inputCharWidth = this._getInputCharWidth();
+ this.lastInputValue && this._setValue(this.lastInputValue);
+ }
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ this.imperativeUpdate(nextProps);
+ }
+
+ shouldComponentUpdate(nextProps) {
+ return (
+ this.props.showEditorOnboarding !== nextProps.showEditorOnboarding ||
+ this.props.editorMode !== nextProps.editorMode
+ );
+ }
+
+ /**
+ * Do all the imperative work needed after a Redux store update.
+ *
+ * @param {Object} nextProps: props passed from shouldComponentUpdate.
+ */
+ imperativeUpdate(nextProps) {
+ if (!nextProps) {
+ return;
+ }
+
+ if (
+ nextProps.autocompleteData !== this.props.autocompleteData &&
+ nextProps.autocompleteData.pendingRequestId === null
+ ) {
+ this.updateAutocompletionPopup(nextProps.autocompleteData);
+ }
+
+ if (nextProps.editorMode !== this.props.editorMode) {
+ if (this.editor) {
+ this.editor.setOption("lineNumbers", nextProps.editorMode);
+ this.editor.setOption("enableCodeFolding", nextProps.editorMode);
+ }
+
+ if (nextProps.editorMode && nextProps.editorWidth) {
+ this.setEditorWidth(nextProps.editorWidth);
+ } else {
+ this.setEditorWidth(null);
+ }
+
+ if (this.autocompletePopup.isOpen) {
+ this.autocompletePopup.hidePopup();
+ }
+ }
+
+ if (
+ nextProps.autocompletePopupPosition !==
+ this.props.autocompletePopupPosition &&
+ this.autocompletePopup
+ ) {
+ this.autocompletePopup.position = nextProps.autocompletePopupPosition;
+ }
+
+ if (
+ nextProps.editorPrettifiedAt &&
+ nextProps.editorPrettifiedAt !== this.props.editorPrettifiedAt
+ ) {
+ this._setValue(
+ beautify.js(this._getValue(), {
+ // Read directly from prefs because this.editor.config.indentUnit and
+ // this.editor.getOption('indentUnit') are not really synced with
+ // prefs.
+ indent_size: Services.prefs.getIntPref("devtools.editor.tabsize"),
+ indent_with_tabs: !Services.prefs.getBoolPref(
+ "devtools.editor.expandtab"
+ ),
+ })
+ );
+ }
+ }
+
+ /**
+ *
+ * @param {Number|null} editorWidth: The width to set the node to. If null, removes any
+ * `width` property on node style.
+ */
+ setEditorWidth(editorWidth) {
+ if (!this.node) {
+ return;
+ }
+
+ if (editorWidth) {
+ this.node.style.width = `${editorWidth}px`;
+ } else {
+ this.node.style.removeProperty("width");
+ }
+ }
+
+ focus() {
+ if (this.editor) {
+ this.editor.focus();
+ }
+ }
+
+ focusPreviousElement() {
+ const inputField = this.editor.codeMirror.getInputField();
+
+ const findPreviousFocusableElement = el => {
+ if (!el || !el.querySelectorAll) {
+ return null;
+ }
+
+ // We only want to get visible focusable element, and for that we can assert that
+ // the offsetParent isn't null. We can do that because we don't have fixed position
+ // element in the console.
+ const items = getFocusableElements(el).filter(
+ ({ offsetParent }) => offsetParent !== null
+ );
+ const inputIndex = items.indexOf(inputField);
+
+ if (items.length === 0 || (inputIndex > -1 && items.length === 1)) {
+ return findPreviousFocusableElement(el.parentNode);
+ }
+
+ const index = inputIndex > 0 ? inputIndex - 1 : items.length - 1;
+ return items[index];
+ };
+
+ const focusableEl = findPreviousFocusableElement(this.node.parentNode);
+ if (focusableEl) {
+ focusableEl.focus();
+ }
+ }
+
+ /**
+ * Execute a string. Execution happens asynchronously in the content process.
+ */
+ _execute() {
+ const value = this._getValue();
+ // In editor mode, we only evaluate the text selection if there's one. The feature isn't
+ // enabled in inline mode as it can be confusing since input is cleared when evaluating.
+ const executeString = this.props.editorMode
+ ? this.getSelectedText() || value
+ : value;
+
+ if (!executeString) {
+ return;
+ }
+
+ if (!this.props.editorMode) {
+ // Calling this.props.terminalInputChanged instead of this.terminalInputChanged
+ // because we want to instantly hide the instant evaluation result, and don't want
+ // the delay we have in this.terminalInputChanged.
+ this.props.terminalInputChanged("");
+ this._setValue("");
+ }
+ this.clearCompletion();
+ this.props.evaluateExpression(executeString);
+ }
+
+ /**
+ * Sets the value of the input field.
+ *
+ * @param string newValue
+ * The new value to set.
+ * @returns void
+ */
+ _setValue(newValue = "") {
+ this.lastInputValue = newValue;
+ this.terminalInputChanged(newValue);
+
+ if (this.editor) {
+ // In order to get the autocomplete popup to work properly, we need to set the
+ // editor text and the cursor in the same operation. If we don't, the text change
+ // is done before the cursor is moved, and the autocompletion call to the server
+ // sends an erroneous query.
+ this.editor.codeMirror.operation(() => {
+ this.editor.setText(newValue);
+
+ // Set the cursor at the end of the input.
+ const lines = newValue.split("\n");
+ this.editor.setCursor({
+ line: lines.length - 1,
+ ch: lines[lines.length - 1].length,
+ });
+ this.editor.setAutoCompletionText();
+ });
+ }
+
+ this.emitForTests("set-input-value");
+ }
+
+ /**
+ * Gets the value from the input field
+ * @returns string
+ */
+ _getValue() {
+ return this.editor ? this.editor.getText() || "" : "";
+ }
+
+ /**
+ * Open the file picker for the user to select a javascript file and open it.
+ *
+ */
+ async _openFile() {
+ const fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.init(
+ this.webConsoleUI.document.defaultView,
+ l10n.getStr("webconsole.input.openJavaScriptFile"),
+ Ci.nsIFilePicker.modeOpen
+ );
+
+ // Append file filters
+ fp.appendFilter(
+ l10n.getStr("webconsole.input.openJavaScriptFileFilter"),
+ "*.js"
+ );
+
+ function readFile(file) {
+ return new Promise(resolve => {
+ IOUtils.read(file.path).then(data => {
+ const decoder = new TextDecoder();
+ resolve(decoder.decode(data));
+ });
+ });
+ }
+
+ const content = await new Promise(resolve => {
+ fp.open(rv => {
+ if (rv == Ci.nsIFilePicker.returnOK) {
+ const file = Cc["@mozilla.org/file/local;1"].createInstance(
+ Ci.nsIFile
+ );
+ file.initWithPath(fp.file.path);
+ readFile(file).then(resolve);
+ }
+ });
+ });
+
+ this._setValue(content);
+ }
+
+ getSelectionStart() {
+ return this.getInputValueBeforeCursor().length;
+ }
+
+ getSelectedText() {
+ return this.editor.getSelection();
+ }
+
+ /**
+ * Even handler for the "beforeChange" event fired by codeMirror. This event is fired
+ * when codeMirror is about to make a change to its DOM representation.
+ */
+ _onEditorBeforeChange(cm, change) {
+ // If the user did not type a character that matches the completion text, then we
+ // clear it before the change is done to prevent a visual glitch.
+ // See Bugs 1491776 & 1558248.
+ const { from, to, origin, text } = change;
+ const isAddedText =
+ from.line === to.line && from.ch === to.ch && origin === "+input";
+
+ // if there was no changes (hitting delete on an empty input, or suppr when at the end
+ // of the input), we bail out.
+ if (
+ !isAddedText &&
+ origin === "+delete" &&
+ from.line === to.line &&
+ from.ch === to.ch
+ ) {
+ return;
+ }
+
+ const addedText = text.join("");
+ const completionText = this.getAutoCompletionText();
+
+ const addedCharacterMatchCompletion =
+ isAddedText && completionText.startsWith(addedText);
+
+ const addedCharacterMatchPopupItem =
+ isAddedText &&
+ this.autocompletePopup.items.some(({ preLabel, label }) =>
+ label.startsWith(preLabel + addedText)
+ );
+ const nextSelectedAutocompleteItemIndex =
+ addedCharacterMatchPopupItem &&
+ this.autocompletePopup.items.findIndex(({ preLabel, label }) =>
+ label.startsWith(preLabel + addedText)
+ );
+
+ if (addedCharacterMatchPopupItem) {
+ this.autocompletePopup.selectItemAtIndex(
+ nextSelectedAutocompleteItemIndex,
+ { preventSelectCallback: true }
+ );
+ }
+
+ if (!completionText || change.canceled || !addedCharacterMatchCompletion) {
+ this.setAutoCompletionText("");
+ }
+
+ if (!addedCharacterMatchCompletion && !addedCharacterMatchPopupItem) {
+ this.autocompletePopup.hidePopup();
+ } else if (
+ !change.canceled &&
+ (completionText ||
+ addedCharacterMatchCompletion ||
+ addedCharacterMatchPopupItem)
+ ) {
+ // The completion text will be updated when the debounced autocomplete update action
+ // is done, so in the meantime we set the pending value to pendingCompletionText.
+ // See Bug 1595068 for more information.
+ this.pendingCompletionText = completionText.substring(text.length);
+ // And we update the preLabel of the matching autocomplete items that may be used
+ // in the acceptProposedAutocompletion function.
+ this.autocompletePopup.items.forEach(item => {
+ if (item.label.startsWith(item.preLabel + addedText)) {
+ item.preLabel += addedText;
+ }
+ });
+ }
+ }
+
+ /**
+ * Even handler for the "blur" event fired by codeMirror.
+ */
+ _onEditorBlur(cm) {
+ if (cm.somethingSelected()) {
+ // If there's a selection when the input is blurred, then we remove it by setting
+ // the cursor at the position that matches the start of the first selection.
+ const [{ head }] = cm.listSelections();
+ cm.setCursor(head, { scroll: false });
+ }
+ }
+
+ /**
+ * Fired after a key is handled through a key map.
+ *
+ * @param {CodeMirror} cm: codeMirror instance
+ * @param {String} key: The key that was handled
+ * @param {Event} e: The keypress event
+ */
+ _onEditorKeyHandled(cm, key, e) {
+ // The autocloseBracket addon handle closing brackets keys when they're typed, but
+ // there's already an existing closing bracket.
+ // ex:
+ // 1. input is `foo(x|)` (where | represents the cursor)
+ // 2. user types `)`
+ // 3. input is now `foo(x)|` (i.e. the typed character wasn't inserted)
+ // In such case, _onEditorBeforeChange isn't triggered, so we need to hide the popup
+ // here. We can do that because this function won't be called when codeMirror _do_
+ // insert the closing char.
+ const closingKeys = [`']'`, `')'`, "'}'"];
+ if (this.autocompletePopup.isOpen && closingKeys.includes(key)) {
+ this.clearCompletion();
+ }
+ }
+
+ /**
+ * Retrieve variable declared in the expression from the CodeMirror state, in order
+ * to display them in the autocomplete popup.
+ */
+ _getExpressionVariables() {
+ const cm = this.editor.codeMirror;
+ const { state } = cm.getTokenAt(cm.getCursor());
+ const variables = [];
+
+ if (state.context) {
+ for (let c = state.context; c; c = c.prev) {
+ for (let v = c.vars; v; v = v.next) {
+ if (v.name) {
+ variables.push(v.name);
+ }
+ }
+ }
+ }
+
+ const keys = ["localVars", "globalVars"];
+ for (const key of keys) {
+ if (state[key]) {
+ for (let v = state[key]; v; v = v.next) {
+ if (v.name) {
+ variables.push(v.name);
+ }
+ }
+ }
+ }
+
+ return variables;
+ }
+
+ /**
+ * The editor "changes" event handler.
+ */
+ _onEditorChanges(cm, changes) {
+ const value = this._getValue();
+
+ if (this.lastInputValue !== value) {
+ // We don't autocomplete if the changes were made by JsTerm (e.g. autocomplete was
+ // accepted).
+ const isJsTermChangeOnly = changes.every(
+ ({ origin }) => origin === JSTERM_CODEMIRROR_ORIGIN
+ );
+
+ if (
+ !isJsTermChangeOnly &&
+ (this.props.autocomplete || this.hasAutocompletionSuggestion())
+ ) {
+ this.autocompleteUpdate(false, null, this._getExpressionVariables());
+ }
+ this.lastInputValue = value;
+ this.terminalInputChanged(value);
+ }
+ }
+
+ /**
+ * Go up/down the history stack of input values.
+ *
+ * @param number direction
+ * History navigation direction: HISTORY_BACK or HISTORY_FORWARD.
+ *
+ * @returns boolean
+ * True if the input value changed, false otherwise.
+ */
+ historyPeruse(direction) {
+ const { history, updateHistoryPosition, getValueFromHistory } = this.props;
+
+ if (!history.entries.length) {
+ return false;
+ }
+
+ const newInputValue = getValueFromHistory(direction);
+ const expression = this._getValue();
+ updateHistoryPosition(direction, expression);
+
+ if (newInputValue != null) {
+ this._setValue(newInputValue);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Test for empty input.
+ *
+ * @return boolean
+ */
+ hasEmptyInput() {
+ return this._getValue() === "";
+ }
+
+ /**
+ * Check if the caret is at a location that allows selecting the previous item
+ * in history when the user presses the Up arrow key.
+ *
+ * @return boolean
+ * True if the caret is at a location that allows selecting the
+ * previous item in history when the user presses the Up arrow key,
+ * otherwise false.
+ */
+ canCaretGoPrevious() {
+ if (!this.editor) {
+ return false;
+ }
+
+ const inputValue = this._getValue();
+ const { line, ch } = this.editor.getCursor();
+ return (line === 0 && ch === 0) || (line === 0 && ch === inputValue.length);
+ }
+
+ /**
+ * Check if the caret is at a location that allows selecting the next item in
+ * history when the user presses the Down arrow key.
+ *
+ * @return boolean
+ * True if the caret is at a location that allows selecting the next
+ * item in history when the user presses the Down arrow key, otherwise
+ * false.
+ */
+ canCaretGoNext() {
+ if (!this.editor) {
+ return false;
+ }
+
+ const inputValue = this._getValue();
+ const multiline = /[\r\n]/.test(inputValue);
+
+ const { line, ch } = this.editor.getCursor();
+ return (
+ (!multiline && ch === 0) ||
+ this.editor.getDoc().getRange({ line: 0, ch: 0 }, { line, ch }).length ===
+ inputValue.length
+ );
+ }
+
+ /**
+ * Takes the data returned by the server and update the autocomplete popup state (i.e.
+ * its visibility and items).
+ *
+ * @param {Object} data
+ * The autocompletion data as returned by the webconsole actor's autocomplete
+ * service. Should be of the following shape:
+ * {
+ * matches: {Array} array of the properties matching the input,
+ * matchProp: {String} The string used to filter the properties,
+ * isElementAccess: {Boolean} True when the input is an element access,
+ * i.e. `document["addEve`.
+ * }
+ * @fires autocomplete-updated
+ */
+ async updateAutocompletionPopup(data) {
+ if (!this.editor) {
+ return;
+ }
+
+ const { matches, matchProp, isElementAccess } = data;
+ if (!matches.length) {
+ this.clearCompletion();
+ return;
+ }
+
+ const inputUntilCursor = this.getInputValueBeforeCursor();
+
+ const items = matches.map(label => {
+ let preLabel = label.substring(0, matchProp.length);
+ // If the user is performing an element access, and if they did not typed a quote,
+ // then we need to adjust the preLabel to match the quote from the label + what
+ // the user entered.
+ if (isElementAccess && /^['"`]/.test(matchProp) === false) {
+ preLabel = label.substring(0, matchProp.length + 1);
+ }
+ return { preLabel, label, isElementAccess };
+ });
+
+ if (items.length) {
+ const { preLabel, label } = items[0];
+ let suffix = label.substring(preLabel.length);
+ if (isElementAccess) {
+ if (!matchProp) {
+ suffix = label;
+ }
+ const inputAfterCursor = this._getValue().substring(
+ inputUntilCursor.length
+ );
+ // If there's not a bracket after the cursor, add it to the completionText.
+ if (!inputAfterCursor.trimLeft().startsWith("]")) {
+ suffix = suffix + "]";
+ }
+ }
+ this.setAutoCompletionText(suffix);
+ }
+
+ const popup = this.autocompletePopup;
+ // We don't want to trigger the onSelect callback since we already set the completion
+ // text a few lines above.
+ popup.setItems(items, 0, {
+ preventSelectCallback: true,
+ });
+
+ const minimumAutoCompleteLength = 2;
+
+ // We want to show the autocomplete popup if:
+ // - there are at least 2 matching results
+ // - OR, if there's 1 result, but whose label does not start like the input (this can
+ // happen with insensitive search: `num` will match `Number`).
+ // - OR, if there's 1 result, but we can't show the completionText (because there's
+ // some text after the cursor), unless the text in the popup is the same as the input.
+ if (
+ items.length >= minimumAutoCompleteLength ||
+ (items.length === 1 && items[0].preLabel !== matchProp) ||
+ (items.length === 1 &&
+ !this.canDisplayAutoCompletionText() &&
+ items[0].label !== matchProp)
+ ) {
+ // We need to show the popup at the "." or "[".
+ const xOffset = -1 * matchProp.length * this._inputCharWidth;
+ const yOffset = 5;
+ const popupAlignElement =
+ this.props.serviceContainer.getJsTermTooltipAnchor();
+ this._openPopupPendingPromise = popup.openPopup(
+ popupAlignElement,
+ xOffset,
+ yOffset,
+ 0,
+ {
+ preventSelectCallback: true,
+ }
+ );
+ await this._openPopupPendingPromise;
+ this._openPopupPendingPromise = null;
+ } else if (
+ items.length < minimumAutoCompleteLength &&
+ (popup.isOpen || this._openPopupPendingPromise)
+ ) {
+ if (this._openPopupPendingPromise) {
+ await this._openPopupPendingPromise;
+ }
+ popup.hidePopup();
+ }
+
+ // Eager evaluation results incorporate the current autocomplete item. We need to
+ // trigger it here as well as in onAutocompleteSelect as we set the items with
+ // preventSelectCallback (which means we won't trigger onAutocompleteSelect when the
+ // popup is open).
+ this.terminalInputChanged(
+ this.getInputValueWithCompletionText().expression
+ );
+
+ this.emit("autocomplete-updated");
+ }
+
+ onAutocompleteSelect() {
+ const { selectedItem } = this.autocompletePopup;
+ if (selectedItem) {
+ const { preLabel, label, isElementAccess } = selectedItem;
+ let suffix = label.substring(preLabel.length);
+
+ // If the user is performing an element access, we need to check if we should add
+ // starting and ending quotes, as well as a closing bracket.
+ if (isElementAccess) {
+ const inputBeforeCursor = this.getInputValueBeforeCursor();
+ if (inputBeforeCursor.trim().endsWith("[")) {
+ suffix = label;
+ }
+
+ const inputAfterCursor = this._getValue().substring(
+ inputBeforeCursor.length
+ );
+ // If there's no closing bracket after the cursor, add it to the completionText.
+ if (!inputAfterCursor.trimLeft().startsWith("]")) {
+ suffix = suffix + "]";
+ }
+ }
+ this.setAutoCompletionText(suffix);
+ } else {
+ this.setAutoCompletionText("");
+ }
+ // Eager evaluation results incorporate the current autocomplete item.
+ this.terminalInputChanged(
+ this.getInputValueWithCompletionText().expression
+ );
+ }
+
+ /**
+ * Clear the current completion information, cancel any pending autocompletion update
+ * and close the autocomplete popup, if needed.
+ * @fires autocomplete-updated
+ */
+ clearCompletion() {
+ this.autocompleteUpdate.cancel();
+ // Update Eager evaluation result as the completion text was removed.
+ this.terminalInputChanged(this._getValue());
+
+ this.setAutoCompletionText("");
+ let onPopupClosed = Promise.resolve();
+ if (this.autocompletePopup) {
+ this.autocompletePopup.clearItems();
+
+ if (this.autocompletePopup.isOpen || this._openPopupPendingPromise) {
+ onPopupClosed = this.autocompletePopup.once("popup-closed");
+
+ if (this._openPopupPendingPromise) {
+ this._openPopupPendingPromise.then(() =>
+ this.autocompletePopup.hidePopup()
+ );
+ } else {
+ this.autocompletePopup.hidePopup();
+ }
+ onPopupClosed.then(() => this.focus());
+ }
+ }
+ onPopupClosed.then(() => this.emit("autocomplete-updated"));
+ }
+
+ /**
+ * Accept the proposed input completion.
+ */
+ acceptProposedCompletion() {
+ const {
+ completionText,
+ numberOfCharsToMoveTheCursorForward,
+ numberOfCharsToReplaceCharsBeforeCursor,
+ } = this.getInputValueWithCompletionText();
+
+ this.autocompleteUpdate.cancel();
+ this.props.autocompleteClear();
+
+ // If the code triggering the opening of the popup was already triggered but not yet
+ // settled, then we need to wait until it's resolved in order to close the popup (See
+ // Bug 1655406).
+ if (this._openPopupPendingPromise) {
+ this._openPopupPendingPromise.then(() =>
+ this.autocompletePopup.hidePopup()
+ );
+ }
+
+ if (completionText) {
+ this.insertStringAtCursor(
+ completionText,
+ numberOfCharsToReplaceCharsBeforeCursor
+ );
+
+ if (numberOfCharsToMoveTheCursorForward) {
+ const { line, ch } = this.editor.getCursor();
+ this.editor.setCursor({
+ line,
+ ch: ch + numberOfCharsToMoveTheCursorForward,
+ });
+ }
+ }
+ }
+
+ /**
+ * Returns an object containing the expression we would get if the user accepted the
+ * current completion text. This is more than the current input + the completion text,
+ * as there are special cases for element access and case-insensitive matches.
+ *
+ * @return {Object}: An object of the following shape:
+ * - {String} expression: The complete expression
+ * - {String} completionText: the completion text only, which should be used
+ * with the next property
+ * - {Integer} numberOfCharsToReplaceCharsBeforeCursor: The number of chars that
+ * should be removed from the current input before the cursor to
+ * cleanly apply the completionText. This is handy when we only want
+ * to insert the completionText.
+ * - {Integer} numberOfCharsToMoveTheCursorForward: The number of chars that the
+ * cursor should be moved after the completion is done. This can
+ * be useful for element access where there's already a closing
+ * quote and/or bracket.
+ */
+ getInputValueWithCompletionText() {
+ const inputBeforeCursor = this.getInputValueBeforeCursor();
+ const inputAfterCursor = this._getValue().substring(
+ inputBeforeCursor.length
+ );
+ let completionText = this.getAutoCompletionText();
+ let numberOfCharsToReplaceCharsBeforeCursor;
+ let numberOfCharsToMoveTheCursorForward = 0;
+
+ // If the autocompletion popup is open, we always get the selected element from there,
+ // since the autocompletion text might not be enough (e.g. `dOcUmEn` should
+ // autocomplete to `document`, but the autocompletion text only shows `t`).
+ if (this.autocompletePopup.isOpen && this.autocompletePopup.selectedItem) {
+ const { selectedItem } = this.autocompletePopup;
+ const { label, preLabel, isElementAccess } = selectedItem;
+
+ completionText = label;
+ numberOfCharsToReplaceCharsBeforeCursor = preLabel.length;
+
+ // If the user is performing an element access, we need to check if we should add
+ // starting and ending quotes, as well as a closing bracket.
+ if (isElementAccess) {
+ const lastOpeningBracketIndex = inputBeforeCursor.lastIndexOf("[");
+ if (lastOpeningBracketIndex > -1) {
+ numberOfCharsToReplaceCharsBeforeCursor = inputBeforeCursor.substring(
+ lastOpeningBracketIndex + 1
+ ).length;
+ }
+
+ // If the autoclose bracket option is enabled, the input might be in a state where
+ // there's already the closing quote and the closing bracket, e.g.
+ // `document["activeEl|"]`, so we don't need to add
+ // Let's retrieve the completionText last character, to see if it's a quote.
+ const completionTextLastChar =
+ completionText[completionText.length - 1];
+ const endingQuote = [`"`, `'`, "`"].includes(completionTextLastChar)
+ ? completionTextLastChar
+ : "";
+ if (
+ endingQuote &&
+ inputAfterCursor.trimLeft().startsWith(endingQuote)
+ ) {
+ completionText = completionText.substring(
+ 0,
+ completionText.length - 1
+ );
+ numberOfCharsToMoveTheCursorForward++;
+ }
+
+ // If there's not a closing bracket already, we add one.
+ if (
+ !inputAfterCursor.trimLeft().match(new RegExp(`^${endingQuote}?]`))
+ ) {
+ completionText = completionText + "]";
+ } else {
+ // if there's already one, we want to move the cursor after the closing bracket.
+ numberOfCharsToMoveTheCursorForward++;
+ }
+ }
+ }
+
+ const expression =
+ inputBeforeCursor.substring(
+ 0,
+ inputBeforeCursor.length -
+ (numberOfCharsToReplaceCharsBeforeCursor || 0)
+ ) +
+ completionText +
+ inputAfterCursor;
+
+ return {
+ completionText,
+ expression,
+ numberOfCharsToMoveTheCursorForward,
+ numberOfCharsToReplaceCharsBeforeCursor,
+ };
+ }
+
+ getInputValueBeforeCursor() {
+ return this.editor
+ ? this.editor
+ .getDoc()
+ .getRange({ line: 0, ch: 0 }, this.editor.getCursor())
+ : null;
+ }
+
+ /**
+ * Insert a string into the console at the cursor location,
+ * moving the cursor to the end of the string.
+ *
+ * @param {string} str
+ * @param {int} numberOfCharsToReplaceCharsBeforeCursor - defaults to 0
+ */
+ insertStringAtCursor(str, numberOfCharsToReplaceCharsBeforeCursor = 0) {
+ if (!this.editor) {
+ return;
+ }
+
+ const cursor = this.editor.getCursor();
+ const from = {
+ line: cursor.line,
+ ch: cursor.ch - numberOfCharsToReplaceCharsBeforeCursor,
+ };
+
+ this.editor
+ .getDoc()
+ .replaceRange(str, from, cursor, JSTERM_CODEMIRROR_ORIGIN);
+ }
+
+ /**
+ * Set the autocompletion text of the input.
+ *
+ * @param string suffix
+ * The proposed suffix for the input value.
+ */
+ setAutoCompletionText(suffix) {
+ if (!this.editor) {
+ return;
+ }
+
+ this.pendingCompletionText = null;
+
+ if (suffix && !this.canDisplayAutoCompletionText()) {
+ suffix = "";
+ }
+
+ this.editor.setAutoCompletionText(suffix);
+ }
+
+ getAutoCompletionText() {
+ const renderedCompletionText =
+ this.editor && this.editor.getAutoCompletionText();
+ return typeof this.pendingCompletionText === "string"
+ ? this.pendingCompletionText
+ : renderedCompletionText;
+ }
+
+ /**
+ * Indicate if the input has an autocompletion suggestion, i.e. that there is either
+ * something in the autocompletion text or that there's a selected item in the
+ * autocomplete popup.
+ */
+ hasAutocompletionSuggestion() {
+ // We can have cases where the popup is opened but we can't display the autocompletion
+ // text.
+ return (
+ this.getAutoCompletionText() ||
+ (this.autocompletePopup.isOpen &&
+ Number.isInteger(this.autocompletePopup.selectedIndex) &&
+ this.autocompletePopup.selectedIndex > -1)
+ );
+ }
+
+ /**
+ * Returns a boolean indicating if we can display an autocompletion text in the input,
+ * i.e. if there is no characters displayed on the same line of the cursor and after it.
+ */
+ canDisplayAutoCompletionText() {
+ if (!this.editor) {
+ return false;
+ }
+
+ const { ch, line } = this.editor.getCursor();
+ const lineContent = this.editor.getLine(line);
+ const textAfterCursor = lineContent.substring(ch);
+ return textAfterCursor === "";
+ }
+
+ /**
+ * Calculates and returns the width of a single character of the input box.
+ * This will be used in opening the popup at the correct offset.
+ *
+ * @returns {Number|null}: Width off the "x" char, or null if the input does not exist.
+ */
+ _getInputCharWidth() {
+ return this.editor ? this.editor.defaultCharWidth() : null;
+ }
+
+ onContextMenu(e) {
+ this.props.serviceContainer.openEditContextMenu(e);
+ }
+
+ destroy() {
+ this.autocompleteUpdate.cancel();
+ this.terminalInputChanged.cancel();
+ this._openPopupPendingPromise = null;
+
+ if (this.autocompletePopup) {
+ this.autocompletePopup.destroy();
+ this.autocompletePopup = null;
+ }
+
+ if (this.editor) {
+ this.resizeObserver.disconnect();
+ this.editor.destroy();
+ this.editor = null;
+ }
+
+ this.webConsoleUI = null;
+ }
+
+ renderOpenEditorButton() {
+ if (this.props.editorMode) {
+ return null;
+ }
+
+ return dom.button({
+ className:
+ "devtools-button webconsole-input-openEditorButton" +
+ (this.props.showEditorOnboarding ? " devtools-feature-callout" : ""),
+ title: l10n.getFormatStr("webconsole.input.openEditorButton.tooltip2", [
+ isMacOS ? "Cmd + B" : "Ctrl + B",
+ ]),
+ onClick: this.props.editorToggle,
+ });
+ }
+
+ renderEvaluationContextSelector() {
+ if (this.props.editorMode || !this.props.showEvaluationContextSelector) {
+ return null;
+ }
+
+ return EvaluationContextSelector(this.props);
+ }
+
+ renderEditorOnboarding() {
+ if (!this.props.showEditorOnboarding) {
+ return null;
+ }
+
+ // We deliberately use getStr, and not getFormatStr, because we want keyboard
+ // shortcuts to be wrapped in their own span.
+ const label = l10n.getStr("webconsole.input.editor.onboarding.label");
+ let [prefix, suffix] = label.split("%1$S");
+ suffix = suffix.split("%2$S");
+
+ const enterString = l10n.getStr("webconsole.enterKey");
+
+ return dom.header(
+ { className: "editor-onboarding" },
+ dom.img({
+ className: "editor-onboarding-fox",
+ src: "chrome://devtools/skin/images/fox-smiling.svg",
+ }),
+ dom.p(
+ {},
+ prefix,
+ dom.span({ className: "editor-onboarding-shortcut" }, enterString),
+ suffix[0],
+ dom.span({ className: "editor-onboarding-shortcut" }, [
+ isMacOS ? `Cmd+${enterString}` : `Ctrl+${enterString}`,
+ ]),
+ suffix[1]
+ ),
+ dom.button(
+ {
+ className: "editor-onboarding-dismiss-button",
+ onClick: () => this.props.editorOnboardingDismiss(),
+ },
+ l10n.getStr("webconsole.input.editor.onboarding.dismiss.label")
+ )
+ );
+ }
+
+ render() {
+ if (!this.props.inputEnabled) {
+ return null;
+ }
+
+ return dom.div(
+ {
+ className: "jsterm-input-container devtools-input",
+ key: "jsterm-container",
+ "aria-live": "off",
+ tabIndex: -1,
+ onContextMenu: this.onContextMenu,
+ ref: node => {
+ this.node = node;
+ },
+ },
+ dom.div(
+ { className: "webconsole-input-buttons" },
+ this.renderEvaluationContextSelector(),
+ this.renderOpenEditorButton()
+ ),
+ this.renderEditorOnboarding()
+ );
+ }
+}
+
+// Redux connect
+
+function mapStateToProps(state) {
+ return {
+ history: getHistory(state),
+ getValueFromHistory: direction => getHistoryValue(state, direction),
+ autocompleteData: getAutocompleteState(state),
+ showEditorOnboarding: state.ui.showEditorOnboarding,
+ showEvaluationContextSelector: state.ui.showEvaluationContextSelector,
+ autocompletePopupPosition: state.prefs.eagerEvaluation ? "top" : "bottom",
+ editorPrettifiedAt: state.ui.editorPrettifiedAt,
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ updateHistoryPosition: (direction, expression) =>
+ dispatch(actions.updateHistoryPosition(direction, expression)),
+ autocompleteUpdate: (force, getterPath, expressionVars) =>
+ dispatch(actions.autocompleteUpdate(force, getterPath, expressionVars)),
+ autocompleteClear: () => dispatch(actions.autocompleteClear()),
+ evaluateExpression: expression =>
+ dispatch(actions.evaluateExpression(expression)),
+ editorToggle: () => dispatch(actions.editorToggle()),
+ editorOnboardingDismiss: () => dispatch(actions.editorOnboardingDismiss()),
+ terminalInputChanged: value =>
+ dispatch(actions.terminalInputChanged(value)),
+ };
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(JSTerm);
diff --git a/devtools/client/webconsole/components/Input/ReverseSearchInput.css b/devtools/client/webconsole/components/Input/ReverseSearchInput.css
new file mode 100644
index 0000000000..6398449959
--- /dev/null
+++ b/devtools/client/webconsole/components/Input/ReverseSearchInput.css
@@ -0,0 +1,125 @@
+/* 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/. */
+
+.reverse-search {
+ display: flex;
+ font-size: inherit;
+ min-height: 26px;
+ color: var(--theme-body-color);
+ padding-block-start: 2px;
+ align-items: baseline;
+ border: 1px solid transparent;
+ border-top-color: var(--theme-splitter-color);
+ transition: border-color 0.2s ease-in-out;
+}
+
+.jsterm-editor .reverse-search {
+ border-inline-end-color: var(--theme-splitter-color);
+}
+
+/* Add a border radius match the borders of the window on Mac OS
+ * and hide the border radius on the right if the sidebar or editor
+ * is open. */
+:root[platform="mac"] .webconsole-app .reverse-search {
+ border-end-start-radius: 5px;
+}
+:root[platform="mac"] .webconsole-app:not(.jsterm-editor, .sidebar-visible) .reverse-search
+{
+ border-end-end-radius: 5px;
+}
+
+.reverse-search:focus-within {
+ outline-offset: -2px;
+ outline: var(--theme-focus-outline);
+}
+
+.reverse-search {
+ flex-shrink: 0;
+}
+
+.reverse-search input {
+ border: none;
+ flex-grow: 1;
+ background: transparent;
+ color: currentColor;
+ background-image: url(chrome://devtools/skin/images/search.svg);
+ background-repeat: no-repeat;
+ background-size: 12px;
+ --background-position-inline: 10px;
+ background-position: var(--background-position-inline) 2px;
+ -moz-context-properties: fill;
+ fill: var(--theme-icon-dimmed-color);
+ text-align: match-parent;
+ unicode-bidi: plaintext;
+ min-width: 80px;
+ flex-shrink: 1;
+ flex-basis: 0;
+}
+
+.reverse-search:dir(ltr) input {
+ /* Be explicit about left/right direction to prevent the text/placeholder
+ * from overlapping the background image when the user changes the text
+ * direction manually (e.g. via Ctrl+Shift). */
+ padding-left: var(--console-inline-start-gutter);
+}
+
+.reverse-search:dir(rtl) input {
+ background-position-x: right var(--background-position-inline);
+ padding-right: var(--console-inline-start-gutter);
+}
+
+.reverse-search input:focus {
+ border: none;
+ outline: none;
+}
+
+.reverse-search:not(.no-result) input:focus {
+ fill: var(--theme-icon-checked-color);
+}
+
+.reverse-search-actions {
+ flex-shrink: 0;
+ display: flex;
+ align-items: baseline;
+}
+
+.reverse-search-info {
+ flex-shrink: 0;
+ padding: 0 8px;
+ color: var(--comment-node-color);
+}
+
+.search-result-button-prev,
+.search-result-button-next,
+.reverse-search-close-button {
+ padding: 4px 0;
+ margin: 0;
+ border-radius: 0;
+}
+
+.search-result-button-prev::before {
+ background-image: url("chrome://devtools/skin/images/arrowhead-up.svg");
+ background-size: 16px;
+ fill: var(--comment-node-color);
+}
+
+.search-result-button-next::before {
+ background-image: url("chrome://devtools/skin/images/arrowhead-down.svg");
+ background-size: 16px;
+ fill: var(--comment-node-color);
+}
+
+.reverse-search-close-button::before {
+ fill: var(--comment-node-color);
+ background-image: url("chrome://devtools/skin/images/close.svg");
+}
+
+.reverse-search.no-result input {
+ fill: var(--error-color);
+}
+
+.reverse-search.no-result,
+.reverse-search.no-result input {
+ color: var(--error-color);
+}
diff --git a/devtools/client/webconsole/components/Input/ReverseSearchInput.js b/devtools/client/webconsole/components/Input/ReverseSearchInput.js
new file mode 100644
index 0000000000..5cece45bc7
--- /dev/null
+++ b/devtools/client/webconsole/components/Input/ReverseSearchInput.js
@@ -0,0 +1,285 @@
+/* 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";
+
+// React & Redux
+const {
+ Component,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const {
+ getReverseSearchTotalResults,
+ getReverseSearchResultPosition,
+ getReverseSearchResult,
+} = require("resource://devtools/client/webconsole/selectors/history.js");
+
+loader.lazyRequireGetter(
+ this,
+ "PropTypes",
+ "resource://devtools/client/shared/vendor/react-prop-types.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "actions",
+ "resource://devtools/client/webconsole/actions/index.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "l10n",
+ "resource://devtools/client/webconsole/utils/messages.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "PluralForm",
+ "resource://devtools/shared/plural-form.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "KeyCodes",
+ "resource://devtools/client/shared/keycodes.js",
+ true
+);
+
+const isMacOS = Services.appinfo.OS === "Darwin";
+
+class ReverseSearchInput extends Component {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ setInputValue: PropTypes.func.isRequired,
+ focusInput: PropTypes.func.isRequired,
+ reverseSearchResult: PropTypes.string,
+ reverseSearchTotalResults: PropTypes.number,
+ reverseSearchResultPosition: PropTypes.number,
+ visible: PropTypes.bool,
+ initialValue: PropTypes.string,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.onInputKeyDown = this.onInputKeyDown.bind(this);
+ }
+
+ componentDidUpdate(prevProps) {
+ const { setInputValue, focusInput } = this.props;
+ if (
+ prevProps.reverseSearchResult !== this.props.reverseSearchResult &&
+ this.props.visible &&
+ this.props.reverseSearchTotalResults > 0
+ ) {
+ setInputValue(this.props.reverseSearchResult);
+ }
+
+ if (prevProps.visible === true && this.props.visible === false) {
+ focusInput();
+ }
+
+ if (
+ prevProps.visible === false &&
+ this.props.visible === true &&
+ this.props.initialValue
+ ) {
+ this.inputNode.value = this.props.initialValue;
+ }
+ }
+
+ onEnterKeyboardShortcut(event) {
+ const { dispatch } = this.props;
+ event.stopPropagation();
+ dispatch(actions.reverseSearchInputToggle());
+ dispatch(actions.evaluateExpression(undefined, "reverse-search"));
+ }
+
+ onEscapeKeyboardShortcut(event) {
+ const { dispatch } = this.props;
+ event.stopPropagation();
+ dispatch(actions.reverseSearchInputToggle());
+ }
+
+ onBackwardNavigationKeyBoardShortcut(event, canNavigate) {
+ const { dispatch } = this.props;
+ event.stopPropagation();
+ event.preventDefault();
+ if (canNavigate) {
+ dispatch(actions.showReverseSearchBack({ access: "keyboard" }));
+ }
+ }
+
+ onForwardNavigationKeyBoardShortcut(event, canNavigate) {
+ const { dispatch } = this.props;
+ event.stopPropagation();
+ event.preventDefault();
+ if (canNavigate) {
+ dispatch(actions.showReverseSearchNext({ access: "keyboard" }));
+ }
+ }
+
+ onInputKeyDown(event) {
+ const { keyCode, key, ctrlKey, shiftKey } = event;
+ const { reverseSearchTotalResults } = this.props;
+
+ // On Enter, we trigger an execute.
+ if (keyCode === KeyCodes.DOM_VK_RETURN) {
+ return this.onEnterKeyboardShortcut(event);
+ }
+
+ const lowerCaseKey = key.toLowerCase();
+
+ // On Escape (and Ctrl + c on OSX), we close the reverse search input.
+ if (
+ keyCode === KeyCodes.DOM_VK_ESCAPE ||
+ (isMacOS && ctrlKey && lowerCaseKey === "c")
+ ) {
+ return this.onEscapeKeyboardShortcut(event);
+ }
+
+ const canNavigate =
+ Number.isInteger(reverseSearchTotalResults) &&
+ reverseSearchTotalResults > 1;
+
+ if (
+ (!isMacOS && key === "F9" && !shiftKey) ||
+ (isMacOS && ctrlKey && lowerCaseKey === "r")
+ ) {
+ return this.onBackwardNavigationKeyBoardShortcut(event, canNavigate);
+ }
+
+ if (
+ (!isMacOS && key === "F9" && shiftKey) ||
+ (isMacOS && ctrlKey && lowerCaseKey === "s")
+ ) {
+ return this.onForwardNavigationKeyBoardShortcut(event, canNavigate);
+ }
+
+ return null;
+ }
+
+ renderSearchInformation() {
+ const { reverseSearchTotalResults, reverseSearchResultPosition } =
+ this.props;
+
+ if (!Number.isInteger(reverseSearchTotalResults)) {
+ return null;
+ }
+
+ let text;
+ if (reverseSearchTotalResults === 0) {
+ text = l10n.getStr("webconsole.reverseSearch.noResult");
+ } else {
+ const resultsString = l10n.getStr("webconsole.reverseSearch.results");
+ text = PluralForm.get(reverseSearchTotalResults, resultsString)
+ .replace("#1", reverseSearchResultPosition)
+ .replace("#2", reverseSearchTotalResults);
+ }
+
+ return dom.div({ className: "reverse-search-info" }, text);
+ }
+
+ renderNavigationButtons() {
+ const { dispatch, reverseSearchTotalResults } = this.props;
+
+ if (
+ !Number.isInteger(reverseSearchTotalResults) ||
+ reverseSearchTotalResults <= 1
+ ) {
+ return null;
+ }
+
+ return [
+ dom.button({
+ key: "search-result-button-prev",
+ className: "devtools-button search-result-button-prev",
+ title: l10n.getFormatStr(
+ "webconsole.reverseSearch.result.previousButton.tooltip",
+ [isMacOS ? "Ctrl + R" : "F9"]
+ ),
+ onClick: () => {
+ dispatch(actions.showReverseSearchBack({ access: "click" }));
+ this.inputNode.focus();
+ },
+ }),
+ dom.button({
+ key: "search-result-button-next",
+ className: "devtools-button search-result-button-next",
+ title: l10n.getFormatStr(
+ "webconsole.reverseSearch.result.nextButton.tooltip",
+ [isMacOS ? "Ctrl + S" : "Shift + F9"]
+ ),
+ onClick: () => {
+ dispatch(actions.showReverseSearchNext({ access: "click" }));
+ this.inputNode.focus();
+ },
+ }),
+ ];
+ }
+
+ render() {
+ const { dispatch, visible, reverseSearchTotalResults } = this.props;
+
+ if (!visible) {
+ return null;
+ }
+
+ const classNames = ["reverse-search"];
+
+ if (reverseSearchTotalResults === 0) {
+ classNames.push("no-result");
+ }
+
+ return dom.div(
+ { className: classNames.join(" ") },
+ dom.input({
+ ref: node => {
+ this.inputNode = node;
+ },
+ autoFocus: true,
+ placeholder: l10n.getStr("webconsole.reverseSearch.input.placeHolder"),
+ className: "reverse-search-input devtools-monospace",
+ onKeyDown: this.onInputKeyDown,
+ onInput: ({ target }) =>
+ dispatch(actions.reverseSearchInputChange(target.value)),
+ }),
+ dom.div(
+ {
+ className: "reverse-search-actions",
+ },
+ this.renderSearchInformation(),
+ this.renderNavigationButtons(),
+ dom.button({
+ className: "devtools-button reverse-search-close-button",
+ title: l10n.getFormatStr(
+ "webconsole.reverseSearch.closeButton.tooltip",
+ ["Esc" + (isMacOS ? " | Ctrl + C" : "")]
+ ),
+ onClick: () => {
+ dispatch(actions.reverseSearchInputToggle());
+ },
+ })
+ )
+ );
+ }
+}
+
+const mapStateToProps = state => ({
+ visible: state.ui.reverseSearchInputVisible,
+ reverseSearchTotalResults: getReverseSearchTotalResults(state),
+ reverseSearchResultPosition: getReverseSearchResultPosition(state),
+ reverseSearchResult: getReverseSearchResult(state),
+});
+
+const mapDispatchToProps = dispatch => ({ dispatch });
+
+module.exports = connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(ReverseSearchInput);
diff --git a/devtools/client/webconsole/components/Input/moz.build b/devtools/client/webconsole/components/Input/moz.build
new file mode 100644
index 0000000000..1feed60d13
--- /dev/null
+++ b/devtools/client/webconsole/components/Input/moz.build
@@ -0,0 +1,14 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ "ConfirmDialog.js",
+ "EagerEvaluation.js",
+ "EditorToolbar.js",
+ "EvaluationContextSelector.js",
+ "EvaluationNotification.js",
+ "JSTerm.js",
+ "ReverseSearchInput.js",
+)
diff --git a/devtools/client/webconsole/components/Output/CollapseButton.js b/devtools/client/webconsole/components/Output/CollapseButton.js
new file mode 100644
index 0000000000..c0594a5855
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/CollapseButton.js
@@ -0,0 +1,33 @@
+/* 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 dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const {
+ l10n,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+const messageToggleDetails = l10n.getStr("messageToggleDetails");
+
+function CollapseButton(props) {
+ const { open, onClick, title = messageToggleDetails } = props;
+
+ return dom.button({
+ "aria-expanded": open ? "true" : "false",
+ "aria-label": title,
+ className: "arrow collapse-button",
+ onClick,
+ onMouseDown: e => {
+ // prevent focus from moving to the disclosure if clicked,
+ // which is annoying if on the input
+ e.preventDefault();
+ // Clearing the text selection to allow the message to collpase.
+ e.target.ownerDocument.defaultView.getSelection().removeAllRanges();
+ },
+ title,
+ });
+}
+
+module.exports = CollapseButton;
diff --git a/devtools/client/webconsole/components/Output/ConsoleOutput.js b/devtools/client/webconsole/components/Output/ConsoleOutput.js
new file mode 100644
index 0000000000..a2469b3b46
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/ConsoleOutput.js
@@ -0,0 +1,381 @@
+/* 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 {
+ Component,
+ createElement,
+ createRef,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/redux/visibility-handler-connect.js");
+const {
+ initialize,
+} = require("resource://devtools/client/webconsole/actions/ui.js");
+const LazyMessageList = require("resource://devtools/client/webconsole/components/Output/LazyMessageList.js");
+
+const {
+ getMutableMessagesById,
+ getAllMessagesUiById,
+ getAllDisabledMessagesById,
+ getAllCssMessagesMatchingElements,
+ getAllNetworkMessagesUpdateById,
+ getLastMessageId,
+ getVisibleMessages,
+ getAllRepeatById,
+ getAllWarningGroupsById,
+ isMessageInWarningGroup,
+} = require("resource://devtools/client/webconsole/selectors/messages.js");
+
+loader.lazyRequireGetter(
+ this,
+ "PropTypes",
+ "resource://devtools/client/shared/vendor/react-prop-types.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "MessageContainer",
+ "resource://devtools/client/webconsole/components/Output/MessageContainer.js",
+ true
+);
+loader.lazyRequireGetter(this, "flags", "resource://devtools/shared/flags.js");
+
+const {
+ MESSAGE_TYPE,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+class ConsoleOutput extends Component {
+ static get propTypes() {
+ return {
+ initialized: PropTypes.bool.isRequired,
+ mutableMessages: PropTypes.object.isRequired,
+ messageCount: PropTypes.number.isRequired,
+ messagesUi: PropTypes.array.isRequired,
+ disabledMessages: PropTypes.array.isRequired,
+ serviceContainer: PropTypes.shape({
+ attachRefToWebConsoleUI: PropTypes.func.isRequired,
+ openContextMenu: PropTypes.func.isRequired,
+ sourceMapURLService: PropTypes.object,
+ }),
+ dispatch: PropTypes.func.isRequired,
+ timestampsVisible: PropTypes.bool,
+ cssMatchingElements: PropTypes.object.isRequired,
+ messagesRepeat: PropTypes.object.isRequired,
+ warningGroups: PropTypes.object.isRequired,
+ networkMessagesUpdate: PropTypes.object.isRequired,
+ visibleMessages: PropTypes.array.isRequired,
+ networkMessageActiveTabId: PropTypes.string.isRequired,
+ onFirstMeaningfulPaint: PropTypes.func.isRequired,
+ editorMode: PropTypes.bool.isRequired,
+ cacheGeneration: PropTypes.number.isRequired,
+ disableVirtualization: PropTypes.bool,
+ lastMessageId: PropTypes.string.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.onContextMenu = this.onContextMenu.bind(this);
+ this.maybeScrollToBottom = this.maybeScrollToBottom.bind(this);
+ this.messageIdsToKeepAlive = new Set();
+ this.ref = createRef();
+ this.lazyMessageListRef = createRef();
+
+ this.resizeObserver = new ResizeObserver(entries => {
+ // If we don't have the outputNode reference, or if the outputNode isn't connected
+ // anymore, we disconnect the resize observer (componentWillUnmount is never called
+ // on this component, so we have to do it here).
+ if (!this.outputNode || !this.outputNode.isConnected) {
+ this.resizeObserver.disconnect();
+ return;
+ }
+
+ if (this.scrolledToBottom) {
+ this.scrollToBottom();
+ }
+ });
+ }
+
+ componentDidMount() {
+ if (this.props.disableVirtualization) {
+ return;
+ }
+
+ if (this.props.visibleMessages.length) {
+ this.scrollToBottom();
+ }
+
+ this.scrollDetectionIntersectionObserver = new IntersectionObserver(
+ entries => {
+ for (const entry of entries) {
+ // Consider that we're not pinned to the bottom anymore if the bottom of the
+ // scrollable area is within 10px of visible (half the typical element height.)
+ this.scrolledToBottom = entry.intersectionRatio > 0;
+ }
+ },
+ { root: this.outputNode, rootMargin: "10px" }
+ );
+
+ this.resizeObserver.observe(this.getElementToObserve());
+
+ const { serviceContainer, onFirstMeaningfulPaint, dispatch } = this.props;
+ serviceContainer.attachRefToWebConsoleUI(
+ "outputScroller",
+ this.ref.current
+ );
+
+ // Waiting for the next paint.
+ new Promise(res => requestAnimationFrame(res)).then(() => {
+ if (onFirstMeaningfulPaint) {
+ onFirstMeaningfulPaint();
+ }
+
+ // Dispatching on next tick so we don't block on action execution.
+ setTimeout(() => {
+ dispatch(initialize());
+ }, 0);
+ });
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillUpdate(nextProps, nextState) {
+ this.isUpdating = true;
+ if (nextProps.cacheGeneration !== this.props.cacheGeneration) {
+ this.messageIdsToKeepAlive = new Set();
+ }
+
+ if (nextProps.editorMode !== this.props.editorMode) {
+ this.resizeObserver.disconnect();
+ }
+
+ const { outputNode } = this;
+ if (!outputNode?.lastChild) {
+ // Force a scroll to bottom when messages are added to an empty console.
+ // This makes the console stay pinned to the bottom if a batch of messages
+ // are added after a page refresh (Bug 1402237).
+ this.shouldScrollBottom = true;
+ this.scrolledToBottom = true;
+ return;
+ }
+
+ const bottomBuffer = this.lazyMessageListRef.current.bottomBuffer;
+ this.scrollDetectionIntersectionObserver.unobserve(bottomBuffer);
+
+ const visibleMessagesDelta =
+ nextProps.visibleMessages.length - this.props.visibleMessages.length;
+ const messagesDelta = nextProps.messageCount - this.props.messageCount;
+ // Evaluation results are never filtered out, so if it's in the store, it will be
+ // visible in the output.
+ const isNewMessageEvaluationResult =
+ messagesDelta > 0 &&
+ nextProps.lastMessageId &&
+ nextProps.mutableMessages.get(nextProps.lastMessageId)?.type ===
+ MESSAGE_TYPE.RESULT;
+
+ // Use an inline function in order to avoid executing the expensive Array.some()
+ // unless condition are meant to do this additional check.
+ const isOpeningGroup = () => {
+ const messagesUiDelta =
+ nextProps.messagesUi.length - this.props.messagesUi.length;
+ return (
+ messagesUiDelta > 0 &&
+ nextProps.messagesUi.some(
+ id =>
+ !this.props.messagesUi.includes(id) &&
+ this.props.visibleMessages.includes(id) &&
+ nextProps.visibleMessages.includes(id)
+ )
+ );
+ };
+
+ // We need to scroll to the bottom if:
+ this.shouldScrollBottom =
+ // - we are reacting to "initialize" action, and we are already scrolled to the bottom
+ (!this.props.initialized &&
+ nextProps.initialized &&
+ this.scrolledToBottom) ||
+ // - the number of messages in the store changed and the new message is an evaluation
+ // result.
+ isNewMessageEvaluationResult ||
+ // - the number of messages displayed changed and we are already scrolled to the
+ // bottom, but not if we are reacting to a group opening.
+ (this.scrolledToBottom && visibleMessagesDelta > 0 && !isOpeningGroup());
+ }
+
+ componentDidUpdate(prevProps) {
+ this.isUpdating = false;
+ this.maybeScrollToBottom();
+ if (this?.outputNode?.lastChild) {
+ const bottomBuffer = this.lazyMessageListRef.current.bottomBuffer;
+ this.scrollDetectionIntersectionObserver.observe(bottomBuffer);
+ }
+
+ if (prevProps.editorMode !== this.props.editorMode) {
+ this.resizeObserver.observe(this.getElementToObserve());
+ }
+ }
+
+ get outputNode() {
+ return this.ref.current;
+ }
+
+ maybeScrollToBottom() {
+ if (this.outputNode && this.shouldScrollBottom) {
+ this.scrollToBottom();
+ }
+ }
+
+ // The maybeScrollToBottom callback we provide to messages needs to be a little bit more
+ // strict than the one we normally use, because they can potentially interrupt a user
+ // scroll (between when the intersection observer registers the scroll break and when
+ // a componentDidUpdate comes through to reconcile it.)
+ maybeScrollToBottomMessageCallback(index) {
+ if (
+ this.outputNode &&
+ this.shouldScrollBottom &&
+ this.scrolledToBottom &&
+ this.lazyMessageListRef.current?.isItemNearBottom(index)
+ ) {
+ this.scrollToBottom();
+ }
+ }
+
+ scrollToBottom() {
+ if (flags.testing && this.outputNode.hasAttribute("disable-autoscroll")) {
+ return;
+ }
+ if (this.outputNode.scrollHeight > this.outputNode.clientHeight) {
+ this.outputNode.scrollTop = this.outputNode.scrollHeight;
+ }
+
+ this.scrolledToBottom = true;
+ }
+
+ getElementToObserve() {
+ // In inline mode, we need to observe the output node parent, which contains both the
+ // output and the input, so we don't trigger the resizeObserver callback when only the
+ // output size changes (e.g. when a network request is expanded).
+ return this.props.editorMode
+ ? this.outputNode
+ : this.outputNode?.parentNode;
+ }
+
+ onContextMenu(e) {
+ this.props.serviceContainer.openContextMenu(e);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ render() {
+ const {
+ cacheGeneration,
+ dispatch,
+ visibleMessages,
+ disabledMessages,
+ mutableMessages,
+ messagesUi,
+ cssMatchingElements,
+ messagesRepeat,
+ warningGroups,
+ networkMessagesUpdate,
+ networkMessageActiveTabId,
+ serviceContainer,
+ timestampsVisible,
+ } = this.props;
+
+ const renderMessage = (messageId, index) => {
+ return createElement(MessageContainer, {
+ dispatch,
+ key: messageId,
+ messageId,
+ serviceContainer,
+ open: messagesUi.includes(messageId),
+ cssMatchingElements: cssMatchingElements.get(messageId),
+ timestampsVisible,
+ disabled: disabledMessages.includes(messageId),
+ repeat: messagesRepeat[messageId],
+ badge: warningGroups.has(messageId)
+ ? warningGroups.get(messageId).length
+ : null,
+ inWarningGroup:
+ warningGroups && warningGroups.size > 0
+ ? isMessageInWarningGroup(
+ mutableMessages.get(messageId),
+ visibleMessages
+ )
+ : false,
+ networkMessageUpdate: networkMessagesUpdate[messageId],
+ networkMessageActiveTabId,
+ getMessage: () => mutableMessages.get(messageId),
+ maybeScrollToBottom: () =>
+ this.maybeScrollToBottomMessageCallback(index),
+ // Whenever a node is expanded, we want to make sure we keep the
+ // message node alive so as to not lose the expanded state.
+ setExpanded: () => this.messageIdsToKeepAlive.add(messageId),
+ });
+ };
+
+ // scrollOverdrawCount tells the list to draw extra elements above and
+ // below the scrollport so that we can avoid flashes of blank space
+ // when scrolling. When `disableVirtualization` is passed we make it as large as the
+ // number of messages to render them all and effectively disabling virtualization (this
+ // should only be used for some actions that requires all the messages to be rendered
+ // in the DOM, like "Copy All Messages").
+ const scrollOverdrawCount = this.props.disableVirtualization
+ ? visibleMessages.length
+ : 20;
+
+ const attrs = {
+ className: "webconsole-output",
+ role: "main",
+ onContextMenu: this.onContextMenu,
+ ref: this.ref,
+ };
+ if (flags.testing) {
+ attrs["data-visible-messages"] = JSON.stringify(visibleMessages);
+ }
+ return dom.div(
+ attrs,
+ createElement(LazyMessageList, {
+ viewportRef: this.ref,
+ items: visibleMessages,
+ itemDefaultHeight: 21,
+ editorMode: this.props.editorMode,
+ scrollOverdrawCount,
+ ref: this.lazyMessageListRef,
+ renderItem: renderMessage,
+ itemsToKeepAlive: this.messageIdsToKeepAlive,
+ serviceContainer,
+ cacheGeneration,
+ shouldScrollBottom: () => this.shouldScrollBottom && this.isUpdating,
+ })
+ );
+ }
+}
+
+function mapStateToProps(state, props) {
+ const mutableMessages = getMutableMessagesById(state);
+ return {
+ initialized: state.ui.initialized,
+ cacheGeneration: state.ui.cacheGeneration,
+ // We need to compute this so lifecycle methods can compare the global message count
+ // on state change (since we can't do it with mutableMessagesById).
+ messageCount: mutableMessages.size,
+ mutableMessages,
+ lastMessageId: getLastMessageId(state),
+ visibleMessages: getVisibleMessages(state),
+ disabledMessages: getAllDisabledMessagesById(state),
+ messagesUi: getAllMessagesUiById(state),
+ cssMatchingElements: getAllCssMessagesMatchingElements(state),
+ messagesRepeat: getAllRepeatById(state),
+ warningGroups: getAllWarningGroupsById(state),
+ networkMessagesUpdate: getAllNetworkMessagesUpdateById(state),
+ timestampsVisible: state.ui.timestampsVisible,
+ networkMessageActiveTabId: state.ui.networkMessageActiveTabId,
+ };
+}
+
+module.exports = connect(mapStateToProps)(ConsoleOutput);
diff --git a/devtools/client/webconsole/components/Output/ConsoleTable.js b/devtools/client/webconsole/components/Output/ConsoleTable.js
new file mode 100644
index 0000000000..f41afce96d
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/ConsoleTable.js
@@ -0,0 +1,272 @@
+/* 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 {
+ Component,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const {
+ getArrayTypeNames,
+} = require("resource://devtools/shared/webconsole/messages.js");
+const {
+ l10n,
+ getDescriptorValue,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+loader.lazyGetter(this, "MODE", function () {
+ return require("resource://devtools/client/shared/components/reps/index.js")
+ .MODE;
+});
+
+const GripMessageBody = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/GripMessageBody.js")
+);
+
+loader.lazyRequireGetter(
+ this,
+ "PropTypes",
+ "resource://devtools/client/shared/vendor/react-prop-types.js"
+);
+
+const TABLE_ROW_MAX_ITEMS = 1000;
+// Match Chrome max column number.
+const TABLE_COLUMN_MAX_ITEMS = 21;
+
+class ConsoleTable extends Component {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ parameters: PropTypes.array.isRequired,
+ serviceContainer: PropTypes.object.isRequired,
+ id: PropTypes.string.isRequired,
+ setExpanded: PropTypes.func,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.getHeaders = this.getHeaders.bind(this);
+ this.getRows = this.getRows.bind(this);
+ }
+
+ getHeaders(columns) {
+ const headerItems = [];
+ columns.forEach((value, key) =>
+ headerItems.push(
+ dom.th(
+ {
+ key,
+ title: value,
+ },
+ value
+ )
+ )
+ );
+ return dom.thead({}, dom.tr({}, headerItems));
+ }
+
+ getRows(columns, items) {
+ const { dispatch, serviceContainer, setExpanded } = this.props;
+
+ const rows = [];
+ items.forEach((item, index) => {
+ const cells = [];
+
+ columns.forEach((value, key) => {
+ const cellValue = item[key];
+ const cellContent =
+ typeof cellValue === "undefined"
+ ? ""
+ : GripMessageBody({
+ grip: cellValue,
+ mode: MODE.SHORT,
+ useQuotes: false,
+ serviceContainer,
+ dispatch,
+ setExpanded,
+ });
+
+ cells.push(
+ dom.td(
+ {
+ key,
+ },
+ cellContent
+ )
+ );
+ });
+ rows.push(dom.tr({}, cells));
+ });
+ return dom.tbody({}, rows);
+ }
+
+ render() {
+ const { parameters } = this.props;
+ const { valueGrip, headersGrip } = getValueAndHeadersGrip(parameters);
+
+ const headers = headersGrip?.preview ? headersGrip.preview.items : null;
+
+ const data = valueGrip?.ownProperties;
+
+ // if we don't have any data, don't show anything.
+ if (!data) {
+ return null;
+ }
+
+ const dataType = getParametersDataType(parameters);
+ const { columns, items } = getTableItems(data, dataType, headers);
+
+ // We need to wrap the <table> in a div so we can have the max-height set properly
+ // without changing the table display.
+ return dom.div(
+ { className: "consoletable-wrapper" },
+ dom.table(
+ {
+ className: "consoletable",
+ },
+ this.getHeaders(columns),
+ this.getRows(columns, items)
+ )
+ );
+ }
+}
+
+function getValueAndHeadersGrip(parameters) {
+ const [valueFront, headersFront] = parameters;
+
+ const headersGrip = headersFront?.getGrip
+ ? headersFront.getGrip()
+ : headersFront;
+
+ const valueGrip = valueFront?.getGrip ? valueFront.getGrip() : valueFront;
+
+ return { valueGrip, headersGrip };
+}
+
+function getParametersDataType(parameters = null) {
+ if (!Array.isArray(parameters) || parameters.length === 0) {
+ return null;
+ }
+ const [firstParam] = parameters;
+ if (!firstParam || !firstParam.getGrip) {
+ return null;
+ }
+ const grip = firstParam.getGrip();
+ return grip.class;
+}
+
+const INDEX_NAME = "_index";
+const VALUE_NAME = "_value";
+
+function getNamedIndexes(type) {
+ return {
+ [INDEX_NAME]: getArrayTypeNames().concat("Object").includes(type)
+ ? l10n.getStr("table.index")
+ : l10n.getStr("table.iterationIndex"),
+ [VALUE_NAME]: l10n.getStr("table.value"),
+ key: l10n.getStr("table.key"),
+ };
+}
+
+function hasValidCustomHeaders(headers) {
+ return (
+ Array.isArray(headers) &&
+ headers.every(
+ header => typeof header === "string" || Number.isInteger(Number(header))
+ )
+ );
+}
+
+function getTableItems(data = {}, type, headers = null) {
+ const namedIndexes = getNamedIndexes(type);
+
+ let columns = new Map();
+ const items = [];
+
+ const addItem = function (item) {
+ items.push(item);
+ Object.keys(item).forEach(key => addColumn(key));
+ };
+
+ const validCustomHeaders = hasValidCustomHeaders(headers);
+
+ const addColumn = function (columnIndex) {
+ const columnExists = columns.has(columnIndex);
+ const hasMaxColumns = columns.size == TABLE_COLUMN_MAX_ITEMS;
+
+ if (
+ !columnExists &&
+ !hasMaxColumns &&
+ (!validCustomHeaders ||
+ headers.includes(columnIndex) ||
+ columnIndex === INDEX_NAME)
+ ) {
+ columns.set(columnIndex, namedIndexes[columnIndex] || columnIndex);
+ }
+ };
+
+ for (let [index, property] of Object.entries(data)) {
+ if (type !== "Object" && index == parseInt(index, 10)) {
+ index = parseInt(index, 10);
+ }
+
+ const item = {
+ [INDEX_NAME]: index,
+ };
+
+ const propertyValue = getDescriptorValue(property);
+ const propertyValueGrip = propertyValue?.getGrip
+ ? propertyValue.getGrip()
+ : propertyValue;
+
+ if (propertyValueGrip?.ownProperties) {
+ const entries = propertyValueGrip.ownProperties;
+ for (const [key, entry] of Object.entries(entries)) {
+ item[key] = getDescriptorValue(entry);
+ }
+ } else if (
+ propertyValueGrip?.preview &&
+ (type === "Map" || type === "WeakMap")
+ ) {
+ item.key = propertyValueGrip.preview.key;
+ item[VALUE_NAME] = propertyValueGrip.preview.value;
+ } else {
+ item[VALUE_NAME] = propertyValue;
+ }
+
+ addItem(item);
+
+ if (items.length === TABLE_ROW_MAX_ITEMS) {
+ break;
+ }
+ }
+
+ // Some headers might not be present in the items, so we make sure to
+ // return all the headers set by the user.
+ if (validCustomHeaders) {
+ headers.forEach(header => addColumn(header));
+ }
+
+ // We want to always have the index column first
+ if (columns.has(INDEX_NAME)) {
+ const index = columns.get(INDEX_NAME);
+ columns.delete(INDEX_NAME);
+ columns = new Map([[INDEX_NAME, index], ...columns.entries()]);
+ }
+
+ // We want to always have the values column last
+ if (columns.has(VALUE_NAME)) {
+ const index = columns.get(VALUE_NAME);
+ columns.delete(VALUE_NAME);
+ columns.set(VALUE_NAME, index);
+ }
+
+ return {
+ columns,
+ items,
+ };
+}
+
+module.exports = ConsoleTable;
diff --git a/devtools/client/webconsole/components/Output/GripMessageBody.js b/devtools/client/webconsole/components/Output/GripMessageBody.js
new file mode 100644
index 0000000000..6ecfe55b8e
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/GripMessageBody.js
@@ -0,0 +1,114 @@
+/* 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";
+
+// React
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ MESSAGE_TYPE,
+ JSTERM_COMMANDS,
+} = require("resource://devtools/client/webconsole/constants.js");
+const {
+ cleanupStyle,
+} = require("resource://devtools/client/shared/components/reps/reps/rep-utils.js");
+const {
+ getObjectInspector,
+} = require("resource://devtools/client/webconsole/utils/object-inspector.js");
+const actions = require("resource://devtools/client/webconsole/actions/index.js");
+
+loader.lazyGetter(this, "objectInspector", function () {
+ return require("resource://devtools/client/shared/components/reps/index.js")
+ .objectInspector;
+});
+
+loader.lazyGetter(this, "MODE", function () {
+ return require("resource://devtools/client/shared/components/reps/index.js")
+ .MODE;
+});
+
+GripMessageBody.displayName = "GripMessageBody";
+
+GripMessageBody.propTypes = {
+ grip: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number,
+ PropTypes.object,
+ ]).isRequired,
+ serviceContainer: PropTypes.shape({
+ createElement: PropTypes.func.isRequired,
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ }),
+ userProvidedStyle: PropTypes.string,
+ useQuotes: PropTypes.bool,
+ escapeWhitespace: PropTypes.bool,
+ type: PropTypes.string,
+ helperType: PropTypes.string,
+ maybeScrollToBottom: PropTypes.func,
+ setExpanded: PropTypes.func,
+};
+
+GripMessageBody.defaultProps = {
+ mode: MODE.LONG,
+};
+
+function GripMessageBody(props) {
+ const {
+ grip,
+ userProvidedStyle,
+ serviceContainer,
+ useQuotes,
+ escapeWhitespace,
+ mode = MODE.LONG,
+ dispatch,
+ maybeScrollToBottom,
+ setExpanded,
+ customFormat = false,
+ } = props;
+
+ let styleObject;
+ if (userProvidedStyle && userProvidedStyle !== "") {
+ styleObject = cleanupStyle(
+ userProvidedStyle,
+ serviceContainer.createElement
+ );
+ }
+
+ const objectInspectorProps = {
+ autoExpandDepth: shouldAutoExpandObjectInspector(props) ? 1 : 0,
+ mode,
+ maybeScrollToBottom,
+ setExpanded,
+ customFormat,
+ onCmdCtrlClick: (node, { depth, event, focused, expanded }) => {
+ const front = objectInspector.utils.node.getFront(node);
+ if (front) {
+ dispatch(actions.showObjectInSidebar(front));
+ }
+ },
+ };
+
+ if (
+ typeof grip === "string" ||
+ (grip && grip.type === "longString") ||
+ (grip?.getGrip && grip.getGrip().type === "longString")
+ ) {
+ Object.assign(objectInspectorProps, {
+ useQuotes,
+ transformEmptyString: true,
+ escapeWhitespace,
+ style: styleObject,
+ });
+ }
+
+ return getObjectInspector(grip, serviceContainer, objectInspectorProps);
+}
+
+function shouldAutoExpandObjectInspector(props) {
+ const { helperType, type } = props;
+
+ return type === MESSAGE_TYPE.DIR || helperType === JSTERM_COMMANDS.INSPECT;
+}
+
+module.exports = GripMessageBody;
diff --git a/devtools/client/webconsole/components/Output/LazyMessageList.js b/devtools/client/webconsole/components/Output/LazyMessageList.js
new file mode 100644
index 0000000000..931b5bb8bd
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/LazyMessageList.js
@@ -0,0 +1,393 @@
+/* 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/.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * MIT License
+ *
+ * Copyright (c) 2019 Oleg Grishechkin
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+"use strict";
+
+const {
+ Fragment,
+ Component,
+ createElement,
+ createRef,
+} = require("resource://devtools/client/shared/vendor/react.js");
+
+loader.lazyRequireGetter(
+ this,
+ "PropTypes",
+ "resource://devtools/client/shared/vendor/react-prop-types.js"
+);
+
+// This element is a webconsole optimization for handling large numbers of
+// console messages. The purpose is to only create DOM elements for messages
+// which are actually visible within the scrollport. This code was based on
+// Oleg Grishechkin's react-viewport-list element - however, it has been quite
+// heavily modified, to the point that it is mostly unrecognizable. The most
+// notable behavioral modification is that the list implements the behavior of
+// pinning the scrollport to the bottom of the scroll container.
+class LazyMessageList extends Component {
+ static get propTypes() {
+ return {
+ viewportRef: PropTypes.shape({ current: PropTypes.instanceOf(Element) })
+ .isRequired,
+ items: PropTypes.array.isRequired,
+ itemsToKeepAlive: PropTypes.shape({
+ has: PropTypes.func,
+ keys: PropTypes.func,
+ size: PropTypes.number,
+ }).isRequired,
+ editorMode: PropTypes.bool.isRequired,
+ itemDefaultHeight: PropTypes.number.isRequired,
+ scrollOverdrawCount: PropTypes.number.isRequired,
+ renderItem: PropTypes.func.isRequired,
+ shouldScrollBottom: PropTypes.func.isRequired,
+ cacheGeneration: PropTypes.number.isRequired,
+ serviceContainer: PropTypes.shape({
+ emitForTests: PropTypes.func.isRequired,
+ }),
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.#initialized = false;
+ this.#topBufferRef = createRef();
+ this.#bottomBufferRef = createRef();
+ this.#viewportHeight = window.innerHeight;
+ this.#startIndex = 0;
+ this.#resizeObserver = null;
+ this.#cachedHeights = [];
+
+ this.#scrollHandlerBinding = this.#scrollHandler.bind(this);
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillUpdate(nextProps, nextState) {
+ if (nextProps.cacheGeneration !== this.props.cacheGeneration) {
+ this.#cachedHeights = [];
+ this.#startIndex = 0;
+ } else if (
+ (this.props.shouldScrollBottom() &&
+ nextProps.items.length > this.props.items.length) ||
+ this.#startIndex > nextProps.items.length - this.#numItemsToDraw
+ ) {
+ this.#startIndex = Math.max(
+ 0,
+ nextProps.items.length - this.#numItemsToDraw
+ );
+ }
+ }
+
+ componentDidUpdate(prevProps) {
+ const { viewportRef, serviceContainer } = this.props;
+ if (!viewportRef.current || !this.#topBufferRef.current) {
+ return;
+ }
+
+ if (!this.#initialized) {
+ // We set these up from a one-time call in componentDidUpdate, rather than in
+ // componentDidMount, because we need the parent to be mounted first, to add
+ // listeners to it, and React orders things such that children mount before
+ // parents.
+ this.#addListeners();
+ }
+
+ if (!this.#initialized || prevProps.editorMode !== this.props.editorMode) {
+ this.#resizeObserver.observe(viewportRef.current);
+ }
+
+ this.#initialized = true;
+
+ // Since we updated, we're now going to compute the heights of all visible
+ // elements and store them in a cache. This allows us to get more accurate
+ // buffer regions to make scrolling correct when these elements no longer
+ // exist.
+ let index = this.#startIndex;
+ let element = this.#topBufferRef.current.nextSibling;
+ let elementRect = element?.getBoundingClientRect();
+ while (
+ Element.isInstance(element) &&
+ index < this.#clampedEndIndex &&
+ element !== this.#bottomBufferRef.current
+ ) {
+ const next = element.nextSibling;
+ const nextRect = next.getBoundingClientRect();
+ this.#cachedHeights[index] = nextRect.top - elementRect.top;
+ element = next;
+ elementRect = nextRect;
+ index++;
+ }
+
+ serviceContainer.emitForTests("lazy-message-list-updated-or-noop");
+ }
+
+ componentWillUnmount() {
+ this.#removeListeners();
+ }
+
+ #initialized;
+ #topBufferRef;
+ #bottomBufferRef;
+ #viewportHeight;
+ #startIndex;
+ #resizeObserver;
+ #cachedHeights;
+ #scrollHandlerBinding;
+
+ get #maxIndex() {
+ return this.props.items.length - 1;
+ }
+
+ get #overdrawHeight() {
+ return this.props.scrollOverdrawCount * this.props.itemDefaultHeight;
+ }
+
+ get #numItemsToDraw() {
+ const scrollingWindowCount = Math.ceil(
+ this.#viewportHeight / this.props.itemDefaultHeight
+ );
+ return scrollingWindowCount + 2 * this.props.scrollOverdrawCount;
+ }
+
+ get #unclampedEndIndex() {
+ return this.#startIndex + this.#numItemsToDraw;
+ }
+
+ // Since the "end index" is computed based off a fixed offset from the start
+ // index, it can exceed the length of our items array. This is just a helper
+ // to ensure we don't exceed that.
+ get #clampedEndIndex() {
+ return Math.min(this.#unclampedEndIndex, this.props.items.length);
+ }
+
+ /**
+ * Increases our start index until we've passed enough elements to cover
+ * the difference in px between where we are and where we want to be.
+ *
+ * @param Number startIndex
+ * The current value of our start index.
+ * @param Number deltaPx
+ * The difference in pixels between where we want to be and
+ * where we are.
+ * @return {Number} The new computed start index.
+ */
+ #increaseStartIndex(startIndex, deltaPx) {
+ for (let i = startIndex + 1; i < this.props.items.length; i++) {
+ deltaPx -= this.#cachedHeights[i];
+ startIndex = i;
+
+ if (deltaPx <= 0) {
+ break;
+ }
+ }
+ return startIndex;
+ }
+
+ /**
+ * Decreases our start index until we've passed enough elements to cover
+ * the difference in px between where we are and where we want to be.
+ *
+ * @param Number startIndex
+ * The current value of our start index.
+ * @param Number deltaPx
+ * The difference in pixels between where we want to be and
+ * where we are.
+ * @return {Number} The new computed start index.
+ */
+ #decreaseStartIndex(startIndex, diff) {
+ for (let i = startIndex - 1; i >= 0; i--) {
+ diff -= this.#cachedHeights[i];
+ startIndex = i;
+
+ if (diff <= 0) {
+ break;
+ }
+ }
+ return startIndex;
+ }
+
+ #scrollHandler() {
+ if (!this.props.viewportRef.current || !this.#topBufferRef.current) {
+ return;
+ }
+
+ const scrollportMin =
+ this.props.viewportRef.current.getBoundingClientRect().top -
+ this.#overdrawHeight;
+ const uppermostItemRect =
+ this.#topBufferRef.current.nextSibling.getBoundingClientRect();
+ const uppermostItemMin = uppermostItemRect.top;
+ const uppermostItemMax = uppermostItemRect.bottom;
+
+ let nextStartIndex = this.#startIndex;
+ const downwardPx = scrollportMin - uppermostItemMax;
+ const upwardPx = uppermostItemMin - scrollportMin;
+ if (downwardPx > 0) {
+ nextStartIndex = this.#increaseStartIndex(nextStartIndex, downwardPx);
+ } else if (upwardPx > 0) {
+ nextStartIndex = this.#decreaseStartIndex(nextStartIndex, upwardPx);
+ }
+
+ nextStartIndex = Math.max(
+ 0,
+ Math.min(nextStartIndex, this.props.items.length - this.#numItemsToDraw)
+ );
+
+ if (nextStartIndex !== this.#startIndex) {
+ this.#startIndex = nextStartIndex;
+ this.forceUpdate();
+ } else {
+ const { serviceContainer } = this.props;
+ serviceContainer.emitForTests("lazy-message-list-updated-or-noop");
+ }
+ }
+
+ #addListeners() {
+ const { viewportRef } = this.props;
+ viewportRef.current.addEventListener("scroll", this.#scrollHandlerBinding);
+ this.#resizeObserver = new ResizeObserver(entries => {
+ this.#viewportHeight =
+ viewportRef.current.parentNode.parentNode.clientHeight;
+ this.forceUpdate();
+ });
+ }
+
+ #removeListeners() {
+ const { viewportRef } = this.props;
+ this.#resizeObserver?.disconnect();
+ viewportRef.current?.removeEventListener(
+ "scroll",
+ this.#scrollHandlerBinding
+ );
+ }
+
+ get bottomBuffer() {
+ return this.#bottomBufferRef.current;
+ }
+
+ isItemNearBottom(index) {
+ return index >= this.props.items.length - this.#numItemsToDraw;
+ }
+
+ render() {
+ const { items, itemDefaultHeight, renderItem, itemsToKeepAlive } =
+ this.props;
+ if (!items.length) {
+ return createElement(Fragment, {
+ key: "LazyMessageList",
+ });
+ }
+
+ // Resize our cached heights to fit if necessary.
+ const countUncached = items.length - this.#cachedHeights.length;
+ if (countUncached > 0) {
+ // It would be lovely if javascript allowed us to resize an array in one
+ // go. I think this is the closest we can get to that. This in theory
+ // allows us to realloc, and doesn't require copying the whole original
+ // array like concat does.
+ this.#cachedHeights.push(...Array(countUncached).fill(itemDefaultHeight));
+ }
+
+ let topBufferHeight = 0;
+ let bottomBufferHeight = 0;
+ // We can't compute the bottom buffer height until the end, so we just
+ // store the index of where it needs to go.
+ let bottomBufferIndex = 0;
+ let currentChild = 0;
+ const startIndex = this.#startIndex;
+ const endIndex = this.#clampedEndIndex;
+ // We preallocate this array to avoid allocations in the loop. The minimum,
+ // and typical length for it is the size of the body plus 2 for the top and
+ // bottom buffers. It can be bigger due to itemsToKeepAlive, but we can't just
+ // add the size, since itemsToKeepAlive could in theory hold items which are
+ // not even in the list.
+ const children = new Array(endIndex - startIndex + 2);
+ const pushChild = c => {
+ if (currentChild >= children.length) {
+ children.push(c);
+ } else {
+ children[currentChild] = c;
+ }
+ return currentChild++;
+ };
+ for (let i = 0; i < items.length; i++) {
+ const itemId = items[i];
+ if (i < startIndex) {
+ if (i == 0 || itemsToKeepAlive.has(itemId)) {
+ // If this is our first item, and we wouldn't otherwise be rendering
+ // it, we want to ensure that it's at the beginning of our children
+ // array to ensure keyboard navigation functions properly.
+ pushChild(renderItem(itemId, i));
+ } else {
+ topBufferHeight += this.#cachedHeights[i];
+ }
+ } else if (i < endIndex) {
+ if (i == startIndex) {
+ pushChild(
+ createElement("div", {
+ key: "LazyMessageListTop",
+ className: "lazy-message-list-top",
+ ref: this.#topBufferRef,
+ style: { height: topBufferHeight },
+ })
+ );
+ }
+ pushChild(renderItem(itemId, i));
+ if (i == endIndex - 1) {
+ // We're just reserving the bottom buffer's spot in the children
+ // array here. We will create the actual element and assign it at
+ // this index after the loop.
+ bottomBufferIndex = pushChild(null);
+ }
+ } else if (i == items.length - 1 || itemsToKeepAlive.has(itemId)) {
+ // Similarly to the logic for our first item, we also want to ensure
+ // that our last item is always rendered as the last item in our
+ // children array.
+ pushChild(renderItem(itemId, i));
+ } else {
+ bottomBufferHeight += this.#cachedHeights[i];
+ }
+ }
+
+ children[bottomBufferIndex] = createElement("div", {
+ key: "LazyMessageListBottom",
+ className: "lazy-message-list-bottom",
+ ref: this.#bottomBufferRef,
+ style: { height: bottomBufferHeight },
+ });
+
+ return createElement(
+ Fragment,
+ {
+ key: "LazyMessageList",
+ },
+ children
+ );
+ }
+}
+
+module.exports = LazyMessageList;
diff --git a/devtools/client/webconsole/components/Output/Message.js b/devtools/client/webconsole/components/Output/Message.js
new file mode 100644
index 0000000000..ee65c8947a
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/Message.js
@@ -0,0 +1,482 @@
+/* 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";
+
+// React & Redux
+const {
+ Component,
+ createFactory,
+ createElement,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const {
+ l10n,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+const actions = require("resource://devtools/client/webconsole/actions/index.js");
+const {
+ MESSAGE_LEVEL,
+ MESSAGE_SOURCE,
+ MESSAGE_TYPE,
+} = require("resource://devtools/client/webconsole/constants.js");
+const {
+ MessageIndent,
+} = require("resource://devtools/client/webconsole/components/Output/MessageIndent.js");
+const MessageIcon = require("resource://devtools/client/webconsole/components/Output/MessageIcon.js");
+const FrameView = createFactory(
+ require("resource://devtools/client/shared/components/Frame.js")
+);
+
+loader.lazyRequireGetter(
+ this,
+ "CollapseButton",
+ "resource://devtools/client/webconsole/components/Output/CollapseButton.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "MessageRepeat",
+ "resource://devtools/client/webconsole/components/Output/MessageRepeat.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "PropTypes",
+ "resource://devtools/client/shared/vendor/react-prop-types.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "SmartTrace",
+ "resource://devtools/client/shared/components/SmartTrace.js"
+);
+
+class Message extends Component {
+ static get propTypes() {
+ return {
+ open: PropTypes.bool,
+ collapsible: PropTypes.bool,
+ collapseTitle: PropTypes.string,
+ disabled: PropTypes.bool,
+ onToggle: PropTypes.func,
+ source: PropTypes.string.isRequired,
+ type: PropTypes.string.isRequired,
+ level: PropTypes.string.isRequired,
+ indent: PropTypes.number.isRequired,
+ inWarningGroup: PropTypes.bool,
+ isBlockedNetworkMessage: PropTypes.bool,
+ topLevelClasses: PropTypes.array.isRequired,
+ messageBody: PropTypes.any.isRequired,
+ repeat: PropTypes.any,
+ frame: PropTypes.any,
+ attachment: PropTypes.any,
+ stacktrace: PropTypes.any,
+ messageId: PropTypes.string,
+ scrollToMessage: PropTypes.bool,
+ exceptionDocURL: PropTypes.string,
+ request: PropTypes.object,
+ dispatch: PropTypes.func,
+ timeStamp: PropTypes.number,
+ timestampsVisible: PropTypes.bool.isRequired,
+ serviceContainer: PropTypes.shape({
+ emitForTests: PropTypes.func.isRequired,
+ onViewSource: PropTypes.func.isRequired,
+ onViewSourceInDebugger: PropTypes.func,
+ onViewSourceInStyleEditor: PropTypes.func,
+ openContextMenu: PropTypes.func.isRequired,
+ openLink: PropTypes.func.isRequired,
+ sourceMapURLService: PropTypes.any,
+ preventStacktraceInitialRenderDelay: PropTypes.bool,
+ }),
+ notes: PropTypes.arrayOf(
+ PropTypes.shape({
+ messageBody: PropTypes.string.isRequired,
+ frame: PropTypes.any,
+ })
+ ),
+ maybeScrollToBottom: PropTypes.func,
+ message: PropTypes.object.isRequired,
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ indent: 0,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.onLearnMoreClick = this.onLearnMoreClick.bind(this);
+ this.toggleMessage = this.toggleMessage.bind(this);
+ this.onContextMenu = this.onContextMenu.bind(this);
+ this.renderIcon = this.renderIcon.bind(this);
+ }
+
+ componentDidMount() {
+ if (this.messageNode) {
+ if (this.props.scrollToMessage) {
+ this.messageNode.scrollIntoView();
+ }
+
+ this.emitNewMessage(this.messageNode);
+ }
+ }
+
+ componentDidCatch(e) {
+ this.setState({ error: e });
+ }
+
+ // Event used in tests. Some message types don't pass it in because existing tests
+ // did not emit for them.
+ emitNewMessage(node) {
+ const { serviceContainer, messageId, timeStamp } = this.props;
+ serviceContainer.emitForTests(
+ "new-messages",
+ new Set([{ node, messageId, timeStamp }])
+ );
+ }
+
+ onLearnMoreClick(e) {
+ const { exceptionDocURL } = this.props;
+ this.props.serviceContainer.openLink(exceptionDocURL, e);
+ e.preventDefault();
+ }
+
+ toggleMessage(e) {
+ // Don't bubble up to the main App component, which redirects focus to input,
+ // making difficult for screen reader users to review output
+ e.stopPropagation();
+ const { open, dispatch, messageId, onToggle, disabled } = this.props;
+
+ if (disabled) {
+ return;
+ }
+
+ // Early exit the function to avoid the message to collapse if the user is
+ // selecting a range in the toggle message.
+ const window = e.target.ownerDocument.defaultView;
+ if (window.getSelection && window.getSelection().type === "Range") {
+ return;
+ }
+
+ // If defined on props, we let the onToggle() method handle the toggling,
+ // otherwise we toggle the message open/closed ourselves.
+ if (onToggle) {
+ onToggle(messageId, e);
+ } else if (open) {
+ dispatch(actions.messageClose(messageId));
+ } else {
+ dispatch(actions.messageOpen(messageId));
+ }
+ }
+
+ onContextMenu(e) {
+ const { serviceContainer, source, request, messageId } = this.props;
+ const messageInfo = {
+ source,
+ request,
+ messageId,
+ };
+ serviceContainer.openContextMenu(e, messageInfo);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ renderIcon() {
+ const { level, inWarningGroup, isBlockedNetworkMessage, type, disabled } =
+ this.props;
+
+ if (inWarningGroup) {
+ return undefined;
+ }
+
+ if (disabled) {
+ return MessageIcon({
+ level: MESSAGE_LEVEL.INFO,
+ type,
+ title: l10n.getStr("webconsole.disableIcon.title"),
+ });
+ }
+
+ if (isBlockedNetworkMessage) {
+ return MessageIcon({
+ level: MESSAGE_LEVEL.ERROR,
+ type: "blockedReason",
+ });
+ }
+
+ return MessageIcon({
+ level,
+ type,
+ });
+ }
+
+ renderTimestamp() {
+ if (!this.props.timestampsVisible) {
+ return null;
+ }
+
+ return dom.span(
+ {
+ className: "timestamp devtools-monospace",
+ },
+ l10n.timestampString(this.props.timeStamp || Date.now())
+ );
+ }
+
+ renderErrorState() {
+ const newBugUrl =
+ "https://bugzilla.mozilla.org/enter_bug.cgi?product=DevTools&component=Console";
+ const timestampEl = this.renderTimestamp();
+
+ return dom.div(
+ {
+ className: "message error message-did-catch",
+ },
+ timestampEl,
+ MessageIcon({ level: "error" }),
+ dom.span(
+ { className: "message-body-wrapper" },
+ dom.span(
+ {
+ className: "message-flex-body",
+ },
+ // Add whitespaces for formatting when copying to the clipboard.
+ timestampEl ? " " : null,
+ dom.span(
+ { className: "message-body devtools-monospace" },
+ l10n.getFormatStr("webconsole.message.componentDidCatch.label", [
+ newBugUrl,
+ ]),
+ dom.button(
+ {
+ className: "devtools-button",
+ onClick: () =>
+ navigator.clipboard.writeText(
+ JSON.stringify(
+ this.props.message,
+ function (key, value) {
+ // The message can hold one or multiple fronts that we need to serialize
+ if (value?.getGrip) {
+ return value.getGrip();
+ }
+ return value;
+ },
+ 2
+ )
+ ),
+ },
+ l10n.getStr(
+ "webconsole.message.componentDidCatch.copyButton.label"
+ )
+ )
+ )
+ )
+ ),
+ dom.br()
+ );
+ }
+
+ // eslint-disable-next-line complexity
+ render() {
+ if (this.state && this.state.error) {
+ return this.renderErrorState();
+ }
+
+ const {
+ open,
+ collapsible,
+ collapseTitle,
+ disabled,
+ source,
+ type,
+ level,
+ indent,
+ inWarningGroup,
+ topLevelClasses,
+ messageBody,
+ frame,
+ stacktrace,
+ serviceContainer,
+ exceptionDocURL,
+ messageId,
+ notes,
+ } = this.props;
+
+ topLevelClasses.push("message", source, type, level);
+ if (open) {
+ topLevelClasses.push("open");
+ }
+
+ if (disabled) {
+ topLevelClasses.push("disabled");
+ }
+
+ const timestampEl = this.renderTimestamp();
+ const icon = this.renderIcon();
+
+ // Figure out if there is an expandable part to the message.
+ let attachment = null;
+ if (this.props.attachment) {
+ attachment = this.props.attachment;
+ } else if (stacktrace && open) {
+ const smartTraceAttributes = {
+ stacktrace,
+ onViewSourceInDebugger:
+ serviceContainer.onViewSourceInDebugger ||
+ serviceContainer.onViewSource,
+ onViewSource: serviceContainer.onViewSource,
+ onReady: this.props.maybeScrollToBottom,
+ sourceMapURLService: serviceContainer.sourceMapURLService,
+ };
+
+ if (serviceContainer.preventStacktraceInitialRenderDelay) {
+ smartTraceAttributes.initialRenderDelay = 0;
+ }
+
+ attachment = dom.div(
+ {
+ className: "stacktrace devtools-monospace",
+ },
+ createElement(SmartTrace, smartTraceAttributes)
+ );
+ }
+
+ // If there is an expandable part, make it collapsible.
+ let collapse = null;
+ if (collapsible && !disabled) {
+ collapse = createElement(CollapseButton, {
+ open,
+ title: collapseTitle,
+ onClick: this.toggleMessage,
+ });
+ }
+
+ let notesNodes;
+ if (notes) {
+ notesNodes = notes.map(note =>
+ dom.span(
+ { className: "message-flex-body error-note" },
+ dom.span(
+ { className: "message-body devtools-monospace" },
+ "note: " + note.messageBody
+ ),
+ dom.span(
+ { className: "message-location devtools-monospace" },
+ note.frame
+ ? FrameView({
+ frame: note.frame,
+ onClick: serviceContainer
+ ? serviceContainer.onViewSourceInDebugger ||
+ serviceContainer.onViewSource
+ : undefined,
+ showEmptyPathAsHost: true,
+ sourceMapURLService: serviceContainer
+ ? serviceContainer.sourceMapURLService
+ : undefined,
+ })
+ : null
+ )
+ )
+ );
+ } else {
+ notesNodes = [];
+ }
+
+ const repeat =
+ this.props.repeat && this.props.repeat > 1
+ ? createElement(MessageRepeat, { repeat: this.props.repeat })
+ : null;
+
+ let onFrameClick;
+ if (serviceContainer && frame) {
+ if (source === MESSAGE_SOURCE.CSS) {
+ onFrameClick =
+ serviceContainer.onViewSourceInStyleEditor ||
+ serviceContainer.onViewSource;
+ } else {
+ // Point everything else to debugger, if source not available,
+ // it will fall back to view-source.
+ onFrameClick =
+ serviceContainer.onViewSourceInDebugger ||
+ serviceContainer.onViewSource;
+ }
+ }
+
+ // Configure the location.
+ const location = frame
+ ? FrameView({
+ className: "message-location devtools-monospace",
+ frame,
+ onClick: onFrameClick,
+ showEmptyPathAsHost: true,
+ sourceMapURLService: serviceContainer
+ ? serviceContainer.sourceMapURLService
+ : undefined,
+ messageSource: source,
+ })
+ : null;
+
+ let learnMore;
+ if (exceptionDocURL) {
+ learnMore = dom.a(
+ {
+ className: "learn-more-link webconsole-learn-more-link",
+ href: exceptionDocURL,
+ title: exceptionDocURL.split("?")[0],
+ onClick: this.onLearnMoreClick,
+ },
+ `[${l10n.getStr("webConsoleMoreInfoLabel")}]`
+ );
+ }
+
+ const bodyElements = Array.isArray(messageBody)
+ ? messageBody
+ : [messageBody];
+
+ return dom.div(
+ {
+ className: topLevelClasses.join(" "),
+ onContextMenu: this.onContextMenu,
+ ref: node => {
+ this.messageNode = node;
+ },
+ "data-message-id": messageId,
+ "data-indent": indent || 0,
+ "aria-live": type === MESSAGE_TYPE.COMMAND ? "off" : "polite",
+ },
+ timestampEl,
+ MessageIndent({
+ indent,
+ inWarningGroup,
+ }),
+ this.props.isBlockedNetworkMessage ? collapse : icon,
+ this.props.isBlockedNetworkMessage ? icon : collapse,
+ dom.span(
+ { className: "message-body-wrapper" },
+ dom.span(
+ {
+ className: "message-flex-body",
+ onClick: collapsible ? this.toggleMessage : undefined,
+ },
+ // Add whitespaces for formatting when copying to the clipboard.
+ timestampEl ? " " : null,
+ dom.span(
+ { className: "message-body devtools-monospace" },
+ ...bodyElements,
+ learnMore
+ ),
+ repeat ? " " : null,
+ repeat,
+ " ",
+ location
+ ),
+ attachment,
+ ...notesNodes
+ ),
+ // If an attachment is displayed, the final newline is handled by the attachment.
+ attachment ? null : dom.br()
+ );
+ }
+}
+
+module.exports = Message;
diff --git a/devtools/client/webconsole/components/Output/MessageContainer.js b/devtools/client/webconsole/components/Output/MessageContainer.js
new file mode 100644
index 0000000000..f1140e80c3
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/MessageContainer.js
@@ -0,0 +1,131 @@
+/* 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";
+
+// React & Redux
+const {
+ Component,
+} = require("resource://devtools/client/shared/vendor/react.js");
+loader.lazyRequireGetter(
+ this,
+ "PropTypes",
+ "resource://devtools/client/shared/vendor/react-prop-types.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "isWarningGroup",
+ "resource://devtools/client/webconsole/utils/messages.js",
+ true
+);
+
+const {
+ MESSAGE_SOURCE,
+ MESSAGE_TYPE,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+const ConsoleApiCall = require("resource://devtools/client/webconsole/components/Output/message-types/ConsoleApiCall.js");
+const ConsoleCommand = require("resource://devtools/client/webconsole/components/Output/message-types/ConsoleCommand.js");
+const CSSWarning = require("resource://devtools/client/webconsole/components/Output/message-types/CSSWarning.js");
+const DefaultRenderer = require("resource://devtools/client/webconsole/components/Output/message-types/DefaultRenderer.js");
+const EvaluationResult = require("resource://devtools/client/webconsole/components/Output/message-types/EvaluationResult.js");
+const NavigationMarker = require("resource://devtools/client/webconsole/components/Output/message-types/NavigationMarker.js");
+const NetworkEventMessage = require("resource://devtools/client/webconsole/components/Output/message-types/NetworkEventMessage.js");
+const PageError = require("resource://devtools/client/webconsole/components/Output/message-types/PageError.js");
+const SimpleTable = require("resource://devtools/client/webconsole/components/Output/message-types/SimpleTable.js");
+const JSTracerTrace = require("resource://devtools/client/webconsole/components/Output/message-types/JSTracerTrace.js");
+const WarningGroup = require("resource://devtools/client/webconsole/components/Output/message-types/WarningGroup.js");
+
+class MessageContainer extends Component {
+ static get propTypes() {
+ return {
+ messageId: PropTypes.string.isRequired,
+ open: PropTypes.bool.isRequired,
+ serviceContainer: PropTypes.object.isRequired,
+ cssMatchingElements: PropTypes.object,
+ timestampsVisible: PropTypes.bool.isRequired,
+ repeat: PropTypes.number,
+ badge: PropTypes.number,
+ indent: PropTypes.number,
+ networkMessageUpdate: PropTypes.object,
+ getMessage: PropTypes.func.isRequired,
+ inWarningGroup: PropTypes.bool,
+ disabled: PropTypes.bool,
+ };
+ }
+
+ shouldComponentUpdate(nextProps) {
+ const triggeringUpdateProps = [
+ "repeat",
+ "open",
+ "cssMatchingElements",
+ "timestampsVisible",
+ "networkMessageUpdate",
+ "badge",
+ "inWarningGroup",
+ "disabled",
+ ];
+
+ return triggeringUpdateProps.some(
+ prop => this.props[prop] !== nextProps[prop]
+ );
+ }
+
+ render() {
+ const message = this.props.getMessage();
+
+ const MessageComponent = getMessageComponent(message);
+ return MessageComponent(Object.assign({ message }, this.props));
+ }
+}
+
+function getMessageComponent(message) {
+ if (!message) {
+ return DefaultRenderer;
+ }
+
+ switch (message.source) {
+ case MESSAGE_SOURCE.CONSOLE_API:
+ return ConsoleApiCall;
+ case MESSAGE_SOURCE.JSTRACER:
+ return JSTracerTrace;
+ case MESSAGE_SOURCE.NETWORK:
+ return NetworkEventMessage;
+ case MESSAGE_SOURCE.CSS:
+ return CSSWarning;
+ case MESSAGE_SOURCE.JAVASCRIPT:
+ switch (message.type) {
+ case MESSAGE_TYPE.COMMAND:
+ return ConsoleCommand;
+ case MESSAGE_TYPE.RESULT:
+ return EvaluationResult;
+ // @TODO this is probably not the right behavior, but works for now.
+ // Chrome doesn't distinguish between page errors and log messages. We
+ // may want to remove the PageError component and just handle errors
+ // with ConsoleApiCall.
+ case MESSAGE_TYPE.LOG:
+ return PageError;
+ default:
+ return DefaultRenderer;
+ }
+ case MESSAGE_SOURCE.CONSOLE_FRONTEND:
+ if (isWarningGroup(message)) {
+ return WarningGroup;
+ }
+ if (message.type === MESSAGE_TYPE.SIMPLE_TABLE) {
+ return SimpleTable;
+ }
+ if (message.type === MESSAGE_TYPE.NAVIGATION_MARKER) {
+ return NavigationMarker;
+ }
+ break;
+ }
+
+ return DefaultRenderer;
+}
+
+module.exports.MessageContainer = MessageContainer;
+
+// Exported so we can test it with unit tests.
+module.exports.getMessageComponent = getMessageComponent;
diff --git a/devtools/client/webconsole/components/Output/MessageIcon.js b/devtools/client/webconsole/components/Output/MessageIcon.js
new file mode 100644
index 0000000000..ed2765414e
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/MessageIcon.js
@@ -0,0 +1,78 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const {
+ l10n,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+const {
+ MESSAGE_TYPE,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+const l10nLevels = {
+ error: "level.error",
+ warn: "level.warn",
+ info: "level.info",
+ log: "level.log",
+ debug: "level.debug",
+ jstracer: "level.jstracer",
+};
+
+// Store common icons so they can be used without recreating the element
+// during render.
+const CONSTANT_ICONS = Object.entries(l10nLevels).reduce(
+ (acc, [key, l10nLabel]) => {
+ acc[key] = getIconElement(l10nLabel);
+ return acc;
+ },
+ {}
+);
+
+function getIconElement(level, type, title) {
+ title = title || l10n.getStr(l10nLevels[level] || level);
+ const classnames = ["icon"];
+
+ if (type === "logPoint") {
+ title = l10n.getStr("logpoint.title");
+ classnames.push("logpoint");
+ } else if (type === "blockedReason") {
+ title = l10n.getStr("blockedrequest.label");
+ } else if (type === MESSAGE_TYPE.COMMAND) {
+ title = l10n.getStr("command.title");
+ } else if (type === MESSAGE_TYPE.RESULT) {
+ title = l10n.getStr("result.title");
+ } else if (level == "level.jstracer" || type == MESSAGE_TYPE.JSTRACER) {
+ // Actual traces uses JSTracerTrace objects and `level` attribute,
+ // while log messages relating to tracing uses ConsoleApiCall, where level == "log" and so rather uses `type` attribute.
+ classnames.push("logtrace");
+ }
+
+ return dom.span({
+ className: classnames.join(" "),
+ title,
+ "aria-live": "off",
+ });
+}
+
+MessageIcon.displayName = "MessageIcon";
+MessageIcon.propTypes = {
+ level: PropTypes.string.isRequired,
+ type: PropTypes.string,
+ title: PropTypes.string,
+};
+
+function MessageIcon(props) {
+ const { level, type, title } = props;
+
+ if (type) {
+ return getIconElement(level, type, title);
+ }
+
+ return CONSTANT_ICONS[level] || getIconElement(level);
+}
+
+module.exports = MessageIcon;
diff --git a/devtools/client/webconsole/components/Output/MessageIndent.js b/devtools/client/webconsole/components/Output/MessageIndent.js
new file mode 100644
index 0000000000..cb77c5ef8c
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/MessageIndent.js
@@ -0,0 +1,47 @@
+/* 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 dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const INDENT_WIDTH = 12;
+
+// Store common indents so they can be used without recreating the element during render.
+const CONSTANT_INDENTS = [getIndentElement(1)];
+const IN_WARNING_GROUP_INDENT = getIndentElement(1, "warning-indent");
+
+function getIndentElement(indent, className) {
+ return dom.span({
+ className: `indent${className ? " " + className : ""}`,
+ style: {
+ width: indent * INDENT_WIDTH,
+ },
+ });
+}
+
+function MessageIndent(props) {
+ const { indent, inWarningGroup } = props;
+
+ if (inWarningGroup) {
+ return IN_WARNING_GROUP_INDENT;
+ }
+
+ if (!indent) {
+ return null;
+ }
+
+ return CONSTANT_INDENTS[indent] || getIndentElement(indent);
+}
+
+MessageIndent.propTypes = {
+ indent: PropTypes.number,
+ inWarningGroup: PropTypes.bool,
+};
+
+module.exports.MessageIndent = MessageIndent;
+
+// Exported so we can test it with unit tests.
+module.exports.INDENT_WIDTH = INDENT_WIDTH;
diff --git a/devtools/client/webconsole/components/Output/MessageRepeat.js b/devtools/client/webconsole/components/Output/MessageRepeat.js
new file mode 100644
index 0000000000..7bf846bcb0
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/MessageRepeat.js
@@ -0,0 +1,35 @@
+/* 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 PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const { PluralForm } = require("resource://devtools/shared/plural-form.js");
+const {
+ l10n,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+const messageRepeatsTooltip = l10n.getStr("messageRepeats.tooltip2");
+
+MessageRepeat.displayName = "MessageRepeat";
+
+MessageRepeat.propTypes = {
+ repeat: PropTypes.number.isRequired,
+};
+
+function MessageRepeat(props) {
+ const { repeat } = props;
+ return dom.span(
+ {
+ className: "message-repeats",
+ title: PluralForm.get(repeat, messageRepeatsTooltip).replace(
+ "#1",
+ repeat
+ ),
+ },
+ repeat
+ );
+}
+
+module.exports = MessageRepeat;
diff --git a/devtools/client/webconsole/components/Output/message-types/CSSWarning.js b/devtools/client/webconsole/components/Output/message-types/CSSWarning.js
new file mode 100644
index 0000000000..cef91c22be
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/CSSWarning.js
@@ -0,0 +1,173 @@
+/* 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 {
+ Component,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const {
+ l10n,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+const actions = require("resource://devtools/client/webconsole/actions/index.js");
+
+const Message = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/Message.js")
+);
+
+loader.lazyRequireGetter(
+ this,
+ "GripMessageBody",
+ "resource://devtools/client/webconsole/components/Output/GripMessageBody.js"
+);
+
+/**
+ * This component is responsible for rendering CSS warnings in the Console panel.
+ *
+ * CSS warnings are expandable when they have associated CSS selectors so the
+ * user can inspect any matching DOM elements. Not all CSS warnings have
+ * associated selectors (those that don't are not expandable) and not all
+ * selectors match elements in the current page (warnings can appear for styles
+ * which don't apply to the current page).
+ *
+ * @extends Component
+ */
+class CSSWarning extends Component {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ inWarningGroup: PropTypes.bool.isRequired,
+ message: PropTypes.object.isRequired,
+ open: PropTypes.bool,
+ cssMatchingElements: PropTypes.object,
+ repeat: PropTypes.any,
+ serviceContainer: PropTypes.object,
+ timestampsVisible: PropTypes.bool.isRequired,
+ setExpanded: PropTypes.func,
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ open: false,
+ };
+ }
+
+ static get displayName() {
+ return "CSSWarning";
+ }
+
+ constructor(props) {
+ super(props);
+ this.onToggle = this.onToggle.bind(this);
+ }
+
+ onToggle(messageId) {
+ const { dispatch, message, cssMatchingElements, open } = this.props;
+
+ if (open) {
+ dispatch(actions.messageClose(messageId));
+ } else if (cssMatchingElements) {
+ // If the message already has information about the elements matching
+ // the selectors associated with this CSS warning, just open the message.
+ dispatch(actions.messageOpen(messageId));
+ } else {
+ // Query the server for elements matching the CSS selectors associated
+ // with this CSS warning and populate the message's additional cssMatchingElements with
+ // the result. It's an async operation and potentially expensive, so we only do it
+ // on demand, once, when the component is first expanded.
+ dispatch(actions.messageGetMatchingElements(message));
+ dispatch(actions.messageOpen(messageId));
+ }
+ }
+
+ render() {
+ const {
+ dispatch,
+ message,
+ open,
+ cssMatchingElements,
+ repeat,
+ serviceContainer,
+ timestampsVisible,
+ inWarningGroup,
+ setExpanded,
+ } = this.props;
+
+ const {
+ id: messageId,
+ indent,
+ cssSelectors,
+ source,
+ type,
+ level,
+ messageText,
+ frame,
+ exceptionDocURL,
+ timeStamp,
+ notes,
+ } = message;
+
+ let messageBody;
+ if (typeof messageText === "string") {
+ messageBody = messageText;
+ } else if (
+ typeof messageText === "object" &&
+ messageText.type === "longString"
+ ) {
+ messageBody = `${message.messageText.initial}…`;
+ }
+
+ // Create a message attachment only when the message is open and there is a result
+ // to the query for elements matching the CSS selectors associated with the message.
+ const attachment =
+ open &&
+ cssMatchingElements !== undefined &&
+ dom.div(
+ { className: "devtools-monospace" },
+ dom.div(
+ { className: "elements-label" },
+ l10n.getFormatStr("webconsole.cssWarningElements.label", [
+ cssSelectors,
+ ])
+ ),
+ GripMessageBody({
+ dispatch,
+ escapeWhitespace: false,
+ grip: cssMatchingElements,
+ serviceContainer,
+ setExpanded,
+ })
+ );
+
+ return Message({
+ attachment,
+ collapsible: !!cssSelectors.length,
+ dispatch,
+ exceptionDocURL,
+ frame,
+ indent,
+ inWarningGroup,
+ level,
+ messageBody,
+ messageId,
+ notes,
+ open,
+ onToggle: this.onToggle,
+ repeat,
+ serviceContainer,
+ source,
+ timeStamp,
+ timestampsVisible,
+ topLevelClasses: [],
+ type,
+ message,
+ });
+ }
+}
+
+module.exports = createFactory(CSSWarning);
diff --git a/devtools/client/webconsole/components/Output/message-types/ConsoleApiCall.js b/devtools/client/webconsole/components/Output/message-types/ConsoleApiCall.js
new file mode 100644
index 0000000000..155075731f
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/ConsoleApiCall.js
@@ -0,0 +1,221 @@
+/* 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";
+
+// React & Redux
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const GripMessageBody = require("resource://devtools/client/webconsole/components/Output/GripMessageBody.js");
+const ConsoleTable = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/ConsoleTable.js")
+);
+const {
+ isGroupType,
+ l10n,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+
+const Message = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/Message.js")
+);
+
+ConsoleApiCall.displayName = "ConsoleApiCall";
+
+ConsoleApiCall.propTypes = {
+ dispatch: PropTypes.func.isRequired,
+ message: PropTypes.object.isRequired,
+ open: PropTypes.bool,
+ serviceContainer: PropTypes.object.isRequired,
+ timestampsVisible: PropTypes.bool.isRequired,
+ maybeScrollToBottom: PropTypes.func,
+};
+
+ConsoleApiCall.defaultProps = {
+ open: false,
+};
+
+function ConsoleApiCall(props) {
+ const {
+ dispatch,
+ message,
+ open,
+ serviceContainer,
+ timestampsVisible,
+ repeat,
+ maybeScrollToBottom,
+ setExpanded,
+ } = props;
+ const {
+ id: messageId,
+ indent,
+ source,
+ type,
+ level,
+ stacktrace,
+ frame,
+ timeStamp,
+ parameters,
+ messageText,
+ prefix,
+ userProvidedStyles,
+ } = message;
+
+ let messageBody;
+ const messageBodyConfig = {
+ dispatch,
+ messageId,
+ parameters,
+ userProvidedStyles,
+ serviceContainer,
+ type,
+ maybeScrollToBottom,
+ setExpanded,
+ // When the object is a parameter of a console.dir call, we always want to show its
+ // properties, like regular object (i.e. not showing the DOM tree for an Element, or
+ // only showing the message + stacktrace for Error object).
+ customFormat: type !== "dir",
+ };
+
+ if (type === "trace") {
+ const traceParametersBody =
+ Array.isArray(parameters) && parameters.length
+ ? [" "].concat(formatReps(messageBodyConfig))
+ : [];
+
+ messageBody = [
+ dom.span({ className: "cm-variable" }, "console.trace()"),
+ ...traceParametersBody,
+ ];
+ } else if (type === "assert") {
+ const reps = formatReps(messageBodyConfig);
+ messageBody = dom.span({}, "Assertion failed: ", reps);
+ } else if (type === "table") {
+ // TODO: Chrome does not output anything, see if we want to keep this
+ messageBody = dom.span({ className: "cm-variable" }, "console.table()");
+ } else if (parameters) {
+ messageBody = formatReps(messageBodyConfig);
+ if (prefix) {
+ messageBody.unshift(
+ dom.span(
+ {
+ className: "console-message-prefix",
+ },
+ `${prefix}: `
+ )
+ );
+ }
+ } else if (typeof messageText === "string") {
+ messageBody = messageText;
+ } else if (messageText) {
+ messageBody = GripMessageBody({
+ dispatch,
+ messageId,
+ grip: messageText,
+ serviceContainer,
+ useQuotes: false,
+ transformEmptyString: true,
+ setExpanded,
+ type,
+ });
+ }
+
+ let attachment = null;
+ if (type === "table") {
+ attachment = ConsoleTable({
+ dispatch,
+ id: message.id,
+ serviceContainer,
+ parameters: message.parameters,
+ });
+ }
+
+ let collapseTitle = null;
+ if (isGroupType(type)) {
+ collapseTitle = l10n.getStr("groupToggle");
+ }
+
+ const collapsible =
+ isGroupType(type) || (type === "error" && Array.isArray(stacktrace));
+ const topLevelClasses = ["cm-s-mozilla"];
+
+ return Message({
+ messageId,
+ open,
+ collapsible,
+ collapseTitle,
+ source,
+ type,
+ level,
+ topLevelClasses,
+ messageBody,
+ repeat,
+ frame,
+ stacktrace,
+ attachment,
+ serviceContainer,
+ dispatch,
+ indent,
+ timeStamp,
+ timestampsVisible,
+ parameters,
+ message,
+ maybeScrollToBottom,
+ });
+}
+
+function formatReps(options = {}) {
+ const {
+ dispatch,
+ loadedObjectProperties,
+ loadedObjectEntries,
+ messageId,
+ parameters,
+ serviceContainer,
+ userProvidedStyles,
+ type,
+ maybeScrollToBottom,
+ setExpanded,
+ customFormat,
+ } = options;
+
+ const elements = [];
+ const parametersLength = parameters.length;
+ for (let i = 0; i < parametersLength; i++) {
+ elements.push(
+ GripMessageBody({
+ dispatch,
+ messageId,
+ grip: parameters[i],
+ key: i,
+ userProvidedStyle: userProvidedStyles ? userProvidedStyles[i] : null,
+ serviceContainer,
+ useQuotes: false,
+ loadedObjectProperties,
+ loadedObjectEntries,
+ type,
+ maybeScrollToBottom,
+ setExpanded,
+ customFormat,
+ })
+ );
+
+ // We need to interleave a space if we are not on the last element AND
+ // if we are not between 2 messages with user provided style.
+ if (
+ i !== parametersLength - 1 &&
+ (!userProvidedStyles ||
+ userProvidedStyles[i] === undefined ||
+ userProvidedStyles[i + 1] === undefined)
+ ) {
+ elements.push(" ");
+ }
+ }
+
+ return elements;
+}
+
+module.exports = ConsoleApiCall;
diff --git a/devtools/client/webconsole/components/Output/message-types/ConsoleCommand.js b/devtools/client/webconsole/components/Output/message-types/ConsoleCommand.js
new file mode 100644
index 0000000000..5cfb87113c
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/ConsoleCommand.js
@@ -0,0 +1,105 @@
+/* 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";
+
+// React & Redux
+const {
+ createElement,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const { ELLIPSIS } = require("resource://devtools/shared/l10n.js");
+const Message = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/Message.js")
+);
+
+ConsoleCommand.displayName = "ConsoleCommand";
+
+ConsoleCommand.propTypes = {
+ message: PropTypes.object.isRequired,
+ timestampsVisible: PropTypes.bool.isRequired,
+ serviceContainer: PropTypes.object,
+ maybeScrollToBottom: PropTypes.func,
+ open: PropTypes.bool,
+};
+
+ConsoleCommand.defaultProps = {
+ open: false,
+};
+
+/**
+ * Displays input from the console.
+ */
+function ConsoleCommand(props) {
+ const {
+ message,
+ timestampsVisible,
+ serviceContainer,
+ maybeScrollToBottom,
+ dispatch,
+ open,
+ } = props;
+
+ const { indent, source, type, level, timeStamp, id: messageId } = message;
+
+ const messageText = trimCode(message.messageText);
+ const messageLines = messageText.split("\n");
+
+ const collapsible = messageLines.length > 5;
+
+ // Show only first 5 lines if its collapsible and closed
+ const visibleMessageText =
+ collapsible && !open
+ ? `${messageLines.slice(0, 5).join("\n")}${ELLIPSIS}`
+ : messageText;
+
+ // This uses a Custom Element to syntax highlight when possible. If it's not
+ // (no CodeMirror editor), then it will just render text.
+ const messageBody = createElement(
+ "syntax-highlighted",
+ null,
+ visibleMessageText
+ );
+
+ // Enable collapsing the code if it has multiple lines
+
+ return Message({
+ messageId,
+ source,
+ type,
+ level,
+ topLevelClasses: [],
+ messageBody,
+ collapsible,
+ open,
+ dispatch,
+ serviceContainer,
+ indent,
+ timeStamp,
+ timestampsVisible,
+ maybeScrollToBottom,
+ message,
+ });
+}
+
+module.exports = ConsoleCommand;
+
+/**
+ * Trim user input to avoid blank lines before and after messages
+ */
+function trimCode(input) {
+ if (typeof input !== "string") {
+ return input;
+ }
+
+ // Trim on both edges if we have a single line of content
+ if (input.trim().includes("\n") === false) {
+ return input.trim();
+ }
+
+ // For multiline input we want to keep the indentation of the first line
+ // with non-whitespace, so we can't .trim()/.trimStart().
+ return input.replace(/^\s*\n/, "").trimEnd();
+}
diff --git a/devtools/client/webconsole/components/Output/message-types/DefaultRenderer.js b/devtools/client/webconsole/components/Output/message-types/DefaultRenderer.js
new file mode 100644
index 0000000000..893e6b04c6
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/DefaultRenderer.js
@@ -0,0 +1,15 @@
+/* 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 dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+DefaultRenderer.displayName = "DefaultRenderer";
+
+function DefaultRenderer(props) {
+ return dom.div({}, "This message type is not supported yet.");
+}
+
+module.exports = DefaultRenderer;
diff --git a/devtools/client/webconsole/components/Output/message-types/EvaluationResult.js b/devtools/client/webconsole/components/Output/message-types/EvaluationResult.js
new file mode 100644
index 0000000000..60d44a9f99
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/EvaluationResult.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";
+
+// React & Redux
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const Message = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/Message.js")
+);
+const GripMessageBody = require("resource://devtools/client/webconsole/components/Output/GripMessageBody.js");
+
+EvaluationResult.displayName = "EvaluationResult";
+
+EvaluationResult.propTypes = {
+ dispatch: PropTypes.func.isRequired,
+ message: PropTypes.object.isRequired,
+ timestampsVisible: PropTypes.bool.isRequired,
+ serviceContainer: PropTypes.object,
+ maybeScrollToBottom: PropTypes.func,
+ open: PropTypes.bool,
+};
+
+EvaluationResult.defaultProps = {
+ open: false,
+};
+
+function EvaluationResult(props) {
+ const {
+ dispatch,
+ message,
+ serviceContainer,
+ timestampsVisible,
+ maybeScrollToBottom,
+ open,
+ setExpanded,
+ } = props;
+
+ const {
+ source,
+ type,
+ helperType,
+ level,
+ id: messageId,
+ indent,
+ hasException,
+ exceptionDocURL,
+ stacktrace,
+ frame,
+ timeStamp,
+ parameters,
+ notes,
+ } = message;
+
+ let messageBody;
+ if (
+ typeof message.messageText !== "undefined" &&
+ message.messageText !== null
+ ) {
+ const messageText = message.messageText?.getGrip
+ ? message.messageText.getGrip()
+ : message.messageText;
+ if (typeof messageText === "string") {
+ messageBody = messageText;
+ } else if (
+ typeof messageText === "object" &&
+ messageText.type === "longString"
+ ) {
+ messageBody = `${messageText.initial}…`;
+ }
+ } else {
+ messageBody = [];
+ if (hasException) {
+ messageBody.push("Uncaught ");
+ }
+ messageBody.push(
+ GripMessageBody({
+ dispatch,
+ messageId,
+ grip: parameters[0],
+ key: "grip",
+ serviceContainer,
+ useQuotes: !hasException,
+ escapeWhitespace: false,
+ type,
+ helperType,
+ maybeScrollToBottom,
+ setExpanded,
+ customFormat: true,
+ })
+ );
+ }
+
+ const topLevelClasses = ["cm-s-mozilla"];
+
+ return Message({
+ dispatch,
+ source,
+ type,
+ level,
+ indent,
+ topLevelClasses,
+ messageBody,
+ messageId,
+ serviceContainer,
+ exceptionDocURL,
+ stacktrace,
+ collapsible: Array.isArray(stacktrace),
+ open,
+ frame,
+ timeStamp,
+ parameters,
+ notes,
+ timestampsVisible,
+ maybeScrollToBottom,
+ message,
+ });
+}
+
+module.exports = EvaluationResult;
diff --git a/devtools/client/webconsole/components/Output/message-types/JSTracerTrace.js b/devtools/client/webconsole/components/Output/message-types/JSTracerTrace.js
new file mode 100644
index 0000000000..7d6ddd2623
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/JSTracerTrace.js
@@ -0,0 +1,174 @@
+/* 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";
+
+// React & Redux
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const GripMessageBody = require("resource://devtools/client/webconsole/components/Output/GripMessageBody.js");
+
+const {
+ MESSAGE_TYPE,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+const Message = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/Message.js")
+);
+
+JSTracerTrace.displayName = "JSTracerTrace";
+
+JSTracerTrace.propTypes = {
+ dispatch: PropTypes.func.isRequired,
+ message: PropTypes.object.isRequired,
+ serviceContainer: PropTypes.object.isRequired,
+ timestampsVisible: PropTypes.bool.isRequired,
+ maybeScrollToBottom: PropTypes.func,
+};
+
+function JSTracerTrace(props) {
+ const {
+ dispatch,
+ message,
+ serviceContainer,
+ timestampsVisible,
+ repeat,
+ maybeScrollToBottom,
+ setExpanded,
+ } = props;
+
+ const {
+ // List of common attributes for all tracer messages
+ timeStamp,
+ prefix,
+ depth,
+ source,
+
+ // Attribute specific to DOM event
+ eventName,
+
+ // Attributes specific to function calls
+ frame,
+ implementation,
+ displayName,
+ parameters,
+
+ // Attributes specific to function call returns
+ returnedValue,
+ relatedTraceId,
+ // See tracer.jsm FRAME_EXIT_REASONS
+ why,
+ } = message;
+
+ // When we are logging a DOM event, we have the `eventName` defined.
+ let messageBody;
+ if (eventName) {
+ messageBody = [dom.span({ className: "jstracer-dom-event" }, eventName)];
+ } else if (typeof relatedTraceId == "number") {
+ messageBody = [
+ dom.span({ className: "jstracer-io" }, "⟵ "),
+ dom.span({ className: "jstracer-display-name" }, displayName),
+ ];
+ } else {
+ messageBody = [
+ dom.span({ className: "jstracer-io" }, "⟶ "),
+ dom.span({ className: "jstracer-implementation" }, implementation),
+ // Add a space in order to improve copy paste rendering
+ dom.span({ className: "jstracer-display-name" }, " " + displayName),
+ ];
+ }
+
+ let messageBodyConfig;
+ if (parameters || why) {
+ messageBodyConfig = {
+ dispatch,
+ serviceContainer,
+ maybeScrollToBottom,
+ setExpanded,
+ type: "",
+ useQuotes: true,
+
+ // Disable custom formatter for now in traces
+ customFormat: false,
+ };
+ }
+ // Arguments will only be passed on-demand
+
+ if (parameters) {
+ messageBody.push("(", ...formatReps(messageBodyConfig, parameters), ")");
+ }
+ // Returned value will also only be passed on-demand
+ if (why) {
+ messageBody.push(
+ // Add a spaces in order to improve copy paste rendering
+ dom.span({ className: "jstracer-exit-frame-reason" }, " " + why + " "),
+ formatRep(messageBodyConfig, returnedValue)
+ );
+ }
+
+ if (prefix) {
+ messageBody.unshift(
+ dom.span(
+ {
+ className: "console-message-prefix",
+ },
+ `${prefix}`
+ )
+ );
+ }
+
+ const topLevelClasses = ["cm-s-mozilla"];
+
+ return Message({
+ collapsible: false,
+ source,
+ level: MESSAGE_TYPE.JSTRACER,
+ topLevelClasses,
+ messageBody,
+ repeat,
+ frame,
+ stacktrace: null,
+ attachment: null,
+ serviceContainer,
+ dispatch,
+ indent: depth,
+ timeStamp,
+ timestampsVisible,
+ parameters,
+ message,
+ maybeScrollToBottom,
+ });
+}
+
+/**
+ * Generated the list of GripMessageBody for a list of objects.
+ * GripMessageBody is Rep's rendering for a given Object, via its object actor's front.
+ */
+function formatReps(messageBodyConfig, objects) {
+ const elements = [];
+ const length = objects.length;
+ for (let i = 0; i < length; i++) {
+ elements.push(formatRep(messageBodyConfig, objects[i], i));
+
+ // We need to interleave a comma if we are not on the last element
+ if (i !== length - 1) {
+ elements.push(", ");
+ }
+ }
+
+ return elements;
+}
+
+function formatRep(messageBodyConfig, grip, key) {
+ return GripMessageBody({
+ ...messageBodyConfig,
+ grip,
+ key,
+ });
+}
+
+module.exports = JSTracerTrace;
diff --git a/devtools/client/webconsole/components/Output/message-types/NavigationMarker.js b/devtools/client/webconsole/components/Output/message-types/NavigationMarker.js
new file mode 100644
index 0000000000..7d14206a6a
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/NavigationMarker.js
@@ -0,0 +1,62 @@
+/* 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";
+
+// React & Redux
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const Message = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/Message.js")
+);
+
+NavigationMarker.displayName = "NavigationMarker";
+
+NavigationMarker.propTypes = {
+ dispatch: PropTypes.func.isRequired,
+ message: PropTypes.object.isRequired,
+ serviceContainer: PropTypes.object.isRequired,
+ timestampsVisible: PropTypes.bool.isRequired,
+ maybeScrollToBottom: PropTypes.func,
+};
+
+function NavigationMarker(props) {
+ const {
+ dispatch,
+ message,
+ serviceContainer,
+ timestampsVisible,
+ maybeScrollToBottom,
+ } = props;
+ const {
+ id: messageId,
+ indent,
+ source,
+ type,
+ level,
+ timeStamp,
+ messageText,
+ } = message;
+
+ return Message({
+ messageId,
+ source,
+ type,
+ level,
+ messageBody: messageText,
+ serviceContainer,
+ dispatch,
+ indent,
+ timeStamp,
+ timestampsVisible,
+ topLevelClasses: [],
+ message,
+ maybeScrollToBottom,
+ });
+}
+
+module.exports = NavigationMarker;
diff --git a/devtools/client/webconsole/components/Output/message-types/NetworkEventMessage.js b/devtools/client/webconsole/components/Output/message-types/NetworkEventMessage.js
new file mode 100644
index 0000000000..ce0961668b
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/NetworkEventMessage.js
@@ -0,0 +1,243 @@
+/* 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";
+
+// React & Redux
+const {
+ createFactory,
+ createElement,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const Message = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/Message.js")
+);
+const actions = require("resource://devtools/client/webconsole/actions/index.js");
+const {
+ isMessageNetworkError,
+ l10n,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+
+loader.lazyRequireGetter(
+ this,
+ "TabboxPanel",
+ "resource://devtools/client/netmonitor/src/components/TabboxPanel.js"
+);
+const {
+ getHTTPStatusCodeURL,
+} = require("resource://devtools/client/netmonitor/src/utils/doc-utils.js");
+const {
+ getUnicodeUrl,
+} = require("resource://devtools/client/shared/unicode-url.js");
+loader.lazyRequireGetter(
+ this,
+ "BLOCKED_REASON_MESSAGES",
+ "resource://devtools/client/netmonitor/src/constants.js",
+ true
+);
+
+const LEARN_MORE = l10n.getStr("webConsoleMoreInfoLabel");
+
+const isMacOS = Services.appinfo.OS === "Darwin";
+
+NetworkEventMessage.displayName = "NetworkEventMessage";
+
+NetworkEventMessage.propTypes = {
+ message: PropTypes.object.isRequired,
+ serviceContainer: PropTypes.shape({
+ openNetworkPanel: PropTypes.func.isRequired,
+ resendNetworkRequest: PropTypes.func.isRequired,
+ }),
+ timestampsVisible: PropTypes.bool.isRequired,
+ networkMessageUpdate: PropTypes.object.isRequired,
+};
+
+/**
+ * This component is responsible for rendering network messages
+ * in the Console panel.
+ *
+ * Network logs are expandable and the user can inspect it inline
+ * within the Console panel (no need to switch to the Network panel).
+ *
+ * HTTP details are rendered using `TabboxPanel` component used to
+ * render contents of the side bar in the Network panel.
+ *
+ * All HTTP details data are fetched from the backend on-demand
+ * when the user is expanding network log for the first time.
+ */
+function NetworkEventMessage({
+ message = {},
+ serviceContainer,
+ timestampsVisible,
+ networkMessageUpdate = {},
+ networkMessageActiveTabId,
+ dispatch,
+ open,
+ disabled,
+}) {
+ const {
+ id,
+ indent,
+ source,
+ type,
+ level,
+ url,
+ method,
+ isXHR,
+ timeStamp,
+ blockedReason,
+ httpVersion,
+ status,
+ statusText,
+ totalTime,
+ } = message;
+
+ const topLevelClasses = ["cm-s-mozilla"];
+ if (isMessageNetworkError(message)) {
+ topLevelClasses.push("error");
+ }
+
+ let statusCode, statusInfo;
+
+ if (
+ httpVersion &&
+ status &&
+ statusText !== undefined &&
+ totalTime !== undefined
+ ) {
+ const statusCodeDocURL = getHTTPStatusCodeURL(
+ status.toString(),
+ "webconsole"
+ );
+ statusCode = dom.span(
+ {
+ className: "status-code",
+ "data-code": status,
+ title: LEARN_MORE,
+ onClick: e => {
+ e.stopPropagation();
+ e.preventDefault();
+ serviceContainer.openLink(statusCodeDocURL, e);
+ },
+ },
+ status
+ );
+ statusInfo = dom.span(
+ { className: "status-info" },
+ `[${httpVersion} `,
+ statusCode,
+ ` ${statusText} ${totalTime}ms]`
+ );
+ }
+
+ if (blockedReason) {
+ statusInfo = dom.span(
+ { className: "status-info" },
+ BLOCKED_REASON_MESSAGES[blockedReason]
+ );
+ topLevelClasses.push("network-message-blocked");
+ }
+
+ // Message body components.
+ const requestMethod = dom.span({ className: "method" }, method);
+ const xhr = isXHR
+ ? dom.span({ className: "xhr" }, l10n.getStr("webConsoleXhrIndicator"))
+ : null;
+ const unicodeURL = getUnicodeUrl(url);
+ const requestUrl = dom.a(
+ {
+ className: "url",
+ title: unicodeURL,
+ href: url,
+ onClick: e => {
+ // The href of the <a> is the actual URL, so we need to prevent the navigation
+ // within the console panel.
+ // We only want to handle Ctrl/Cmd + click to open the link in a new tab.
+ e.preventDefault();
+ const shouldOpenLink =
+ (isMacOS && e.metaKey) || (!isMacOS && e.ctrlKey);
+ if (shouldOpenLink) {
+ e.stopPropagation();
+ serviceContainer.openLink(url, e);
+ }
+ },
+ },
+ unicodeURL
+ );
+ const statusBody = statusInfo
+ ? dom.a({ className: "status" }, statusInfo)
+ : null;
+
+ const messageBody = [xhr, requestMethod, requestUrl, statusBody];
+
+ // API consumed by Net monitor UI components. Most of the method
+ // are not needed in context of the Console panel (atm) and thus
+ // let's just provide empty implementation.
+ // Individual methods might be implemented step by step as needed.
+ const connector = {
+ viewSourceInDebugger: (srcUrl, line, column) => {
+ serviceContainer.onViewSourceInDebugger({ url: srcUrl, line, column });
+ },
+ getLongString: grip => {
+ return serviceContainer.getLongString(grip);
+ },
+ triggerActivity: () => {},
+ requestData: (requestId, dataType) => {
+ return serviceContainer.requestData(requestId, dataType);
+ },
+ };
+
+ // Only render the attachment if the network-event is
+ // actually opened (performance optimization) and its not disabled.
+ const attachment =
+ open &&
+ !disabled &&
+ dom.div(
+ {
+ className: "network-info network-monitor",
+ },
+ createElement(TabboxPanel, {
+ connector,
+ activeTabId: networkMessageActiveTabId,
+ request: networkMessageUpdate,
+ sourceMapURLService: serviceContainer.sourceMapURLService,
+ openLink: serviceContainer.openLink,
+ selectTab: tabId => {
+ dispatch(actions.selectNetworkMessageTab(tabId));
+ },
+ openNetworkDetails: enabled => {
+ if (!enabled) {
+ dispatch(actions.messageClose(id));
+ }
+ },
+ hideToggleButton: true,
+ showMessagesView: false,
+ })
+ );
+
+ const request = { url, method };
+ return Message({
+ dispatch,
+ messageId: id,
+ source,
+ type,
+ level,
+ indent,
+ collapsible: true,
+ open,
+ disabled,
+ attachment,
+ topLevelClasses,
+ timeStamp,
+ messageBody,
+ serviceContainer,
+ request,
+ timestampsVisible,
+ isBlockedNetworkMessage: !!blockedReason,
+ message,
+ });
+}
+
+module.exports = NetworkEventMessage;
diff --git a/devtools/client/webconsole/components/Output/message-types/PageError.js b/devtools/client/webconsole/components/Output/message-types/PageError.js
new file mode 100644
index 0000000000..01828e968e
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/PageError.js
@@ -0,0 +1,130 @@
+/* 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";
+
+// React & Redux
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const Message = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/Message.js")
+);
+const GripMessageBody = require("resource://devtools/client/webconsole/components/Output/GripMessageBody.js");
+loader.lazyGetter(this, "REPS", function () {
+ return require("resource://devtools/client/shared/components/reps/index.js")
+ .REPS;
+});
+loader.lazyGetter(this, "MODE", function () {
+ return require("resource://devtools/client/shared/components/reps/index.js")
+ .MODE;
+});
+
+PageError.displayName = "PageError";
+
+PageError.propTypes = {
+ message: PropTypes.object.isRequired,
+ open: PropTypes.bool,
+ timestampsVisible: PropTypes.bool.isRequired,
+ serviceContainer: PropTypes.object,
+ maybeScrollToBottom: PropTypes.func,
+ setExpanded: PropTypes.func,
+ inWarningGroup: PropTypes.bool.isRequired,
+};
+
+PageError.defaultProps = {
+ open: false,
+};
+
+function PageError(props) {
+ const {
+ dispatch,
+ message,
+ open,
+ repeat,
+ serviceContainer,
+ timestampsVisible,
+ maybeScrollToBottom,
+ setExpanded,
+ inWarningGroup,
+ } = props;
+ const {
+ id: messageId,
+ source,
+ type,
+ level,
+ messageText,
+ stacktrace,
+ frame,
+ exceptionDocURL,
+ timeStamp,
+ notes,
+ parameters,
+ hasException,
+ isPromiseRejection,
+ } = message;
+
+ const messageBody = [];
+
+ const repsProps = {
+ useQuotes: false,
+ escapeWhitespace: false,
+ openLink: serviceContainer.openLink,
+ };
+
+ if (hasException) {
+ const prefix = `Uncaught${isPromiseRejection ? " (in promise)" : ""} `;
+ messageBody.push(
+ prefix,
+ GripMessageBody({
+ key: "body",
+ dispatch,
+ messageId,
+ grip: parameters[0],
+ serviceContainer,
+ type,
+ customFormat: true,
+ maybeScrollToBottom,
+ setExpanded,
+ ...repsProps,
+ })
+ );
+ } else {
+ messageBody.push(
+ REPS.StringRep.rep({
+ key: "bodytext",
+ object: messageText,
+ mode: MODE.LONG,
+ ...repsProps,
+ })
+ );
+ }
+
+ return Message({
+ dispatch,
+ messageId,
+ open,
+ collapsible: Array.isArray(stacktrace),
+ source,
+ type,
+ level,
+ topLevelClasses: [],
+ indent: message.indent,
+ inWarningGroup,
+ messageBody,
+ repeat,
+ frame,
+ stacktrace,
+ serviceContainer,
+ exceptionDocURL,
+ timeStamp,
+ notes,
+ timestampsVisible,
+ maybeScrollToBottom,
+ message,
+ });
+}
+
+module.exports = PageError;
diff --git a/devtools/client/webconsole/components/Output/message-types/SimpleTable.js b/devtools/client/webconsole/components/Output/message-types/SimpleTable.js
new file mode 100644
index 0000000000..35580e8163
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/SimpleTable.js
@@ -0,0 +1,130 @@
+/* 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 {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const GripMessageBody = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/GripMessageBody.js")
+);
+
+loader.lazyRequireGetter(
+ this,
+ "PropTypes",
+ "resource://devtools/client/shared/vendor/react-prop-types.js"
+);
+
+loader.lazyGetter(this, "MODE", function () {
+ return require("resource://devtools/client/shared/components/reps/index.js")
+ .MODE;
+});
+
+const Message = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/Message.js")
+);
+
+SimpleTable.displayName = "SimpleTable";
+
+SimpleTable.propTypes = {
+ columns: PropTypes.object.isRequired,
+ items: PropTypes.array.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ serviceContainer: PropTypes.object.isRequired,
+ message: PropTypes.object.isRequired,
+ timestampsVisible: PropTypes.bool.isRequired,
+ open: PropTypes.bool,
+};
+
+function SimpleTable(props) {
+ const { dispatch, message, serviceContainer, timestampsVisible, open } =
+ props;
+
+ const {
+ source,
+ type,
+ level,
+ id: messageId,
+ indent,
+ timeStamp,
+ columns,
+ items,
+ } = message;
+
+ // if we don't have any data, don't show anything.
+ if (!items.length) {
+ return null;
+ }
+ const headerItems = [];
+ columns.forEach((value, key) =>
+ headerItems.push(
+ dom.th(
+ {
+ key,
+ title: value,
+ },
+ value
+ )
+ )
+ );
+
+ const rowItems = items.map((item, index) => {
+ const cells = [];
+
+ columns.forEach((_, key) => {
+ const cellValue = item[key];
+ const cellContent =
+ typeof cellValue === "undefined"
+ ? ""
+ : GripMessageBody({
+ grip: cellValue,
+ mode: MODE.SHORT,
+ useQuotes: false,
+ serviceContainer,
+ dispatch,
+ });
+
+ cells.push(
+ dom.td(
+ {
+ key,
+ },
+ cellContent
+ )
+ );
+ });
+ return dom.tr({ key: index }, cells);
+ });
+
+ const attachment = dom.table(
+ {
+ className: "simple-table",
+ role: "grid",
+ },
+ dom.thead({}, dom.tr({ className: "simple-table-header" }, headerItems)),
+ dom.tbody({}, rowItems)
+ );
+
+ const topLevelClasses = ["cm-s-mozilla"];
+ return Message({
+ attachment,
+ dispatch,
+ indent,
+ level,
+ messageId,
+ open,
+ serviceContainer,
+ source,
+ timeStamp,
+ timestampsVisible,
+ topLevelClasses,
+ type,
+ message,
+ messageBody: [],
+ });
+}
+
+module.exports = SimpleTable;
diff --git a/devtools/client/webconsole/components/Output/message-types/WarningGroup.js b/devtools/client/webconsole/components/Output/message-types/WarningGroup.js
new file mode 100644
index 0000000000..d54976dbcd
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/WarningGroup.js
@@ -0,0 +1,80 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// React & Redux
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const Message = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/Message.js")
+);
+
+const { PluralForm } = require("resource://devtools/shared/plural-form.js");
+const {
+ l10n,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+const messageCountTooltip = l10n.getStr(
+ "webconsole.warningGroup.messageCount.tooltip"
+);
+
+WarningGroup.displayName = "WarningGroup";
+
+WarningGroup.propTypes = {
+ dispatch: PropTypes.func.isRequired,
+ message: PropTypes.object.isRequired,
+ timestampsVisible: PropTypes.bool.isRequired,
+ serviceContainer: PropTypes.object,
+ badge: PropTypes.number.isRequired,
+};
+
+function WarningGroup(props) {
+ const {
+ dispatch,
+ message,
+ serviceContainer,
+ timestampsVisible,
+ badge,
+ open,
+ } = props;
+
+ const { source, type, level, id: messageId, indent, timeStamp } = message;
+
+ const messageBody = [
+ message.messageText,
+ " ",
+ dom.span(
+ {
+ className: "warning-group-badge",
+ title: PluralForm.get(badge, messageCountTooltip).replace("#1", badge),
+ },
+ badge
+ ),
+ ];
+ const topLevelClasses = ["cm-s-mozilla"];
+
+ return Message({
+ badge,
+ collapsible: true,
+ dispatch,
+ indent,
+ level,
+ messageBody,
+ messageId,
+ open,
+ serviceContainer,
+ source,
+ timeStamp,
+ timestampsVisible,
+ topLevelClasses,
+ type,
+ message,
+ });
+}
+
+module.exports = WarningGroup;
diff --git a/devtools/client/webconsole/components/Output/message-types/moz.build b/devtools/client/webconsole/components/Output/message-types/moz.build
new file mode 100644
index 0000000000..926eb80abe
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/message-types/moz.build
@@ -0,0 +1,18 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ "ConsoleApiCall.js",
+ "ConsoleCommand.js",
+ "CSSWarning.js",
+ "DefaultRenderer.js",
+ "EvaluationResult.js",
+ "JSTracerTrace.js",
+ "NavigationMarker.js",
+ "NetworkEventMessage.js",
+ "PageError.js",
+ "SimpleTable.js",
+ "WarningGroup.js",
+)
diff --git a/devtools/client/webconsole/components/Output/moz.build b/devtools/client/webconsole/components/Output/moz.build
new file mode 100644
index 0000000000..9844c2fdeb
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/moz.build
@@ -0,0 +1,21 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ "message-types",
+]
+
+DevToolsModules(
+ "CollapseButton.js",
+ "ConsoleOutput.js",
+ "ConsoleTable.js",
+ "GripMessageBody.js",
+ "LazyMessageList.js",
+ "Message.js",
+ "MessageContainer.js",
+ "MessageIcon.js",
+ "MessageIndent.js",
+ "MessageRepeat.js",
+)
diff --git a/devtools/client/webconsole/components/SideBar.js b/devtools/client/webconsole/components/SideBar.js
new file mode 100644
index 0000000000..e29d7dbbed
--- /dev/null
+++ b/devtools/client/webconsole/components/SideBar.js
@@ -0,0 +1,128 @@
+/* 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 {
+ Component,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const GridElementWidthResizer = createFactory(
+ require("resource://devtools/client/shared/components/splitter/GridElementWidthResizer.js")
+);
+loader.lazyRequireGetter(
+ this,
+ "dom",
+ "resource://devtools/client/shared/vendor/react-dom-factories.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "getObjectInspector",
+ "resource://devtools/client/webconsole/utils/object-inspector.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "actions",
+ "resource://devtools/client/webconsole/actions/index.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "PropTypes",
+ "resource://devtools/client/shared/vendor/react-prop-types.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "reps",
+ "resource://devtools/client/shared/components/reps/index.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "l10n",
+ "resource://devtools/client/webconsole/utils/messages.js",
+ true
+);
+
+class SideBar extends Component {
+ static get propTypes() {
+ return {
+ serviceContainer: PropTypes.object,
+ dispatch: PropTypes.func.isRequired,
+ front: PropTypes.object,
+ onResized: PropTypes.func,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.onClickSidebarClose = this.onClickSidebarClose.bind(this);
+ }
+
+ shouldComponentUpdate(nextProps) {
+ const { front } = nextProps;
+ return front !== this.props.front;
+ }
+
+ onClickSidebarClose() {
+ this.props.dispatch(actions.sidebarClose());
+ }
+
+ render() {
+ const { front, serviceContainer } = this.props;
+
+ const objectInspector = getObjectInspector(front, serviceContainer, {
+ autoExpandDepth: 1,
+ mode: reps.MODE.SHORT,
+ autoFocusRoot: true,
+ pathPrefix: "WebConsoleSidebar",
+ customFormat: false,
+ });
+
+ return [
+ dom.aside(
+ {
+ className: "sidebar",
+ key: "sidebar",
+ ref: node => {
+ this.node = node;
+ },
+ },
+ dom.header(
+ {
+ className: "devtools-toolbar webconsole-sidebar-toolbar",
+ },
+ dom.button({
+ className: "devtools-button sidebar-close-button",
+ title: l10n.getStr("webconsole.closeSidebarButton.tooltip"),
+ onClick: this.onClickSidebarClose,
+ })
+ ),
+ dom.aside(
+ {
+ className: "sidebar-contents",
+ },
+ objectInspector
+ )
+ ),
+ GridElementWidthResizer({
+ key: "resizer",
+ enabled: true,
+ position: "start",
+ className: "sidebar-resizer",
+ getControlledElementNode: () => this.node,
+ }),
+ ];
+ }
+}
+
+function mapStateToProps(state, props) {
+ return {
+ front: state.ui.frontInSidebar,
+ };
+}
+
+module.exports = connect(mapStateToProps)(SideBar);
diff --git a/devtools/client/webconsole/components/moz.build b/devtools/client/webconsole/components/moz.build
new file mode 100644
index 0000000000..0b9eac77a5
--- /dev/null
+++ b/devtools/client/webconsole/components/moz.build
@@ -0,0 +1,15 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ "FilterBar",
+ "Input",
+ "Output",
+]
+
+DevToolsModules(
+ "App.js",
+ "SideBar.js",
+)
diff --git a/devtools/client/webconsole/constants.js b/devtools/client/webconsole/constants.js
new file mode 100644
index 0000000000..7d44666e4f
--- /dev/null
+++ b/devtools/client/webconsole/constants.js
@@ -0,0 +1,231 @@
+/* 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 actionTypes = {
+ APPEND_NOTIFICATION: "APPEND_NOTIFICATION",
+ APPEND_TO_HISTORY: "APPEND_TO_HISTORY",
+ AUTOCOMPLETE_CLEAR: "AUTOCOMPLETE_CLEAR",
+ AUTOCOMPLETE_DATA_RECEIVE: "AUTOCOMPLETE_DATA_RECEIVE",
+ AUTOCOMPLETE_PENDING_REQUEST: "AUTOCOMPLETE_PENDING_REQUEST",
+ AUTOCOMPLETE_RETRIEVE_FROM_CACHE: "AUTOCOMPLETE_RETRIEVE_FROM_CACHE",
+ AUTOCOMPLETE_TOGGLE: "AUTOCOMPLETE_TOGGLE",
+ BATCH_ACTIONS: "BATCH_ACTIONS",
+ CLEAR_HISTORY: "CLEAR_HISTORY",
+ ENABLE_NETWORK_MONITORING: "ENABLE_NETWORK_MONITORING",
+ EDITOR_TOGGLE: "EDITOR_TOGGLE",
+ EDITOR_ONBOARDING_DISMISS: "EDITOR_ONBOARDING_DISMISS",
+ EDITOR_PRETTY_PRINT: "EDITOR_PRETTY_PRINT",
+ EVALUATE_EXPRESSION: "EVALUATE_EXPRESSION",
+ SET_TERMINAL_INPUT: "SET_TERMINAL_INPUT",
+ SET_TERMINAL_EAGER_RESULT: "SET_TERMINAL_EAGER_RESULT",
+ FILTER_TEXT_SET: "FILTER_TEXT_SET",
+ FILTER_TOGGLE: "FILTER_TOGGLE",
+ FILTERS_CLEAR: "FILTERS_CLEAR",
+ FILTERBAR_DISPLAY_MODE_SET: "FILTERBAR_DISPLAY_MODE_SET",
+ HISTORY_LOADED: "HISTORY_LOADED",
+ INITIALIZE: "INITIALIZE",
+ MESSAGE_CLOSE: "MESSAGE_CLOSE",
+ MESSAGE_OPEN: "MESSAGE_OPEN",
+ CSS_MESSAGE_ADD_MATCHING_ELEMENTS: "CSS_MESSAGE_ADD_MATCHING_ELEMENTS",
+ MESSAGE_REMOVE: "MESSAGE_REMOVE",
+ MESSAGES_ADD: "MESSAGES_ADD",
+ MESSAGES_CLEAR: "MESSAGES_CLEAR",
+ MESSAGES_DISABLE: "MESSAGES_DISABLE",
+ NETWORK_MESSAGES_UPDATE: "NETWORK_MESSAGES_UPDATE",
+ NETWORK_UPDATES_REQUEST: "NETWORK_UPDATES_REQUEST",
+ PERSIST_TOGGLE: "PERSIST_TOGGLE",
+ PRIVATE_MESSAGES_CLEAR: "PRIVATE_MESSAGES_CLEAR",
+ REMOVE_NOTIFICATION: "REMOVE_NOTIFICATION",
+ FRONTS_TO_RELEASE_CLEAR: "FRONTS_TO_RELEASE_CLEAR",
+ REVERSE_SEARCH_INPUT_TOGGLE: "REVERSE_SEARCH_INPUT_TOGGLE",
+ SELECT_NETWORK_MESSAGE_TAB: "SELECT_NETWORK_MESSAGE_TAB",
+ SHOW_OBJECT_IN_SIDEBAR: "SHOW_OBJECT_IN_SIDEBAR",
+ SIDEBAR_CLOSE: "SIDEBAR_CLOSE",
+ SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE: "SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE",
+ SHOW_EVALUATION_NOTIFICATION: "SHOW_EVALUATION_NOTIFICATION",
+ TARGET_MESSAGES_REMOVE: "TARGET_MESSAGES_REMOVE",
+ TIMESTAMPS_TOGGLE: "TIMESTAMPS_TOGGLE",
+ UPDATE_HISTORY_POSITION: "UPDATE_HISTORY_POSITION",
+ REVERSE_SEARCH_INPUT_CHANGE: "REVERSE_SEARCH_INPUT_CHANGE",
+ REVERSE_SEARCH_NEXT: "REVERSE_SEARCH_NEXT",
+ REVERSE_SEARCH_BACK: "REVERSE_SEARCH_BACK",
+ EAGER_EVALUATION_TOGGLE: "EAGER_EVALUATION_TOGGLE",
+ WARNING_GROUPS_TOGGLE: "WARNING_GROUPS_TOGGLE",
+ WILL_NAVIGATE: "WILL_NAVIGATE",
+ EDITOR_SET_WIDTH: "EDITOR_SET_WIDTH",
+};
+
+const prefs = {
+ PREFS: {
+ // Filter preferences only have the suffix since they can be used either for the
+ // webconsole or the browser console.
+ FILTER: {
+ ERROR: "filter.error",
+ WARN: "filter.warn",
+ INFO: "filter.info",
+ LOG: "filter.log",
+ DEBUG: "filter.debug",
+ CSS: "filter.css",
+ NET: "filter.net",
+ NETXHR: "filter.netxhr",
+ },
+ UI: {
+ // Persist is only used by the webconsole.
+ PERSIST: "devtools.webconsole.persistlog",
+ // Max number of entries in history list.
+ INPUT_HISTORY_COUNT: "devtools.webconsole.inputHistoryCount",
+ // Is editor mode enabled.
+ EDITOR: "input.editor",
+ // Display timestamp in messages.
+ MESSAGE_TIMESTAMP: "devtools.webconsole.timestampMessages",
+ // Store the editor width.
+ EDITOR_WIDTH: "input.editorWidth",
+ // Show the Editor onboarding UI
+ EDITOR_ONBOARDING: "devtools.webconsole.input.editorOnboarding",
+ // Show the Input Context the selector
+ CONTEXT_SELECTOR: "devtools.webconsole.input.context",
+ // Persist the "enable network monitoring" option
+ ENABLE_NETWORK_MONITORING:
+ "devtools.browserconsole.enableNetworkMonitoring",
+ },
+ FEATURES: {
+ // We use the same pref to enable the sidebar on webconsole and browser console.
+ SIDEBAR_TOGGLE: "devtools.webconsole.sidebarToggle",
+ AUTOCOMPLETE: "devtools.webconsole.input.autocomplete",
+ EAGER_EVALUATION: "devtools.webconsole.input.eagerEvaluation",
+ GROUP_WARNINGS: "devtools.webconsole.groupWarningMessages",
+ },
+ },
+};
+
+const FILTERS = {
+ CSS: "css",
+ DEBUG: "debug",
+ ERROR: "error",
+ INFO: "info",
+ LOG: "log",
+ NET: "net",
+ NETXHR: "netxhr",
+ TEXT: "text",
+ WARN: "warn",
+};
+
+const DEFAULT_FILTERS_VALUES = {
+ [FILTERS.TEXT]: "",
+ [FILTERS.ERROR]: true,
+ [FILTERS.WARN]: true,
+ [FILTERS.LOG]: true,
+ [FILTERS.INFO]: true,
+ [FILTERS.DEBUG]: true,
+ [FILTERS.CSS]: false,
+ [FILTERS.NET]: false,
+ [FILTERS.NETXHR]: false,
+};
+
+const DEFAULT_FILTERS = Object.keys(DEFAULT_FILTERS_VALUES).filter(
+ filter => DEFAULT_FILTERS_VALUES[filter] !== false
+);
+
+const chromeRDPEnums = {
+ MESSAGE_SOURCE: {
+ XML: "xml",
+ CSS: "css",
+ JAVASCRIPT: "javascript",
+ NETWORK: "network",
+ CONSOLE_API: "console-api",
+ // Messages emitted by the console frontend itself (i.e. similar messages grouping
+ // header).
+ CONSOLE_FRONTEND: "console-frontend",
+ STORAGE: "storage",
+ APPCACHE: "appcache",
+ RENDERING: "rendering",
+ SECURITY: "security",
+ OTHER: "other",
+ DEPRECATION: "deprecation",
+ // Related to JavaScript Tracer
+ JSTRACER: "jstracer",
+ },
+ MESSAGE_TYPE: {
+ LOG: "log",
+ DIR: "dir",
+ TABLE: "table",
+ // Related to console.trace() (and not the JavaScript Tracer)
+ TRACE: "trace",
+ CLEAR: "clear",
+ START_GROUP: "startGroup",
+ START_GROUP_COLLAPSED: "startGroupCollapsed",
+ END_GROUP: "endGroup",
+ CONTENT_BLOCKING_GROUP: "contentBlockingWarningGroup",
+ STORAGE_ISOLATION_GROUP: "storageIsolationWarningGroup",
+ TRACKING_PROTECTION_GROUP: "trackingProtectionWarningGroup",
+ COOKIE_SAMESITE_GROUP: "cookieSameSiteGroup",
+ CORS_GROUP: "CORSWarningGroup",
+ CSP_GROUP: "CSPWarningGroup",
+ ASSERT: "assert",
+ DEBUG: "debug",
+ PROFILE: "profile",
+ PROFILE_END: "profileEnd",
+ // Undocumented in Chrome RDP, but is used for evaluation results.
+ RESULT: "result",
+ // Undocumented in Chrome RDP, but is used for input.
+ COMMAND: "command",
+ // Undocumented in Chrome RDP, but is used for messages that should not
+ // output anything (e.g. `console.time()` calls).
+ NULL_MESSAGE: "nullMessage",
+ NAVIGATION_MARKER: "navigationMarker",
+ SIMPLE_TABLE: "simpleTable",
+ JSTRACER: "jstracer",
+ },
+ MESSAGE_LEVEL: {
+ LOG: "log",
+ ERROR: "error",
+ WARN: "warn",
+ DEBUG: "debug",
+ INFO: "info",
+ },
+};
+
+const jstermCommands = {
+ JSTERM_COMMANDS: {
+ INSPECT: "inspectObject",
+ },
+};
+
+// Constants used for defining the direction of JSTerm input history navigation.
+const historyCommands = {
+ HISTORY_BACK: -1,
+ HISTORY_FORWARD: 1,
+};
+
+const urls = {
+ // URL opened when executing `:help` command in the input
+ HELP_URL:
+ "https://firefox-source-docs.mozilla.org/devtools-user/web_console/helpers/",
+};
+
+const evaluationNotifications = {
+ ORIGINAL_VARIABLE_MAPPING: "originalVariableMapping",
+};
+
+// Combine into a single constants object
+module.exports = Object.assign(
+ {
+ FILTERS,
+ DEFAULT_FILTERS,
+ DEFAULT_FILTERS_VALUES,
+ FILTERBAR_DISPLAY_MODES: {
+ NARROW: "narrow",
+ WIDE: "wide",
+ },
+ },
+ actionTypes,
+ chromeRDPEnums,
+ jstermCommands,
+ prefs,
+ historyCommands,
+ urls,
+ historyCommands,
+ evaluationNotifications
+);
diff --git a/devtools/client/webconsole/enhancers/actor-releaser.js b/devtools/client/webconsole/enhancers/actor-releaser.js
new file mode 100644
index 0000000000..222de2b083
--- /dev/null
+++ b/devtools/client/webconsole/enhancers/actor-releaser.js
@@ -0,0 +1,56 @@
+/* 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 {
+ MESSAGES_ADD,
+ MESSAGES_CLEAR,
+ PRIVATE_MESSAGES_CLEAR,
+ FRONTS_TO_RELEASE_CLEAR,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+/**
+ * This enhancer is responsible for releasing actors on the backend.
+ * When messages with arguments are removed from the store we should also
+ * clean up the backend.
+ */
+function enableActorReleaser(webConsoleUI) {
+ return next => (reducer, initialState, enhancer) => {
+ function releaseActorsEnhancer(state, action) {
+ state = reducer(state, action);
+
+ const { type } = action;
+ if (
+ webConsoleUI &&
+ [MESSAGES_ADD, MESSAGES_CLEAR, PRIVATE_MESSAGES_CLEAR].includes(type)
+ ) {
+ const { frontInSidebar } = state.ui;
+ let { frontsToRelease } = state.messages;
+ // Ignore the front for object still displayed in the sidebar, if there is one.
+ frontsToRelease = frontInSidebar
+ ? frontsToRelease.filter(
+ front => frontInSidebar.actorID !== front.actorID
+ )
+ : state.messages.frontsToRelease;
+
+ webConsoleUI.hud.commands.objectCommand
+ .releaseObjects(frontsToRelease)
+ // Emit an event we can listen to to make sure all the fronts were released.
+ .then(() => webConsoleUI.emitForTests("fronts-released"));
+
+ // Reset `frontsToRelease` in message reducer.
+ state = reducer(state, {
+ type: FRONTS_TO_RELEASE_CLEAR,
+ });
+ }
+
+ return state;
+ }
+
+ return next(releaseActorsEnhancer, initialState, enhancer);
+ };
+}
+
+module.exports = enableActorReleaser;
diff --git a/devtools/client/webconsole/enhancers/batching.js b/devtools/client/webconsole/enhancers/batching.js
new file mode 100644
index 0000000000..7343c11785
--- /dev/null
+++ b/devtools/client/webconsole/enhancers/batching.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/. */
+
+"use strict";
+
+const {
+ BATCH_ACTIONS,
+} = require("resource://devtools/client/shared/redux/middleware/debounce.js");
+
+/**
+ * A enhancer for the store to handle batched actions.
+ */
+function enableBatching() {
+ return next => (reducer, initialState, enhancer) => {
+ function batchingReducer(state, action) {
+ switch (action.type) {
+ case BATCH_ACTIONS:
+ return action.actions.reduce(batchingReducer, state);
+ default:
+ return reducer(state, action);
+ }
+ }
+
+ if (typeof initialState === "function" && typeof enhancer === "undefined") {
+ enhancer = initialState;
+ initialState = undefined;
+ }
+
+ return next(batchingReducer, initialState, enhancer);
+ };
+}
+
+module.exports = enableBatching;
diff --git a/devtools/client/webconsole/enhancers/css-error-reporting.js b/devtools/client/webconsole/enhancers/css-error-reporting.js
new file mode 100644
index 0000000000..513552db4d
--- /dev/null
+++ b/devtools/client/webconsole/enhancers/css-error-reporting.js
@@ -0,0 +1,43 @@
+/* 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 {
+ INITIALIZE,
+ FILTER_TOGGLE,
+ FILTERS,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+/**
+ * This is responsible for ensuring that error reporting is enabled if the CSS
+ * filter is toggled on.
+ */
+function ensureCSSErrorReportingEnabled(webConsoleUI) {
+ let watchingCSSMessages = false;
+ return next => (reducer, initialState, enhancer) => {
+ function ensureErrorReportingEnhancer(state, action) {
+ state = reducer(state, action);
+
+ // If we're already watching CSS messages, or if the CSS filter is disabled,
+ // we don't do anything.
+ if (!webConsoleUI || watchingCSSMessages || !state.filters.css) {
+ return state;
+ }
+
+ const cssFilterToggled =
+ action.type == FILTER_TOGGLE && action.filter == FILTERS.CSS;
+
+ if (cssFilterToggled || action.type == INITIALIZE) {
+ watchingCSSMessages = true;
+ webConsoleUI.watchCssMessages();
+ }
+
+ return state;
+ }
+ return next(ensureErrorReportingEnhancer, initialState, enhancer);
+ };
+}
+
+module.exports = ensureCSSErrorReportingEnabled;
diff --git a/devtools/client/webconsole/enhancers/message-cache-clearing.js b/devtools/client/webconsole/enhancers/message-cache-clearing.js
new file mode 100644
index 0000000000..f2f4b90eab
--- /dev/null
+++ b/devtools/client/webconsole/enhancers/message-cache-clearing.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/. */
+
+"use strict";
+
+const {
+ MESSAGES_CLEAR,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+/**
+ * This enhancer is responsible for clearing the messages caches using the
+ * webconsoleFront when the user clear the messages (either by direct UI action, or via
+ * `console.clear()`).
+ */
+function enableMessagesCacheClearing(webConsoleUI) {
+ return next => (reducer, initialState, enhancer) => {
+ function messagesCacheClearingEnhancer(state, action) {
+ const storeHadMessages =
+ state?.messages?.mutableMessagesById &&
+ state.messages.mutableMessagesById.size > 0;
+ state = reducer(state, action);
+
+ if (storeHadMessages && webConsoleUI && action.type === MESSAGES_CLEAR) {
+ webConsoleUI.clearMessagesCache();
+
+ // cleans up all the network data provider internal state
+ webConsoleUI.networkDataProvider?.destroy();
+
+ if (webConsoleUI.hud?.toolbox) {
+ webConsoleUI.hud.toolbox.setErrorCount(0);
+ }
+ }
+ return state;
+ }
+
+ return next(messagesCacheClearingEnhancer, initialState, enhancer);
+ };
+}
+
+module.exports = enableMessagesCacheClearing;
diff --git a/devtools/client/webconsole/enhancers/moz.build b/devtools/client/webconsole/enhancers/moz.build
new file mode 100644
index 0000000000..79bb7dd191
--- /dev/null
+++ b/devtools/client/webconsole/enhancers/moz.build
@@ -0,0 +1,11 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ "actor-releaser.js",
+ "batching.js",
+ "css-error-reporting.js",
+ "message-cache-clearing.js",
+)
diff --git a/devtools/client/webconsole/index.html b/devtools/client/webconsole/index.html
new file mode 100644
index 0000000000..a70ff9cc2f
--- /dev/null
+++ b/devtools/client/webconsole/index.html
@@ -0,0 +1,138 @@
+<!-- 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=""
+ id="devtools-webconsole"
+ windowtype="devtools:webconsole"
+ width="900"
+ height="350"
+ persist="screenX screenY width height sizemode"
+>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+ <link rel="stylesheet" href="chrome://global/skin/global.css" />
+ <link rel="stylesheet" href="chrome://devtools/skin/webconsole.css" />
+ <link rel="stylesheet" href="chrome://devtools/skin/components-frame.css" />
+ <link
+ rel="stylesheet"
+ href="chrome://devtools/content/shared/components/SmartTrace.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://devtools/content/shared/components/NotificationBox.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://devtools/content/shared/components/splitter/GridElementResizer.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://devtools/content/shared/components/AppErrorBoundary.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://devtools/content/framework/components/ChromeDebugToolbar.css"
+ />
+ <!-- Console components -->
+ <link
+ rel="stylesheet"
+ href="chrome://devtools/content/webconsole/components/App.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://devtools/content/webconsole/components/Input/EagerEvaluation.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://devtools/content/webconsole/components/Input/EvaluationNotification.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://devtools/content/webconsole/components/Input/EvaluationContextSelector.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://devtools/content/webconsole/components/Input/ReverseSearchInput.css"
+ />
+ <!-- CodeMirror CSS -->
+ <link
+ rel="stylesheet"
+ href="chrome://devtools/content/shared/sourceeditor/codemirror/lib/codemirror.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://devtools/content/shared/sourceeditor/codemirror/addon/dialog/dialog.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://devtools/content/shared/sourceeditor/codemirror/mozilla.css"
+ />
+
+ <!-- ObjectInspector/Reps styles -->
+ <link
+ rel="stylesheet"
+ href="chrome://devtools/content/shared/components/reps/reps.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://devtools/content/shared/components/Tree.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://devtools/content/shared/components/object-inspector/components/ObjectInspector.css"
+ />
+ <!-- Embedded Network Request detail styles -->
+ <link
+ rel="stylesheet"
+ href="chrome://devtools/content/shared/components/Accordion.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://devtools/content/shared/components/MdnLink.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://devtools/content/shared/components/tabs/Tabs.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://devtools/content/shared/components/tree/TreeView.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://devtools/content/netmonitor/src/assets/styles/variables.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://devtools/content/netmonitor/src/assets/styles/NetworkDetailsBar.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://devtools/content/netmonitor/src/assets/styles/StatusCode.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://devtools/content/netmonitor/src/assets/styles/RequestList.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://devtools/content/netmonitor/src/assets/styles/UrlPreview.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://devtools/content/netmonitor/src/assets/styles/HeadersPanel.css"
+ />
+
+ <script src="chrome://devtools/content/shared/theme-switching.js"></script>
+ </head>
+ <body class="theme-sidebar" role="application">
+ <main
+ id="app-wrapper"
+ class="theme-body"
+ role="document"
+ aria-live="polite"
+ ></main>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/middleware/event-telemetry.js b/devtools/client/webconsole/middleware/event-telemetry.js
new file mode 100644
index 0000000000..bf5b203af6
--- /dev/null
+++ b/devtools/client/webconsole/middleware/event-telemetry.js
@@ -0,0 +1,129 @@
+/* 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 {
+ FILTER_TEXT_SET,
+ FILTER_TOGGLE,
+ DEFAULT_FILTERS_RESET,
+ EVALUATE_EXPRESSION,
+ MESSAGES_ADD,
+ PERSIST_TOGGLE,
+ REVERSE_SEARCH_INPUT_TOGGLE,
+ REVERSE_SEARCH_NEXT,
+ REVERSE_SEARCH_BACK,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+/**
+ * Event telemetry middleware is responsible for logging specific events to telemetry.
+ */
+function eventTelemetryMiddleware(telemetry, store) {
+ return next => action => {
+ const oldState = store.getState();
+ const res = next(action);
+
+ const state = store.getState();
+
+ const filterChangeActions = [
+ FILTER_TEXT_SET,
+ FILTER_TOGGLE,
+ DEFAULT_FILTERS_RESET,
+ ];
+
+ if (filterChangeActions.includes(action.type)) {
+ filterChange({
+ action,
+ state,
+ oldState,
+ telemetry,
+ });
+ } else if (action.type === MESSAGES_ADD) {
+ messagesAdd({ action, telemetry });
+ } else if (action.type === PERSIST_TOGGLE) {
+ telemetry.recordEvent(
+ "persist_changed",
+ "webconsole",
+ String(state.ui.persistLogs)
+ );
+ } else if (action.type === EVALUATE_EXPRESSION) {
+ // Send telemetry event. If we are in the browser toolbox we send -1 as the
+ // toolbox session id.
+
+ telemetry.recordEvent("execute_js", "webconsole", null, {
+ lines: action.expression.split(/\n/).length,
+ input: state.ui.editor ? "multiline" : "inline",
+ });
+
+ if (action.from === "reverse-search") {
+ telemetry.recordEvent("reverse_search", "webconsole", null, {
+ functionality: "evaluate expression",
+ });
+ }
+ } else if (
+ action.type === REVERSE_SEARCH_INPUT_TOGGLE &&
+ state.ui.reverseSearchInputVisible
+ ) {
+ telemetry.recordEvent("reverse_search", "webconsole", action.access, {
+ functionality: "open",
+ });
+ } else if (action.type === REVERSE_SEARCH_NEXT) {
+ telemetry.recordEvent("reverse_search", "webconsole", action.access, {
+ functionality: "navigate next",
+ });
+ } else if (action.type === REVERSE_SEARCH_BACK) {
+ telemetry.recordEvent("reverse_search", "webconsole", action.access, {
+ functionality: "navigate previous",
+ });
+ }
+
+ return res;
+ };
+}
+
+function filterChange({ action, state, oldState, telemetry }) {
+ const oldFilterState = oldState.filters;
+ const filterState = state.filters;
+ const activeFilters = [];
+ const inactiveFilters = [];
+ for (const [key, value] of Object.entries(filterState)) {
+ if (value) {
+ activeFilters.push(key);
+ } else {
+ inactiveFilters.push(key);
+ }
+ }
+
+ let trigger;
+ if (action.type === FILTER_TOGGLE) {
+ trigger = action.filter;
+ } else if (action.type === DEFAULT_FILTERS_RESET) {
+ trigger = "reset";
+ } else if (action.type === FILTER_TEXT_SET) {
+ if (oldFilterState.text !== "" && filterState.text !== "") {
+ return;
+ }
+
+ trigger = "text";
+ }
+
+ telemetry.recordEvent("filters_changed", "webconsole", null, {
+ trigger,
+ active: activeFilters.join(","),
+ inactive: inactiveFilters.join(","),
+ });
+}
+
+function messagesAdd({ action, telemetry }) {
+ const { messages } = action;
+ for (const message of messages) {
+ if (message.level === "error" && message.source === "javascript") {
+ telemetry
+ .getKeyedHistogramById("DEVTOOLS_JAVASCRIPT_ERROR_DISPLAYED")
+ .add(message.errorMessageName || "Unknown", true);
+ }
+ }
+}
+
+module.exports = eventTelemetryMiddleware;
diff --git a/devtools/client/webconsole/middleware/history-persistence.js b/devtools/client/webconsole/middleware/history-persistence.js
new file mode 100644
index 0000000000..b6b6ca9f7c
--- /dev/null
+++ b/devtools/client/webconsole/middleware/history-persistence.js
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ APPEND_TO_HISTORY,
+ CLEAR_HISTORY,
+ EVALUATE_EXPRESSION,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+const historyActions = require("resource://devtools/client/webconsole/actions/history.js");
+
+loader.lazyRequireGetter(
+ this,
+ "asyncStorage",
+ "resource://devtools/shared/async-storage.js"
+);
+
+/**
+ * History persistence middleware is responsible for loading
+ * and maintaining history of executed expressions in JSTerm.
+ */
+function historyPersistenceMiddleware(webConsoleUI, store) {
+ let historyLoaded = false;
+ asyncStorage.getItem("webConsoleHistory").then(
+ value => {
+ if (Array.isArray(value)) {
+ store.dispatch(historyActions.historyLoaded(value));
+ }
+ historyLoaded = true;
+ },
+ err => {
+ historyLoaded = true;
+ console.error(err);
+ }
+ );
+
+ return next => action => {
+ const res = next(action);
+
+ const triggerStoreActions = [
+ APPEND_TO_HISTORY,
+ CLEAR_HISTORY,
+ EVALUATE_EXPRESSION,
+ ];
+
+ // Save the current history entries when modified, but wait till
+ // entries from the previous session are loaded.
+ const { isPrivate } =
+ webConsoleUI.hud?.commands?.targetCommand?.targetFront?.targetForm || {};
+
+ if (
+ !isPrivate &&
+ historyLoaded &&
+ triggerStoreActions.includes(action.type)
+ ) {
+ const state = store.getState();
+ asyncStorage
+ .setItem("webConsoleHistory", state.history.entries)
+ .catch(e => {
+ console.error("Error when saving WebConsole input history", e);
+ });
+ }
+
+ return res;
+ };
+}
+
+module.exports = historyPersistenceMiddleware;
diff --git a/devtools/client/webconsole/middleware/moz.build b/devtools/client/webconsole/middleware/moz.build
new file mode 100644
index 0000000000..ecb2088ca4
--- /dev/null
+++ b/devtools/client/webconsole/middleware/moz.build
@@ -0,0 +1,10 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ "event-telemetry.js",
+ "history-persistence.js",
+ "performance-marker.js",
+)
diff --git a/devtools/client/webconsole/middleware/performance-marker.js b/devtools/client/webconsole/middleware/performance-marker.js
new file mode 100644
index 0000000000..93fcee279c
--- /dev/null
+++ b/devtools/client/webconsole/middleware/performance-marker.js
@@ -0,0 +1,27 @@
+/* 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 {
+ MESSAGES_ADD,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+const {
+ createPerformanceMarkerMiddleware,
+} = require("resource://devtools/client/shared/redux/middleware/performance-marker.js");
+
+module.exports = function (sessionId) {
+ return createPerformanceMarkerMiddleware({
+ [MESSAGES_ADD]: {
+ label: "WebconsoleAddMessages",
+ sessionId,
+ getMarkerDescription({ action, state }) {
+ const { messages } = action;
+ const totalMessageCount = state.messages.mutableMessagesById.size;
+ return `${messages.length} messages handled, store now has ${totalMessageCount} messages`;
+ },
+ },
+ });
+};
diff --git a/devtools/client/webconsole/moz.build b/devtools/client/webconsole/moz.build
new file mode 100644
index 0000000000..a3ced413e3
--- /dev/null
+++ b/devtools/client/webconsole/moz.build
@@ -0,0 +1,44 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ "actions",
+ "components",
+ "enhancers",
+ "middleware",
+ "reducers",
+ "selectors",
+ "utils",
+]
+DevToolsModules(
+ "browser-console-manager.js",
+ "browser-console.js",
+ "constants.js",
+ "panel.js",
+ "service-container.js",
+ "store.js",
+ "types.js",
+ "utils.js",
+ "webconsole-ui.js",
+ "webconsole-wrapper.js",
+ "webconsole.js",
+)
+
+BROWSER_CHROME_MANIFESTS += [
+ "test/browser/_browser_console.toml",
+ "test/browser/_jsterm.toml",
+ "test/browser/_webconsole.toml",
+ "test/node/fixtures/stubs/stubs.toml",
+]
+
+MOCHITEST_CHROME_MANIFESTS += [
+ "test/chrome/chrome.toml",
+]
+
+XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.toml"]
+
+with Files("**"):
+ BUG_COMPONENT = ("DevTools", "Console")
diff --git a/devtools/client/webconsole/panel.js b/devtools/client/webconsole/panel.js
new file mode 100644
index 0000000000..7229c21a9d
--- /dev/null
+++ b/devtools/client/webconsole/panel.js
@@ -0,0 +1,105 @@
+/* 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";
+
+loader.lazyRequireGetter(
+ this,
+ "WebConsole",
+ "resource://devtools/client/webconsole/webconsole.js"
+);
+loader.lazyGetter(this, "EventEmitter", () =>
+ require("resource://devtools/shared/event-emitter.js")
+);
+
+/**
+ * A DevToolPanel that controls the Web Console.
+ */
+function WebConsolePanel(iframeWindow, toolbox, commands) {
+ this._frameWindow = iframeWindow;
+ this._toolbox = toolbox;
+ this._commands = commands;
+ EventEmitter.decorate(this);
+}
+
+exports.WebConsolePanel = WebConsolePanel;
+
+WebConsolePanel.prototype = {
+ hud: null,
+
+ /**
+ * Called by the WebConsole's onkey command handler.
+ * If the WebConsole is opened, check if the JSTerm's input line has focus.
+ * If not, focus it.
+ */
+ focusInput() {
+ this.hud.jsterm.focus();
+ },
+
+ /**
+ * Open is effectively an asynchronous constructor.
+ *
+ * @return object
+ * A promise that is resolved when the Web Console completes opening.
+ */
+ async open() {
+ try {
+ const parentDoc = this._toolbox.doc;
+ const iframe = parentDoc.getElementById(
+ "toolbox-panel-iframe-webconsole"
+ );
+
+ // Make sure the iframe content window is ready.
+ const win = iframe.contentWindow;
+ const doc = win && win.document;
+ if (!doc || doc.readyState !== "complete") {
+ await new Promise(resolve => {
+ iframe.addEventListener("load", resolve, {
+ capture: true,
+ once: true,
+ });
+ });
+ }
+
+ const webConsoleUIWindow = iframe.contentWindow.wrappedJSObject;
+ const chromeWindow = iframe.ownerDocument.defaultView;
+
+ // Open the Web Console.
+ this.hud = new WebConsole(
+ this._toolbox,
+ this._commands,
+ webConsoleUIWindow,
+ chromeWindow
+ );
+ await this.hud.init();
+
+ // Pipe 'reloaded' event from WebConsoleUI to WebConsolePanel.
+ // These events are listened by the Toolbox.
+ this.hud.ui.on("reloaded", () => {
+ this.emit("reloaded");
+ });
+ } catch (e) {
+ const msg = "WebConsolePanel open failed. " + e.error + ": " + e.message;
+ dump(msg + "\n");
+ console.error(msg, e);
+ }
+
+ return this;
+ },
+
+ get currentTarget() {
+ return this._toolbox.target;
+ },
+
+ destroy() {
+ if (!this._toolbox) {
+ return;
+ }
+ this.hud.destroy();
+ this.hud = null;
+ this._frameWindow = null;
+ this._toolbox = null;
+ this.emit("destroyed");
+ },
+};
diff --git a/devtools/client/webconsole/reducers/autocomplete.js b/devtools/client/webconsole/reducers/autocomplete.js
new file mode 100644
index 0000000000..348ff9f7f9
--- /dev/null
+++ b/devtools/client/webconsole/reducers/autocomplete.js
@@ -0,0 +1,181 @@
+/* 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 {
+ AUTOCOMPLETE_CLEAR,
+ AUTOCOMPLETE_DATA_RECEIVE,
+ AUTOCOMPLETE_PENDING_REQUEST,
+ AUTOCOMPLETE_RETRIEVE_FROM_CACHE,
+ EVALUATE_EXPRESSION,
+ UPDATE_HISTORY_POSITION,
+ REVERSE_SEARCH_INPUT_CHANGE,
+ REVERSE_SEARCH_BACK,
+ REVERSE_SEARCH_NEXT,
+ WILL_NAVIGATE,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+function getDefaultState(overrides = {}) {
+ return Object.freeze({
+ cache: null,
+ matches: [],
+ matchProp: null,
+ isElementAccess: false,
+ pendingRequestId: null,
+ isUnsafeGetter: false,
+ getterPath: null,
+ authorizedEvaluations: [],
+ ...overrides,
+ });
+}
+
+function autocomplete(state = getDefaultState(), action) {
+ switch (action.type) {
+ case WILL_NAVIGATE:
+ return getDefaultState();
+ case AUTOCOMPLETE_RETRIEVE_FROM_CACHE:
+ return autoCompleteRetrieveFromCache(state, action);
+ case AUTOCOMPLETE_PENDING_REQUEST:
+ return {
+ ...state,
+ cache: null,
+ pendingRequestId: action.id,
+ };
+ case AUTOCOMPLETE_DATA_RECEIVE:
+ if (action.id !== state.pendingRequestId) {
+ return state;
+ }
+
+ if (action.data.matches === null) {
+ return getDefaultState();
+ }
+
+ if (action.data.isUnsafeGetter) {
+ // We only want to display the getter confirm popup if the last char is a dot or
+ // an opening bracket, or if the user forced the autocompletion with Ctrl+Space.
+ if (
+ action.input.endsWith(".") ||
+ action.input.endsWith("[") ||
+ action.force
+ ) {
+ return {
+ ...getDefaultState(),
+ isUnsafeGetter: true,
+ getterPath: action.data.getterPath,
+ authorizedEvaluations: action.authorizedEvaluations,
+ };
+ }
+
+ return {
+ ...state,
+ pendingRequestId: null,
+ };
+ }
+
+ return {
+ ...state,
+ authorizedEvaluations: action.authorizedEvaluations,
+ getterPath: null,
+ isUnsafeGetter: false,
+ pendingRequestId: null,
+ cache: {
+ input: action.input,
+ frameActorId: action.frameActorId,
+ ...action.data,
+ },
+ ...action.data,
+ };
+ // Reset the autocomplete data when:
+ // - clear is explicitely called
+ // - the user navigates the history
+ // - or an expression was evaluated.
+ case AUTOCOMPLETE_CLEAR:
+ return getDefaultState({
+ authorizedEvaluations: state.authorizedEvaluations,
+ });
+ case EVALUATE_EXPRESSION:
+ case UPDATE_HISTORY_POSITION:
+ case REVERSE_SEARCH_INPUT_CHANGE:
+ case REVERSE_SEARCH_BACK:
+ case REVERSE_SEARCH_NEXT:
+ return getDefaultState();
+ }
+
+ return state;
+}
+
+/**
+ * Retrieve from cache action reducer.
+ *
+ * @param {Object} state
+ * @param {Object} action
+ * @returns {Object} new state.
+ */
+function autoCompleteRetrieveFromCache(state, action) {
+ const { input } = action;
+ const { cache } = state;
+
+ let filterBy = input;
+ if (cache.isElementAccess) {
+ // if we're performing an element access, we can simply retrieve whatever comes
+ // after the last opening bracket.
+ filterBy = input.substring(input.lastIndexOf("[") + 1);
+ } else {
+ // Find the last non-alphanumeric other than "_", ":", or "$" if it exists.
+ const lastNonAlpha = input.match(/[^a-zA-Z0-9_$:][a-zA-Z0-9_$:]*$/);
+ // If input contains non-alphanumerics, use the part after the last one
+ // to filter the cache.
+ if (lastNonAlpha) {
+ filterBy = input.substring(input.lastIndexOf(lastNonAlpha) + 1);
+ }
+ }
+ const stripWrappingQuotes = s =>
+ s.replace(/^['"`](.+(?=['"`]$))['"`]$/g, "$1");
+ const filterByLc = filterBy.toLocaleLowerCase();
+ const looseMatching =
+ !filterBy || filterBy[0].toLocaleLowerCase() === filterBy[0];
+ const needStripQuote = cache.isElementAccess && !/^[`"']/.test(filterBy);
+ const newList = cache.matches.filter(l => {
+ if (needStripQuote) {
+ l = stripWrappingQuotes(l);
+ }
+
+ if (looseMatching) {
+ return l.toLocaleLowerCase().startsWith(filterByLc);
+ }
+
+ return l.startsWith(filterBy);
+ });
+
+ newList.sort((a, b) => {
+ const startingQuoteRegex = /^('|"|`)/;
+ const aFirstMeaningfulChar = startingQuoteRegex.test(a) ? a[1] : a[0];
+ const bFirstMeaningfulChar = startingQuoteRegex.test(b) ? b[1] : b[0];
+ const lA =
+ aFirstMeaningfulChar.toLocaleLowerCase() === aFirstMeaningfulChar;
+ const lB =
+ bFirstMeaningfulChar.toLocaleLowerCase() === bFirstMeaningfulChar;
+ if (lA === lB) {
+ if (a === filterBy) {
+ return -1;
+ }
+ if (b === filterBy) {
+ return 1;
+ }
+ return a.localeCompare(b);
+ }
+ return lA ? -1 : 1;
+ });
+
+ return {
+ ...state,
+ isUnsafeGetter: false,
+ getterPath: null,
+ matches: newList,
+ matchProp: filterBy,
+ isElementAccess: cache.isElementAccess,
+ };
+}
+
+exports.autocomplete = autocomplete;
diff --git a/devtools/client/webconsole/reducers/filters.js b/devtools/client/webconsole/reducers/filters.js
new file mode 100644
index 0000000000..61b993ada8
--- /dev/null
+++ b/devtools/client/webconsole/reducers/filters.js
@@ -0,0 +1,32 @@
+/* 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 constants = require("resource://devtools/client/webconsole/constants.js");
+
+const FilterState = overrides =>
+ Object.freeze(cloneState(constants.DEFAULT_FILTERS_VALUES, overrides));
+
+function filters(state = FilterState(), action) {
+ switch (action.type) {
+ case constants.FILTER_TOGGLE:
+ const { filter } = action;
+ const active = !state[filter];
+ return cloneState(state, { [filter]: active });
+ case constants.FILTERS_CLEAR:
+ return FilterState();
+ case constants.FILTER_TEXT_SET:
+ const { text } = action;
+ return cloneState(state, { [constants.FILTERS.TEXT]: text });
+ }
+
+ return state;
+}
+
+function cloneState(state, overrides) {
+ return Object.assign({}, state, overrides);
+}
+
+exports.FilterState = FilterState;
+exports.filters = filters;
diff --git a/devtools/client/webconsole/reducers/history.js b/devtools/client/webconsole/reducers/history.js
new file mode 100644
index 0000000000..adfca885c5
--- /dev/null
+++ b/devtools/client/webconsole/reducers/history.js
@@ -0,0 +1,244 @@
+/* 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 {
+ APPEND_TO_HISTORY,
+ CLEAR_HISTORY,
+ EVALUATE_EXPRESSION,
+ HISTORY_LOADED,
+ UPDATE_HISTORY_POSITION,
+ HISTORY_BACK,
+ HISTORY_FORWARD,
+ REVERSE_SEARCH_INPUT_TOGGLE,
+ REVERSE_SEARCH_INPUT_CHANGE,
+ REVERSE_SEARCH_BACK,
+ REVERSE_SEARCH_NEXT,
+ SET_TERMINAL_INPUT,
+ SET_TERMINAL_EAGER_RESULT,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+/**
+ * Create default initial state for this reducer.
+ */
+function getInitialState() {
+ return {
+ // Array with history entries
+ entries: [],
+
+ // Holds position (index) in history entries that the user is
+ // currently viewing. This is reset to this.entries.length when
+ // APPEND_TO_HISTORY action is fired.
+ position: undefined,
+
+ // Backups the original user value (if any) that can be set in
+ // the input field. It might be used again if the user doesn't
+ // pick up anything from the history and wants to return all
+ // the way back to see the original input text.
+ originalUserValue: null,
+
+ reverseSearchEnabled: false,
+ currentReverseSearchResults: null,
+ currentReverseSearchResultsPosition: null,
+
+ terminalInput: null,
+ terminalEagerResult: null,
+ };
+}
+
+function history(state = getInitialState(), action, prefsState) {
+ switch (action.type) {
+ case APPEND_TO_HISTORY:
+ case EVALUATE_EXPRESSION:
+ return appendToHistory(state, prefsState, action.expression);
+ case CLEAR_HISTORY:
+ return clearHistory(state);
+ case HISTORY_LOADED:
+ return historyLoaded(state, action.entries);
+ case UPDATE_HISTORY_POSITION:
+ return updateHistoryPosition(state, action.direction, action.expression);
+ case REVERSE_SEARCH_INPUT_TOGGLE:
+ return reverseSearchInputToggle(state, action);
+ case REVERSE_SEARCH_INPUT_CHANGE:
+ return reverseSearchInputChange(state, action.value);
+ case REVERSE_SEARCH_BACK:
+ return reverseSearchBack(state);
+ case REVERSE_SEARCH_NEXT:
+ return reverseSearchNext(state);
+ case SET_TERMINAL_INPUT:
+ return setTerminalInput(state, action.expression);
+ case SET_TERMINAL_EAGER_RESULT:
+ return setTerminalEagerResult(state, action.result);
+ }
+ return state;
+}
+
+function appendToHistory(state, prefsState, expression) {
+ // Clone state
+ state = { ...state };
+ state.entries = [...state.entries];
+
+ // Append new expression only if it isn't the same as
+ // the one recently added.
+ if (expression.trim() != state.entries[state.entries.length - 1]) {
+ state.entries.push(expression);
+ }
+
+ // Remove entries if the limit is reached
+ if (state.entries.length > prefsState.historyCount) {
+ state.entries.splice(0, state.entries.length - prefsState.historyCount);
+ }
+
+ state.position = state.entries.length;
+ state.originalUserValue = null;
+
+ return state;
+}
+
+function clearHistory(state) {
+ return getInitialState();
+}
+
+/**
+ * Handling HISTORY_LOADED action that is fired when history
+ * entries created in previous Firefox session are loaded
+ * from async-storage.
+ *
+ * Loaded entries are appended before the ones that were
+ * added to the state in this session.
+ */
+function historyLoaded(state, entries) {
+ const newEntries = [...entries, ...state.entries];
+ return {
+ ...state,
+ entries: newEntries,
+ // Default position is at the end of the list
+ // (at the latest inserted item).
+ position: newEntries.length,
+ originalUserValue: null,
+ };
+}
+
+function updateHistoryPosition(state, direction, expression) {
+ // Handle UP arrow key => HISTORY_BACK
+ // Handle DOWN arrow key => HISTORY_FORWARD
+ if (direction == HISTORY_BACK) {
+ if (state.position <= 0) {
+ return state;
+ }
+
+ // Clone state
+ state = { ...state };
+
+ // Store the current input value when the user starts
+ // browsing through the history.
+ if (state.position == state.entries.length) {
+ state.originalUserValue = expression || "";
+ }
+
+ state.position--;
+ } else if (direction == HISTORY_FORWARD) {
+ if (state.position >= state.entries.length) {
+ return state;
+ }
+
+ state = {
+ ...state,
+ position: state.position + 1,
+ };
+ }
+
+ return state;
+}
+
+function reverseSearchInputToggle(state, action) {
+ const { initialValue = "" } = action;
+
+ // We're going to close the reverse search, let's clean the state
+ if (state.reverseSearchEnabled) {
+ return {
+ ...state,
+ reverseSearchEnabled: false,
+ position: undefined,
+ currentReverseSearchResults: null,
+ currentReverseSearchResultsPosition: null,
+ };
+ }
+
+ // If we're enabling the reverse search, we treat it as a reverse search input change,
+ // since we can have an initial value.
+ return reverseSearchInputChange(state, initialValue);
+}
+
+function reverseSearchInputChange(state, searchString) {
+ if (searchString === "") {
+ return {
+ ...state,
+ position: undefined,
+ currentReverseSearchResults: null,
+ currentReverseSearchResultsPosition: null,
+ };
+ }
+
+ searchString = searchString.toLocaleLowerCase();
+ const matchingEntries = state.entries.filter(entry =>
+ entry.toLocaleLowerCase().includes(searchString)
+ );
+ // We only return unique entries, but we want to keep the latest entry in the array if
+ // it's duplicated (e.g. if we have [1,2,1], we want to get [2,1], not [1,2]).
+ // To do that, we need to reverse the matching entries array, provide it to a Set,
+ // transform it back to an array and reverse it again.
+ const uniqueEntries = new Set(matchingEntries.reverse());
+ const currentReverseSearchResults = Array.from(
+ new Set(uniqueEntries)
+ ).reverse();
+
+ return {
+ ...state,
+ position: undefined,
+ currentReverseSearchResults,
+ currentReverseSearchResultsPosition: currentReverseSearchResults.length - 1,
+ };
+}
+
+function reverseSearchBack(state) {
+ let nextPosition = state.currentReverseSearchResultsPosition - 1;
+ if (nextPosition < 0) {
+ nextPosition = state.currentReverseSearchResults.length - 1;
+ }
+
+ return {
+ ...state,
+ currentReverseSearchResultsPosition: nextPosition,
+ };
+}
+
+function reverseSearchNext(state) {
+ let previousPosition = state.currentReverseSearchResultsPosition + 1;
+ if (previousPosition >= state.currentReverseSearchResults.length) {
+ previousPosition = 0;
+ }
+
+ return {
+ ...state,
+ currentReverseSearchResultsPosition: previousPosition,
+ };
+}
+
+function setTerminalInput(state, expression) {
+ return {
+ ...state,
+ terminalInput: expression,
+ terminalEagerResult: !expression ? null : state.terminalEagerResult,
+ };
+}
+
+function setTerminalEagerResult(state, result) {
+ return {
+ ...state,
+ terminalEagerResult: result,
+ };
+}
+
+exports.history = history;
diff --git a/devtools/client/webconsole/reducers/index.js b/devtools/client/webconsole/reducers/index.js
new file mode 100644
index 0000000000..4dbc9d37f0
--- /dev/null
+++ b/devtools/client/webconsole/reducers/index.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/. */
+"use strict";
+
+const {
+ autocomplete,
+} = require("resource://devtools/client/webconsole/reducers/autocomplete.js");
+const {
+ filters,
+} = require("resource://devtools/client/webconsole/reducers/filters.js");
+const {
+ messages,
+} = require("resource://devtools/client/webconsole/reducers/messages.js");
+const {
+ prefs,
+} = require("resource://devtools/client/webconsole/reducers/prefs.js");
+const { ui } = require("resource://devtools/client/webconsole/reducers/ui.js");
+const {
+ notifications,
+} = require("resource://devtools/client/webconsole/reducers/notifications.js");
+const {
+ history,
+} = require("resource://devtools/client/webconsole/reducers/history.js");
+
+exports.reducers = {
+ autocomplete,
+ filters,
+ messages,
+ prefs,
+ ui,
+ notifications,
+ history,
+};
diff --git a/devtools/client/webconsole/reducers/messages.js b/devtools/client/webconsole/reducers/messages.js
new file mode 100644
index 0000000000..1bbad87a7a
--- /dev/null
+++ b/devtools/client/webconsole/reducers/messages.js
@@ -0,0 +1,1723 @@
+/* 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 {
+ isGroupType,
+ isMessageNetworkError,
+ l10n,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+
+const constants = require("resource://devtools/client/webconsole/constants.js");
+const { DEFAULT_FILTERS, FILTERS, MESSAGE_TYPE, MESSAGE_SOURCE } = constants;
+
+loader.lazyRequireGetter(
+ this,
+ "getGripPreviewItems",
+ "resource://devtools/client/shared/components/reps/index.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "getUnicodeUrlPath",
+ "resource://devtools/client/shared/unicode-url.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "getSourceNames",
+ "resource://devtools/client/shared/source-utils.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ [
+ "areMessagesSimilar",
+ "createWarningGroupMessage",
+ "isWarningGroup",
+ "getWarningGroupType",
+ "getDescriptorValue",
+ "getParentWarningGroupMessageId",
+ "getNaturalOrder",
+ ],
+ "resource://devtools/client/webconsole/utils/messages.js",
+ true
+);
+
+const {
+ UPDATE_REQUEST,
+} = require("resource://devtools/client/netmonitor/src/constants.js");
+
+const {
+ processNetworkUpdates,
+} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js");
+
+const MessageState = overrides =>
+ Object.freeze(
+ Object.assign(
+ {
+ // List of all the messages added to the console. Unlike other properties, this Map
+ // will be mutated on state changes for performance reasons.
+ mutableMessagesById: new Map(),
+ // Array of message ids, in chronological order. We use a dedicated property to store
+ // the order (instead of relying on the order of insertion in mutableMessagesById)
+ // as we might receive messages that need to be inserted at a specific index. Doing
+ // so on the Map can be costly, especially when the Map holds lots of messages.
+ mutableMessagesOrder: [],
+ // List of elements matching the selector of CSS Warning messages(populated
+ // on-demand via the UI).
+ cssMessagesMatchingElements: new Map(),
+ // Array of the visible messages.
+ visibleMessages: [],
+ // Object for the filtered messages.
+ filteredMessagesCount: getDefaultFiltersCounter(),
+ // List of the message ids which are opened.
+ messagesUiById: [],
+ // Map of the form {groupMessageId : groupArray},
+ // where groupArray is the list of of all the parent groups' ids of the groupMessageId.
+ // This handles console API groups.
+ groupsById: new Map(),
+ // Message id of the current console API group (no corresponding console.groupEnd yet).
+ currentGroup: null,
+ // This group handles "warning groups" (Content Blocking, CORS, CSP, …)
+ warningGroupsById: new Map(),
+ // Array of fronts to release (i.e. fronts logged in removed messages).
+ // This array *should not* be consumed by any UI component.
+ frontsToRelease: [],
+ // Map of the form {messageId : numberOfRepeat}
+ repeatById: {},
+ // Map of the form {messageId : networkInformation}
+ // `networkInformation` holds request, response, totalTime, ...
+ networkMessagesUpdateById: {},
+ // Id of the last messages that was added.
+ lastMessageId: null,
+ // List of the message ids which are disabled
+ disabledMessagesById: [],
+ },
+ overrides
+ )
+ );
+
+function cloneState(state) {
+ return {
+ visibleMessages: [...state.visibleMessages],
+ filteredMessagesCount: { ...state.filteredMessagesCount },
+ messagesUiById: [...state.messagesUiById],
+ cssMessagesMatchingElements: new Map(state.cssMessagesMatchingElements),
+ groupsById: new Map(state.groupsById),
+ frontsToRelease: [...state.frontsToRelease],
+ repeatById: { ...state.repeatById },
+ networkMessagesUpdateById: { ...state.networkMessagesUpdateById },
+ warningGroupsById: new Map(state.warningGroupsById),
+ // no need to mutate the properties below as they're not directly triggering re-render
+ mutableMessagesById: state.mutableMessagesById,
+ mutableMessagesOrder: state.mutableMessagesOrder,
+ currentGroup: state.currentGroup,
+ lastMessageId: state.lastMessageId,
+ disabledMessagesById: [...state.disabledMessagesById],
+ };
+}
+
+/**
+ * Add a console message to the state.
+ *
+ * @param {ConsoleMessage} newMessage: The message to add to the state.
+ * @param {MessageState} state: The message state ( = managed by this reducer).
+ * @param {FiltersState} filtersState: The filters state.
+ * @param {PrefsState} prefsState: The preferences state.
+ * @param {UiState} uiState: The ui state.
+ * @returns {MessageState} a new messages state.
+ */
+// eslint-disable-next-line complexity
+function addMessage(newMessage, state, filtersState, prefsState, uiState) {
+ const { mutableMessagesById, groupsById, repeatById } = state;
+
+ if (newMessage.type === constants.MESSAGE_TYPE.NAVIGATION_MARKER) {
+ // We set the state's currentGroup property to null after navigating
+ state.currentGroup = null;
+ }
+ const { currentGroup } = state;
+
+ if (newMessage.type === constants.MESSAGE_TYPE.NULL_MESSAGE) {
+ // When the message has a NULL type, we don't add it.
+ return state;
+ }
+
+ if (newMessage.type === constants.MESSAGE_TYPE.END_GROUP) {
+ // Compute the new current group.
+ state.currentGroup = getNewCurrentGroup(currentGroup, groupsById);
+ return state;
+ }
+
+ const lastMessage = mutableMessagesById.get(state.lastMessageId);
+ // It can happen that the new message was actually emitted earlier than the last message,
+ // which means we need to insert it at the right position.
+ const isUnsorted =
+ lastMessage && lastMessage.timeStamp > newMessage.timeStamp;
+
+ if (lastMessage && mutableMessagesById.size > 0) {
+ if (
+ lastMessage.groupId === currentGroup &&
+ areMessagesSimilar(lastMessage, newMessage)
+ ) {
+ state.repeatById[lastMessage.id] = (repeatById[lastMessage.id] || 1) + 1;
+ return state;
+ }
+ }
+
+ // Store the id of the message as being the last one being added.
+ if (!isUnsorted) {
+ state.lastMessageId = newMessage.id;
+ }
+
+ // Add the new message with a reference to the parent group.
+ const parentGroups = getParentGroups(currentGroup, groupsById);
+ if (!isWarningGroup(newMessage)) {
+ newMessage.groupId = currentGroup;
+ newMessage.indent = parentGroups.length;
+ }
+
+ // Check if the current message could be placed in a Warning Group.
+ // This needs to be done before setting the new message in mutableMessagesById so we have a
+ // proper message.
+ const warningGroupType = getWarningGroupType(newMessage);
+
+ // If the preference for warning grouping is true, and the new message could be in a
+ // warning group.
+ if (prefsState.groupWarnings && warningGroupType !== null) {
+ const warningGroupMessageId = getParentWarningGroupMessageId(newMessage);
+
+ // If there's no warning group for the type/innerWindowID yet
+ if (!state.mutableMessagesById.has(warningGroupMessageId)) {
+ // We create it and add it to the store.
+ const groupMessage = createWarningGroupMessage(
+ warningGroupMessageId,
+ warningGroupType,
+ newMessage
+ );
+ state = addMessage(
+ groupMessage,
+ state,
+ filtersState,
+ prefsState,
+ uiState
+ );
+ }
+
+ // We add the new message to the appropriate warningGroup.
+ state.warningGroupsById.get(warningGroupMessageId).push(newMessage.id);
+
+ // If the warningGroup message is not visible yet, but should be.
+ if (
+ !state.visibleMessages.includes(warningGroupMessageId) &&
+ getMessageVisibility(
+ state.mutableMessagesById.get(warningGroupMessageId),
+ {
+ messagesState: state,
+ filtersState,
+ prefsState,
+ uiState,
+ }
+ ).visible
+ ) {
+ // Then we put it in the visibleMessages properties, at the position of the first
+ // warning message inside the warningGroup.
+ // If that first warning message is in a console.group, we place it before the
+ // outermost console.group message.
+ const firstWarningMessageId = state.warningGroupsById.get(
+ warningGroupMessageId
+ )[0];
+ const firstWarningMessage = state.mutableMessagesById.get(
+ firstWarningMessageId
+ );
+ const outermostGroupId = getOutermostGroup(
+ firstWarningMessage,
+ groupsById
+ );
+ const groupIndex = state.visibleMessages.indexOf(outermostGroupId);
+ const warningMessageIndex = state.visibleMessages.indexOf(
+ firstWarningMessageId
+ );
+
+ if (groupIndex > -1) {
+ // We remove the warning message
+ if (warningMessageIndex > -1) {
+ state.visibleMessages.splice(warningMessageIndex, 1);
+ }
+
+ // And we put the warning group before the console.group
+ state.visibleMessages.splice(groupIndex, 0, warningGroupMessageId);
+ } else {
+ // If the warning message is not in a console.group, we replace it by the
+ // warning group message.
+ state.visibleMessages.splice(
+ warningMessageIndex,
+ 1,
+ warningGroupMessageId
+ );
+ }
+ }
+ }
+
+ // If we're creating a warningGroup, we init the array for its children.
+ if (isWarningGroup(newMessage)) {
+ state.warningGroupsById.set(newMessage.id, []);
+ }
+
+ const addedMessage = Object.freeze(newMessage);
+
+ // If the new message isn't the "oldest" one, then we need to insert it at the right
+ // position in the message map.
+ if (isUnsorted) {
+ let newMessageIndex = 0;
+ // This is can be on a hot path, so we're not using `findIndex`, which could be slow.
+ // Furthermore, there's a high chance the message beed to be inserted somewhere at the
+ // end of the list, so we loop through mutableMessagesOrder in reverse order.
+ for (let i = state.mutableMessagesOrder.length - 1; i >= 0; i--) {
+ const message = state.mutableMessagesById.get(
+ state.mutableMessagesOrder[i]
+ );
+ if (message.timeStamp <= addedMessage.timeStamp) {
+ newMessageIndex = i + 1;
+ break;
+ }
+ }
+
+ state.mutableMessagesOrder.splice(newMessageIndex, 0, addedMessage.id);
+ } else {
+ state.mutableMessagesOrder.push(addedMessage.id);
+ }
+ state.mutableMessagesById.set(addedMessage.id, addedMessage);
+
+ if (newMessage.type === "trace") {
+ // We want the stacktrace to be open by default.
+ state.messagesUiById.push(newMessage.id);
+ } else if (isGroupType(newMessage.type)) {
+ state.currentGroup = newMessage.id;
+ state.groupsById.set(newMessage.id, parentGroups);
+
+ if (newMessage.type === constants.MESSAGE_TYPE.START_GROUP) {
+ // We want the group to be open by default.
+ state.messagesUiById.push(newMessage.id);
+ }
+ }
+
+ const { visible, cause } = getMessageVisibility(addedMessage, {
+ messagesState: state,
+ filtersState,
+ prefsState,
+ uiState,
+ });
+
+ if (visible) {
+ // If the message is part of a visible warning group, we want to add it after the last
+ // visible message of the group.
+ const warningGroupId = getParentWarningGroupMessageId(newMessage);
+ if (warningGroupId && state.visibleMessages.includes(warningGroupId)) {
+ // Defaults to the warning group message.
+ let index = state.visibleMessages.indexOf(warningGroupId);
+
+ // We loop backward through the warning group's messages to get the latest visible
+ // messages in it.
+ const messagesInWarningGroup =
+ state.warningGroupsById.get(warningGroupId);
+ for (let i = messagesInWarningGroup.length - 1; i >= 0; i--) {
+ const idx = state.visibleMessages.indexOf(messagesInWarningGroup[i]);
+ if (idx > -1) {
+ index = idx;
+ break;
+ }
+ }
+ // Inserts the new warning message at the wanted location "in" the warning group.
+ state.visibleMessages.splice(index + 1, 0, newMessage.id);
+ } else if (isUnsorted) {
+ // If the new message wasn't the "oldest" one, then we need to insert its id at
+ // the right position in the array.
+ // This is can be on a hot path, so we're not using `findIndex`, which could be slow.
+ // Furthermore, there's a high chance the message beed to be inserted somewhere at the
+ // end of the list, so we loop through visibleMessages in reverse order.
+ let index = 0;
+ for (let i = state.visibleMessages.length - 1; i >= 0; i--) {
+ const id = state.visibleMessages[i];
+ if (
+ state.mutableMessagesById.get(id).timeStamp <= newMessage.timeStamp
+ ) {
+ index = i + 1;
+ break;
+ }
+ }
+ state.visibleMessages.splice(index, 0, newMessage.id);
+ } else {
+ state.visibleMessages.push(newMessage.id);
+ }
+ maybeSortVisibleMessages(state, false);
+ } else if (DEFAULT_FILTERS.includes(cause)) {
+ state.filteredMessagesCount.global++;
+ state.filteredMessagesCount[cause]++;
+ }
+
+ // Append received network-data also into networkMessagesUpdateById
+ // that is responsible for collecting (lazy loaded) HTTP payload data.
+ if (newMessage.source == "network") {
+ state.networkMessagesUpdateById[newMessage.actor] = newMessage;
+ }
+
+ return state;
+}
+
+// eslint-disable-next-line complexity
+function messages(
+ state = MessageState(),
+ action,
+ filtersState,
+ prefsState,
+ uiState
+) {
+ const {
+ mutableMessagesById,
+ cssMessagesMatchingElements,
+ messagesUiById,
+ networkMessagesUpdateById,
+ groupsById,
+ visibleMessages,
+ disabledMessagesById,
+ } = state;
+
+ const { logLimit } = prefsState;
+
+ let newState;
+ switch (action.type) {
+ case constants.MESSAGES_ADD:
+ // If the action holds more messages than the log limit, we can preemptively remove
+ // messages that will never be rendered.
+ const batchHasMoreMessagesThanLogLimit =
+ action.messages.length > logLimit;
+ const list = batchHasMoreMessagesThanLogLimit ? [] : action.messages;
+ if (batchHasMoreMessagesThanLogLimit) {
+ let prunableCount = 0;
+ let lastMessage = null;
+ for (let i = action.messages.length - 1; i >= 0; i--) {
+ const message = action.messages[i];
+ if (
+ !message.groupId &&
+ !isGroupType(message.type) &&
+ message.type !== MESSAGE_TYPE.END_GROUP
+ ) {
+ const messagesSimilar = areMessagesSimilar(lastMessage, message);
+ if (!messagesSimilar) {
+ prunableCount++;
+ }
+ // Once we've added the max number of messages that can be added, stop.
+ // Except for repeated messages, where we keep adding over the limit.
+ if (prunableCount <= logLimit || messagesSimilar) {
+ list.unshift(action.messages[i]);
+ } else {
+ break;
+ }
+ } else {
+ list.unshift(message);
+ }
+ lastMessage = message;
+ }
+ }
+
+ newState = cloneState(state);
+ for (const message of list) {
+ newState = addMessage(
+ message,
+ newState,
+ filtersState,
+ prefsState,
+ uiState
+ );
+ }
+
+ return limitTopLevelMessageCount(newState, logLimit);
+
+ case constants.MESSAGES_CLEAR:
+ const frontsToRelease = [];
+ for (const message of state.mutableMessagesById.values()) {
+ // We want to minimize time spent in reducer as much as we can, so we're using
+ // prototype.push.apply here as it seems faster than other solutions (e.g. the
+ // spread operator, Array#concat, …)
+ Array.prototype.push.apply(
+ frontsToRelease,
+ getAllFrontsInMessage(message)
+ );
+ }
+ return MessageState({
+ // Store all actors from removed messages. This array is used by
+ // `releaseActorsEnhancer` to release all of those backend actors.
+ frontsToRelease,
+ });
+
+ case constants.PRIVATE_MESSAGES_CLEAR: {
+ const removedIds = new Set();
+ for (const [id, message] of mutableMessagesById) {
+ if (message.private === true) {
+ removedIds.add(id);
+ }
+ }
+
+ // If there's no private messages, there's no need to change the state.
+ if (removedIds.size === 0) {
+ return state;
+ }
+
+ return removeMessagesFromState(
+ {
+ ...state,
+ },
+ removedIds
+ );
+ }
+
+ case constants.TARGET_MESSAGES_REMOVE: {
+ const removedIds = new Set();
+ for (const [id, message] of mutableMessagesById) {
+ // Remove message from the target but not evaluations and their results, so
+ // 1. we're consistent with the filtering behavior, i.e. we never hide those
+ // 2. when switching mode from multiprocess to parent process and back to multi,
+ // if we'd clear evaluations we wouldn't have a way to get them back, unlike
+ // log messages and errors, which are still available in the server caches).
+ if (
+ message.targetFront == action.targetFront &&
+ message.type !== MESSAGE_TYPE.COMMAND &&
+ message.type !== MESSAGE_TYPE.RESULT
+ ) {
+ removedIds.add(id);
+ }
+ }
+
+ return removeMessagesFromState(
+ {
+ ...state,
+ },
+ removedIds
+ );
+ }
+
+ case constants.MESSAGES_DISABLE:
+ return {
+ ...state,
+ disabledMessagesById: [...disabledMessagesById, ...action.ids],
+ };
+
+ case constants.MESSAGE_OPEN:
+ const openState = { ...state };
+ openState.messagesUiById = [...messagesUiById, action.id];
+ const currMessage = mutableMessagesById.get(action.id);
+
+ // If the message is a console.group/groupCollapsed or a warning group.
+ if (isGroupType(currMessage.type) || isWarningGroup(currMessage)) {
+ // We want to make its children visible
+ const messagesToShow = [];
+ for (const id of state.mutableMessagesOrder) {
+ const message = mutableMessagesById.get(id);
+ if (
+ !visibleMessages.includes(message.id) &&
+ ((isWarningGroup(currMessage) && !!getWarningGroupType(message)) ||
+ (isGroupType(currMessage.type) &&
+ getParentGroups(message.groupId, groupsById).includes(
+ action.id
+ ))) &&
+ getMessageVisibility(message, {
+ messagesState: openState,
+ filtersState,
+ prefsState,
+ uiState,
+ // We want to check if the message is in an open group
+ // only if it is not a direct child of the group we're opening.
+ checkGroup: message.groupId !== action.id,
+ }).visible
+ ) {
+ messagesToShow.push(id);
+ }
+ }
+
+ // We can then insert the messages ids right after the one of the group.
+ const insertIndex = visibleMessages.indexOf(action.id) + 1;
+ openState.visibleMessages = [
+ ...visibleMessages.slice(0, insertIndex),
+ ...messagesToShow,
+ ...visibleMessages.slice(insertIndex),
+ ];
+ }
+ return openState;
+
+ case constants.MESSAGE_CLOSE:
+ const closeState = { ...state };
+ const messageId = action.id;
+ const index = closeState.messagesUiById.indexOf(messageId);
+ closeState.messagesUiById.splice(index, 1);
+ closeState.messagesUiById = [...closeState.messagesUiById];
+
+ // If the message is a group
+ if (isGroupType(mutableMessagesById.get(messageId).type)) {
+ // Hide all its children, unless they're in a warningGroup.
+ closeState.visibleMessages = visibleMessages.filter((id, i, arr) => {
+ const message = mutableMessagesById.get(id);
+ const warningGroupMessage = mutableMessagesById.get(
+ getParentWarningGroupMessageId(message)
+ );
+
+ // If the message is in a warning group, then we return its current visibility.
+ if (
+ shouldGroupWarningMessages(
+ warningGroupMessage,
+ closeState,
+ prefsState
+ )
+ ) {
+ return arr.includes(id);
+ }
+
+ const parentGroups = getParentGroups(message.groupId, groupsById);
+ return parentGroups.includes(messageId) === false;
+ });
+ } else if (isWarningGroup(mutableMessagesById.get(messageId))) {
+ // If the message was a warningGroup, we hide all the messages in the group.
+ const groupMessages = closeState.warningGroupsById.get(messageId);
+ closeState.visibleMessages = visibleMessages.filter(
+ id => !groupMessages.includes(id)
+ );
+ }
+ return closeState;
+
+ case constants.CSS_MESSAGE_ADD_MATCHING_ELEMENTS:
+ return {
+ ...state,
+ cssMessagesMatchingElements: new Map(cssMessagesMatchingElements).set(
+ action.id,
+ action.elements
+ ),
+ };
+
+ case constants.NETWORK_MESSAGES_UPDATE:
+ const updatedState = {
+ ...state,
+ networkMessagesUpdateById: {
+ ...networkMessagesUpdateById,
+ },
+ };
+ let hasNetworkError = null;
+ for (const message of action.messages) {
+ const { id } = message;
+ updatedState.mutableMessagesById.set(id, message);
+ updatedState.networkMessagesUpdateById[id] = {
+ ...(updatedState.networkMessagesUpdateById[id] || {}),
+ ...message,
+ };
+
+ if (isMessageNetworkError(message)) {
+ hasNetworkError = true;
+ }
+ }
+
+ // If the message updates contained a network error, then we may have to display it.
+ if (hasNetworkError) {
+ return setVisibleMessages({
+ messagesState: updatedState,
+ filtersState,
+ prefsState,
+ uiState,
+ });
+ }
+
+ return updatedState;
+
+ case UPDATE_REQUEST:
+ case constants.NETWORK_UPDATES_REQUEST: {
+ newState = {
+ ...state,
+ networkMessagesUpdateById: {
+ ...networkMessagesUpdateById,
+ },
+ };
+
+ // Netmonitor's UPDATE_REQUEST action comes for only one request
+ const updates =
+ action.type == UPDATE_REQUEST
+ ? [{ id: action.id, data: action.data }]
+ : action.updates;
+ for (const { id, data } of updates) {
+ const request = newState.networkMessagesUpdateById[id];
+ if (!request) {
+ continue;
+ }
+ newState.networkMessagesUpdateById[id] = {
+ ...request,
+ ...processNetworkUpdates(data),
+ };
+ }
+ return newState;
+ }
+
+ case constants.FRONTS_TO_RELEASE_CLEAR:
+ return {
+ ...state,
+ frontsToRelease: [],
+ };
+
+ case constants.WARNING_GROUPS_TOGGLE:
+ // There's no warningGroups, and the pref was set to false,
+ // we don't need to do anything.
+ if (!prefsState.groupWarnings && state.warningGroupsById.size === 0) {
+ return state;
+ }
+
+ let needSort = false;
+ for (const msgId of state.mutableMessagesOrder) {
+ const message = state.mutableMessagesById.get(msgId);
+ const warningGroupType = getWarningGroupType(message);
+ if (warningGroupType) {
+ const warningGroupMessageId = getParentWarningGroupMessageId(message);
+
+ // If there's no warning group for the type/innerWindowID yet.
+ if (!state.mutableMessagesById.has(warningGroupMessageId)) {
+ // We create it and add it to the store.
+ const groupMessage = createWarningGroupMessage(
+ warningGroupMessageId,
+ warningGroupType,
+ message
+ );
+ state = addMessage(
+ groupMessage,
+ state,
+ filtersState,
+ prefsState,
+ uiState
+ );
+ }
+
+ // We add the new message to the appropriate warningGroup.
+ const warningGroup = state.warningGroupsById.get(
+ warningGroupMessageId
+ );
+ if (warningGroup && !warningGroup.includes(msgId)) {
+ warningGroup.push(msgId);
+ }
+
+ needSort = true;
+ }
+ }
+
+ // If we don't have any warning messages that could be in a group, we don't do
+ // anything.
+ if (!needSort) {
+ return state;
+ }
+
+ return setVisibleMessages({
+ messagesState: state,
+ filtersState,
+ prefsState,
+ uiState,
+ // If the user disabled warning groups, we want the messages to be sorted by their
+ // timestamps.
+ forceTimestampSort: !prefsState.groupWarnings,
+ });
+
+ case constants.MESSAGE_REMOVE:
+ return removeMessagesFromState(
+ {
+ ...state,
+ },
+ new Set([action.id])
+ );
+
+ case constants.FILTER_TOGGLE:
+ case constants.FILTER_TEXT_SET:
+ case constants.FILTERS_CLEAR:
+ case constants.DEFAULT_FILTERS_RESET:
+ return setVisibleMessages({
+ messagesState: state,
+ filtersState,
+ prefsState,
+ uiState,
+ });
+ }
+
+ return state;
+}
+
+function setVisibleMessages({
+ messagesState,
+ filtersState,
+ prefsState,
+ uiState,
+ forceTimestampSort = false,
+}) {
+ const {
+ mutableMessagesById,
+ mutableMessagesOrder,
+ visibleMessages,
+ messagesUiById,
+ } = messagesState;
+
+ const messagesToShow = new Set();
+ const matchedGroups = new Set();
+ const filtered = getDefaultFiltersCounter();
+
+ mutableMessagesOrder.forEach(msgId => {
+ const message = mutableMessagesById.get(msgId);
+ const groupParentId = message.groupId;
+ let hasMatchedAncestor = false;
+ const ancestors = [];
+
+ if (groupParentId) {
+ let ancestorId = groupParentId;
+
+ // we track the message's ancestors and their state
+ while (ancestorId) {
+ ancestors.push({
+ ancestorId,
+ matchedFilters: matchedGroups.has(ancestorId),
+ isOpen: messagesUiById.includes(ancestorId),
+ isCurrentlyVisible: visibleMessages.includes(ancestorId),
+ });
+ if (!hasMatchedAncestor && matchedGroups.has(ancestorId)) {
+ hasMatchedAncestor = true;
+ }
+ ancestorId = mutableMessagesById.get(ancestorId).groupId;
+ }
+ }
+
+ const { visible, cause } = getMessageVisibility(message, {
+ messagesState,
+ filtersState,
+ prefsState,
+ uiState,
+ hasMatchedAncestor,
+ });
+
+ // if the message is not visible but passes the search filters, we show its visible ancestors
+ if (!visible && passSearchFilters(message, filtersState)) {
+ const tmp = [];
+ ancestors.forEach(msg => {
+ if (msg.isCurrentlyVisible) {
+ tmp.push(msg.ancestorId);
+ }
+ });
+ tmp.reverse().forEach(id => {
+ messagesToShow.add(id);
+ });
+ }
+ if (visible) {
+ // if the visible message is a child of a group, we add its ancestors to the visible messages
+ if (groupParentId) {
+ // We need to reverse the visibleAncestors array to show the groups in the correct order
+ ancestors.reverse().forEach(msg => {
+ messagesToShow.add(msg.ancestorId);
+ });
+ }
+
+ // we keep track of matched startGroup and startGroupCollapsed messages so we don't filter their children
+ if (
+ message.type === "startGroup" ||
+ message.type === "startGroupCollapsed"
+ ) {
+ matchedGroups.add(msgId);
+ }
+
+ messagesToShow.add(msgId);
+ } else if (DEFAULT_FILTERS.includes(cause)) {
+ filtered.global = filtered.global + 1;
+ filtered[cause] = filtered[cause] + 1;
+ }
+ });
+
+ const newState = {
+ ...messagesState,
+ visibleMessages: Array.from(messagesToShow),
+ filteredMessagesCount: filtered,
+ };
+
+ maybeSortVisibleMessages(
+ newState,
+ // Only sort for warningGroups if the feature is enabled
+ prefsState.groupWarnings,
+ forceTimestampSort
+ );
+
+ return newState;
+}
+
+/**
+ * Returns the new current group id given the previous current group and the groupsById
+ * state property.
+ *
+ * @param {String} currentGroup: id of the current group
+ * @param {Map} groupsById
+ * @param {Array} ignoredIds: An array of ids which can't be the new current group.
+ * @returns {String|null} The new current group id, or null if there isn't one.
+ */
+function getNewCurrentGroup(currentGroup, groupsById, ignoredIds = new Set()) {
+ if (!currentGroup) {
+ return null;
+ }
+
+ // Retrieve the parent groups of the current group.
+ const parents = groupsById.get(currentGroup);
+
+ // If there's at least one parent, make the first one the new currentGroup.
+ if (Array.isArray(parents) && parents.length) {
+ // If the found group must be ignored, let's search for its parent.
+ if (ignoredIds.has(parents[0])) {
+ return getNewCurrentGroup(parents[0], groupsById, ignoredIds);
+ }
+
+ return parents[0];
+ }
+
+ return null;
+}
+
+function getParentGroups(currentGroup, groupsById) {
+ let groups = [];
+ if (currentGroup) {
+ // If there is a current group, we add it as a parent
+ groups = [currentGroup];
+
+ // As well as all its parents, if it has some.
+ const parentGroups = groupsById.get(currentGroup);
+ if (Array.isArray(parentGroups) && parentGroups.length) {
+ groups = groups.concat(parentGroups);
+ }
+ }
+
+ return groups;
+}
+
+function getOutermostGroup(message, groupsById) {
+ const groups = getParentGroups(message.groupId, groupsById);
+ if (groups.length === 0) {
+ return null;
+ }
+ return groups[groups.length - 1];
+}
+
+/**
+ * Remove all top level messages that exceeds message limit.
+ * Also populate an array of all backend actors associated with these
+ * messages so they can be released.
+ */
+function limitTopLevelMessageCount(newState, logLimit) {
+ let topLevelCount =
+ newState.groupsById.size === 0
+ ? newState.mutableMessagesById.size
+ : getToplevelMessageCount(newState);
+
+ if (topLevelCount <= logLimit) {
+ return newState;
+ }
+
+ const removedMessagesId = new Set();
+
+ let cleaningGroup = false;
+ for (const id of newState.mutableMessagesOrder) {
+ const message = newState.mutableMessagesById.get(id);
+ // If we were cleaning a group and the current message does not have
+ // a groupId, we're done cleaning.
+ if (cleaningGroup === true && !message.groupId) {
+ cleaningGroup = false;
+ }
+
+ // If we're not cleaning a group and the message count is below the logLimit,
+ // we exit the loop.
+ if (cleaningGroup === false && topLevelCount <= logLimit) {
+ break;
+ }
+
+ // If we're not currently cleaning a group, and the current message is identified
+ // as a group, set the cleaning flag to true.
+ if (cleaningGroup === false && newState.groupsById.has(id)) {
+ cleaningGroup = true;
+ }
+
+ if (!message.groupId) {
+ topLevelCount--;
+ }
+
+ removedMessagesId.add(id);
+ }
+
+ return removeMessagesFromState(newState, removedMessagesId);
+}
+
+/**
+ * Clean the properties for a given state object and an array of removed messages ids.
+ * Be aware that this function MUTATE the `state` argument.
+ *
+ * @param {MessageState} state
+ * @param {Set} removedMessagesIds
+ * @returns {MessageState}
+ */
+function removeMessagesFromState(state, removedMessagesIds) {
+ if (removedMessagesIds.size === 0) {
+ return state;
+ }
+
+ const frontsToRelease = [];
+ const visibleMessages = [...state.visibleMessages];
+ removedMessagesIds.forEach(id => {
+ const index = visibleMessages.indexOf(id);
+ if (index > -1) {
+ visibleMessages.splice(index, 1);
+ }
+
+ // We want to minimize time spent in reducer as much as we can, so we're using
+ // prototype.push.apply here as it seems faster than other solutions (e.g. the
+ // spread operator, Array#concat, …)
+ Array.prototype.push.apply(
+ frontsToRelease,
+ getAllFrontsInMessage(state.mutableMessagesById.get(id))
+ );
+ });
+
+ if (state.visibleMessages.length > visibleMessages.length) {
+ state.visibleMessages = visibleMessages;
+ }
+
+ if (frontsToRelease.length) {
+ state.frontsToRelease = state.frontsToRelease.concat(frontsToRelease);
+ }
+
+ const isInRemovedId = id => removedMessagesIds.has(id);
+ const mapHasRemovedIdKey = map => {
+ for (const id of removedMessagesIds) {
+ if (map.has(id)) {
+ return true;
+ }
+ }
+ return false;
+ };
+ const objectHasRemovedIdKey = obj =>
+ Object.keys(obj).findIndex(isInRemovedId) !== -1;
+
+ const cleanUpMap = map => {
+ const clonedMap = new Map(map);
+ removedMessagesIds.forEach(id => clonedMap.delete(id));
+ return clonedMap;
+ };
+ const cleanUpObject = object =>
+ [...Object.entries(object)].reduce((res, [id, value]) => {
+ if (!isInRemovedId(id)) {
+ res[id] = value;
+ }
+ return res;
+ }, {});
+
+ removedMessagesIds.forEach(id => {
+ state.mutableMessagesById.delete(id);
+
+ state.mutableMessagesOrder.splice(
+ state.mutableMessagesOrder.indexOf(id),
+ 1
+ );
+ });
+
+ if (state.disabledMessagesById.find(isInRemovedId)) {
+ state.disabledMessagesById = state.disabledMessagesById.filter(
+ id => !isInRemovedId(id)
+ );
+ }
+
+ if (state.messagesUiById.find(isInRemovedId)) {
+ state.messagesUiById = state.messagesUiById.filter(
+ id => !isInRemovedId(id)
+ );
+ }
+
+ if (isInRemovedId(state.currentGroup)) {
+ state.currentGroup = getNewCurrentGroup(
+ state.currentGroup,
+ state.groupsById,
+ removedMessagesIds
+ );
+ }
+
+ if (mapHasRemovedIdKey(state.cssMessagesMatchingElements)) {
+ state.cssMessagesMatchingElements = cleanUpMap(
+ state.cssMessagesMatchingElements
+ );
+ }
+ if (mapHasRemovedIdKey(state.groupsById)) {
+ state.groupsById = cleanUpMap(state.groupsById);
+ }
+
+ if (objectHasRemovedIdKey(state.repeatById)) {
+ state.repeatById = cleanUpObject(state.repeatById);
+ }
+
+ if (objectHasRemovedIdKey(state.networkMessagesUpdateById)) {
+ state.networkMessagesUpdateById = cleanUpObject(
+ state.networkMessagesUpdateById
+ );
+ }
+
+ return state;
+}
+
+/**
+ * Get an array of all the fronts logged in a specific message.
+ *
+ * @param {Message} message: The message to get actors from.
+ * @return {Array<ObjectFront|LongStringFront>} An array containing all the fronts logged
+ * in a message.
+ */
+function getAllFrontsInMessage(message) {
+ const { parameters, messageText } = message;
+
+ const fronts = [];
+ const isFront = p => p && typeof p.release === "function";
+
+ if (Array.isArray(parameters)) {
+ message.parameters.forEach(parameter => {
+ if (isFront(parameter)) {
+ fronts.push(parameter);
+ }
+ });
+ }
+
+ if (isFront(messageText)) {
+ fronts.push(messageText);
+ }
+
+ return fronts;
+}
+
+/**
+ * Returns total count of top level messages (those which are not
+ * within a group).
+ */
+function getToplevelMessageCount(state) {
+ let count = 0;
+ state.mutableMessagesById.forEach(message => {
+ if (!message.groupId) {
+ count++;
+ }
+ });
+ return count;
+}
+
+/**
+ * Check if a message should be visible in the console output, and if not, what
+ * causes it to be hidden.
+ * @param {Message} message: The message to check
+ * @param {Object} option: An option object of the following shape:
+ * - {MessageState} messagesState: The current messages state
+ * - {FilterState} filtersState: The current filters state
+ * - {PrefsState} prefsState: The current preferences state
+ * - {UiState} uiState: The current ui state
+ * - {Boolean} checkGroup: Set to false to not check if a message should
+ * be visible because it is in a console.group.
+ * - {Boolean} checkParentWarningGroupVisibility: Set to false to not
+ * check if a message should be visible because it is in a
+ * warningGroup and the warningGroup is visible.
+ * - {Boolean} hasMatchedAncestor: Set to true if message is part of a
+ * group that has been set to visible
+ *
+ * @return {Object} An object of the following form:
+ * - visible {Boolean}: true if the message should be visible
+ * - cause {String}: if visible is false, what causes the message to be hidden.
+ */
+// eslint-disable-next-line complexity
+function getMessageVisibility(
+ message,
+ {
+ messagesState,
+ filtersState,
+ prefsState,
+ uiState,
+ checkGroup = true,
+ checkParentWarningGroupVisibility = true,
+ hasMatchedAncestor = false,
+ }
+) {
+ const warningGroupMessageId = getParentWarningGroupMessageId(message);
+ const parentWarningGroupMessage = messagesState.mutableMessagesById.get(
+ warningGroupMessageId
+ );
+
+ // Do not display the message if it's in closed group and not in a warning group.
+ if (
+ checkGroup &&
+ !isInOpenedGroup(
+ message,
+ messagesState.groupsById,
+ messagesState.messagesUiById
+ ) &&
+ !shouldGroupWarningMessages(
+ parentWarningGroupMessage,
+ messagesState,
+ prefsState
+ )
+ ) {
+ return {
+ visible: false,
+ cause: "closedGroup",
+ };
+ }
+
+ // If the message is a warningGroup, check if it should be displayed.
+ if (isWarningGroup(message)) {
+ if (!shouldGroupWarningMessages(message, messagesState, prefsState)) {
+ return {
+ visible: false,
+ cause: "warningGroupHeuristicNotMet",
+ };
+ }
+
+ // Hide a warningGroup if the warning filter is off.
+ if (!filtersState[FILTERS.WARN]) {
+ // We don't include any cause as we don't want that message to be reflected in the
+ // message count.
+ return {
+ visible: false,
+ };
+ }
+
+ // Display a warningGroup if at least one of its message will be visible.
+ const childrenMessages = messagesState.warningGroupsById.get(message.id);
+ const hasVisibleChild =
+ childrenMessages &&
+ childrenMessages.some(id => {
+ const child = messagesState.mutableMessagesById.get(id);
+ if (!child) {
+ return false;
+ }
+
+ const { visible, cause } = getMessageVisibility(child, {
+ messagesState,
+ filtersState,
+ prefsState,
+ uiState,
+ checkParentWarningGroupVisibility: false,
+ });
+ return visible && cause !== "visibleWarningGroup";
+ });
+
+ if (hasVisibleChild) {
+ return {
+ visible: true,
+ cause: "visibleChild",
+ };
+ }
+ }
+
+ // Do not display the message if it can be in a warningGroup, and the group is
+ // displayed but collapsed.
+ if (
+ parentWarningGroupMessage &&
+ shouldGroupWarningMessages(
+ parentWarningGroupMessage,
+ messagesState,
+ prefsState
+ ) &&
+ !messagesState.messagesUiById.includes(warningGroupMessageId)
+ ) {
+ return {
+ visible: false,
+ cause: "closedWarningGroup",
+ };
+ }
+
+ // Display a message if it is in a warningGroup that is visible. We don't check the
+ // warningGroup visibility if `checkParentWarningGroupVisibility` is false, because
+ // it means we're checking the warningGroup visibility based on the visibility of its
+ // children, which would cause an infinite loop.
+ const parentVisibility =
+ parentWarningGroupMessage && checkParentWarningGroupVisibility
+ ? getMessageVisibility(parentWarningGroupMessage, {
+ messagesState,
+ filtersState,
+ prefsState,
+ uiState,
+ checkGroup,
+ checkParentWarningGroupVisibility,
+ })
+ : null;
+ if (
+ parentVisibility &&
+ parentVisibility.visible &&
+ parentVisibility.cause !== "visibleChild"
+ ) {
+ return {
+ visible: true,
+ cause: "visibleWarningGroup",
+ };
+ }
+
+ // Some messages can't be filtered out (e.g. groups).
+ // So, always return visible: true for those.
+ if (isUnfilterable(message)) {
+ return {
+ visible: true,
+ };
+ }
+
+ // Let's check all level filters (error, warn, log, …) and return visible: false
+ // and the message level as a cause if the function returns false.
+ if (!passLevelFilters(message, filtersState)) {
+ return {
+ visible: false,
+ cause: message.level,
+ };
+ }
+
+ if (!passCssFilters(message, filtersState)) {
+ return {
+ visible: false,
+ cause: FILTERS.CSS,
+ };
+ }
+
+ if (!passNetworkFilter(message, filtersState)) {
+ return {
+ visible: false,
+ cause: FILTERS.NET,
+ };
+ }
+
+ if (!passXhrFilter(message, filtersState)) {
+ return {
+ visible: false,
+ cause: FILTERS.NETXHR,
+ };
+ }
+
+ // This should always be the last check, or we might report that a message was hidden
+ // because of text search, while it may be hidden because its category is disabled.
+ // Do not check for search filters if it is part of a group and one of its ancestor
+ // has matched the current search filters and set to visible
+ if (!hasMatchedAncestor && !passSearchFilters(message, filtersState)) {
+ return {
+ visible: false,
+ cause: FILTERS.TEXT,
+ };
+ }
+
+ return {
+ visible: true,
+ };
+}
+
+function isUnfilterable(message) {
+ return [
+ MESSAGE_TYPE.COMMAND,
+ MESSAGE_TYPE.RESULT,
+ MESSAGE_TYPE.NAVIGATION_MARKER,
+ ].includes(message.type);
+}
+
+function isInOpenedGroup(message, groupsById, messagesUI) {
+ return (
+ !message.groupId ||
+ (!isGroupClosed(message.groupId, messagesUI) &&
+ !hasClosedParentGroup(groupsById.get(message.groupId), messagesUI))
+ );
+}
+
+function hasClosedParentGroup(group, messagesUI) {
+ return group.some(groupId => isGroupClosed(groupId, messagesUI));
+}
+
+function isGroupClosed(groupId, messagesUI) {
+ return messagesUI.includes(groupId) === false;
+}
+
+/**
+ * Returns true if the message shouldn't be hidden because of the network filter state.
+ *
+ * @param {Object} message - The message to check the filter against.
+ * @param {FilterState} filters - redux "filters" state.
+ * @returns {Boolean}
+ */
+function passNetworkFilter(message, filters) {
+ // The message passes the filter if it is not a network message,
+ // or if it is an xhr one,
+ // or if the network filter is on.
+ return (
+ message.source !== MESSAGE_SOURCE.NETWORK ||
+ message.isXHR === true ||
+ filters[FILTERS.NET] === true ||
+ (filters[FILTERS.ERROR] && isMessageNetworkError(message))
+ );
+}
+
+/**
+ * Returns true if the message shouldn't be hidden because of the xhr filter state.
+ *
+ * @param {Object} message - The message to check the filter against.
+ * @param {FilterState} filters - redux "filters" state.
+ * @returns {Boolean}
+ */
+function passXhrFilter(message, filters) {
+ // The message passes the filter if it is not a network message,
+ // or if it is a non-xhr one,
+ // or if the xhr filter is on.
+ return (
+ message.source !== MESSAGE_SOURCE.NETWORK ||
+ message.isXHR === false ||
+ filters[FILTERS.NETXHR] === true ||
+ (filters[FILTERS.ERROR] && isMessageNetworkError(message))
+ );
+}
+
+/**
+ * Returns true if the message shouldn't be hidden because of levels filter state.
+ *
+ * @param {Object} message - The message to check the filter against.
+ * @param {FilterState} filters - redux "filters" state.
+ * @returns {Boolean}
+ */
+function passLevelFilters(message, filters) {
+ // The message passes the filter if it is not a console call,
+ // or if its level matches the state of the corresponding filter.
+ return (
+ (message.source !== MESSAGE_SOURCE.CONSOLE_API &&
+ message.source !== MESSAGE_SOURCE.JAVASCRIPT) ||
+ filters[message.level] === true ||
+ (filters[FILTERS.ERROR] && isMessageNetworkError(message))
+ );
+}
+
+/**
+ * Returns true if the message shouldn't be hidden because of the CSS filter state.
+ *
+ * @param {Object} message - The message to check the filter against.
+ * @param {FilterState} filters - redux "filters" state.
+ * @returns {Boolean}
+ */
+function passCssFilters(message, filters) {
+ // The message passes the filter if it is not a CSS message,
+ // or if the CSS filter is on.
+ return message.source !== MESSAGE_SOURCE.CSS || filters.css === true;
+}
+
+/**
+ * Returns true if the message shouldn't be hidden because of search filter state.
+ *
+ * @param {Object} message - The message to check the filter against.
+ * @param {FilterState} filters - redux "filters" state.
+ * @returns {Boolean}
+ */
+function passSearchFilters(message, filters) {
+ const trimmed = (filters.text || "").trim();
+
+ // "-"-prefix switched to exclude mode
+ const exclude = trimmed.startsWith("-");
+ const term = exclude ? trimmed.slice(1) : trimmed;
+
+ // This regex matches a very basic regex with an optional i flag
+ const regexMatch = /^\/(?<search>.+)\/(?<flags>i)?$/.exec(term);
+ let regex;
+ if (regexMatch !== null) {
+ const flags = "m" + (regexMatch.groups.flags || "");
+ try {
+ regex = new RegExp(regexMatch.groups.search, flags);
+ } catch (e) {}
+ }
+ const matchStr = regex
+ ? str => regex.test(str)
+ : str => str.toLocaleLowerCase().includes(term.toLocaleLowerCase());
+
+ // If there is no search, the message passes the filter.
+ if (!term) {
+ return true;
+ }
+
+ const matched =
+ // Look for a match in parameters.
+ isTextInParameters(matchStr, message.parameters) ||
+ // Look for a match in location.
+ isTextInFrame(matchStr, message.frame) ||
+ // Look for a match in net events.
+ isTextInNetEvent(matchStr, message) ||
+ // Look for a match in stack-trace.
+ isTextInStackTrace(matchStr, message.stacktrace) ||
+ // Look for a match in messageText.
+ isTextInMessageText(matchStr, message.messageText) ||
+ // Look for a match in notes.
+ isTextInNotes(matchStr, message.notes) ||
+ // Look for a match in prefix.
+ isTextInPrefix(matchStr, message.prefix) ||
+ // Look for a match in displayName.
+ isTextInDisplayName(matchStr, message.displayName);
+
+ return matched ? !exclude : exclude;
+}
+
+/**
+ * Returns true if given text is included in provided stack frame.
+ */
+function isTextInFrame(matchStr, frame) {
+ if (!frame) {
+ return false;
+ }
+
+ const { functionName, line, column, source } = frame;
+ const { short } = getSourceNames(source);
+ const unicodeShort = getUnicodeUrlPath(short);
+
+ const str = `${
+ functionName ? functionName + " " : ""
+ }${unicodeShort}:${line}:${column}`;
+ return matchStr(str);
+}
+
+/**
+ * Returns true if given text is included in provided parameters.
+ */
+function isTextInParameters(matchStr, parameters) {
+ if (!parameters) {
+ return false;
+ }
+
+ return parameters.some(parameter => isTextInParameter(matchStr, parameter));
+}
+
+/**
+ * Returns true if given text is included in provided parameter.
+ */
+function isTextInParameter(matchStr, parameter) {
+ const paramGrip =
+ parameter && parameter.getGrip ? parameter.getGrip() : parameter;
+
+ if (paramGrip && paramGrip.class && matchStr(paramGrip.class)) {
+ return true;
+ }
+
+ const parameterType = typeof parameter;
+ if (parameterType !== "object" && parameterType !== "undefined") {
+ const str = paramGrip + "";
+ if (matchStr(str)) {
+ return true;
+ }
+ }
+
+ const previewItems = getGripPreviewItems(paramGrip);
+ for (const item of previewItems) {
+ if (isTextInParameter(matchStr, item)) {
+ return true;
+ }
+ }
+
+ if (paramGrip && paramGrip.ownProperties) {
+ for (const [key, desc] of Object.entries(paramGrip.ownProperties)) {
+ if (matchStr(key)) {
+ return true;
+ }
+
+ if (isTextInParameter(matchStr, getDescriptorValue(desc))) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Returns true if given text is included in provided net event grip.
+ */
+function isTextInNetEvent(matchStr, { method, url } = {}) {
+ if (!method && !url) {
+ return false;
+ }
+ return matchStr(method) || matchStr(url);
+}
+
+/**
+ * Returns true if given text is included in provided stack trace.
+ */
+function isTextInStackTrace(matchStr, stacktrace) {
+ if (!Array.isArray(stacktrace)) {
+ return false;
+ }
+
+ // isTextInFrame expect the properties of the frame object to be in the same
+ // order they are rendered in the Frame component.
+ return stacktrace.some(frame =>
+ isTextInFrame(matchStr, {
+ functionName:
+ frame.functionName || l10n.getStr("stacktrace.anonymousFunction"),
+ source: frame.filename,
+ lineNumber: frame.lineNumber,
+ columnNumber: frame.columnNumber,
+ })
+ );
+}
+
+/**
+ * Returns true if given text is included in `messageText` field.
+ */
+function isTextInMessageText(matchStr, messageText) {
+ if (!messageText) {
+ return false;
+ }
+
+ if (typeof messageText === "string") {
+ return matchStr(messageText);
+ }
+
+ const grip =
+ messageText && messageText.getGrip ? messageText.getGrip() : messageText;
+ if (grip && grip.type === "longString") {
+ return matchStr(grip.initial);
+ }
+
+ return true;
+}
+
+/**
+ * Returns true if given text is included in JS Trace display name.
+ */
+function isTextInDisplayName(matchStr, displayName) {
+ return displayName && matchStr(displayName);
+}
+
+/**
+ * Returns true if given text is included in notes.
+ */
+function isTextInNotes(matchStr, notes) {
+ if (!Array.isArray(notes)) {
+ return false;
+ }
+
+ return notes.some(
+ note =>
+ // Look for a match in location.
+ isTextInFrame(matchStr, note.frame) ||
+ // Look for a match in messageBody.
+ (note.messageBody && matchStr(note.messageBody))
+ );
+}
+
+/**
+ * Returns true if given text is included in prefix.
+ */
+function isTextInPrefix(matchStr, prefix) {
+ if (!prefix) {
+ return false;
+ }
+
+ return matchStr(`${prefix}: `);
+}
+
+function getDefaultFiltersCounter() {
+ const count = DEFAULT_FILTERS.reduce((res, filter) => {
+ res[filter] = 0;
+ return res;
+ }, {});
+ count.global = 0;
+ return count;
+}
+
+/**
+ * Sort state.visibleMessages if needed.
+ *
+ * @param {MessageState} state
+ * @param {Boolean} sortWarningGroupMessage: set to true to sort warningGroup
+ * messages. Default to false, as in some
+ * situations we already take care of putting
+ * the ids at the right position.
+ * @param {Boolean} timeStampSort: set to true to sort messages by their timestamps.
+ */
+function maybeSortVisibleMessages(
+ state,
+ sortWarningGroupMessage = false,
+ timeStampSort = false
+) {
+ if (state.warningGroupsById.size > 0 && sortWarningGroupMessage) {
+ state.visibleMessages.sort((a, b) => {
+ const messageA = state.mutableMessagesById.get(a);
+ const messageB = state.mutableMessagesById.get(b);
+
+ const warningGroupIdA = getParentWarningGroupMessageId(messageA);
+ const warningGroupIdB = getParentWarningGroupMessageId(messageB);
+
+ const warningGroupA = state.mutableMessagesById.get(warningGroupIdA);
+ const warningGroupB = state.mutableMessagesById.get(warningGroupIdB);
+
+ const aFirst = -1;
+ const bFirst = 1;
+
+ // If both messages are in a warningGroup, or if both are not in warningGroups.
+ if (
+ (warningGroupA && warningGroupB) ||
+ (!warningGroupA && !warningGroupB)
+ ) {
+ return getNaturalOrder(messageA, messageB);
+ }
+
+ // If `a` is in a warningGroup (and `b` isn't).
+ if (warningGroupA) {
+ // If `b` is the warningGroup of `a`, `a` should be after `b`.
+ if (warningGroupIdA === messageB.id) {
+ return bFirst;
+ }
+ // `b` is a regular message, we place `a` before `b` if `b` came after `a`'s
+ // warningGroup.
+ return getNaturalOrder(warningGroupA, messageB);
+ }
+
+ // If `b` is in a warningGroup (and `a` isn't).
+ if (warningGroupB) {
+ // If `a` is the warningGroup of `b`, `a` should be before `b`.
+ if (warningGroupIdB === messageA.id) {
+ return aFirst;
+ }
+ // `a` is a regular message, we place `a` after `b` if `a` came after `b`'s
+ // warningGroup.
+ return getNaturalOrder(messageA, warningGroupB);
+ }
+
+ return 0;
+ });
+ }
+
+ if (timeStampSort) {
+ state.visibleMessages.sort((a, b) => {
+ const messageA = state.mutableMessagesById.get(a);
+ const messageB = state.mutableMessagesById.get(b);
+ return getNaturalOrder(messageA, messageB);
+ });
+ }
+}
+
+/**
+ * Returns if a given type of warning message should be grouped.
+ *
+ * @param {ConsoleMessage} warningGroupMessage
+ * @param {MessageState} messagesState
+ * @param {PrefsState} prefsState
+ */
+function shouldGroupWarningMessages(
+ warningGroupMessage,
+ messagesState,
+ prefsState
+) {
+ if (!warningGroupMessage) {
+ return false;
+ }
+
+ // Only group if the preference is ON.
+ if (!prefsState.groupWarnings) {
+ return false;
+ }
+
+ // We group warning messages if there are at least 2 messages that could go in it.
+ const warningGroup = messagesState.warningGroupsById.get(
+ warningGroupMessage.id
+ );
+ if (!warningGroup || !Array.isArray(warningGroup)) {
+ return false;
+ }
+
+ return warningGroup.length > 1;
+}
+
+exports.messages = messages;
diff --git a/devtools/client/webconsole/reducers/moz.build b/devtools/client/webconsole/reducers/moz.build
new file mode 100644
index 0000000000..b6380ba3be
--- /dev/null
+++ b/devtools/client/webconsole/reducers/moz.build
@@ -0,0 +1,15 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ "autocomplete.js",
+ "filters.js",
+ "history.js",
+ "index.js",
+ "messages.js",
+ "notifications.js",
+ "prefs.js",
+ "ui.js",
+)
diff --git a/devtools/client/webconsole/reducers/notifications.js b/devtools/client/webconsole/reducers/notifications.js
new file mode 100644
index 0000000000..d3a2866986
--- /dev/null
+++ b/devtools/client/webconsole/reducers/notifications.js
@@ -0,0 +1,58 @@
+/* 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 {
+ APPEND_NOTIFICATION,
+ REMOVE_NOTIFICATION,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+loader.lazyRequireGetter(
+ this,
+ ["appendNotification", "removeNotificationWithValue"],
+ "resource://devtools/client/shared/components/NotificationBox.js",
+ true
+);
+
+/**
+ * Create default initial state for this reducer. The state is composed
+ * from list of notifications.
+ */
+function getInitialState() {
+ return {
+ notifications: undefined,
+ };
+}
+
+/**
+ * Reducer function implementation. This reducers is responsible
+ * for maintaining list of notifications. It's consumed by
+ * `NotificationBox` component.
+ */
+function notifications(state = getInitialState(), action) {
+ switch (action.type) {
+ case APPEND_NOTIFICATION:
+ return append(state, action);
+ case REMOVE_NOTIFICATION:
+ return remove(state, action);
+ }
+
+ return state;
+}
+
+// Helpers
+
+function append(state, action) {
+ return appendNotification(state, action);
+}
+
+function remove(state, action) {
+ return removeNotificationWithValue(state.notifications, action.value);
+}
+
+// Exports
+
+module.exports = {
+ notifications,
+};
diff --git a/devtools/client/webconsole/reducers/prefs.js b/devtools/client/webconsole/reducers/prefs.js
new file mode 100644
index 0000000000..e19f5acd32
--- /dev/null
+++ b/devtools/client/webconsole/reducers/prefs.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/. */
+"use strict";
+
+const {
+ EAGER_EVALUATION_TOGGLE,
+ WARNING_GROUPS_TOGGLE,
+ AUTOCOMPLETE_TOGGLE,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+const PrefState = overrides =>
+ Object.freeze(
+ Object.assign(
+ {
+ logLimit: 1000,
+ sidebarToggle: false,
+ groupWarnings: false,
+ autocomplete: false,
+ eagerEvaluation: false,
+ historyCount: 50,
+ },
+ overrides
+ )
+ );
+
+const dict = {
+ [EAGER_EVALUATION_TOGGLE]: "eagerEvaluation",
+ [WARNING_GROUPS_TOGGLE]: "groupWarnings",
+ [AUTOCOMPLETE_TOGGLE]: "autocomplete",
+};
+
+function prefs(state = PrefState(), action) {
+ const pref = dict[action.type];
+ if (pref) {
+ return {
+ ...state,
+ [pref]: !state[pref],
+ };
+ }
+
+ return state;
+}
+
+module.exports = {
+ PrefState,
+ prefs,
+};
diff --git a/devtools/client/webconsole/reducers/ui.js b/devtools/client/webconsole/reducers/ui.js
new file mode 100644
index 0000000000..f3d8f0ca77
--- /dev/null
+++ b/devtools/client/webconsole/reducers/ui.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";
+
+const {
+ INITIALIZE,
+ MESSAGES_CLEAR,
+ PERSIST_TOGGLE,
+ REVERSE_SEARCH_INPUT_TOGGLE,
+ SELECT_NETWORK_MESSAGE_TAB,
+ SHOW_OBJECT_IN_SIDEBAR,
+ SIDEBAR_CLOSE,
+ SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE,
+ TIMESTAMPS_TOGGLE,
+ FILTERBAR_DISPLAY_MODE_SET,
+ FILTERBAR_DISPLAY_MODES,
+ EDITOR_ONBOARDING_DISMISS,
+ EDITOR_TOGGLE,
+ EDITOR_PRETTY_PRINT,
+ EDITOR_SET_WIDTH,
+ ENABLE_NETWORK_MONITORING,
+ SHOW_EVALUATION_NOTIFICATION,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+const {
+ PANELS,
+} = require("resource://devtools/client/netmonitor/src/constants.js");
+
+const UiState = overrides =>
+ Object.freeze(
+ Object.assign(
+ {
+ initialized: false,
+ networkMessageActiveTabId: PANELS.HEADERS,
+ persistLogs: false,
+ sidebarVisible: false,
+ timestampsVisible: true,
+ frontInSidebar: null,
+ closeButtonVisible: false,
+ reverseSearchInputVisible: false,
+ reverseSearchInitialValue: "",
+ editor: false,
+ editorWidth: null,
+ editorPrettifiedAt: null,
+ showEditorOnboarding: false,
+ filterBarDisplayMode: FILTERBAR_DISPLAY_MODES.WIDE,
+ cacheGeneration: 0,
+ // Only used in the browser toolbox console/ browser console
+ // turned off by default
+ enableNetworkMonitoring: false,
+ notification: null,
+ },
+ overrides
+ )
+ );
+
+function ui(state = UiState(), action) {
+ switch (action.type) {
+ case PERSIST_TOGGLE:
+ return { ...state, persistLogs: !state.persistLogs };
+ case TIMESTAMPS_TOGGLE:
+ return { ...state, timestampsVisible: !state.timestampsVisible };
+ case SELECT_NETWORK_MESSAGE_TAB:
+ return { ...state, networkMessageActiveTabId: action.id };
+ case SIDEBAR_CLOSE:
+ return {
+ ...state,
+ sidebarVisible: false,
+ frontInSidebar: null,
+ };
+ case INITIALIZE:
+ return { ...state, initialized: true };
+ case MESSAGES_CLEAR:
+ return {
+ ...state,
+ sidebarVisible: false,
+ frontInSidebar: null,
+ cacheGeneration: state.cacheGeneration + 1,
+ };
+ case SHOW_OBJECT_IN_SIDEBAR:
+ if (action.front === state.frontInSidebar) {
+ return state;
+ }
+ return { ...state, sidebarVisible: true, frontInSidebar: action.front };
+ case SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE:
+ return { ...state, closeButtonVisible: action.shouldDisplayButton };
+ case SHOW_EVALUATION_NOTIFICATION:
+ if (state.notification == action.notification) {
+ return state;
+ }
+ return { ...state, notification: action.notification };
+ case REVERSE_SEARCH_INPUT_TOGGLE:
+ return {
+ ...state,
+ reverseSearchInputVisible: !state.reverseSearchInputVisible,
+ reverseSearchInitialValue: action.initialValue || "",
+ };
+ case FILTERBAR_DISPLAY_MODE_SET:
+ return {
+ ...state,
+ filterBarDisplayMode: action.displayMode,
+ };
+ case EDITOR_TOGGLE:
+ return {
+ ...state,
+ editor: !state.editor,
+ };
+ case EDITOR_ONBOARDING_DISMISS:
+ return {
+ ...state,
+ showEditorOnboarding: false,
+ };
+ case EDITOR_SET_WIDTH:
+ return {
+ ...state,
+ editorWidth: action.width,
+ };
+ case EDITOR_PRETTY_PRINT:
+ return {
+ ...state,
+ editorPrettifiedAt: Date.now(),
+ };
+ case ENABLE_NETWORK_MONITORING:
+ return {
+ ...state,
+ enableNetworkMonitoring: !state.enableNetworkMonitoring,
+ };
+ }
+
+ return state;
+}
+
+module.exports = {
+ UiState,
+ ui,
+};
diff --git a/devtools/client/webconsole/selectors/autocomplete.js b/devtools/client/webconsole/selectors/autocomplete.js
new file mode 100644
index 0000000000..cc868031ae
--- /dev/null
+++ b/devtools/client/webconsole/selectors/autocomplete.js
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function getAutocompleteState(state) {
+ return state.autocomplete;
+}
+
+module.exports = {
+ getAutocompleteState,
+};
diff --git a/devtools/client/webconsole/selectors/filters.js b/devtools/client/webconsole/selectors/filters.js
new file mode 100644
index 0000000000..c2be779442
--- /dev/null
+++ b/devtools/client/webconsole/selectors/filters.js
@@ -0,0 +1,10 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+function getAllFilters(state) {
+ return state.filters;
+}
+
+exports.getAllFilters = getAllFilters;
diff --git a/devtools/client/webconsole/selectors/history.js b/devtools/client/webconsole/selectors/history.js
new file mode 100644
index 0000000000..531db8b8a8
--- /dev/null
+++ b/devtools/client/webconsole/selectors/history.js
@@ -0,0 +1,95 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ HISTORY_BACK,
+ HISTORY_FORWARD,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+function getHistory(state) {
+ return state.history;
+}
+
+function getHistoryEntries(state) {
+ return state.history.entries;
+}
+
+function getHistoryValue(state, direction) {
+ if (direction == HISTORY_BACK) {
+ return getPreviousHistoryValue(state);
+ }
+ if (direction == HISTORY_FORWARD) {
+ return getNextHistoryValue(state);
+ }
+ return null;
+}
+
+function getNextHistoryValue(state) {
+ if (state.history.position < state.history.entries.length - 1) {
+ return state.history.entries[state.history.position + 1];
+ }
+
+ // The user didn't pick up anything from the history and returned
+ // back to the previous value (if any) that was in the input box.
+ return state.history.originalUserValue;
+}
+
+function getPreviousHistoryValue(state) {
+ if (state.history.position > 0) {
+ return state.history.entries[state.history.position - 1];
+ }
+ return null;
+}
+
+function getReverseSearchResult(state) {
+ const { history } = state;
+ const { currentReverseSearchResults, currentReverseSearchResultsPosition } =
+ history;
+
+ if (
+ !Array.isArray(currentReverseSearchResults) ||
+ currentReverseSearchResults.length === 0 ||
+ !Number.isInteger(currentReverseSearchResultsPosition)
+ ) {
+ return null;
+ }
+ return currentReverseSearchResults[currentReverseSearchResultsPosition];
+}
+
+function getReverseSearchResultPosition(state) {
+ const { history } = state;
+ const { currentReverseSearchResultsPosition } = history;
+ if (!Number.isInteger(currentReverseSearchResultsPosition)) {
+ return currentReverseSearchResultsPosition;
+ }
+
+ return currentReverseSearchResultsPosition + 1;
+}
+
+function getReverseSearchTotalResults(state) {
+ const { history } = state;
+ const { currentReverseSearchResults } = history;
+ if (!currentReverseSearchResults) {
+ return null;
+ }
+
+ return currentReverseSearchResults.length;
+}
+
+function getTerminalEagerResult(state) {
+ const { history } = state;
+ return history.terminalEagerResult;
+}
+
+module.exports = {
+ getHistory,
+ getHistoryEntries,
+ getHistoryValue,
+ getReverseSearchResult,
+ getReverseSearchResultPosition,
+ getReverseSearchTotalResults,
+ getTerminalEagerResult,
+};
diff --git a/devtools/client/webconsole/selectors/messages.js b/devtools/client/webconsole/selectors/messages.js
new file mode 100644
index 0000000000..1030b62518
--- /dev/null
+++ b/devtools/client/webconsole/selectors/messages.js
@@ -0,0 +1,93 @@
+/* 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";
+
+loader.lazyRequireGetter(
+ this,
+ ["getParentWarningGroupMessageId", "getWarningGroupType"],
+ "resource://devtools/client/webconsole/utils/messages.js",
+ true
+);
+
+function getMutableMessagesById(state) {
+ return state.messages.mutableMessagesById;
+}
+
+function getMessage(state, id) {
+ return getMutableMessagesById(state).get(id);
+}
+
+function getAllMessagesUiById(state) {
+ return state.messages.messagesUiById;
+}
+
+function getAllDisabledMessagesById(state) {
+ return state.messages.disabledMessagesById;
+}
+
+function getAllCssMessagesMatchingElements(state) {
+ return state.messages.cssMessagesMatchingElements;
+}
+
+function getAllGroupsById(state) {
+ return state.messages.groupsById;
+}
+
+function getCurrentGroup(state) {
+ return state.messages.currentGroup;
+}
+
+function getVisibleMessages(state) {
+ return state.messages.visibleMessages;
+}
+
+function getFilteredMessagesCount(state) {
+ return state.messages.filteredMessagesCount;
+}
+
+function getAllRepeatById(state) {
+ return state.messages.repeatById;
+}
+
+function getAllNetworkMessagesUpdateById(state) {
+ return state.messages.networkMessagesUpdateById;
+}
+
+function getGroupsById(state) {
+ return state.messages.groupsById;
+}
+
+function getAllWarningGroupsById(state) {
+ return state.messages.warningGroupsById;
+}
+
+function getLastMessageId(state) {
+ return state.messages.lastMessageId;
+}
+
+function isMessageInWarningGroup(message, visibleMessages = []) {
+ if (!getWarningGroupType(message)) {
+ return false;
+ }
+
+ return visibleMessages.includes(getParentWarningGroupMessageId(message));
+}
+
+module.exports = {
+ getAllGroupsById,
+ getAllWarningGroupsById,
+ getMutableMessagesById,
+ getAllCssMessagesMatchingElements,
+ getAllMessagesUiById,
+ getAllDisabledMessagesById,
+ getAllNetworkMessagesUpdateById,
+ getAllRepeatById,
+ getCurrentGroup,
+ getFilteredMessagesCount,
+ getGroupsById,
+ getLastMessageId,
+ getMessage,
+ getVisibleMessages,
+ isMessageInWarningGroup,
+};
diff --git a/devtools/client/webconsole/selectors/moz.build b/devtools/client/webconsole/selectors/moz.build
new file mode 100644
index 0000000000..61f0f5243a
--- /dev/null
+++ b/devtools/client/webconsole/selectors/moz.build
@@ -0,0 +1,14 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ "autocomplete.js",
+ "filters.js",
+ "history.js",
+ "messages.js",
+ "notifications.js",
+ "prefs.js",
+ "ui.js",
+)
diff --git a/devtools/client/webconsole/selectors/notifications.js b/devtools/client/webconsole/selectors/notifications.js
new file mode 100644
index 0000000000..5d349c2a16
--- /dev/null
+++ b/devtools/client/webconsole/selectors/notifications.js
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+function getAllNotifications(state) {
+ return state.notifications.notifications;
+}
+
+module.exports = {
+ getAllNotifications,
+};
diff --git a/devtools/client/webconsole/selectors/prefs.js b/devtools/client/webconsole/selectors/prefs.js
new file mode 100644
index 0000000000..17d911a1ab
--- /dev/null
+++ b/devtools/client/webconsole/selectors/prefs.js
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+function getAllPrefs(state) {
+ return state.prefs;
+}
+
+function getLogLimit(state) {
+ return state.prefs.logLimit;
+}
+
+module.exports = {
+ getAllPrefs,
+ getLogLimit,
+};
diff --git a/devtools/client/webconsole/selectors/ui.js b/devtools/client/webconsole/selectors/ui.js
new file mode 100644
index 0000000000..f08a2c422b
--- /dev/null
+++ b/devtools/client/webconsole/selectors/ui.js
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function getAllUi(state) {
+ return state.ui;
+}
+
+module.exports = {
+ getAllUi,
+};
diff --git a/devtools/client/webconsole/service-container.js b/devtools/client/webconsole/service-container.js
new file mode 100644
index 0000000000..a41e49477a
--- /dev/null
+++ b/devtools/client/webconsole/service-container.js
@@ -0,0 +1,72 @@
+/* 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 {
+ createContextMenu,
+} = require("resource://devtools/client/webconsole/utils/context-menu.js");
+
+const {
+ createEditContextMenu,
+} = require("resource://devtools/client/framework/toolbox-context-menu.js");
+const {
+ getLongStringFullText,
+} = require("resource://devtools/client/shared/string-utils.js");
+
+function setupServiceContainer({
+ webConsoleUI,
+ hud,
+ toolbox,
+ webConsoleWrapper,
+}) {
+ const serviceContainer = {
+ openContextMenu: (event, message) =>
+ createContextMenu(event, message, webConsoleWrapper),
+
+ openEditContextMenu: event => {
+ const { screenX, screenY } = event;
+ const menu = createEditContextMenu(window, "webconsole-menu");
+ // Emit the "menu-open" event for testing.
+ menu.once("open", () => webConsoleWrapper.emitForTests("menu-open"));
+ menu.popup(screenX, screenY, hud.chromeWindow.document);
+ },
+
+ // NOTE these methods are proxied currently because the
+ // service container is passed down the tree. These methods should eventually
+ // be moved to redux actions.
+ recordTelemetryEvent: (event, extra = {}) => hud.recordEvent(event, extra),
+ openLink: (url, e) => hud.openLink(url, e),
+ openNodeInInspector: grip => hud.openNodeInInspector(grip),
+ getInputSelection: () => hud.getInputSelection(),
+ onViewSource: location => hud.viewSource(location.url, location.line),
+ resendNetworkRequest: requestId => hud.resendNetworkRequest(requestId),
+ focusInput: () => hud.focusInput(),
+ setInputValue: value => hud.setInputValue(value),
+ getLongString: grip => getLongStringFullText(hud.commands.client, grip),
+ getJsTermTooltipAnchor: () => webConsoleUI.getJsTermTooltipAnchor(),
+ emitForTests: (event, value) => webConsoleUI.emitForTests(event, value),
+ attachRefToWebConsoleUI: (id, node) => webConsoleUI.attachRef(id, node),
+ requestData: (id, type) =>
+ webConsoleUI.networkDataProvider.requestData(id, type),
+ createElement: nodename => webConsoleWrapper.createElement(nodename),
+ };
+
+ if (toolbox) {
+ const { highlight, unhighlight } = toolbox.getHighlighter();
+
+ Object.assign(serviceContainer, {
+ sourceMapURLService: toolbox.sourceMapURLService,
+ highlightDomElement: highlight,
+ unHighlightDomElement: unhighlight,
+ onViewSourceInDebugger: location => hud.onViewSourceInDebugger(location),
+ onViewSourceInStyleEditor: location =>
+ hud.onViewSourceInStyleEditor(location),
+ });
+ }
+
+ return serviceContainer;
+}
+
+module.exports.setupServiceContainer = setupServiceContainer;
diff --git a/devtools/client/webconsole/store.js b/devtools/client/webconsole/store.js
new file mode 100644
index 0000000000..029d46f439
--- /dev/null
+++ b/devtools/client/webconsole/store.js
@@ -0,0 +1,160 @@
+/* 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";
+
+// State
+const {
+ FilterState,
+} = require("resource://devtools/client/webconsole/reducers/filters.js");
+const {
+ PrefState,
+} = require("resource://devtools/client/webconsole/reducers/prefs.js");
+const {
+ UiState,
+} = require("resource://devtools/client/webconsole/reducers/ui.js");
+
+// Redux
+const {
+ applyMiddleware,
+ compose,
+ createStore,
+} = require("resource://devtools/client/shared/vendor/redux.js");
+
+// Prefs
+const { PREFS } = require("resource://devtools/client/webconsole/constants.js");
+const {
+ getPrefsService,
+} = require("resource://devtools/client/webconsole/utils/prefs.js");
+
+// Reducers
+const {
+ reducers,
+} = require("resource://devtools/client/webconsole/reducers/index.js");
+
+// Middlewares
+const {
+ ignore,
+} = require("resource://devtools/client/shared/redux/middleware/ignore.js");
+const eventTelemetry = require("resource://devtools/client/webconsole/middleware/event-telemetry.js");
+const historyPersistence = require("resource://devtools/client/webconsole/middleware/history-persistence.js");
+const performanceMarker = require("resource://devtools/client/webconsole/middleware/performance-marker.js");
+const {
+ thunk,
+} = require("resource://devtools/client/shared/redux/middleware/thunk.js");
+
+// Enhancers
+const enableBatching = require("resource://devtools/client/webconsole/enhancers/batching.js");
+const enableActorReleaser = require("resource://devtools/client/webconsole/enhancers/actor-releaser.js");
+const ensureCSSErrorReportingEnabled = require("resource://devtools/client/webconsole/enhancers/css-error-reporting.js");
+const enableMessagesCacheClearing = require("resource://devtools/client/webconsole/enhancers/message-cache-clearing.js");
+
+/**
+ * Create and configure store for the Console panel. This is the place
+ * where various enhancers and middleware can be registered.
+ */
+function configureStore(webConsoleUI, options = {}) {
+ const prefsService = getPrefsService(webConsoleUI);
+ const { getBoolPref, getIntPref } = prefsService;
+
+ const logLimit =
+ options.logLimit || Math.max(getIntPref("devtools.hud.loglimit"), 1);
+ const sidebarToggle = getBoolPref(PREFS.FEATURES.SIDEBAR_TOGGLE);
+ const autocomplete = getBoolPref(PREFS.FEATURES.AUTOCOMPLETE);
+ const eagerEvaluation = getBoolPref(PREFS.FEATURES.EAGER_EVALUATION);
+ const groupWarnings = getBoolPref(PREFS.FEATURES.GROUP_WARNINGS);
+ const historyCount = getIntPref(PREFS.UI.INPUT_HISTORY_COUNT);
+
+ const initialState = {
+ prefs: PrefState({
+ logLimit,
+ sidebarToggle,
+ autocomplete,
+ eagerEvaluation,
+ historyCount,
+ groupWarnings,
+ }),
+ filters: FilterState({
+ error: getBoolPref(PREFS.FILTER.ERROR),
+ warn: getBoolPref(PREFS.FILTER.WARN),
+ info: getBoolPref(PREFS.FILTER.INFO),
+ debug: getBoolPref(PREFS.FILTER.DEBUG),
+ log: getBoolPref(PREFS.FILTER.LOG),
+ css: getBoolPref(PREFS.FILTER.CSS),
+ net: getBoolPref(PREFS.FILTER.NET),
+ netxhr: getBoolPref(PREFS.FILTER.NETXHR),
+ }),
+ ui: UiState({
+ networkMessageActiveTabId: "headers",
+ persistLogs: getBoolPref(PREFS.UI.PERSIST),
+ editor: getBoolPref(PREFS.UI.EDITOR),
+ editorWidth: getIntPref(PREFS.UI.EDITOR_WIDTH),
+ showEditorOnboarding: getBoolPref(PREFS.UI.EDITOR_ONBOARDING),
+ timestampsVisible: getBoolPref(PREFS.UI.MESSAGE_TIMESTAMP),
+ showEvaluationContextSelector: getBoolPref(PREFS.UI.CONTEXT_SELECTOR),
+ enableNetworkMonitoring:
+ webConsoleUI.isBrowserConsole || webConsoleUI.isBrowserToolboxConsole
+ ? getBoolPref(PREFS.UI.ENABLE_NETWORK_MONITORING)
+ : true,
+ }),
+ };
+
+ const { toolbox } = options.thunkArgs;
+ const sessionId = (toolbox && toolbox.sessionId) || -1;
+ const middleware = applyMiddleware(
+ performanceMarker(sessionId),
+ ignore,
+ thunk({
+ prefsService,
+ ...options.thunkArgs,
+ }),
+ historyPersistence.bind(null, webConsoleUI),
+ eventTelemetry.bind(null, options.telemetry)
+ );
+
+ return createStore(
+ createRootReducer(),
+ initialState,
+ compose(
+ middleware,
+ enableActorReleaser(webConsoleUI),
+ enableMessagesCacheClearing(webConsoleUI),
+ ensureCSSErrorReportingEnabled(webConsoleUI),
+ // ⚠️ Keep this one last so it will be executed before all the other ones. This is
+ // needed so batched actions can be "unbatched" and handled in the other enhancers.
+ enableBatching()
+ )
+ );
+}
+
+function createRootReducer() {
+ return function rootReducer(state, action) {
+ // We want to compute the new state for all properties except
+ // "messages" and "history". These two reducers are handled
+ // separately since they are receiving additional arguments.
+ const newState = Object.entries(reducers).reduce((res, [key, reducer]) => {
+ if (key !== "messages" && key !== "history") {
+ res[key] = reducer(state[key], action);
+ }
+ return res;
+ }, {});
+
+ // Pass prefs state as additional argument to the history reducer.
+ newState.history = reducers.history(state.history, action, newState.prefs);
+
+ // Specifically pass the updated filters, prefs and ui states as additional arguments.
+ newState.messages = reducers.messages(
+ state.messages,
+ action,
+ newState.filters,
+ newState.prefs,
+ newState.ui
+ );
+
+ return newState;
+ };
+}
+
+// Provide the store factory for test code so that each test is working with
+// its own instance.
+module.exports.configureStore = configureStore;
diff --git a/devtools/client/webconsole/test/README.md b/devtools/client/webconsole/test/README.md
new file mode 100644
index 0000000000..9a4f6e2fd5
--- /dev/null
+++ b/devtools/client/webconsole/test/README.md
@@ -0,0 +1,61 @@
+# Console Tests
+
+The console panel uses currently two different frameworks for tests:
+
+- Mochitest - [Mochitest](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Mochitest) is an automated testing framework built on top of the MochiKit JavaScript libraries. It's just one of the automated regression testing frameworks used by Mozilla.
+
+Mochitests are located in `devtools/client/webconsole/test/browser/` and can be run with the following command:
+
+```sh
+./mach test devtools/client/webconsole/test/browser/
+```
+
+These tests can be run on CI when pushing to TRY. Not all tests are enabled at the moment since they were copied over from the old frontend (See Bug 1400847).
+
+- Mocha + Enzyme - [mocha](https://mochajs.org/) Mocha is JavaScript test framework running on Node.js
+ [Enzyme](http://airbnb.io/enzyme/) is a JavaScript Testing utility for React that makes it easier to assert, manipulate, and traverse your React Components' output.
+
+These tests are located in `tests/node/`, and can be run with the following command:
+
+```sh
+cd devtools/client/webconsole/test/node && npm install && npm test
+```
+
+or using yarn with
+
+```sh
+cd devtools/client/webconsole/test/node && yarn && yarn test
+```
+
+---
+
+The team is leaning towards Enzyme since it's well known and suitable for React.
+It's also easier to contribute to tests written on top of Enzyme.
+
+# Stubs
+
+Many tests depends on fix data structures (aka stubs) that mimic
+<abbr title="Remote Debugging Protocol">RDP<abbr> packets that represents Console logs.
+Stubs are stored in `test/fixtures` directory and you might automatically generate them.
+
+## Append new stubs
+
+- Append new entry into the `getCommands` function return value in on of the `\tests\browser\browser_webconsole_stubs_*.js`
+ and run the generator using `mach` command, with the `WEBCONSOLE_STUBS_UPDATE=true` environment variable.
+
+For console API stubs, you can do:
+
+`./mach test devtools/client/webconsole/test/browser/browser_webconsole_stubs_console_api.js --headless --setenv WEBCONSOLE_STUBS_UPDATE=true`
+
+This will override `tests/node/fixtures/stubs/consoleApi.js`.
+
+The same can be done in:
+
+- `browser_webconsole_stubs_css_message.js` (writes to `tests/node/fixtures/stubs/cssMessage.js`)
+- `browser_webconsole_stubs_evaluation_result.js` (writes to `tests/node/fixtures/stubs/evaluationResult.js`)
+- `browser_webconsole_stubs_network_event.js` (writes to `tests/node/fixtures/stubs/networkEvent.js`)
+- `browser_webconsole_stubs_page_error.js` (writes to `tests/node/fixtures/stubs/pageError.js`)
+
+If you made some changes that impact all stubs, you can update all at once using:
+
+`./mach test devtools/client/webconsole/test/browser/browser_webconsole_stubs --headless --setenv WEBCONSOLE_STUBS_UPDATE=true`
diff --git a/devtools/client/webconsole/test/browser/_browser_console.toml b/devtools/client/webconsole/test/browser/_browser_console.toml
new file mode 100644
index 0000000000..d3d3d42146
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/_browser_console.toml
@@ -0,0 +1,117 @@
+[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
+ "http2",
+]
+
+["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.js"]
+
+["browser_console_content_object_context_menu.js"]
+
+["browser_console_content_object_in_sidebar.js"]
+
+["browser_console_context_menu_entries.js"]
+skip-if = [
+ "os == 'linux'", # Bug 1440059, disabled for all build types
+ "os == 'win'", # Bug 1609460
+ "os == 'mac' && !debug", # 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
+ "http2",
+]
+
+["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_microtask.js"]
+
+["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", # Bug 1591590
+ "debug", # Bug 1591590
+ "ccov", # Bug 1591590
+ "os == 'win' && bits == 64 && !debug", # Bug 1591590
+]
diff --git a/devtools/client/webconsole/test/browser/_jsterm.toml b/devtools/client/webconsole/test/browser/_jsterm.toml
new file mode 100644
index 0000000000..c9be46216a
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/_jsterm.toml
@@ -0,0 +1,283 @@
+[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-properties-with-non-alphanumeric-names.js"]
+skip-if = ["win11_2009 && debug && bits == 32"] # Bug 1620856
+
+["browser_jsterm_autocomplete_accept_no_scroll.js"]
+
+["browser_jsterm_autocomplete_array_no_index.js"]
+
+["browser_jsterm_autocomplete_arrow_keys.js"]
+skip-if = ["win11_2009 && debug && bits == 32"] # Bug 1620856
+
+["browser_jsterm_autocomplete_await.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_del_key.js"]
+
+["browser_jsterm_autocomplete_disabled.js"]
+
+["browser_jsterm_autocomplete_eager_evaluation.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"]
+fail-if = ["a11y_checks"] # Bug 1849028
+
+["browser_jsterm_autocomplete_helpers.js"]
+
+["browser_jsterm_autocomplete_in_chrome_tab.js"]
+
+["browser_jsterm_autocomplete_in_debugger_stackframe.js"]
+
+["browser_jsterm_autocomplete_inside_text.js"]
+
+["browser_jsterm_autocomplete_mapped_variables.js"]
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+ "a11y_checks", # Bugs 1849028 and 1802390 - clicked <input> is inconsistently not accessible
+]
+
+["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.js"]
+
+["browser_jsterm_autocomplete_return_key_no_selection.js"]
+
+["browser_jsterm_autocomplete_toggle.js"]
+
+["browser_jsterm_autocomplete_width.js"]
+
+["browser_jsterm_autocomplete_will_navigate.js"]
+
+["browser_jsterm_await.js"]
+
+["browser_jsterm_await_assignments.js"]
+
+["browser_jsterm_await_concurrent.js"]
+
+["browser_jsterm_await_concurrent_same_result.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_block_command.js"]
+
+["browser_jsterm_commands.js"]
+
+["browser_jsterm_completion.js"]
+
+["browser_jsterm_completion_bracket.js"]
+
+["browser_jsterm_completion_bracket_cached_results.js"]
+
+["browser_jsterm_completion_case_sensitivity.js"]
+
+["browser_jsterm_completion_dollar_underscore.js"]
+
+["browser_jsterm_completion_dollar_zero.js"]
+
+["browser_jsterm_completion_perfect_match.js"]
+
+["browser_jsterm_content_defined_helpers.js"]
+
+["browser_jsterm_context_menu_labels.js"]
+
+["browser_jsterm_copy_command.js"]
+
+["browser_jsterm_ctrl_a_select_all.js"]
+
+["browser_jsterm_ctrl_key_nav.js"]
+run-if = ["os == 'mac'"] # The tested ctrl+key shortcuts are OSX only
+
+["browser_jsterm_document_no_xray.js"]
+
+["browser_jsterm_eager_evaluation.js"]
+
+["browser_jsterm_eager_evaluation_element_highlight.js"]
+
+["browser_jsterm_eager_evaluation_in_debugger_stackframe.js"]
+
+["browser_jsterm_eager_evaluation_warnings.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.js"]
+
+["browser_jsterm_editor_execute_selection.js"]
+
+["browser_jsterm_editor_gutter.js"]
+
+["browser_jsterm_editor_onboarding.js"]
+
+["browser_jsterm_editor_resize.js"]
+fail-if = ["a11y_checks"] # Bug 1849028
+
+["browser_jsterm_editor_reverse_search_button.js"]
+
+["browser_jsterm_editor_reverse_search_keyboard_navigation.js"]
+
+["browser_jsterm_editor_toggle_keyboard_shortcut.js"]
+
+["browser_jsterm_editor_toolbar.js"]
+
+["browser_jsterm_error_docs.js"]
+
+["browser_jsterm_error_outside_valid_range.js"]
+
+["browser_jsterm_evaluation_context_selector.js"]
+
+["browser_jsterm_evaluation_context_selector_iframe_picker.js"]
+
+["browser_jsterm_evaluation_context_selector_inspector.js"]
+
+["browser_jsterm_evaluation_context_selector_pause_in_debugger.js"]
+
+["browser_jsterm_evaluation_context_selector_targets_update.js"]
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
+
+["browser_jsterm_evaluation_notification_original_variable_mapping_warning.js"]
+
+["browser_jsterm_file_load_save_keyboard_shortcut.js"]
+
+["browser_jsterm_focus_reload.js"]
+
+["browser_jsterm_helper_clear.js"]
+
+["browser_jsterm_helper_dollar.js"]
+
+["browser_jsterm_helper_dollar_dollar.js"]
+
+["browser_jsterm_helper_dollar_x.js"]
+
+["browser_jsterm_helper_help.js"]
+
+["browser_jsterm_helper_keys_values.js"]
+
+["browser_jsterm_helpers_no_override.js"]
+
+["browser_jsterm_hide_when_devtools_chrome_enabled_false.js"]
+fail-if = ["a11y_checks"] # Bug 1849028
+
+["browser_jsterm_history.js"]
+
+["browser_jsterm_history_arrow_keys.js"]
+
+["browser_jsterm_history_command.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"]
+fail-if = ["a11y_checks"] # Bug 1849028
+
+["browser_jsterm_multiline.js"]
+
+["browser_jsterm_no_input_and_tab_key_pressed.js"]
+
+["browser_jsterm_null_undefined.js"]
+
+["browser_jsterm_popup_close_on_tab_switch.js"]
+
+["browser_jsterm_screenshot_command_clipboard.js"]
+
+["browser_jsterm_screenshot_command_file.js"]
+
+["browser_jsterm_screenshot_command_fixed_header.js"]
+
+["browser_jsterm_screenshot_command_selector.js"]
+
+["browser_jsterm_screenshot_command_user.js"]
+
+["browser_jsterm_screenshot_command_warnings.js"]
+skip-if = ["os == 'linux' && bits == 64 && !debug"] # Bug 1701439
+
+["browser_jsterm_selfxss.js"]
+
+["browser_jsterm_syntax_highlight_output.js"]
+
+["browser_jsterm_trace_command.js"]
diff --git a/devtools/client/webconsole/test/browser/_webconsole.toml b/devtools/client/webconsole/test/browser/_webconsole.toml
new file mode 100644
index 0000000000..0b302b6481
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/_webconsole.toml
@@ -0,0 +1,827 @@
+[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-csp-many-errors.html",
+ "test-csp-many-errors.html^headers^",
+ "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-worklet.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-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
+ "http2",
+]
+
+["browser_webconsole_async_stack.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_batching.js"]
+
+["browser_webconsole_bidi_string_isolation.js"]
+
+["browser_webconsole_block_mixedcontent_securityerrors.js"]
+tags = "mcb"
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
+
+["browser_webconsole_cached_messages.js"]
+
+["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
+ "http2",
+]
+
+["browser_webconsole_cached_messages_no_duplicate.js"]
+
+["browser_webconsole_certificate_messages.js"]
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
+
+["browser_webconsole_checkloaduri_errors.js"]
+
+["browser_webconsole_clear_cache.js"]
+
+["browser_webconsole_click_function_to_mapped_source.js"]
+https_first_disabled = true
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
+
+["browser_webconsole_click_function_to_prettyprinted_source.js"]
+https_first_disabled = true
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
+
+["browser_webconsole_click_function_to_source.js"]
+https_first_disabled = true
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
+
+["browser_webconsole_clickable_urls.js"]
+https_first_disabled = true
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
+
+["browser_webconsole_close_groups_after_navigation.js"]
+
+["browser_webconsole_close_sidebar.js"]
+skip-if = ["true"] # Bug 1405250
+
+["browser_webconsole_close_unfocused_window.js"]
+
+["browser_webconsole_closing_after_completion.js"]
+
+["browser_webconsole_console_api_iframe.js"]
+
+["browser_webconsole_console_dir.js"]
+
+["browser_webconsole_console_dir_uninspectable.js"]
+
+["browser_webconsole_console_error_expand_object.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_console_group.js"]
+
+["browser_webconsole_console_group_open_no_scroll.js"]
+
+["browser_webconsole_console_logging_workers_api.js"]
+skip-if = [
+ "tsan", # Bug 1767724
+ "release_or_beta", # requires dom.postMessage.sharedArrayBuffer.bypassCOOP_COEP.insecure.enabled
+ "os == 'linux'", # Bug 1767724
+ "apple_catalina && debug", # Bug 1767724
+]
+
+["browser_webconsole_console_profile_unavailable.js"]
+
+["browser_webconsole_console_table.js"]
+https_first_disabled = true
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
+
+["browser_webconsole_console_table_fallback.js"]
+
+["browser_webconsole_console_table_post_alterations.js"]
+
+["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
+ "http2",
+]
+
+["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
+ "http2",
+]
+
+["browser_webconsole_context_menu_copy_message_with_async_stacktrace.js"]
+
+["browser_webconsole_context_menu_copy_message_with_framework_stacktrace.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_context_menu_copy_object.js"]
+skip-if = ["apple_catalina"] # Bug 1713158
+
+["browser_webconsole_context_menu_export_console_output.js"]
+
+["browser_webconsole_context_menu_object_in_sidebar.js"]
+
+["browser_webconsole_context_menu_open_url.js"]
+https_first_disabled = true
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
+
+["browser_webconsole_context_menu_reveal_in_inspector.js"]
+
+["browser_webconsole_context_menu_store_as_global.js"]
+skip-if = ["apple_catalina"] # Bug 1713158
+
+["browser_webconsole_cors_errors.js"]
+https_first_disabled = true
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
+
+["browser_webconsole_csp_ignore_reflected_xss_message.js"]
+https_first_disabled = true
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
+
+["browser_webconsole_csp_too_many_reports.js"]
+
+["browser_webconsole_csp_violation.js"]
+
+["browser_webconsole_cspro.js"]
+https_first_disabled = true
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
+
+["browser_webconsole_css_error_impacted_elements.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_custom_formatters.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_custom_formatters_errors.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_deprecation_warning.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_document_focus.js"]
+
+["browser_webconsole_duplicate_errors.js"]
+
+["browser_webconsole_enable_network_monitoring.js"]
+
+["browser_webconsole_error_with_grouped_stack.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["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"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["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"]
+
+["browser_webconsole_execution_scope.js"]
+https_first_disabled = true
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
+
+["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
+ "http2",
+]
+
+["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_in_line_layout.js"]
+
+["browser_webconsole_ineffective_iframe_sandbox_warning.js"]
+
+["browser_webconsole_init.js"]
+
+["browser_webconsole_input_field_focus_on_panel_select.js"]
+
+["browser_webconsole_input_focus.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_insecure_passwords_about_blank_web_console_warning.js"]
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
+
+["browser_webconsole_insecure_passwords_web_console_warning.js"]
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
+
+["browser_webconsole_inspect_cross_domain_object.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_keyboard_accessibility.js"]
+
+["browser_webconsole_lenient_this_warning.js"]
+
+["browser_webconsole_limit_multiline.js"]
+
+["browser_webconsole_location_debugger_link.js"]
+
+["browser_webconsole_location_logpoint_debugger_link.js"]
+skip-if = ["ccov"] #Bug 1594897
+
+["browser_webconsole_location_styleeditor_link.js"]
+
+["browser_webconsole_logErrorInPage.js"]
+
+["browser_webconsole_logWarningInPage.js"]
+
+["browser_webconsole_logging_exceptions.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_loglimit.js"]
+
+["browser_webconsole_logs_exceptions_order.js"]
+
+["browser_webconsole_longstring.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_longstring_getter.js"]
+skip-if = ["a11y_checks"] # Bugs 1849028 and 1858041 clicked BUTTON.arrow is inconsistently not focusable
+
+["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_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.js"]
+skip-if = ["a11y_checks"] # Bug 1849028 and 1849179 for causing crashes
+
+["browser_webconsole_network_messages_expand_before_updates.js"]
+skip-if = [
+ "os == 'win'", # Bug 1689101
+ "os == 'linux'", # Bug 1689101
+]
+
+["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"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_network_requests_from_chrome.js"]
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
+
+["browser_webconsole_network_reset_filter.js"]
+
+["browser_webconsole_network_unicode.js"]
+
+["browser_webconsole_nodes_highlight.js"]
+
+["browser_webconsole_nodes_select.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["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"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+skip-if = ["apple_catalina"] # Bug 1713158
+
+["browser_webconsole_object_in_sidebar_keyboard_nav.js"]
+
+["browser_webconsole_object_inspector.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_object_inspector__proto__.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_object_inspector_array_getters.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_object_inspector_entries.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_object_inspector_getters.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_object_inspector_getters_prototype.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_object_inspector_getters_shadowed.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_object_inspector_key_sorting.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_object_inspector_local_session_storage.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_object_inspector_nested_promise.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_object_inspector_nested_proxy.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_object_inspector_private_properties.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_object_inspector_scroll.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_object_inspector_selected_text.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_object_inspector_symbols.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_object_inspector_while_debugging_and_inspecting.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_object_inspector_wrapped_primitive_value.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["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
+ "http2",
+]
+
+["browser_webconsole_promise_rejected_object.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_record_tuple.js"]
+
+["browser_webconsole_reopen_closed_tab.js"]
+
+["browser_webconsole_repeat_different_objects.js"]
+
+["browser_webconsole_requestStorageAccess_errors.js"]
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+ "a11y_checks", # Bugs 1858041, 1849028 and 1849179 for causing crashes
+]
+
+["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"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_same_origin_errors.js"]
+
+["browser_webconsole_sandbox_update_after_navigation.js"]
+
+["browser_webconsole_script_errordoc_urls.js"]
+
+["browser_webconsole_scroll.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["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
+ "http2",
+]
+
+["browser_webconsole_sourcemap_error.js"]
+
+["browser_webconsole_sourcemap_invalid.js"]
+
+["browser_webconsole_sourcemap_nosource.js"]
+skip-if = [
+ "verify",
+ "http3", # Bug 1829298
+ "http2",
+]
+
+["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 = [
+ "win11_2009", # Bug 1798331
+ "http3", # Bug 1829298
+ "http2",
+]
+
+["browser_webconsole_stubs_page_error.js"]
+
+["browser_webconsole_stubs_platform_messages.js"]
+
+["browser_webconsole_telemetry_execute_js.js"]
+
+["browser_webconsole_telemetry_filters_changed.js"]
+
+["browser_webconsole_telemetry_js_errors.js"]
+
+["browser_webconsole_telemetry_jump_to_definition.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_telemetry_object_expanded.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_webconsole_telemetry_persist_toggle_changed.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
+ "http2",
+]
+
+["browser_webconsole_uncaught_exception.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["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
+ "http2",
+]
+
+["browser_webconsole_warning_group_cookies.js"]
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
+
+["browser_webconsole_warning_group_csp.js"]
+
+["browser_webconsole_warning_group_multiples.js"]
+
+["browser_webconsole_warning_group_storage_isolation.js"]
+skip-if = ["true"] # Bug 1765369
+
+["browser_webconsole_warning_groups.js"]
+
+["browser_webconsole_warning_groups_filtering.js"]
+
+["browser_webconsole_warning_groups_outside_console_group.js"]
+
+["browser_webconsole_warning_groups_toggle.js"]
+
+["browser_webconsole_wasm_errors.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_console.js"]
+skip-if = ["release_or_beta"] # requires dom.postMessage.sharedArrayBuffer.bypassCOOP_COEP.insecure.enabled
+
+["browser_webconsole_worklet_error.js"]
diff --git a/devtools/client/webconsole/test/browser/browser_console.js b/devtools/client/webconsole/test/browser/browser_console.js
new file mode 100644
index 0000000000..131678415b
--- /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");
+ Assert.greater(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..eb86151a22
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_consolejsm_output.js
@@ -0,0 +1,138 @@
+/* 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();
+
+ 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 { ConsoleAPI } = ChromeUtils.importESModule(
+ "resource://gre/modules/Console.sys.mjs"
+ );
+ const console = new ConsoleAPI();
+
+ 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..a46a99050b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_console_screenshot.js
@@ -0,0 +1,109 @@
+/* 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 = new FileUtils.File(
+ PathUtils.join(PathUtils.tempDir, "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..4234f8427a
--- /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 = 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..ffd0e017ae
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete-properties-with-non-alphanumeric-names.js
@@ -0,0 +1,47 @@
+/* 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;
+ Assert.greater(
+ 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..cfdb17d8da
--- /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");
+
+ Assert.greater(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..8998728e39
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_commands.js
@@ -0,0 +1,66 @@
+/* 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`;
+
+const {
+ WebConsoleCommandsManager,
+} = require("resource://devtools/server/actors/webconsole/commands/manager.js");
+
+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 =
+ WebConsoleCommandsManager.getAllColonCommandNames().map(name => `:${name}`);
+ 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..0514745027
--- /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");
+ Assert.greater(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..a2d85a8697
--- /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;
+ Assert.equal(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..db76341ef6
--- /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 = 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..dcb1ce774e
--- /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 = 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..e7ac63b3e0
--- /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 = 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..d2197c7702
--- /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 = 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..3ac24ba074
--- /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 = 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.getCurrentThreadFrames();
+
+ 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..ee1bcda927
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_mapped_variables.js
@@ -0,0 +1,132 @@
+/* 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 () {
+ await pushPref("devtools.debugger.map-scopes-enabled", true);
+ const hud = await openNewTabAndConsole(TEST_URI);
+ const { jsterm } = hud;
+ const { autocompletePopup: popup } = jsterm;
+ const toolbox = gDevTools.getToolboxForTab(gBrowser.selectedTab);
+
+ info("Opening Debugger and enabling map scopes");
+ await openDebugger();
+ const dbg = createDebuggerContext(toolbox);
+
+ 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("Switch to the debugger and disabling map scopes");
+ await toolbox.selectTool("jsdebugger");
+ await toggleMapScopes(dbg);
+ await toolbox.selectTool("webconsole");
+
+ 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..79e28cb48d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_nav_and_tab_key.js
@@ -0,0 +1,136 @@
+/* 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");
+ Assert.less(
+ popup.selectedIndex,
+ currentSelectionIndex,
+ "Index is less after Page UP"
+ );
+
+ currentSelectionIndex = popup.selectedIndex;
+ EventUtils.synthesizeKey("KEY_PageDown");
+ Assert.greater(
+ 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..e085841575
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_autocomplete_width.js
@@ -0,0 +1,92 @@
+/* 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;
+ Assert.greaterOrEqual(
+ 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;
+ Assert.greaterOrEqual(
+ newPopupWidth,
+ originalWidth,
+ `The popup width was updated (${originalWidth}px -> ${newPopupWidth}px)`
+ );
+ Assert.greaterOrEqual(
+ 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..1c1cc87341
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_await.js
@@ -0,0 +1,84 @@
+/* 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;
+ Assert.greaterOrEqual(
+ 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..421837e9eb
--- /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 = 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_commands.js b/devtools/client/webconsole/test/browser/browser_jsterm_commands.js
new file mode 100644
index 0000000000..7ff80228fe
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_commands.js
@@ -0,0 +1,67 @@
+"use strict";
+
+const TEST_URI = "data:text/html,test-page";
+
+const {
+ HELP_URL,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+add_task(async function () {
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ info("Trigger the :help command");
+ // Hook the browser method in order to prevent the URL from opening
+ // as mochitest forbids opening remote URLs.
+ const onUrlOpened = new Promise(resolve => {
+ const originalMethod = window.openWebLinkIn;
+ window.openWebLinkIn = url => {
+ window.openWebLinkIn = originalMethod;
+ resolve(url);
+ };
+ });
+ execute(hud, ":help");
+ info("Wait for the help URL to be requested to open");
+ const url = await onUrlOpened;
+ is(url, HELP_URL, "Expected url was opened when executing :help");
+
+ info("Execute only ':'");
+ await executeAndWaitForMessageByType(
+ hud,
+ ":",
+ "Missing a command name after ':'",
+ ".console-api.error"
+ );
+
+ // Any space after a `:` would lead to the same exception
+ info("Execute only ': '");
+ await executeAndWaitForMessageByType(
+ hud,
+ ": ",
+ "Missing a command name after ':'",
+ ".console-api.error"
+ );
+
+ info("Execute only ': foo'");
+ await executeAndWaitForMessageByType(
+ hud,
+ ": foo",
+ "Missing a command name after ':'",
+ ".console-api.error"
+ );
+
+ info("Execute unsupported command ':bar'");
+ await executeAndWaitForMessageByType(
+ hud,
+ ":bar",
+ "bar' is not a valid command",
+ ".console-api.error"
+ );
+
+ info("Execute string with two commands");
+ await executeAndWaitForMessageByType(
+ hud,
+ ":help :help",
+ "Executing multiple commands in one evaluation is not supported",
+ ".console-api.error"
+ );
+});
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..3069e80242
--- /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 = 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..194cc64531
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_eager_evaluation.js
@@ -0,0 +1,391 @@
+/* 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];
+var $$ = 42;
+</script>
+<h1>title</h1>
+`;
+
+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 () {
+ // Open the inspector first to select a node, so that we can later test "$0"
+ 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();
+
+ // 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 web console commands");
+ setInputValue(hud, "help()");
+ await waitForNoEagerEvaluationResult(hud);
+
+ setInputValue(hud, "$0");
+ await waitForEagerEvaluationResult(hud, `<h1>`);
+
+ setInputValue(hud, "$('html')");
+ await waitForEagerEvaluationResult(hud, `<html>`);
+
+ setInputValue(hud, "$$");
+ await waitForEagerEvaluationResult(hud, `42`);
+
+ 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..c4202db6dc
--- /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 = 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_evaluation_notification_original_variable_mapping_warning.js b/devtools/client/webconsole/test/browser/browser_jsterm_evaluation_notification_original_variable_mapping_warning.js
new file mode 100644
index 0000000000..21af7c2efd
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_evaluation_notification_original_variable_mapping_warning.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/. */
+
+// Tests that the evaluation warning notification shows in the console when the debugger
+// is paused in an non-pretty printed original file with original variable mapping disabled.
+
+"use strict";
+
+const l10n = new LocalizationHelper(
+ "devtools/client/locales/webconsole.properties"
+);
+const TEST_URI =
+ "https://example.com/browser/devtools/client/webconsole/test/browser/test-autocomplete-mapped.html";
+
+add_task(async function () {
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.debugger.map-scopes-enabled");
+ });
+ const hud = await openNewTabAndConsole(TEST_URI);
+ const toolbox = gDevTools.getToolboxForTab(gBrowser.selectedTab);
+
+ info("Opening Debugger with original variable mapping disabled");
+ await openDebugger();
+ const dbg = createDebuggerContext(toolbox);
+
+ info("Waiting for pause");
+ // This calls firstCall() on the content page and waits for pause(without waiting for loaded scopes). (firstCall
+ // has a debugger statement)
+ await pauseDebugger(dbg, { shouldWaitForLoadScopes: false });
+
+ info("Assert the warning notification in the split console");
+ await toolbox.openSplitConsole();
+
+ // Wait for the notification to be displayed in the console
+ await waitUntil(() => findEvaluationNotificationMessage(hud));
+
+ const notificationMessage = l10n.getStr(
+ "evaluationNotifcation.noOriginalVariableMapping.msg"
+ );
+
+ is(
+ findEvaluationNotificationMessage(hud),
+ notificationMessage,
+ "The Original variable mapping warning is displayed"
+ );
+
+ info("Assert the warning notification in the full console panel");
+ await toolbox.selectTool("webconsole");
+ is(
+ findEvaluationNotificationMessage(hud),
+ notificationMessage,
+ "The Original variable mapping warning is displayed"
+ );
+
+ await toolbox.selectTool("jsdebugger");
+
+ const loadedScopes = waitForLoadedScopes(dbg);
+ dbg.actions.toggleMapScopes();
+ await loadedScopes;
+
+ await toolbox.selectTool("webconsole");
+
+ ok(
+ !findEvaluationNotificationMessage(hud),
+ "The Original variable mapping warning is no longer displayed"
+ );
+
+ await resume(dbg);
+});
+
+function findEvaluationNotificationMessage(hud) {
+ return hud.ui.outputNode.querySelector(".evaluation-notification__text")
+ ?.innerText;
+}
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..73e93b47f7
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_file_load_save_keyboard_shortcut.js
@@ -0,0 +1,91 @@
+/* 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 = new FileUtils.File(
+ PathUtils.join(PathUtils.tempDir, `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 = new FileUtils.File(
+ PathUtils.join(PathUtils.tempDir, 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..0e458eebbb
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_helper_dollar.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 = `data:text/html,<!DOCTYPE html>
+<main>
+ <ul>
+ <li>First</li>
+ <li>Second</li>
+ </ul>
+ <ul id="myList">
+ <li id="myListItem1" class="inMyList">First</li>
+ <li id="myListItem2" class="inMyList">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");
+
+ message = await executeAndWaitForResultMessage(
+ hud,
+ "$('li', document.querySelector('ul#myList'))",
+ '<li id="myListItem1" class="inMyList">'
+ );
+ ok(message, "`$('li', document.querySelector('ul#myList'))` worked");
+
+ message = await executeAndWaitForErrorMessage(
+ hud,
+ "$('li', $(':foo'))",
+ "':foo' is not a valid selector"
+ );
+ ok(message, "`$('li', $(':foo'))` returns an error message");
+
+ message = await executeAndWaitForResultMessage(
+ hud,
+ "$('li', $('div'))",
+ "<li>"
+ );
+ ok(message, "`$('li', $('div'))` worked");
+});
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..1a56fd600f
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_helper_dollar_dollar.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = `data:text/html,<!DOCTYPE html>
+<main>
+ <ul>
+ <li>First</li>
+ <li>Second</li>
+ </ul>
+ <ul id="myList">
+ <li id="myListItem1" class="inMyList">First</li>
+ <li id="myListItem2" class="inMyList">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(4) [ li, li, li#myListItem1.inMyList, li#myListItem2.inMyList ]"
+ );
+ ok(message, "`$$('main > ul > li')` worked");
+
+ message = await executeAndWaitForResultMessage(
+ hud,
+ "$$('main > ul > li').map(el => el.tagName).join(' - ')",
+ "LI - LI - 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");
+
+ message = await executeAndWaitForResultMessage(
+ hud,
+ "$$('li', document.querySelector('ul#myList'))",
+ "Array [ li#myListItem1.inMyList, li#myListItem2.inMyList ]"
+ );
+ ok(message, "`$$('li', document.querySelector('ul#myList'))` worked");
+
+ message = await executeAndWaitForErrorMessage(
+ hud,
+ "$$('li', $(':foo'))",
+ "':foo' is not a valid selector"
+ );
+ ok(message, "`$$('li', $(':foo'))` returns an error message");
+
+ message = await executeAndWaitForResultMessage(
+ hud,
+ "$$('li', $('div'))",
+ "Array(4) [ li, li, li#myListItem1.inMyList, li#myListItem2.inMyList ]"
+ );
+ ok(message, "`$$('li', $('div'))` worked");
+});
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_helpers_no_override.js b/devtools/client/webconsole/test/browser/browser_jsterm_helpers_no_override.js
new file mode 100644
index 0000000000..674e92c038
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_helpers_no_override.js
@@ -0,0 +1,143 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that console helpers are not set when existing variables with similar names
+// exists in the page.
+
+const TEST_URI = `data:text/html,<!DOCTYPE html><script>
+ var $ = "var_$";
+ const $$ = "const_$$";
+ let help = "let_help";
+ globalThis.values = "globalThis_values";
+ function keys() {
+ return "function_keys";
+ }
+
+ function pauseInDebugger() {
+ let $x = "let_$x";
+ debugger;
+ }
+ </script>`;
+
+add_task(async function () {
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ let message = await executeAndWaitForResultMessage(hud, "$", "var_$");
+ ok(message, "$ was not overridden");
+
+ message = await executeAndWaitForResultMessage(hud, "$$", "const_$$");
+ ok(message, "$$ was not overridden");
+
+ message = await executeAndWaitForResultMessage(hud, "help", "let_help");
+ ok(message, "help was not overridden");
+
+ message = await executeAndWaitForResultMessage(
+ hud,
+ "values",
+ "globalThis_values"
+ );
+ ok(message, "values was not overridden");
+
+ message = await executeAndWaitForResultMessage(
+ hud,
+ "keys()",
+ "function_keys"
+ );
+ ok(message, "keys was not overridden");
+
+ info("Pause in Debugger");
+ await openDebugger();
+ const toolbox = gDevTools.getToolboxForTab(gBrowser.selectedTab);
+ const dbg = createDebuggerContext(toolbox);
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ content.wrappedJSObject.pauseInDebugger();
+ });
+ await pauseDebugger(dbg);
+
+ info("Go back to Console");
+ await toolbox.selectTool("webconsole");
+ message = await executeAndWaitForResultMessage(hud, "$x", "let_$x");
+ ok(
+ message,
+ `frame variable "$x" was not overridden while paused in the debugger`
+ );
+
+ await resume(dbg);
+});
+
+add_task(async function () {
+ // Test the behavior when the variables are declared in the script.
+
+ const hud = await openNewTabAndConsole("data:text/html,Nothing");
+
+ let message = await executeAndWaitForResultMessage(
+ hud,
+ "var $ = 'var_$'; $",
+ "var_$"
+ );
+ ok(message, "$ was not overridden");
+
+ message = await executeAndWaitForResultMessage(
+ hud,
+ "const $$ = 'const_$$'; $$",
+ "const_$$"
+ );
+ ok(message, "$$ was not overridden");
+
+ message = await executeAndWaitForResultMessage(
+ hud,
+ "function keys() { return 'function_keys'; } keys()",
+ "function_keys"
+ );
+ ok(message, "keys was not overridden");
+
+ // Hoisted variable.
+ message = await executeAndWaitForResultMessage(
+ hud,
+ "{ var clear = 'var_clear'; } clear",
+ "var_clear"
+ );
+ ok(message, "clear was not overridden");
+
+ // Hoisted function.
+ message = await executeAndWaitForResultMessage(
+ hud,
+ "{ function help() { return 'function_help'; } } help()",
+ "function_help"
+ );
+ ok(message, "help was not overridden");
+
+ // Lexical variables are not hoisted.
+ message = await executeAndWaitForResultMessage(
+ hud,
+ "{ let values = 'let_values'; } typeof values",
+ "function"
+ );
+ ok(message, "values was not overridden because it's not global");
+});
+
+add_task(async function () {
+ // NOTE: Open a new page to use the same binding name again.
+
+ const hud = await openNewTabAndConsole("data:text/html,Nothing");
+
+ // An assignment on `globalThis` property inside the script doesn't shadow
+ // the helper binding.
+ let message = await executeAndWaitForResultMessage(
+ hud,
+ "globalThis.values = 'globalThis_values'; typeof values",
+ "function"
+ );
+ ok(message, "values was overridden because it's not declaration");
+
+ // Executing `values` again shadows the helper binding, given it exists
+ // before the execution.
+ message = await executeAndWaitForResultMessage(
+ hud,
+ "values",
+ "globalThis_values"
+ );
+ ok(message, "values was not overridden if it exists before the execution");
+});
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..2b7d92f9c5
--- /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.source.id.includes("/originalSource-")
+ ) {
+ return false;
+ }
+
+ return (
+ selectedLocation.source.id.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..f872745bbd
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_file.js
@@ -0,0 +1,130 @@
+/* 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 = new FileUtils.File(
+ PathUtils.join(PathUtils.tempDir, "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..50af233317
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_fixed_header.js
@@ -0,0 +1,70 @@
+/* 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 = new FileUtils.File(
+ PathUtils.join(PathUtils.tempDir, "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..c6e35add8d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_selector.js
@@ -0,0 +1,145 @@
+/* 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 = new FileUtils.File(
+ PathUtils.join(
+ PathUtils.tempDir,
+ "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 = new FileUtils.File(
+ PathUtils.join(PathUtils.tempDir, "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_jsterm_trace_command.js b/devtools/client/webconsole/test/browser/browser_jsterm_trace_command.js
new file mode 100644
index 0000000000..2bd1149a49
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_trace_command.js
@@ -0,0 +1,217 @@
+/* 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 the Javascript Tracing feature via the Web Console :trace command.
+
+"use strict";
+
+const TEST_URI = `data:text/html;charset=utf-8,<!DOCTYPE html>
+<body>
+ <div>
+ <h1>Testing trace command</h1>
+ <script>
+ function main() {}
+ function someNoise() {}
+ </script>
+ </div>
+ <div><p></p></div>
+</body>`;
+
+add_task(async function testBasicRecord() {
+ await pushPref("devtools.debugger.features.javascript-tracing", true);
+
+ const hud = await openNewTabAndConsole(TEST_URI);
+ ok(hud, "web console opened");
+
+ info("Test unsupported param error message");
+ let msg = await evaluateExpressionInConsole(
+ hud,
+ ":trace --unsupported-param",
+ "console-api"
+ );
+ is(
+ msg.textContent.trim(),
+ ":trace command doesn't support 'unsupported-param' argument."
+ );
+
+ info("Test the help argument");
+ msg = await evaluateExpressionInConsole(hud, ":trace --help", "console-api");
+ ok(msg.textContent.includes("Toggles the JavaScript tracer"));
+
+ info("Test toggling the tracer ON");
+ // Pass `console-api` specific classname as the command results don't log anything.
+ // Instead a JSTRACER_STATE resource logs a console-api message.
+ msg = await evaluateExpressionInConsole(
+ hud,
+ ":trace --logMethod console --prefix foo --values --on-next-interaction",
+ "console-api"
+ );
+ is(
+ msg.textContent.trim(),
+ "Waiting for next user interaction before tracing (next mousedown or keydown event)"
+ );
+
+ info("Trigger some code before the user interaction");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.wrappedJSObject.someNoise();
+ });
+
+ info("Simulate a user interaction by trigerring a key event on the page");
+ await BrowserTestUtils.synthesizeKey("a", {}, gBrowser.selectedBrowser);
+
+ info("Trigger some code to log some traces");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.wrappedJSObject.main("arg", 2);
+ });
+
+ info("Ensure a message notified about the tracer actual start");
+ await waitFor(
+ () => !!findConsoleAPIMessage(hud, `Started tracing to Web Console`)
+ );
+
+ // Assert that we also see the custom prefix, as well as function arguments
+ await waitFor(
+ () =>
+ !!findTracerMessages(hud, `foo: ⟶ interpreter λ main("arg", 2)`).length
+ );
+ is(
+ findTracerMessages(hud, `someNoise`).length,
+ 0,
+ "The code running before the key press should not be traced"
+ );
+
+ // But now that the tracer is active, we will be able to log this call to someNoise
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.wrappedJSObject.someNoise();
+ });
+ await waitFor(
+ () => !!findTracerMessages(hud, `foo: ⟶ interpreter λ someNoise()`).length
+ );
+
+ info("Test toggling the tracer OFF");
+ msg = await evaluateExpressionInConsole(hud, ":trace", "console-api");
+ is(msg.textContent.trim(), "Stopped tracing");
+
+ info("Clear past traces");
+ hud.ui.clearOutput();
+ await waitFor(
+ () => !findTracerMessages(hud, `foo: ⟶ interpreter λ main("arg", 2)`).length
+ );
+ ok("Console was cleared");
+
+ info("Trigger some code again");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.wrappedJSObject.main();
+ });
+
+ // Let some time for traces to appear
+ await wait(1000);
+
+ ok(
+ !findTracerMessages(hud, `foo: ⟶ interpreter λ main("arg", 2)`).length,
+ "We really stopped recording traces, and no trace appear in the console"
+ );
+});
+
+add_task(async function testLimitedRecord() {
+ await pushPref("devtools.debugger.features.javascript-tracing", true);
+
+ const jsCode = `function foo1() {foo2()}; function foo2() {foo3()}; function foo3() {}; function bar() {}`;
+ const hud = await openNewTabAndConsole(
+ "data:text/html," + encodeURIComponent(`<script>${jsCode}</script>`)
+ );
+ ok(hud, "web console opened");
+
+ info("Test toggling the tracer ON");
+ // Pass `console-api` specific classname as the command results aren't logged as "result".
+ // Instead the frontend log a message as a console API message.
+ await evaluateExpressionInConsole(
+ hud,
+ ":trace --logMethod console --max-depth 1",
+ "console-api"
+ );
+
+ info("Execute some code trigerring the tracer");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.wrappedJSObject.foo1();
+ });
+
+ await waitFor(() => !!findTracerMessages(hud, `λ foo1`).length);
+ ok(true, "foo1 trace was printed to console");
+
+ is(
+ findTracerMessages(hud, `λ foo2`).length,
+ 0,
+ "We only see the first function thanks to the depth limit"
+ );
+
+ info("Test toggling the tracer OFF");
+ await evaluateExpressionInConsole(hud, ":trace", "console-api");
+
+ info("Clear past traces");
+ hud.ui.clearOutput();
+ await waitFor(() => !findTracerMessages(hud, `λ foo1`).length);
+ ok("Console was cleared");
+
+ info("Re-enable the tracing, but with a max record limit");
+ await evaluateExpressionInConsole(
+ hud,
+ ":trace --logMethod console --max-records 1",
+ "console-api"
+ );
+
+ info("Trigger some code again");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.wrappedJSObject.foo1();
+ content.wrappedJSObject.bar();
+ });
+
+ await waitFor(() => !!findTracerMessages(hud, `λ foo3`).length);
+ await waitFor(() => !!findConsoleAPIMessage(hud, `Stopped tracing`));
+ is(
+ findTracerMessages(hud, `λ foo1`).length,
+ 1,
+ "Found the whole depth for the first event loop 1/3"
+ );
+ is(
+ findTracerMessages(hud, `λ foo2`).length,
+ 1,
+ "Found the whole depth for the first event loop 2/3"
+ );
+ is(
+ findTracerMessages(hud, `λ foo3`).length,
+ 1,
+ "Found the whole depth for the first event loop 3/3"
+ );
+ is(
+ findTracerMessages(hud, `λ bar`).length,
+ 0,
+ "But the second event loop was ignored"
+ );
+ ok(
+ !!findConsoleAPIMessage(
+ hud,
+ `Stopped tracing (reason: max-records)`,
+ ".console-api"
+ ),
+ "And the tracer was automatically stopped"
+ );
+
+ info("Enable tracing one last time without any restriction");
+ await evaluateExpressionInConsole(
+ hud,
+ ":trace --logMethod console",
+ "console-api"
+ );
+ info("Trigger various code again");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.wrappedJSObject.foo1();
+ content.wrappedJSObject.bar();
+ });
+ await waitFor(() => !!findTracerMessages(hud, `λ foo1`).length);
+ await waitFor(() => !!findTracerMessages(hud, `λ foo2`).length);
+ await waitFor(() => !!findTracerMessages(hud, `λ foo3`).length);
+ await waitFor(() => !!findTracerMessages(hud, `λ bar`).length);
+ ok(true, "All traces were printed to console");
+});
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..cfceff96af
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_bidi_string_isolation.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 = "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");
+ 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);
+ Assert.greaterOrEqual(
+ rect.x,
+ parentRect.x,
+ "Rect should start inside parent"
+ );
+ Assert.lessOrEqual(
+ rect.x + rect.width,
+ parentRect.x + parentRect.width,
+ "Rect should end inside parent"
+ );
+ if (prevRect) {
+ Assert.greaterOrEqual(
+ 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..5b0ccda4d6
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_clear_cache.js
@@ -0,0 +1,122 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// 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"
+ );
+});
+
+add_task(async function consoleClearPersist() {
+ // persist logs
+ await pushPref("devtools.webconsole.persistlog", true);
+ const tab = await addTab(TEST_URI);
+ let hud = await openConsole(tab);
+
+ // Test that we also clear the cache when calling console.clear().
+ const CACHED_MESSAGE = "CACHED_MESSAGE_PERSIST";
+ await logTextToConsole(hud, CACHED_MESSAGE);
+
+ info("Send a console.clear() from the content page");
+
+ const onConsoleClearPrevented = waitForMessageByType(
+ hud,
+ "console.clear() was prevented",
+ ".console-api"
+ );
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.wrappedJSObject.console.clear();
+ });
+
+ await onConsoleClearPrevented;
+
+ 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 for persist");
+ is(
+ findConsoleAPIMessage(hud, CACHED_MESSAGE),
+ undefined,
+ "The 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..560742a6f7
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_console_logging_workers_api.js
@@ -0,0 +1,147 @@
+/* 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 () {
+ // Allow using SharedArrayBuffer in the test without special HTTP Headers
+ await pushPref(
+ "dom.postMessage.sharedArrayBuffer.bypassCOOP_COEP.insecure.enabled",
+ true
+ );
+
+ 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");
+ }
+
+ const sabMessage = await waitFor(() =>
+ findConsoleAPIMessage(hud, "sab-from-worker")
+ );
+ ok(sabMessage.textContent.includes("SharedArrayBuffer"));
+
+ info("Check that Arrays are properly logged");
+ const arrayMessage = await waitFor(() =>
+ findConsoleAPIMessage(hud, '[ "array-item", 42, {…} ]')
+ );
+ ok(arrayMessage, "Array 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..c64381520b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_console_table.js
@@ -0,0 +1,504 @@
+/* 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");
+ Assert.greater(
+ tableWrapperNode.scrollHeight,
+ tableWrapperNode.clientHeight,
+ testCase.info + " table overflows"
+ );
+ Assert.notStrictEqual(
+ 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..14198768cc
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_object.js
@@ -0,0 +1,184 @@
+/* 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);
+ /* Verify that the conflicting binding on user code doesn't break the
+ * functionality. */
+ function copy() { alert("user-defined function is called"); }
+</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..a119f4c06c
--- /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 = new FileUtils.File(
+ PathUtils.join(PathUtils.tempDir, `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..4925b03a41
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_reveal_in_inspector.js
@@ -0,0 +1,144 @@
+/* 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
+ );
+ Assert.notStrictEqual(
+ 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..e4b534f599
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_store_as_global.js
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test the "Store as global variable" menu item works even if the page
+// has a global variable conflicts with extra bindings.
+
+"use strict";
+
+const TEST_URI = `data:text/html;charset=utf-8,<!DOCTYPE html><script>
+ /* Verify a conflicting global doesn't break the feature. */
+ var _self = "wrong value";
+
+ 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_too_many_reports.js b/devtools/client/webconsole/test/browser/browser_webconsole_csp_too_many_reports.js
new file mode 100644
index 0000000000..41e98a2a42
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_csp_too_many_reports.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * This tests loads a page that triggers so many CSP reports that they throttled
+ * and a console error is logged.
+ */
+
+"use strict";
+
+const TEST_URI =
+ "data:text/html;charset=utf8,<!DOCTYPE html>Web Console CSP too many reports test";
+const TEST_VIOLATIONS =
+ "https://example.com/browser/devtools/client/webconsole/" +
+ "test/browser/test-csp-many-errors.html";
+const CSP_VIOLATION_MSG =
+ "Content-Security-Policy: The page\u2019s settings blocked the loading of a resource " +
+ "at inline (\u201cstyle-src\u201d).";
+const CSP_TOO_MANY_REPORTS_MSG =
+ "Content-Security-Policy: Prevented too many CSP reports from being sent within a short period of time.";
+
+add_task(async function () {
+ // Reduce the limit to reduce the log spam.
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.csp.reporting.limit.count", 10]],
+ });
+
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ const onCspViolationMessage = waitForMessageByType(
+ hud,
+ CSP_VIOLATION_MSG,
+ ".error"
+ ).then(() => info("Got violation message."));
+
+ const onCspTooManyReportsMessage = waitForMessageByType(
+ hud,
+ CSP_TOO_MANY_REPORTS_MSG,
+ ".error"
+ ).then(() => info("Got too many reports message."));
+
+ info("Load a page with CSP warnings.");
+ await navigateTo(TEST_VIOLATIONS);
+
+ info("Waiting for console messages.");
+ await Promise.all([onCspViolationMessage, onCspTooManyReportsMessage]);
+ ok(true, "Got error about too many reports");
+
+ await clearOutput(hud);
+});
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..ebaca341b3
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_csp_violation.js
@@ -0,0 +1,145 @@
+/* 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:25";
+ 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}`);
+ Assert.equal(
+ 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}`);
+ Assert.equal(
+ 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:25";
+ 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);
+ Assert.equal(
+ 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:40";
+
+ 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}`);
+ Assert.equal(
+ 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..4276384bfa
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_css_error_impacted_elements.js
@@ -0,0 +1,146 @@
+/* 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>
+ main {
+ & button {
+ cursor: unknownCursor;
+ }
+ }
+ </style>
+ <main>
+ <button id=1>Button 1</button>
+ <button id=2>Button 2</button>
+ </main>
+ <button id=out>Button 3</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();
+
+ const impactedElementsLabel = await waitFor(() =>
+ messageNode.querySelector(".elements-label")
+ );
+ is(
+ impactedElementsLabel.innerText,
+ "Elements matching selector: :is(main) button",
+ "The message was expanded and shows the expected selector"
+ );
+
+ const objectInspector = messageNode.querySelector(".object-inspector");
+ ok(
+ objectInspector.textContent.includes("NodeList [ button#1, button#2 ]"),
+ `The message shows the impacted elements (got "${objectInspector.textContent}")`
+ );
+
+ let node = objectInspector.querySelector(".objectBox-node");
+ let openInInspectorIcon = node.querySelector(".open-inspector");
+ Assert.notStrictEqual(
+ 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");
+ Assert.notStrictEqual(
+ 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..7fd026fe80
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_custom_formatters.js
@@ -0,0 +1,205 @@
+/* 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 () {
+ 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);
+ await testProxyObject(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 testProxyObject(hud) {
+ info("Test that Proxy objects can be handled by custom formatter");
+ await testCustomFormatting(hud, {
+ hasCustomFormatter: true,
+ messageText: "Formatted Proxy",
+ headerStyles: "font-weight: bold;",
+ });
+}
+
+async function testCustomFormatting(
+ hud,
+ { hasCustomFormatter, messageText, headerStyles, bodyText, bodyStyles }
+) {
+ const node = await waitFor(() => {
+ return findMessageVirtualizedByType({
+ hud,
+ text: messageText,
+ typeSelector: ".console-api",
+ });
+ });
+
+ 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..780a1b3ec1
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_custom_formatters_errors.js
@@ -0,0 +1,200 @@
+/* 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 () {
+ 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 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:19",
+ });
+}
+
+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`);
+ const node = await testCustomFormatting(hud, {
+ messageText: `Custom formatter failed: devtoolsFormatters[17].header threw`,
+ });
+ ok(
+ node.textContent.includes("window.windowUtils is undefined"),
+ "Access to windowUtils triggered expected error"
+ );
+}
+
+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"
+ );
+ }
+
+ return headerNode;
+}
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..f2b4ee49f6
--- /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.getCurrentThreadFrames();
+
+ 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..9583202c24
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_eval_in_debugger_stackframe2.js
@@ -0,0 +1,78 @@
+/* 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"
+ );
+ Assert.strictEqual(
+ firstCallEvaluationResult,
+ unresolvedSymbol,
+ "firstCall was not evaluated yet"
+ );
+
+ info("Resuming the thread");
+ dbg.actions.resume();
+
+ await onFirstCallMessageReceived;
+ Assert.notStrictEqual(
+ 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..6026f7039b
--- /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 = 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..d1c1d647fe
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_in_line_layout.js
@@ -0,0 +1,142 @@
+/* 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;
+ Assert.greater(
+ 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")
+ );
+
+ Assert.greater(
+ 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"
+ );
+ Assert.lessOrEqual(
+ node.offsetHeight,
+ node.ownerDocument.body.offsetHeight,
+ "console is not taller than document body"
+ );
+ const childSumHeight = [...node.childNodes].reduce(
+ (height, n) => height + n.offsetHeight,
+ 0
+ );
+ Assert.greaterOrEqual(
+ 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..589a898197
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_inspect_cross_domain_object.js
@@ -0,0 +1,134 @@
+/* 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");
+ Assert.notStrictEqual(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");
+ Assert.notStrictEqual(
+ 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..3dc5066663
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_keyboard_accessibility.js
@@ -0,0 +1,258 @@
+/* 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 HTML_FILENAME = `test.html`;
+const HTML_CONTENT = `<!DOCTYPE html><p>Test keyboard accessibility</p>
+ <script>
+ for (let i = 1; i <= 100; i++) {
+ console.log("console message " + i);
+ }
+ function logTrace() {
+ const sub = () => console.trace("console trace message");
+ sub();
+ }
+ logTrace();
+ </script>
+ `;
+
+const TRACE_FRAME_LINE_REGEX = /test\.html:\d+:?\d*/;
+
+const httpServer = createTestHTTPServer();
+httpServer.registerContentType("html", "text/html");
+httpServer.registerPathHandler(
+ "/" + HTML_FILENAME,
+ function (request, response) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(HTML_CONTENT);
+ }
+);
+
+const port = httpServer.identity.primaryPort;
+const TEST_URI = `http://localhost:${port}/${HTML_FILENAME}`;
+
+add_task(async function () {
+ // Force tabfocus for all elements on OSX.
+ SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 7]] });
+
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ info("Web Console opened");
+ const outputScroller = hud.ui.outputScroller;
+ const traceMsgNode = await waitFor(
+ () => findConsoleAPIMessage(hud, "console trace message"),
+ "waiting for all the messages to be displayed",
+ 100,
+ 1000
+ );
+
+ // wait for all the stacktrace frames to be rendered.
+ await waitFor(() =>
+ traceMsgNode.querySelector(".message-body-wrapper > .stacktrace .frames")
+ );
+
+ let currentPosition = outputScroller.scrollTop;
+ const bottom = currentPosition;
+ hud.jsterm.focus();
+
+ info("Check Page up keyboard shortcut");
+ EventUtils.synthesizeKey("KEY_PageUp");
+ isnot(
+ outputScroller.scrollTop,
+ currentPosition,
+ "scroll position changed after page up"
+ );
+
+ info("Check Page down keyboard shortcut");
+ currentPosition = outputScroller.scrollTop;
+ EventUtils.synthesizeKey("KEY_PageDown");
+ Assert.greater(
+ outputScroller.scrollTop,
+ currentPosition,
+ "scroll position now at bottom"
+ );
+
+ info("Check Home keyboard shortcut");
+ EventUtils.synthesizeKey("KEY_Home");
+ is(outputScroller.scrollTop, 0, "scroll position now at top");
+
+ info("Check End keyboard shortcut");
+ EventUtils.synthesizeKey("KEY_End");
+ const scrollTop = outputScroller.scrollTop;
+ ok(
+ scrollTop > 0 && Math.abs(scrollTop - bottom) <= 5,
+ "scroll position now at bottom"
+ );
+
+ info("Hit Shift-Tab to focus switch to editor mode button");
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ ok(
+ outputScroller.ownerDocument.activeElement.classList.contains(
+ "webconsole-input-openEditorButton"
+ ),
+ "switch to editor mode button is focused"
+ );
+
+ info("Check stacktrace frames can be focused");
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ ok(
+ outputScroller.ownerDocument.activeElement.closest(".message.trace"),
+ "The active element is in the trace message"
+ );
+ is(
+ TRACE_FRAME_LINE_REGEX.exec(
+ outputScroller.ownerDocument.activeElement.innerText
+ )?.[0],
+ "test.html:10",
+ `last frame of the stacktrace is focused ${outputScroller.ownerDocument.activeElement.innerText}`
+ );
+ is(
+ outputScroller.ownerDocument.activeElement.getAttribute("class"),
+ "frame",
+ "active element has expected class"
+ );
+
+ info("Hit Tab to navigate to second frame of the stacktrace");
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ ok(
+ outputScroller.ownerDocument.activeElement.closest(".message.trace"),
+ "The active element is in the trace message"
+ );
+ is(
+ TRACE_FRAME_LINE_REGEX.exec(
+ outputScroller.ownerDocument.activeElement.innerText
+ )?.[0],
+ "test.html:8",
+ "second frame of the stacktrace is focused"
+ );
+ is(
+ outputScroller.ownerDocument.activeElement.getAttribute("class"),
+ "frame",
+ "active element has expected class"
+ );
+
+ info("Hit Tab to navigate to first frame of the stacktrace");
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ ok(
+ outputScroller.ownerDocument.activeElement.closest(".message.trace"),
+ "The active element is in the trace message"
+ );
+ is(
+ TRACE_FRAME_LINE_REGEX.exec(
+ outputScroller.ownerDocument.activeElement.innerText
+ )?.[0],
+ "test.html:7",
+ "third frame of the stacktrace is focused"
+ );
+ is(
+ outputScroller.ownerDocument.activeElement.getAttribute("class"),
+ "frame",
+ "active element has expected class"
+ );
+
+ info("Hit Tab to navigate to the message location");
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ ok(
+ outputScroller.ownerDocument.activeElement.closest(".message.trace"),
+ "The active element is in the trace message"
+ );
+ is(
+ outputScroller.ownerDocument.activeElement.getAttribute("class"),
+ "frame-link-source",
+ "active element is the console trace message location"
+ );
+ is(
+ TRACE_FRAME_LINE_REGEX.exec(
+ outputScroller.ownerDocument.activeElement.innerText
+ )?.[0],
+ "test.html:7:33",
+ "active element is expected message location"
+ );
+
+ info("Hit Tab to navigate to the previous message location");
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ ok(
+ !outputScroller.ownerDocument.activeElement.closest(".message.trace"),
+ "The active element is not in the trace message"
+ );
+ is(
+ outputScroller.ownerDocument.activeElement.getAttribute("class"),
+ "frame-link-source",
+ "active element is the console trace message location"
+ );
+ is(
+ TRACE_FRAME_LINE_REGEX.exec(
+ outputScroller.ownerDocument.activeElement.innerText
+ )[0],
+ "test.html:4:15",
+ "active element is expected message location"
+ );
+
+ info("Clear output");
+ hud.jsterm.focus();
+
+ 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..91c3738d5b
--- /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 = 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..d44246654f
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_location_logpoint_debugger_link.js
@@ -0,0 +1,188 @@
+/* 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");
+
+ Assert.strictEqual(
+ 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..4c0ac151a4
--- /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 = 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..248da4533b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_message_categories.js
@@ -0,0 +1,158 @@
+/* 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: ".error",
+ },
+];
+
+add_task(async function () {
+ requestLongerTimeout(2);
+
+ // Disable bfcache for Fission for now.
+ // If Fission is disabled, the pref is no-op.
+ await pushPref("fission.bfcacheInParent", false);
+ 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 } = test;
+
+ 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 ("${matchString}") to be displayed in the hud`);
+ await onMessageLogged;
+ ok(true, `Message "${matchString}" displayed`);
+}
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_network_attach.js b/devtools/client/webconsole/test/browser/browser_webconsole_network_attach.js
new file mode 100644
index 0000000000..9993423518
--- /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 = 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..e5549a686e
--- /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 = 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..8a92ef07dc
--- /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 = 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..d34970b511
--- /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");
+ Assert.notStrictEqual(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..821246438e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_nodes_select.js
@@ -0,0 +1,70 @@
+/* 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");
+ Assert.notStrictEqual(node, null, "Node was logged as expected");
+
+ const openInInspectorIcon = node.querySelector(".open-inspector");
+ Assert.notStrictEqual(
+ 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..a00be2db53
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector.js
@@ -0,0 +1,169 @@
+/* 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;
+
+ let arrayOiArrowButton = arrayOi.querySelector(".arrow");
+ is(
+ arrayOiArrowButton.getAttribute("title"),
+ "Expand",
+ "Toggle button has expected title when node is collapsed"
+ );
+
+ info("Expanding the array object inspector");
+
+ let onArrayOiMutation = waitForNodeMutation(arrayOi, {
+ childList: true,
+ });
+ arrayOiArrowButton.click();
+ await onArrayOiMutation;
+
+ arrayOiArrowButton = arrayOi.querySelector(".arrow");
+ ok(
+ arrayOi.querySelector(".arrow").classList.contains("expanded"),
+ "Toggle button of the root node of the tree is expanded after clicking on it"
+ );
+ is(
+ arrayOiArrowButton.getAttribute("title"),
+ "Collapse",
+ "Toggle button has expected title when node is expanded"
+ );
+
+ 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..16f1261862
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_entries.js
@@ -0,0 +1,831 @@
+/* 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);
+ // enable custom highlight API
+ await pushPref("dom.customHighlightAPI.enabled", 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.CSS.highlights.set("search", new content.Highlight());
+ content.CSS.highlights.set("glow", new content.Highlight());
+ content.CSS.highlights.set("anchor", new content.Highlight());
+
+ 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,
+ content.CSS.highlights
+ );
+
+ 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,
+ 12,
+ "There is the expected number of object inspectors"
+ );
+
+ const [
+ smallMapOi,
+ mapOi,
+ largeMapOi,
+ smallSetOi,
+ setOi,
+ largeSetOi,
+ urlSearchParamsOi,
+ headersOi,
+ formDataOi,
+ midiInputsOi,
+ midiOutputsOi,
+ highlightsRegistryOi,
+ ] = 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);
+ await testHighlightsRegistry(highlightsRegistryOi);
+});
+
+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");
+ Assert.greater(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"
+ );
+}
+
+async function testHighlightsRegistry(oi) {
+ is(
+ oi.textContent,
+ `HighlightRegistry(3) { search → Highlight, glow → Highlight, anchor → Highlight }`,
+ "HighlightRegistry has expected content"
+ );
+
+ info("Expanding the HighlightRegistry");
+ 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 HighlightRegistry");
+ 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,
+ `0: search → Highlight { priority: 0, type: "highlight", size: 0 }`,
+ "First entry is displayed as expected"
+ );
+ is(
+ oiNodes[4].textContent,
+ `1: glow → Highlight { priority: 0, type: "highlight", size: 0 }`,
+ `Second entry is displayed as expected`
+ );
+ is(
+ oiNodes[5].textContent,
+ `2: anchor → Highlight { priority: 0, type: "highlight", size: 0 }`,
+ `Third entry entry is displayed as expected`
+ );
+
+ info("Expand last entry");
+ onOiMutation = waitForNodeMutation(oi, {
+ childList: true,
+ });
+ oiNodes[5].querySelector(".arrow").click();
+ await onOiMutation;
+
+ oiNodes = oi.querySelectorAll(".node");
+ // There are now 9 nodes, the 7 original ones, <key> and <value>
+ is(oiNodes.length, 9, "There is the expected number of nodes in the tree");
+ is(oiNodes[6].textContent, `<key>: "anchor"`, `Got expected key node`);
+ is(
+ oiNodes[7].textContent,
+ `<value>: Highlight { priority: 0, type: "highlight", size: 0 }`,
+ `Got expected value node`
+ );
+
+ info("Expand Highlight object");
+ onOiMutation = waitForNodeMutation(oi, {
+ childList: true,
+ });
+ oiNodes[7].querySelector(".arrow").click();
+ await onOiMutation;
+
+ oiNodes = oi.querySelectorAll(".node");
+ // There are now 13 nodes, the 9 previous ones, and all the properties of the Highlight object
+ is(oiNodes.length, 13, "There is the expected number of nodes in the tree");
+ is(oiNodes[8].textContent, `priority: 0`, `Got expected priority property`);
+ is(oiNodes[9].textContent, `size: 0`, `Got expected size property`);
+ is(
+ oiNodes[10].textContent,
+ `type: "highlight"`,
+ `Got expected type property`
+ );
+ is(
+ oiNodes[11].textContent,
+ `<prototype>: HighlightPrototype { add: add(), clear: clear(), delete: delete(), … }`,
+ `Got expected prototype property`
+ );
+}
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_object_inspector_wrapped_primitive_value.js b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_wrapped_primitive_value.js
new file mode 100644
index 0000000000..d8919e00d4
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_object_inspector_wrapped_primitive_value.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that primitive value is shown
+
+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(
+ "wrapped-primitive-value-test",
+ Object(123),
+ Object(false),
+ Object("")
+ );
+ });
+
+ const node = await waitFor(() =>
+ findConsoleAPIMessage(hud, "wrapped-primitive-value-test")
+ );
+
+ const objectInspectors = [...node.querySelectorAll(".tree")];
+
+ is(
+ objectInspectors.length,
+ 3,
+ "There is the expected number of object inspectors"
+ );
+ const [intOi, boolOi, stringOi] = objectInspectors;
+ await expandOiAndCheckPrimitiveValue(intOi, "123");
+ await expandOiAndCheckPrimitiveValue(boolOi, "false");
+ await expandOiAndCheckPrimitiveValue(stringOi, `""`);
+});
+
+async function expandOiAndCheckPrimitiveValue(oi, expectedPrimitiveValue) {
+ 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 primitiveValueNode = [...getObjectInspectorNodes(oi)].find(nodes =>
+ nodes.textContent.includes("<primitive value>")
+ );
+
+ ok(
+ primitiveValueNode.textContent.includes(
+ `<primitive value>: ${expectedPrimitiveValue}`
+ ),
+ "There is the expected <primitive value> node"
+ );
+}
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..08fa17d629
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_persist.js
@@ -0,0 +1,327 @@
+/* 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();
+});
+
+add_task(async function consoleClearPersist() {
+ info("Testing that messages persist on console.clear if logs are persisted");
+
+ await pushPref("devtools.webconsole.persistlog", true);
+ const hud = await openNewTabAndConsole(TEST_COM_URI);
+
+ await logAndAssertInitialMessages(hud);
+
+ info("Send a console.clear() and another log from the content page");
+ const onConsoleClearPrevented = waitForMessageByType(
+ hud,
+ "console.clear() was prevented",
+ ".console-api"
+ );
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.wrappedJSObject.console.clear();
+ content.wrappedJSObject.console.log("after clear");
+ });
+
+ await waitForMessageByType(hud, "after clear", ".log");
+ await onConsoleClearPrevented;
+ ok(true, "console.clear was handled by the client");
+
+ Assert.strictEqual(
+ findAllMessages(hud).length,
+ INITIAL_LOGS_NUMBER + 2,
+ "All initial messages are still displayed, with the 2 new ones"
+ );
+
+ 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.
+ Assert.greaterOrEqual(
+ lastMessage.timeStamp,
+ timeBeforeNavigation,
+ "The navigation message has a timestamp newer (or equal) than the time before the navigation..."
+ );
+ Assert.less(
+ 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..29fd3d491b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_sidebar_scroll.js
@@ -0,0 +1,55 @@
+/* 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);
+ Assert.greater(
+ 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..7253e3f7a5
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_split.js
@@ -0,0 +1,371 @@
+/* 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"
+ );
+ Assert.greater(
+ currentUIState.deckHeight,
+ 0,
+ "Deck has a height > 0 when console is split"
+ );
+ Assert.greater(
+ 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"
+ );
+ Assert.greater(
+ currentUIState.deckHeight,
+ 0,
+ "Deck has a height > 0 when console is split"
+ );
+ Assert.greater(
+ 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"
+ );
+ Assert.greater(
+ currentUIState.deckHeight,
+ 0,
+ "Deck has a height > 0 when console is split"
+ );
+ Assert.greater(
+ 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..2442b64b5b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_split_persist.js
@@ -0,0 +1,144 @@
+/* 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";
+ Assert.greater(
+ 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..a75ce28843
--- /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 = 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..34a5a62c5e
--- /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
+ Assert.greater(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..b801f3b898
--- /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];
+
+ Assert.greater(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..eaa94095a8
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_warn_about_replaced_api.js
@@ -0,0 +1,62 @@
+/* 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.startLoadingURIString(
+ 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..c895031b26
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_worker_promise_error.js
@@ -0,0 +1,24 @@
+/* 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 () {
+ 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_console.js b/devtools/client/webconsole/test/browser/browser_webconsole_worklet_console.js
new file mode 100644
index 0000000000..dee5c4a54c
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_worklet_console.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that console api usage in worklet show in the console
+
+"use strict";
+
+const TEST_URI =
+ "https://example.com/browser/devtools/client/webconsole/" +
+ "test/browser/test-console-worklet.html";
+
+add_task(async function () {
+ // Allow using SharedArrayBuffer in the test without special HTTP Headers
+ await pushPref(
+ "dom.postMessage.sharedArrayBuffer.bypassCOOP_COEP.insecure.enabled",
+ true
+ );
+
+ const hud = await openNewTabAndConsole(TEST_URI);
+
+ await waitFor(() => findConsoleAPIMessage(hud, "string"));
+ await waitFor(() => findConsoleAPIMessage(hud, "42"));
+ const objectMessage = await waitFor(() =>
+ findConsoleAPIMessage(hud, "object")
+ );
+ ok(
+ objectMessage
+ .querySelector(".message-body")
+ .textContent.includes(`Object { object: true }`),
+ "The simple object is logged as expected"
+ );
+ await waitFor(() => findConsoleAPIMessage(hud, "SharedArrayBuffer"));
+ ok(true, "SharedArrayBuffer object is logged");
+});
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..f17642e457
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_worklet_error.js
@@ -0,0 +1,22 @@
+/* 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 () {
+ 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..1a74f2deee
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/head.js
@@ -0,0 +1,1916 @@
+/* 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 = 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 = 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 = 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(frame);
+ await onScopes;
+}
+
+async function pauseDebugger(dbg, options = { shouldWaitForLoadScopes: true }) {
+ info("Waiting for debugger to pause");
+ const onPaused = waitForPaused(dbg, null, options);
+ 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,
+ SpecialPowers.wrap(window).browsingContext.currentWindowContext
+ );
+ 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..7ad442f96a
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/shared-head.js
@@ -0,0 +1,517 @@
+/* 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);
+}
+function findTracerMessages(hud, text, extraSelector = "") {
+ return findMessagesByType(hud, text, ".jstracer" + 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..68fcda8ad2
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-console-custom-formatters.html
@@ -0,0 +1,128 @@
+<!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 proxy = new Proxy({}, {foo: "bar"});
+
+ const variables = [
+ "string",
+ 1337,
+ { noFormat: true },
+ { customFormatHeader: "header" },
+ { customFormatHeaderAndBody: "body" },
+ { customFormatObjectAndConfig: true },
+ proxy,
+ ];
+
+ 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)
+ ]
+ }
+ ]
+ ]}
+ },
+ {
+ header: (obj, config) => {
+ if (obj === proxy) {
+ return [
+ "span",
+ {"style": "font-weight: bold;"},
+ "Formatted Proxy",
+ ];
+ }
+ return null;
+ },
+ hasBody: obj => false
+ },
+ ];
+
+ 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..8a08cee607
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-console-workers.html
@@ -0,0 +1,31 @@
+<!-- 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"));
+ console.log(["array-item", 42, { key: "value" }]);
+ console.log("sab-from-worker", e.data.sab);
+ };
+ `);
+
+ function logFromWorker(msg) {
+ const sab = new SharedArrayBuffer(1024);
+ worker.postMessage({type: "log", msg, sab});
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/browser/test-console-worklet.html b/devtools/client/webconsole/test/browser/test-console-worklet.html
new file mode 100644
index 0000000000..4f9dfdb60b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-console-worklet.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Worklet console generator</title>
+ <!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+</head>
+
+<script type="worklet">
+registerProcessor('test-param', class param extends AudioWorkletProcessor {
+ constructor() {
+ super();
+ this.port.onmessage = e => {
+ console.log("worklet", "string");
+ console.log("worklet", 42);
+ console.log("worklet", { object: true });
+ console.log("worklet", e.data.data); // Log the SharedArrayBuffer
+ };
+ }
+ process(input, output, parameters) {
+ return true;
+ }
+});
+</script>
+
+<script>
+"use strict";
+const ac = new AudioContext();
+const e = document.querySelector("script[type=worklet]")
+const blob = new Blob([e.textContent], {type: "application/javascript"});
+const url = URL.createObjectURL(blob);
+ac.audioWorklet.addModule(url).then(() => {
+ const nodea = new AudioWorkletNode(ac, 'test-param');
+
+ // Instantiate and pass a SharedArrayBuffer to the Worklet
+ const normal_sab = new SharedArrayBuffer(1024);
+ nodea.port.postMessage({data: normal_sab });
+});
+</script>
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-many-errors.html b/devtools/client/webconsole/test/browser/test-csp-many-errors.html
new file mode 100644
index 0000000000..52d05517c8
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-csp-many-errors.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Test for Bug 1839165 - Verify that we warn about too many reports.</title>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<div></div>
+<script>
+"use strict";
+
+function run() {
+ const div = document.querySelector("div");
+ // We limit the number of allowed CSP reports to 10.
+ for (let i = 0; i < 15; i++) {
+ div.setAttribute("style", `background-color: rgb(0, 0, ${i});`);
+ }
+}
+run();
+</script>
+</body>
+</html>
diff --git a/devtools/client/webconsole/test/browser/test-csp-many-errors.html^headers^ b/devtools/client/webconsole/test/browser/test-csp-many-errors.html^headers^
new file mode 100644
index 0000000000..20cc933daf
--- /dev/null
+++ b/devtools/client/webconsole/test/browser/test-csp-many-errors.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: style-src 'none'; report-uri https://example.com/ignored/; \ No newline at end of file
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-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");
diff --git a/devtools/client/webconsole/test/chrome/chrome.toml b/devtools/client/webconsole/test/chrome/chrome.toml
new file mode 100644
index 0000000000..bda01925dc
--- /dev/null
+++ b/devtools/client/webconsole/test/chrome/chrome.toml
@@ -0,0 +1,6 @@
+[DEFAULT]
+
+support-files = ["head.js"]
+
+["test_render_perf.html"]
+skip-if = ["true"] # Bug 1306783
diff --git a/devtools/client/webconsole/test/chrome/head.js b/devtools/client/webconsole/test/chrome/head.js
new file mode 100644
index 0000000000..2f984d564d
--- /dev/null
+++ b/devtools/client/webconsole/test/chrome/head.js
@@ -0,0 +1,14 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* exported Task, browserRequire */
+
+"use strict";
+
+var { BrowserLoader } = ChromeUtils.import(
+ "resource://devtools/shared/loader/browser-loader.js"
+);
+
+var { require: browserRequire } = BrowserLoader({
+ baseURI: "resource://devtools/client/webconsole/",
+ window,
+});
diff --git a/devtools/client/webconsole/test/chrome/test_render_perf.html b/devtools/client/webconsole/test/chrome/test_render_perf.html
new file mode 100644
index 0000000000..f0c96fe67c
--- /dev/null
+++ b/devtools/client/webconsole/test/chrome/test_render_perf.html
@@ -0,0 +1,240 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+ <meta charset="utf8">
+ <title>Test for getRepeatId()</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="head.js"></script>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test for render perf</p>
+<div id="output"></div>
+
+<script type="text/javascript">
+"use strict";
+
+// To analyze the profile results:
+// > ./mach mochitest test_render_perf.html
+// Then open https://profiler.firefox.com and drag the json file printed at the end of
+// this test
+
+const NUM_MESSAGES = 4000;
+const NUM_STREAMING = 100;
+const {FileUtils} = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+Services.prefs.setIntPref("devtools.hud.loglimit", NUM_MESSAGES);
+const WebConsoleWrapper = browserRequire("devtools/client/webconsole/webconsole-wrapper");
+const actions =
+ browserRequire("devtools/client/webconsole/actions/index");
+const EventEmitter = browserRequire("devtools/shared/event-emitter");
+const testPackets = Array.from({length: NUM_MESSAGES}).map((el, id) => ({
+ "from": "server1.conn4.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "foobar",
+ `${id % 2 === 0 ? "Even" : "Odd"} text`,
+ id,
+ ],
+ "columnNumber": 1,
+ "counter": null,
+ "filename": "file:///test.html",
+ "functionName": "",
+ "groupName": "",
+ "level": "log",
+ "lineNumber": 1,
+ "private": false,
+ "styles": [],
+ "timeStamp": 1455064271115 + id,
+ "timer": null,
+ "workerType": "none",
+ "category": "webdev",
+ },
+}));
+const lastPacket = testPackets.pop();
+
+function startMeasure(label) {
+ const startLabel = label + "start";
+ performance.mark(startLabel);
+ return {
+ stop: (clear = true) => {
+ performance.measure(label, startLabel);
+ const entries = performance.getEntriesByName(label);
+ if (clear) {
+ performance.clearMeasures(label);
+ }
+ return entries[entries.length - 1];
+ },
+ };
+}
+
+function wait(time) {
+ return new Promise(resolve => setTimeout(resolve, time));
+}
+
+async function addAllMessages(wrapper) {
+ testPackets.forEach((packet) => wrapper.dispatchMessageAdd(packet));
+ // Only wait for the last packet to minimize work.
+ await wrapper.dispatchMessageAdd(lastPacket, true);
+ await new Promise(resolve => requestAnimationFrame(resolve));
+}
+
+async function addMessage(wrapper, message) {
+ await wrapper.dispatchMessageAdd(message, true);
+}
+
+function getTimes(times) {
+ times = times.sort();
+ const totalTime = times.reduce((sum, t) => sum + t);
+ const avg = totalTime / times.length;
+ const median = times.length % 2 !== 0
+ ? times[Math.floor(times.length / 2)]
+ : (times[(times.length / 2) - 1] + times[times.length / 2]) / 2;
+ return {avg, median};
+}
+
+async function clearMessages(wrapper) {
+ wrapper.dispatchMessagesClear();
+ await new Promise(resolve => requestAnimationFrame(resolve));
+}
+
+async function testBulkLogging(wrapper) {
+ await clearMessages(wrapper);
+ const bulkTimes = [];
+ const iterations = 5;
+ for (let i = 0; i < iterations; i++) {
+ const measure = startMeasure("bulk log");
+ await addAllMessages(wrapper);
+ const {duration} = measure.stop();
+
+ info(`took ${duration} ms to render bulk messages (iteration ${i})`);
+ bulkTimes.push(duration);
+
+ // Do not clear the store on last iteration so the next test can use the messages.
+ if (i !== iterations - 1) {
+ // Wait before clearing messages so those events are more spotable on the profile.
+ await wait(500);
+ await clearMessages(wrapper);
+ await wait(500);
+ }
+ }
+ const {avg, median} = getTimes(bulkTimes);
+
+ info(`BULK: On average, it took ${avg} ms (median ${median} ms) ` +
+ `to render ${NUM_MESSAGES} messages`);
+}
+
+async function testFiltering(wrapper) {
+ const measureFilter = startMeasure("filtering");
+
+ const measureFilterOff = startMeasure("filtering off");
+ await wrapper.getStore().dispatch(actions.filterToggle("log"));
+ const measureFilterOffEntry = measureFilterOff.stop();
+ info(`Filter toggle time (off): ${measureFilterOffEntry.duration}`);
+
+ const measureFilterOn = startMeasure("filtering on");
+ await wrapper.getStore().dispatch(actions.filterToggle("log"));
+ const measureFilterOnEntry = measureFilterOn.stop();
+ info(`Filter toggle time (on): ${measureFilterOnEntry.duration}`);
+ measureFilter.stop();
+}
+
+async function testStreamLogging(wrapper, clear = true) {
+ const streamMeasureLabel = "stream" + (clear === false ? " and prune" : "");
+ const streamMeasure = startMeasure(streamMeasureLabel);
+ if (clear === true) {
+ await clearMessages(wrapper);
+ }
+
+ const measureLabel = "stream - add single message";
+ for (let i = 0; i < NUM_STREAMING; i++) {
+ const measure = startMeasure(measureLabel);
+ await addMessage(wrapper, testPackets[i]);
+ measure.stop(false);
+ await new Promise(resolve => setTimeout(resolve, 100));
+ }
+
+ const streamTimes = performance.getEntriesByName(measureLabel)
+ .map(entry => entry.duration);
+ performance.clearMeasures(measureLabel);
+ const { avg, median } = getTimes(streamTimes);
+ info(`STREAMING${clear === false ? " AND PRUNING" : ""}: On average, ` +
+ `it took ${avg} ms (median ${median} ms) for each message`);
+ streamMeasure.stop();
+}
+
+window.onload = async function() {
+ // This test does costly work multiple times to have better performance data.
+ // It doesn't run in automation
+ SimpleTest.requestLongerTimeout(3);
+
+ const wrapper = new WebConsoleWrapper(
+ document.getElementById("output"),
+ {hud: EventEmitter.decorate({proxy: {}}), focus: () => {}},
+ {},
+ null,
+ document,
+ );
+ wrapper.init();
+
+ // From https://github.com/firefox-devtools/profiler/blob/b73eb73df04c7df51464fa50eeadef3dc7f5d4e2/docs/gecko-profile-format.md#L21
+ const settings = {
+ entries: 100000000,
+ interval: 1,
+ features: ["js"],
+ threads: ["GeckoMain"],
+ };
+ Services.profiler.StartProfiler(
+ settings.entries,
+ settings.interval,
+ settings.features,
+ settings.threads,
+ );
+ info("Profiler has started");
+
+ await wait(500);
+
+ await testBulkLogging(wrapper);
+
+ await wait(500);
+
+ await testFiltering(wrapper);
+
+ await wait(500);
+
+ // first pass, without clearing the store.
+ await testStreamLogging(wrapper, false);
+
+ await wait(500);
+ // second pass, with an empty store.
+ await testStreamLogging(wrapper, true);
+
+ ok(true, "Tests finished");
+
+ const file = new FileUtils.File(PathUtils.join(PathUtils.tempDir, `test_render_perf_${Date.now()}.json`));
+ Services.profiler.dumpProfileToFile(file.path);
+ Services.profiler.StopProfiler();
+
+ info(`
+
+SAVING PROFILE: ${file.path}
+
+To upload the profile, run the following commands:
+
+ gzip ${file.path}
+ curl -O https://raw.githubusercontent.com/firefox-devtools/profiler-server/master/tools/decode_jwt_payload.py
+ curl 'https://api.profiler.firefox.com/compressed-store' --compressed --data-binary @${file.path}.gz --header 'Accept: application/vnd.firefox-profiler+json;version=1' \\
+ | python3 decode_jwt_payload.py \\
+ | awk '{print "Hosted at: https://profiler.firefox.com/public/"$1}'
+
+
+`);
+
+ SimpleTest.finish();
+};
+</script>
+</body>
+</html>
diff --git a/devtools/client/webconsole/test/node/.eslintrc.js b/devtools/client/webconsole/test/node/.eslintrc.js
new file mode 100644
index 0000000000..d6776ad3f7
--- /dev/null
+++ b/devtools/client/webconsole/test/node/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ env: {
+ jest: true,
+ },
+};
diff --git a/devtools/client/webconsole/test/node/babel.config.js b/devtools/client/webconsole/test/node/babel.config.js
new file mode 100644
index 0000000000..9d4fec58b9
--- /dev/null
+++ b/devtools/client/webconsole/test/node/babel.config.js
@@ -0,0 +1,24 @@
+/* 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";
+
+module.exports = {
+ plugins: [
+ "@babel/plugin-proposal-class-properties",
+ "@babel/plugin-proposal-optional-chaining",
+ "@babel/plugin-proposal-nullish-coalescing-operator",
+ "@babel/plugin-proposal-private-methods",
+ "transform-amd-to-commonjs",
+ ],
+ presets: [
+ "@babel/preset-react",
+ [
+ "@babel/preset-env",
+ {
+ modules: "commonjs",
+ },
+ ],
+ ],
+};
diff --git a/devtools/client/webconsole/test/node/components/console-api-call.log-messages.test.js b/devtools/client/webconsole/test/node/components/console-api-call.log-messages.test.js
new file mode 100644
index 0000000000..afc55c211c
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/console-api-call.log-messages.test.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test utils.
+const expect = require("expect");
+const { render } = require("enzyme");
+
+// React
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ setupStore,
+} = require("resource://devtools/client/webconsole/test/node/helpers.js");
+const Provider = createFactory(
+ require("resource://devtools/client/shared/vendor/react-redux.js").Provider
+);
+
+// Components under test.
+const ConsoleApiCall = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/message-types/ConsoleApiCall.js")
+);
+
+const {
+ stubPreparedMessages,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+
+const serviceContainer = require("resource://devtools/client/webconsole/test/node/fixtures/serviceContainer.js");
+
+describe("ConsoleAPICall component for platform message", () => {
+ describe("Services.console.logStringMessage", () => {
+ it("renders logMessage grips", () => {
+ const message = stubPreparedMessages.get("platform-simple-message");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text()).toBe("foobar test");
+
+ // There should not be the location
+ expect(wrapper.find(".message-location").text()).toBe("");
+ });
+
+ it("renders longString logMessage grips", () => {
+ const message = stubPreparedMessages.get("platform-longString-message");
+
+ // We need to wrap the ConsoleApiElement in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ ConsoleApiCall({ message, serviceContainer })
+ )
+ );
+
+ expect(wrapper.find(".message-body").text()).toInclude(
+ `a\n${"a".repeat(100)}`
+ );
+ });
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/console-api-call.test.js b/devtools/client/webconsole/test/node/components/console-api-call.test.js
new file mode 100644
index 0000000000..7e10002f5c
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/console-api-call.test.js
@@ -0,0 +1,695 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test utils.
+const expect = require("expect");
+const { render, mount } = require("enzyme");
+const sinon = require("sinon");
+
+// React
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const Provider = createFactory(
+ require("resource://devtools/client/shared/vendor/react-redux.js").Provider
+);
+const {
+ setupStore,
+} = require("resource://devtools/client/webconsole/test/node/helpers.js");
+
+// Components under test.
+const ConsoleApiCall = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/message-types/ConsoleApiCall.js")
+);
+const {
+ MESSAGE_OPEN,
+ MESSAGE_CLOSE,
+} = require("resource://devtools/client/webconsole/constants.js");
+const {
+ INDENT_WIDTH,
+} = require("resource://devtools/client/webconsole/components/Output/MessageIndent.js");
+const {
+ prepareMessage,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+
+// Test fakes.
+const {
+ stubPreparedMessages,
+ stubPackets,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+const serviceContainer = require("resource://devtools/client/webconsole/test/node/fixtures/serviceContainer.js");
+
+describe("ConsoleAPICall component:", () => {
+ describe("console.log", () => {
+ it("renders string grips", () => {
+ const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text()).toBe("foobar test");
+ expect(wrapper.find(".objectBox-string").length).toBe(2);
+ const selector =
+ "div.message.cm-s-mozilla span span.message-flex-body " +
+ "span.message-body.devtools-monospace";
+ expect(wrapper.find(selector).length).toBe(1);
+
+ // There should be the location
+ const locationLink = wrapper.find(`.message-location`);
+ expect(locationLink.length).toBe(1);
+ expect(locationLink.text()).toBe("test-console-api.html:1:35");
+ });
+
+ it("renders string grips with custom style", () => {
+ const message = stubPreparedMessages.get("console.log(%cfoobar)");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ const elements = wrapper.find(".objectBox-string");
+ expect(elements.text()).toBe("foobar");
+ expect(elements.length).toBe(2);
+
+ const firstElementStyle = elements.eq(0).prop("style");
+ // Allowed styles are applied accordingly on the first element.
+ expect(firstElementStyle.color).toBe(`blue`);
+ expect(firstElementStyle["font-size"]).toBe(`1.3em`);
+ // Forbidden styles are not applied.
+ expect(firstElementStyle["background-image"]).toBe(undefined);
+ expect(firstElementStyle.position).toBe(undefined);
+ expect(firstElementStyle.top).toBe(undefined);
+
+ const secondElementStyle = elements.eq(1).prop("style");
+ // Allowed styles are applied accordingly on the second element.
+ expect(secondElementStyle.color).toBe(`red`);
+ expect(secondElementStyle["line-height"]).toBe("1.5");
+ // Forbidden styles are not applied.
+ expect(secondElementStyle.background).toBe(undefined);
+ });
+
+ it("renders string grips with data-url background", () => {
+ const message = stubPreparedMessages.get("console.log(%cfoobar)");
+
+ const dataURL =
+ "url(data:image/png,base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAYAAABy6+R8AAAAAXNSR0IArs4c6QAAAaZJREFUKBV9UjFLQlEUPueaaUUEDZESNTYI9RfK5oimIGxJcy4IodWxpeZKiHBwc6ghIiIaWqNB/ANiBSU6vHfvU+89nSPpINKB996993zfPef7zkP4CyLCzl02Y7VNg4aE8y2QoYrTVJg+ublCROpjURb0ko11mt2i05BkEDjt+CGgvzUYehrvqtTUefFD8KpXoemK1ich1MDALppIPITROARqlwzWJKdbtihYIWH7dv/AenRBAdYmOriKmUJDEv1oHaVnOy39bn27wJjsfLl0qawHaWkFNOWGCUKcOSs0uM0c6wPyWC+H4k2Ce+b+w89yMDKC0LPzWadgORTJxh8YM5ITIdUmw4b5jt9MssaJrdD9DtZGMvjQ+zEbvUoBVgWj2K2CWGsDeyqih4n1zeyk1S4vlcDCteRRbGwe7z2yO0lOiOU5YA3SklTgYee5/WVw1IVoZGnxrVTv+e4dpmIyB74xSayPBQ8GS5qvZgIRjPFfUQlHQ+s9kpSUil9bOxl2U0aQIO0Mf6tA6hoi4Xsw7QfGsHv4OiAJ8b/4XNmeC9pYRgTvF+HgISP3T9PvAAAAAElFTkSuQmCC)";
+
+ message.userProvidedStyles[0] = `background-image: ${dataURL}`;
+
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+ const elements = wrapper.find(".objectBox-string");
+ const firstElementStyle = elements.eq(0).prop("style");
+
+ // data-url background applied
+ expect(firstElementStyle["background-image"]).toBe(dataURL);
+ });
+
+ it("renders custom styled logs with empty style as expected", () => {
+ const message = stubPreparedMessages.get(
+ 'console.log("%cHello%c|%cWorld")'
+ );
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ const elements = wrapper.find(".objectBox-string");
+ expect(elements.text()).toBe("Hello|World");
+ expect(elements.length).toBe(3);
+
+ const firstElementStyle = elements.eq(0).prop("style");
+ // Allowed styles are applied accordingly on the first element.
+ expect(firstElementStyle.color).toBe("red");
+
+ const secondElementStyle = elements.eq(1).prop("style");
+ expect(secondElementStyle.color).toBe(undefined);
+
+ const thirdElementStyle = elements.eq(2).prop("style");
+ // Allowed styles are applied accordingly on the third element.
+ expect(thirdElementStyle.color).toBe("blue");
+ });
+
+ it("renders prefixed messages", () => {
+ const packet = stubPackets.get("console.log('foobar', 'test')");
+ const stub = {
+ ...packet,
+ message: {
+ ...packet.message,
+ prefix: "MyNicePrefix",
+ },
+ };
+
+ const wrapper = render(
+ ConsoleApiCall({
+ message: prepareMessage(stub, { getNextId: () => "p" }),
+ serviceContainer,
+ })
+ );
+ const prefix = wrapper.find(".console-message-prefix");
+ expect(prefix.text()).toBe("MyNicePrefix: ");
+
+ expect(wrapper.find(".message-body").text()).toBe(
+ "MyNicePrefix: foobar test"
+ );
+
+ // There should be the location
+ const locationLink = wrapper.find(`.message-location`);
+ expect(locationLink.length).toBe(1);
+ expect(locationLink.text()).toBe("test-console-api.html:1:35");
+ });
+
+ it("renders repeat node", () => {
+ const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+ const wrapper = render(
+ ConsoleApiCall({
+ message,
+ serviceContainer,
+ repeat: 107,
+ })
+ );
+
+ expect(wrapper.find(".message-repeats").text()).toBe("107");
+ expect(wrapper.find(".message-repeats").prop("title")).toBe(
+ "107 repeats"
+ );
+
+ const selector =
+ "span > span.message-flex-body > " +
+ "span.message-body.devtools-monospace + span.message-repeats";
+ expect(wrapper.find(selector).length).toBe(1);
+ });
+
+ it("has the expected indent", () => {
+ const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+
+ const indent = 10;
+ let wrapper = render(
+ ConsoleApiCall({
+ message: Object.assign({}, message, { indent }),
+ serviceContainer,
+ })
+ );
+ expect(wrapper.prop("data-indent")).toBe(`${indent}`);
+ const indentEl = wrapper.find(".indent");
+ expect(indentEl.prop("style").width).toBe(`${indent * INDENT_WIDTH}px`);
+
+ wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+ expect(wrapper.prop("data-indent")).toBe(`0`);
+ // there's no indent element where the indent is 0
+ expect(wrapper.find(".indent").length).toBe(0);
+ });
+
+ it("renders a timestamp when passed a truthy timestampsVisible prop", () => {
+ const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+ const wrapper = render(
+ ConsoleApiCall({
+ message,
+ serviceContainer,
+ timestampsVisible: true,
+ })
+ );
+ const {
+ timestampString,
+ } = require("resource://devtools/client/webconsole/utils/l10n.js");
+
+ expect(wrapper.find(".timestamp").text()).toBe(
+ timestampString(message.timeStamp)
+ );
+ });
+
+ it("does not render a timestamp when not asked to", () => {
+ const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+ const wrapper = render(
+ ConsoleApiCall({
+ message,
+ serviceContainer,
+ })
+ );
+
+ expect(wrapper.find(".timestamp").length).toBe(0);
+ });
+ });
+
+ describe("console.count", () => {
+ it("renders", () => {
+ const messages = [
+ {
+ key: "console.count('bar')",
+ expectedBodyText: "bar: 1",
+ },
+ {
+ key: "console.count | default: 1",
+ expectedBodyText: "default: 1",
+ },
+ {
+ key: "console.count | default: 2",
+ expectedBodyText: "default: 2",
+ },
+ {
+ key: "console.count | test counter: 1",
+ expectedBodyText: "test counter: 1",
+ },
+ {
+ key: "console.count | test counter: 2",
+ expectedBodyText: "test counter: 2",
+ },
+ {
+ key: "console.count | default: 3",
+ expectedBodyText: "default: 3",
+ },
+ {
+ key: "console.count | default: 4",
+ expectedBodyText: "default: 4",
+ },
+ {
+ key: "console.count | test counter: 3",
+ expectedBodyText: "test counter: 3",
+ },
+ {
+ key: "console.countReset | test counter: 0",
+ expectedBodyText: "test counter: 0",
+ },
+ {
+ key: "console.countReset | counterDoesntExist",
+ expectedBodyText: "Counter “test counter” doesn’t exist.",
+ },
+ ];
+
+ for (const { key, expectedBodyText } of messages) {
+ const message = stubPreparedMessages.get(key);
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text()).toBe(expectedBodyText);
+ }
+ });
+ });
+
+ describe("console.assert", () => {
+ it("renders", () => {
+ const message = stubPreparedMessages.get(
+ "console.assert(false, {message: 'foobar'})"
+ );
+
+ // We need to wrap the ConsoleApiElement in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ ConsoleApiCall({ message, serviceContainer })
+ )
+ );
+
+ expect(wrapper.find(".message-body").text()).toBe(
+ 'Assertion failed: Object { message: "foobar" }'
+ );
+ });
+ });
+
+ describe("console.time", () => {
+ it("does not show anything", () => {
+ const message = stubPreparedMessages.get("console.time('bar')");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text()).toBe("");
+ });
+ it("shows an error if called again", () => {
+ const message = stubPreparedMessages.get("timerAlreadyExists");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text()).toBe(
+ "Timer “bar” already exists."
+ );
+ });
+ });
+
+ describe("console.timeLog", () => {
+ it("renders as expected", () => {
+ let message = stubPreparedMessages.get("console.timeLog('bar') - 1");
+ // We need to wrap the ConsoleApiElement in a Provider in order for the
+ // ObjectInspector to work.
+ let wrapper = render(
+ Provider(
+ { store: setupStore() },
+ ConsoleApiCall({ message, serviceContainer })
+ )
+ );
+
+ expect(wrapper.find(".message-body").text()).toBe(message.parameters[0]);
+ expect(wrapper.find(".message-body").text()).toMatch(
+ /^bar: \d+(\.\d+)?ms$/
+ );
+
+ message = stubPreparedMessages.get("console.timeLog('bar') - 2");
+ // We need to wrap the ConsoleApiElement in a Provider in order for the
+ // ObjectInspector to work.
+ wrapper = render(
+ Provider(
+ { store: setupStore() },
+ ConsoleApiCall({ message, serviceContainer })
+ )
+ );
+ expect(wrapper.find(".message-body").text()).toMatch(
+ /^bar: \d+(\.\d+)?ms second call Object \{ state\: 1 \}$/
+ );
+ });
+ it("shows an error if the timer doesn't exist", () => {
+ const message = stubPreparedMessages.get("timeLog.timerDoesntExist");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text()).toBe(
+ "Timer “bar” doesn’t exist."
+ );
+ });
+ });
+
+ describe("console.timeEnd", () => {
+ it("renders as expected", () => {
+ const message = stubPreparedMessages.get("console.timeEnd('bar')");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text()).toBe(message.messageText);
+ expect(wrapper.find(".message-body").text()).toMatch(
+ /^bar: \d+(\.\d+)?ms - timer ended$/
+ );
+ });
+ it("shows an error if the timer doesn't exist", () => {
+ const message = stubPreparedMessages.get("timeEnd.timerDoesntExist");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text()).toBe(
+ "Timer “bar” doesn’t exist."
+ );
+ });
+ });
+
+ // Unskip will happen in Bug 1529548.
+ describe.skip("console.trace", () => {
+ it("renders", () => {
+ const message = stubPreparedMessages.get("console.trace()");
+ const wrapper = render(
+ ConsoleApiCall({ message, serviceContainer, open: true })
+ );
+ const filepath =
+ "https://example.com/browser/devtools/client/webconsole/" +
+ "test/fixtures/stub-generators/" +
+ "test-console-api.html";
+
+ expect(wrapper.find(".message-body").text()).toBe("console.trace()");
+
+ const frameLinks = wrapper.find(`.stack-trace span.frame-link[data-url]`);
+ expect(frameLinks.length).toBe(3);
+
+ expect(
+ frameLinks.eq(0).find(".frame-link-function-display-name").text()
+ ).toBe("testStacktraceFiltering");
+ expect(frameLinks.eq(0).find(".frame-link-filename").text()).toBe(
+ filepath
+ );
+
+ expect(
+ frameLinks.eq(1).find(".frame-link-function-display-name").text()
+ ).toBe("foo");
+ expect(frameLinks.eq(1).find(".frame-link-filename").text()).toBe(
+ filepath
+ );
+
+ expect(
+ frameLinks.eq(2).find(".frame-link-function-display-name").text()
+ ).toBe("triggerPacket");
+ expect(frameLinks.eq(2).find(".frame-link-filename").text()).toBe(
+ filepath
+ );
+
+ // it should not be collapsible.
+ expect(wrapper.find(`.theme-twisty`).length).toBe(0);
+ });
+ it("render with arguments", () => {
+ const message = stubPreparedMessages.get(
+ "console.trace('bar', {'foo': 'bar'}, [1,2,3])"
+ );
+ // We need to wrap the ConsoleApiElement in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ ConsoleApiCall({ message, serviceContainer, open: true })
+ )
+ );
+
+ const filepath =
+ "https://example.com/browser/devtools/client/webconsole/" +
+ "test/fixtures/stub-generators/test-console-api.html";
+
+ expect(wrapper.find(".message-body").text()).toBe(
+ 'console.trace() bar Object { foo: "bar" } Array(3) [ 1, 2, 3 ]'
+ );
+
+ const frameLinks = wrapper.find(`.stack-trace span.frame-link[data-url]`);
+ expect(frameLinks.length).toBe(3);
+
+ expect(
+ frameLinks.eq(0).find(".frame-link-function-display-name").text()
+ ).toBe("testStacktraceWithLog");
+ expect(frameLinks.eq(0).find(".frame-link-filename").text()).toBe(
+ filepath
+ );
+
+ expect(
+ frameLinks.eq(1).find(".frame-link-function-display-name").text()
+ ).toBe("foo");
+ expect(frameLinks.eq(1).find(".frame-link-filename").text()).toBe(
+ filepath
+ );
+
+ expect(
+ frameLinks.eq(2).find(".frame-link-function-display-name").text()
+ ).toBe("triggerPacket");
+ expect(frameLinks.eq(2).find(".frame-link-filename").text()).toBe(
+ filepath
+ );
+
+ // it should not be collapsible.
+ expect(wrapper.find(`.theme-twisty`).length).toBe(0);
+ });
+ });
+
+ describe("console.group", () => {
+ it("renders", () => {
+ const message = stubPreparedMessages.get("console.group('bar')");
+ const wrapper = render(
+ ConsoleApiCall({ message, serviceContainer, open: true })
+ );
+
+ expect(wrapper.find(".message-body").text()).toBe("bar");
+ expect(wrapper.find(".collapse-button[aria-expanded=true]").length).toBe(
+ 1
+ );
+ });
+
+ it("renders group with custom style", () => {
+ const message = stubPreparedMessages.get("console.group(%cfoo%cbar)");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+ expect(wrapper.find(".message-body").text()).toBe("foobar");
+
+ const elements = wrapper.find(".objectBox-string");
+ expect(elements.length).toBe(2);
+
+ const firstElementStyle = elements.eq(0).prop("style");
+ // Allowed styles are applied accordingly on the first element.
+ expect(firstElementStyle.color).toBe(`blue`);
+ expect(firstElementStyle["font-size"]).toBe(`1.3em`);
+ // Forbidden styles are not applied.
+ expect(firstElementStyle["background-image"]).toBe(undefined);
+ expect(firstElementStyle.position).toBe(undefined);
+ expect(firstElementStyle.top).toBe(undefined);
+
+ const secondElementStyle = elements.eq(1).prop("style");
+ // Allowed styles are applied accordingly on the second element.
+ expect(secondElementStyle.color).toBe(`red`);
+ // Forbidden styles are not applied.
+ expect(secondElementStyle.background).toBe(undefined);
+ });
+
+ it("toggle the group when the collapse button is clicked", () => {
+ const store = setupStore();
+ store.dispatch = sinon.spy();
+ const message = stubPreparedMessages.get("console.group('bar')");
+
+ let wrapper = mount(
+ Provider(
+ { store },
+ ConsoleApiCall({
+ message,
+ open: true,
+ dispatch: store.dispatch,
+ serviceContainer,
+ })
+ )
+ );
+ wrapper.find(".collapse-button[aria-expanded='true']").simulate("click");
+ let call = store.dispatch.getCall(0);
+ expect(call.args[0]).toEqual({
+ id: message.id,
+ type: MESSAGE_CLOSE,
+ });
+
+ wrapper = mount(
+ Provider(
+ { store },
+ ConsoleApiCall({
+ message,
+ open: false,
+ dispatch: store.dispatch,
+ serviceContainer,
+ })
+ )
+ );
+ wrapper.find(".collapse-button").simulate("click");
+ call = store.dispatch.getCall(1);
+ expect(call.args[0]).toEqual({
+ id: message.id,
+ type: MESSAGE_OPEN,
+ });
+ });
+
+ it("toggle the group when the group name is clicked", () => {
+ const store = setupStore();
+ store.dispatch = sinon.spy();
+ const message = stubPreparedMessages.get("console.group('bar')");
+
+ let wrapper = mount(
+ Provider(
+ { store },
+ ConsoleApiCall({
+ message,
+ open: true,
+ dispatch: store.dispatch,
+ serviceContainer,
+ })
+ )
+ );
+ wrapper.find(".message-flex-body").simulate("click");
+ let call = store.dispatch.getCall(0);
+ expect(call.args[0]).toEqual({
+ id: message.id,
+ type: MESSAGE_CLOSE,
+ });
+
+ wrapper = mount(
+ Provider(
+ { store },
+ ConsoleApiCall({
+ message,
+ open: false,
+ dispatch: store.dispatch,
+ serviceContainer,
+ })
+ )
+ );
+ wrapper.find(".message-flex-body").simulate("click");
+ call = store.dispatch.getCall(1);
+ expect(call.args[0]).toEqual({
+ id: message.id,
+ type: MESSAGE_OPEN,
+ });
+ });
+
+ it("doesn't toggle the group when the location link is clicked", () => {
+ const store = setupStore();
+ store.dispatch = sinon.spy();
+ const message = stubPreparedMessages.get("console.group('bar')");
+
+ const wrapper = mount(
+ Provider(
+ { store },
+ ConsoleApiCall({
+ message,
+ open: true,
+ dispatch: store.dispatch,
+ serviceContainer,
+ })
+ )
+ );
+ wrapper.find(".frame-link-source").simulate("click");
+ const call = store.dispatch.getCall(0);
+ expect(call).toNotExist();
+ });
+ });
+
+ describe("console.groupEnd", () => {
+ it("does not show anything", () => {
+ const message = stubPreparedMessages.get("console.groupEnd('bar')");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text()).toBe("");
+ });
+ });
+
+ describe("console.groupCollapsed", () => {
+ it("renders", () => {
+ const message = stubPreparedMessages.get("console.groupCollapsed('foo')");
+ const wrapper = render(
+ ConsoleApiCall({ message, serviceContainer, open: false })
+ );
+
+ expect(wrapper.find(".message-body").text()).toBe("foo");
+ expect(wrapper.find(".collapse-button:not(.expanded)").length).toBe(1);
+ });
+
+ it("renders group with custom style", () => {
+ const message = stubPreparedMessages.get(
+ "console.groupCollapsed(%cfoo%cbaz)"
+ );
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ const elements = wrapper.find(".objectBox-string");
+ expect(elements.text()).toBe("foobaz");
+ expect(elements.length).toBe(2);
+
+ const firstElementStyle = elements.eq(0).prop("style");
+ // Allowed styles are applied accordingly on the first element.
+ expect(firstElementStyle.color).toBe(`blue`);
+ expect(firstElementStyle["font-size"]).toBe(`1.3em`);
+ // Forbidden styles are not applied.
+ expect(firstElementStyle["background-image"]).toBe(undefined);
+ expect(firstElementStyle.position).toBe(undefined);
+ expect(firstElementStyle.top).toBe(undefined);
+
+ const secondElementStyle = elements.eq(1).prop("style");
+ // Allowed styles are applied accordingly on the second element.
+ expect(secondElementStyle.color).toBe(`red`);
+ // Forbidden styles are not applied.
+ expect(secondElementStyle.background).toBe(undefined);
+ });
+ });
+
+ describe("console.dirxml", () => {
+ it("renders", () => {
+ const message = stubPreparedMessages.get("console.dirxml(window)");
+ // We need to wrap the ConsoleApiElement in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ ConsoleApiCall({ message, serviceContainer })
+ )
+ );
+
+ expect(wrapper.find(".message-body").text()).toBe(
+ "Window https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html"
+ );
+ });
+ });
+
+ describe("console.dir", () => {
+ it("renders", () => {
+ const message = stubPreparedMessages.get("console.dir({C, M, Y, K})");
+
+ // We need to wrap the ConsoleApiElement in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ ConsoleApiCall({ message, serviceContainer })
+ )
+ );
+
+ expect(wrapper.find(".message-body").text()).toBe(
+ `Object { cyan: "C", magenta: "M", yellow: "Y", black: "K" }`
+ );
+ });
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/console-output.test.js b/devtools/client/webconsole/test/node/components/console-output.test.js
new file mode 100644
index 0000000000..a12c2fbae2
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/console-output.test.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+// Test utils.
+const expect = require("expect");
+const { render } = require("enzyme");
+const Provider = createFactory(
+ require("resource://devtools/client/shared/vendor/react-redux.js").Provider
+);
+
+const ConsoleOutput = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/ConsoleOutput.js")
+);
+const serviceContainer = require("resource://devtools/client/webconsole/test/node/fixtures/serviceContainer.js");
+const {
+ setupStore,
+} = require("resource://devtools/client/webconsole/test/node/helpers.js");
+
+const MESSAGES_NUMBER = 100;
+function getDefaultProps() {
+ const store = setupStore(
+ Array.from({ length: MESSAGES_NUMBER })
+ // Alternate message so we don't trigger the repeat mechanism.
+ .map((_, i) => (i % 2 ? "console.log(null)" : "console.log(NaN)"))
+ );
+
+ return {
+ store,
+ serviceContainer,
+ };
+}
+
+describe("ConsoleOutput component:", () => {
+ it("Render every message", () => {
+ const Services = require("resource://devtools/client/shared/test-helpers/jest-fixtures/Services.js");
+ Services.prefs.setBoolPref("devtools.testing", true);
+
+ // We need to wrap the ConsoleApiElement in a Provider in order for the
+ // ObjectInspector to work.
+ const rendered = render(
+ Provider({ store: setupStore() }, ConsoleOutput(getDefaultProps()))
+ );
+
+ Services.prefs.setBoolPref("devtools.testing", false);
+ const visibleMessages = JSON.parse(rendered.prop("data-visible-messages"));
+ expect(visibleMessages.length).toBe(MESSAGES_NUMBER);
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/css-warning.test.js b/devtools/client/webconsole/test/node/components/css-warning.test.js
new file mode 100644
index 0000000000..ce15b360e6
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/css-warning.test.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test utils.
+const expect = require("expect");
+const { render, mount } = require("enzyme");
+const sinon = require("sinon");
+
+// React
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const Provider = createFactory(
+ require("resource://devtools/client/shared/vendor/react-redux.js").Provider
+);
+const {
+ setupStore,
+} = require("resource://devtools/client/webconsole/test/node/helpers.js");
+
+// Components under test.
+const CSSWarning = require("resource://devtools/client/webconsole/components/Output/message-types/CSSWarning.js");
+const {
+ MESSAGE_OPEN,
+ MESSAGE_CLOSE,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+// Test fakes.
+const {
+ stubPreparedMessages,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+const serviceContainer = require("resource://devtools/client/webconsole/test/node/fixtures/serviceContainer.js");
+
+describe("CSSWarning component:", () => {
+ it("renders", () => {
+ const message = stubPreparedMessages.get(
+ "Unknown property ‘such-unknown-property’. Declaration dropped."
+ );
+ const wrapper = render(
+ CSSWarning({
+ message,
+ serviceContainer,
+ timestampsVisible: true,
+ })
+ );
+ const {
+ timestampString,
+ } = require("resource://devtools/client/webconsole/utils/l10n.js");
+
+ expect(wrapper.find(".timestamp").text()).toBe(
+ timestampString(message.timeStamp)
+ );
+
+ expect(wrapper.find(".message-body").text()).toBe(
+ "Unknown property ‘such-unknown-property’. Declaration dropped."
+ );
+
+ // There shouldn't be a matched elements label rendered by default.
+ const elementLabel = wrapper.find(`.elements-label`);
+ expect(elementLabel.length).toBe(0);
+
+ // There should be a location.
+ const locationLink = wrapper.find(`.message-location`);
+ expect(locationLink.length).toBe(1);
+ expect(locationLink.text()).toBe("test-css-message.html:3:27");
+ });
+
+ it("closes an open message when the collapse button is clicked", () => {
+ const store = setupStore();
+ store.dispatch = sinon.spy();
+ const message = stubPreparedMessages.get(
+ "Unknown property ‘such-unknown-property’. Declaration dropped."
+ );
+
+ const wrapper = mount(
+ Provider(
+ { store },
+ CSSWarning({
+ message,
+ open: true,
+ dispatch: store.dispatch,
+ serviceContainer,
+ })
+ )
+ );
+
+ wrapper.find(".collapse-button[aria-expanded='true']").simulate("click");
+
+ const call = store.dispatch.getCall(0);
+ expect(call.args[0]).toEqual({
+ id: message.id,
+ type: MESSAGE_CLOSE,
+ });
+ });
+
+ it("opens a closed message when the collapse button is clicked", () => {
+ const store = setupStore();
+ store.dispatch = sinon.spy();
+ const message = stubPreparedMessages.get(
+ "Unknown property ‘such-unknown-property’. Declaration dropped."
+ );
+
+ const wrapper = mount(
+ Provider(
+ { store },
+ CSSWarning({
+ message,
+ open: false,
+ // fake the existence of cssMatchingElements to test just MESSAGE_OPEN action
+ cssMatchingElements: {},
+ dispatch: store.dispatch,
+ serviceContainer,
+ })
+ )
+ );
+
+ wrapper.find(".collapse-button[aria-expanded='false']").simulate("click");
+
+ const call = store.dispatch.getCall(0);
+ expect(call.args[0]).toEqual({
+ id: message.id,
+ type: MESSAGE_OPEN,
+ });
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/eager-evaluation.test.js b/devtools/client/webconsole/test/node/components/eager-evaluation.test.js
new file mode 100644
index 0000000000..df4bc0dd09
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/eager-evaluation.test.js
@@ -0,0 +1,154 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const expect = require("expect");
+const { render } = require("enzyme");
+
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const Provider = createFactory(
+ require("resource://devtools/client/shared/vendor/react-redux.js").Provider
+);
+
+const EagerEvaluation = createFactory(
+ require("resource://devtools/client/webconsole/components/Input/EagerEvaluation.js")
+);
+
+const {
+ setupStore,
+} = require("resource://devtools/client/webconsole/test/node/helpers.js");
+const {
+ SET_TERMINAL_EAGER_RESULT,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+const {
+ stubPackets,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+
+function getEagerEvaluation(overrides = {}) {
+ return EagerEvaluation({
+ highlightDomElement: () => {},
+ unHighlightDomElement: () => {},
+ ...overrides,
+ });
+}
+
+describe("EagerEvaluation component:", () => {
+ it("render Date result", () => {
+ const store = setupStore();
+ store.dispatch({
+ type: SET_TERMINAL_EAGER_RESULT,
+ result: stubPackets.get("new Date(0)").result,
+ });
+
+ const wrapper = render(Provider({ store }, getEagerEvaluation()));
+
+ expect(wrapper.hasClass("eager-evaluation-result")).toBe(true);
+ expect(wrapper.find(".eager-evaluation-result__row").length).toBe(1);
+ expect(wrapper.text()).toBe(
+ "Date Thu Jan 01 1970 01:00:00 GMT+0100 (Central European Standard Time)"
+ );
+ });
+
+ it("render falsy integer (0) result", () => {
+ const store = setupStore();
+ store.dispatch({
+ type: SET_TERMINAL_EAGER_RESULT,
+ result: 0,
+ });
+
+ const wrapper = render(Provider({ store }, getEagerEvaluation()));
+
+ expect(wrapper.hasClass("eager-evaluation-result")).toBe(true);
+ expect(wrapper.find(".eager-evaluation-result__row").length).toBe(1);
+ expect(wrapper.text()).toBe("0");
+ });
+
+ it("render false result", () => {
+ const store = setupStore();
+ store.dispatch({
+ type: SET_TERMINAL_EAGER_RESULT,
+ result: false,
+ });
+
+ const wrapper = render(Provider({ store }, getEagerEvaluation()));
+
+ expect(wrapper.hasClass("eager-evaluation-result")).toBe(true);
+ expect(wrapper.find(".eager-evaluation-result__row").length).toBe(1);
+ expect(wrapper.text()).toBe("false");
+ });
+
+ it("render empty string result", () => {
+ const store = setupStore();
+ store.dispatch({
+ type: SET_TERMINAL_EAGER_RESULT,
+ result: "",
+ });
+
+ const wrapper = render(Provider({ store }, getEagerEvaluation()));
+
+ expect(wrapper.hasClass("eager-evaluation-result")).toBe(true);
+ expect(wrapper.find(".eager-evaluation-result__row").length).toBe(1);
+ expect(wrapper.text()).toBe(`""`);
+ });
+
+ it("render null grip result", () => {
+ const store = setupStore();
+ store.dispatch({
+ type: SET_TERMINAL_EAGER_RESULT,
+ result: { type: "null" },
+ });
+
+ const wrapper = render(Provider({ store }, getEagerEvaluation()));
+
+ expect(wrapper.hasClass("eager-evaluation-result")).toBe(true);
+ expect(wrapper.find(".eager-evaluation-result__row").length).toBe(1);
+ expect(wrapper.text()).toBe("null");
+ });
+
+ it("render undefined grip result", () => {
+ const store = setupStore();
+ store.dispatch({
+ type: SET_TERMINAL_EAGER_RESULT,
+ result: { type: "undefined" },
+ });
+
+ const wrapper = render(Provider({ store }, getEagerEvaluation()));
+
+ expect(wrapper.hasClass("eager-evaluation-result")).toBe(true);
+ expect(wrapper.find(".eager-evaluation-result__row").length).toBe(1);
+ expect(wrapper.text()).toBe("undefined");
+ });
+
+ it("do not render null result", () => {
+ // This is not to be confused with a grip describing `null` (which is {type: "null"})
+ const store = setupStore();
+ store.dispatch({
+ type: SET_TERMINAL_EAGER_RESULT,
+ result: null,
+ });
+
+ const wrapper = render(Provider({ store }, getEagerEvaluation()));
+
+ expect(wrapper.hasClass("eager-evaluation-result")).toBe(true);
+ expect(wrapper.find(".eager-evaluation-result__row").length).toBe(0);
+ expect(wrapper.text()).toBe("");
+ });
+
+ it("do not render undefined result", () => {
+ // This is not to be confused with a grip describing `undefined` (which is {type: "undefined"})
+ const store = setupStore();
+ store.dispatch({
+ type: SET_TERMINAL_EAGER_RESULT,
+ result: undefined,
+ });
+
+ const wrapper = render(Provider({ store }, getEagerEvaluation()));
+
+ expect(wrapper.hasClass("eager-evaluation-result")).toBe(true);
+ expect(wrapper.find(".eager-evaluation-result__row").length).toBe(0);
+ expect(wrapper.text()).toBe("");
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/evaluation-result.test.js b/devtools/client/webconsole/test/node/components/evaluation-result.test.js
new file mode 100644
index 0000000000..36d2e2a9ff
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/evaluation-result.test.js
@@ -0,0 +1,505 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test utils.
+const expect = require("expect");
+const { render, mount } = require("enzyme");
+const sinon = require("sinon");
+
+// React
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const Provider = createFactory(
+ require("resource://devtools/client/shared/vendor/react-redux.js").Provider
+);
+const {
+ formatErrorTextWithCausedBy,
+ setupStore,
+} = require("resource://devtools/client/webconsole/test/node/helpers.js");
+
+// Components under test.
+const EvaluationResult = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/message-types/EvaluationResult.js")
+);
+const {
+ INDENT_WIDTH,
+} = require("resource://devtools/client/webconsole/components/Output/MessageIndent.js");
+
+// Test fakes.
+const {
+ stubPreparedMessages,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+const serviceContainer = require("resource://devtools/client/webconsole/test/node/fixtures/serviceContainer.js");
+
+describe("EvaluationResult component:", () => {
+ it.skip("renders a grip result", () => {
+ const message = stubPreparedMessages.get("new Date(0)");
+ // We need to wrap the ConsoleApiElement in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ EvaluationResult({ message, serviceContainer })
+ )
+ );
+
+ expect(wrapper.find(".message-body").text()).toBe(
+ "Date 1970-01-01T00:00:00.000Z"
+ );
+
+ expect(wrapper.hasClass("message")).toBe(true);
+ expect(wrapper.hasClass("log")).toBe(true);
+ });
+
+ it("renders an error", () => {
+ const message = stubPreparedMessages.get("asdf()");
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text()).toBe(
+ "Uncaught ReferenceError: asdf is not defined[Learn More]"
+ );
+
+ expect(wrapper.hasClass("message")).toBe(true);
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("renders an error with a longString exception message", () => {
+ const message = stubPreparedMessages.get("longString message Error");
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+
+ const text = wrapper.find(".message-body").text();
+ expect(text.startsWith("Uncaught Error: Long error Long error")).toBe(true);
+ expect(wrapper.hasClass("message")).toBe(true);
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("renders thrown empty string", () => {
+ const message = stubPreparedMessages.get(`eval throw ""`);
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe("Uncaught <empty string>");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("renders thrown string", () => {
+ const message = stubPreparedMessages.get(`eval throw "tomato"`);
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe("Uncaught tomato");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Boolean", () => {
+ const message = stubPreparedMessages.get(`eval throw false`);
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe("Uncaught false");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Number", () => {
+ const message = stubPreparedMessages.get(`eval throw 0`);
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe("Uncaught 0");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown null", () => {
+ const message = stubPreparedMessages.get(`eval throw null`);
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe("Uncaught null");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown undefined", () => {
+ const message = stubPreparedMessages.get(`eval throw undefined`);
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe("Uncaught undefined");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Symbol", () => {
+ const message = stubPreparedMessages.get(`eval throw Symbol`);
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe('Uncaught Symbol("potato")');
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Object", () => {
+ const message = stubPreparedMessages.get(`eval throw Object`);
+ // We need to wrap the EvaluationResult in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ EvaluationResult({ message, serviceContainer })
+ )
+ );
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught Object { vegetable: "cucumber" }`);
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Error Object", () => {
+ const message = stubPreparedMessages.get(`eval throw Error Object`);
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe("Uncaught Error: pumpkin");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Error object with custom name", () => {
+ const message = stubPreparedMessages.get(
+ `eval throw Error Object with custom name`
+ );
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe("Uncaught JuicyError: pineapple");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Error object with an error cause", () => {
+ const message = stubPreparedMessages.get(
+ `eval throw Error Object with error cause`
+ );
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe(
+ "Uncaught Error: something went wrong\nCaused by: SyntaxError: original error"
+ );
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Error object with an error cause chain", () => {
+ const message = stubPreparedMessages.get(
+ `eval throw Error Object with cause chain`
+ );
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe(
+ [
+ "Uncaught Error: err-d",
+ "Caused by: Error: err-c",
+ "Caused by: Error: err-b",
+ "Caused by: Error: err-a",
+ ].join("\n")
+ );
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Error object with a cyclical error cause chain", () => {
+ const message = stubPreparedMessages.get(
+ `eval throw Error Object with cyclical cause chain`
+ );
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe(
+ [
+ "Uncaught Error: err-y",
+ "Caused by: Error: err-x",
+ // TODO: it shouldn't be displayed like this. This will
+ // be fixed in Bug 1719605
+ "Caused by: undefined",
+ ].join("\n")
+ );
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Error object with a falsy cause", () => {
+ const message = stubPreparedMessages.get(
+ `eval throw Error Object with falsy cause`
+ );
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe("Uncaught Error: false cause\nCaused by: false");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Error object with a null cause", () => {
+ const message = stubPreparedMessages.get(
+ `eval throw Error Object with null cause`
+ );
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe("Uncaught Error: null cause\nCaused by: null");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Error object with an undefined cause", () => {
+ const message = stubPreparedMessages.get(
+ `eval throw Error Object with undefined cause`
+ );
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe("Uncaught Error: undefined cause\nCaused by: undefined");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Error object with a number cause", () => {
+ const message = stubPreparedMessages.get(
+ `eval throw Error Object with number cause`
+ );
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe("Uncaught Error: number cause\nCaused by: 0");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Error object with a string cause", () => {
+ const message = stubPreparedMessages.get(
+ `eval throw Error Object with string cause`
+ );
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe(
+ `Uncaught Error: string cause\nCaused by: "cause message"`
+ );
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render thrown Error object with object cause", () => {
+ const message = stubPreparedMessages.get(
+ `eval throw Error Object with object cause`
+ );
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe(`Uncaught Error: object cause\nCaused by: Object { … }`);
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("render pending Promise", () => {
+ const message = stubPreparedMessages.get(`eval pending promise`);
+ // We need to wrap the EvaluationResult in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ EvaluationResult({ message, serviceContainer })
+ )
+ );
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Promise { <state>: "pending" }`);
+ });
+
+ it("render Promise.resolve result", () => {
+ const message = stubPreparedMessages.get(`eval Promise.resolve`);
+ // We need to wrap the EvaluationResult in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ EvaluationResult({ message, serviceContainer })
+ )
+ );
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Promise { <state>: "fulfilled", <value>: 123 }`);
+ });
+
+ it("render Promise.reject result", () => {
+ const message = stubPreparedMessages.get(`eval Promise.reject`);
+ // We need to wrap the EvaluationResult in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ EvaluationResult({ message, serviceContainer })
+ )
+ );
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Promise { <state>: "rejected", <reason>: "ouch" }`);
+ });
+
+ it("render promise fulfilled in microtask", () => {
+ // See Bug 1439963
+ const message = stubPreparedMessages.get(`eval resolved promise`);
+ // We need to wrap the EvaluationResult in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ EvaluationResult({ message, serviceContainer })
+ )
+ );
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Promise { <state>: "fulfilled", <value>: 246 }`);
+ });
+
+ it("render promise rejected in microtask", () => {
+ // See Bug 1439963
+ const message = stubPreparedMessages.get(`eval rejected promise`);
+ // We need to wrap the EvaluationResult in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ EvaluationResult({ message, serviceContainer })
+ )
+ );
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(
+ `Promise { <state>: "rejected", <reason>: ReferenceError }`
+ );
+ });
+
+ it("render rejected promise with Error with cause", () => {
+ const message = stubPreparedMessages.get(`eval rejected promise`);
+ // We need to wrap the EvaluationResult in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ EvaluationResult({ message, serviceContainer })
+ )
+ );
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(
+ `Promise { <state>: "rejected", <reason>: ReferenceError }`
+ );
+ });
+
+ it("renders an inspect command result", () => {
+ const message = stubPreparedMessages.get("inspect({a: 1})");
+ // We need to wrap the ConsoleApiElement in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ EvaluationResult({ message, serviceContainer })
+ )
+ );
+
+ expect(wrapper.find(".message-body").text()).toBe("Object { a: 1 }");
+ });
+
+ it("displays a [Learn more] link", () => {
+ const store = setupStore();
+
+ const message = stubPreparedMessages.get("asdf()");
+
+ serviceContainer.openLink = sinon.spy();
+ const wrapper = mount(
+ Provider(
+ { store },
+ EvaluationResult({
+ message,
+ serviceContainer,
+ dispatch: () => {},
+ })
+ )
+ );
+
+ const url =
+ "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined";
+ const learnMore = wrapper.find(".learn-more-link");
+ expect(learnMore.length).toBe(1);
+ expect(learnMore.prop("title")).toBe(url);
+
+ learnMore.simulate("click");
+ const call = serviceContainer.openLink.getCall(0);
+ expect(call.args[0]).toEqual(message.exceptionDocURL);
+ });
+
+ it("has the expected indent", () => {
+ const message = stubPreparedMessages.get("new Date(0)");
+
+ const indent = 10;
+ // We need to wrap the ConsoleApiElement in a Provider in order for the
+ // ObjectInspector to work.
+ let wrapper = render(
+ Provider(
+ { store: setupStore() },
+ EvaluationResult({
+ message: Object.assign({}, message, { indent }),
+ serviceContainer,
+ })
+ )
+ );
+ expect(wrapper.prop("data-indent")).toBe(`${indent}`);
+ const indentEl = wrapper.find(".indent");
+ expect(indentEl.prop("style").width).toBe(`${indent * INDENT_WIDTH}px`);
+
+ wrapper = render(
+ Provider(
+ { store: setupStore() },
+ EvaluationResult({ message, serviceContainer })
+ )
+ );
+ expect(wrapper.prop("data-indent")).toBe(`0`);
+ // there's no indent element where the indent is 0
+ expect(wrapper.find(".indent").length).toBe(0);
+ });
+
+ it("has location information", () => {
+ const message = stubPreparedMessages.get("1 + @");
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+
+ const locationLink = wrapper.find(`.message-location`);
+ expect(locationLink.length).toBe(1);
+ expect(locationLink.text()).toBe("debugger eval code:1:5");
+ });
+
+ it("has a timestamp when passed a truthy timestampsVisible prop", () => {
+ const message = stubPreparedMessages.get("new Date(0)");
+ // We need to wrap the ConsoleApiElement in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ EvaluationResult({
+ message,
+ serviceContainer,
+ timestampsVisible: true,
+ })
+ )
+ );
+ const {
+ timestampString,
+ } = require("resource://devtools/client/webconsole/utils/l10n.js");
+
+ expect(wrapper.find(".timestamp").text()).toBe(
+ timestampString(message.timeStamp)
+ );
+ });
+
+ it("does not have a timestamp when timestampsVisible prop is falsy", () => {
+ const message = stubPreparedMessages.get("new Date(0)");
+ // We need to wrap the ConsoleApiElement in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ EvaluationResult({
+ message,
+ serviceContainer,
+ timestampsVisible: false,
+ })
+ )
+ );
+
+ expect(wrapper.find(".timestamp").length).toBe(0);
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/filter-bar.test.js b/devtools/client/webconsole/test/node/components/filter-bar.test.js
new file mode 100644
index 0000000000..15fb026d9f
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/filter-bar.test.js
@@ -0,0 +1,226 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const expect = require("expect");
+const sinon = require("sinon");
+const { render, mount, shallow } = require("enzyme");
+
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const Provider = createFactory(
+ require("resource://devtools/client/shared/vendor/react-redux.js").Provider
+);
+
+const actions = require("resource://devtools/client/webconsole/actions/index.js");
+const FilterButton = require("resource://devtools/client/webconsole/components/FilterBar/FilterButton.js");
+const FilterBar = createFactory(
+ require("resource://devtools/client/webconsole/components/FilterBar/FilterBar.js")
+);
+const {
+ FILTERBAR_DISPLAY_MODES,
+} = require("resource://devtools/client/webconsole/constants.js");
+const {
+ MESSAGES_CLEAR,
+ FILTERS,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+const {
+ setupStore,
+ clearPrefs,
+} = require("resource://devtools/client/webconsole/test/node/helpers.js");
+const serviceContainer = require("resource://devtools/client/webconsole/test/node/fixtures/serviceContainer.js");
+
+function getFilterBar(overrides = {}) {
+ return FilterBar({
+ serviceContainer,
+ attachRefToWebConsoleUI: () => {},
+ webConsoleUI: {
+ document,
+ wrapper: {},
+ },
+ ...overrides,
+ });
+}
+
+describe("FilterBar component:", () => {
+ afterEach(() => {
+ clearPrefs();
+ });
+
+ it("initial render", () => {
+ const store = setupStore();
+
+ const wrapper = render(Provider({ store }, getFilterBar()));
+ const toolbar = wrapper.find(
+ ".devtools-toolbar.webconsole-filterbar-primary"
+ );
+
+ // Clear button
+ const clearButton = toolbar.children().eq(0);
+ expect(clearButton.attr("class")).toBe(
+ "devtools-button devtools-clear-icon"
+ );
+ expect(clearButton.attr("title")).toBe("Clear the Web Console output");
+
+ // Separator
+ expect(toolbar.children().eq(1).attr("class")).toBe("devtools-separator");
+
+ // Text filter
+ const textInput = toolbar.children().eq(2);
+ expect(textInput.attr("class")).toBe("devtools-searchbox");
+
+ // Text filter input
+ const textFilter = textInput.children().eq(0);
+ expect(textFilter.attr("class")).toBe("devtools-filterinput");
+ expect(textFilter.attr("placeholder")).toBe("Filter Output");
+ expect(textFilter.attr("type")).toBe("search");
+ expect(textFilter.attr("value")).toBe("");
+
+ // Text filter input clear button
+ const textFilterClearButton = textInput.children().eq(1);
+ expect(textFilterClearButton.attr("class")).toBe(
+ "devtools-searchinput-clear"
+ );
+
+ // Settings menu icon
+ expect(
+ wrapper.find(".webconsole-console-settings-menu-button").length
+ ).toBe(1);
+ });
+
+ it("displays the number of hidden messages when a search hide messages", () => {
+ const store = setupStore([
+ "console.log('foobar', 'test')",
+ "console.info('info message');",
+ "console.warn('danger, will robinson!')",
+ "console.debug('debug message');",
+ "console.error('error message');",
+ ]);
+ store.dispatch(actions.filterTextSet("qwerty"));
+
+ const wrapper = mount(Provider({ store }, getFilterBar()));
+
+ const message = wrapper.find(".devtools-searchinput-summary");
+ expect(message.text()).toBe("5 hidden");
+ expect(message.prop("title")).toBe("5 items hidden by text filter");
+ });
+
+ it("displays the number of hidden messages when a search hide 1 message", () => {
+ const store = setupStore([
+ "console.log('foobar', 'test')",
+ "console.info('info message');",
+ ]);
+ store.dispatch(actions.filterTextSet("foobar"));
+
+ const wrapper = mount(Provider({ store }, getFilterBar()));
+
+ const message = wrapper.find(".devtools-searchinput-summary");
+ expect(message.text()).toBe("1 hidden");
+ expect(message.prop("title")).toBe("1 item hidden by text filter");
+ });
+
+ it("displays the expected number of hidden messages when multiple filters", () => {
+ const store = setupStore([
+ "console.log('foobar', 'test')",
+ "console.info('info message');",
+ "console.warn('danger, will robinson!')",
+ "console.debug('debug message');",
+ "console.error('error message');",
+ ]);
+ store.dispatch(actions.filterTextSet("qwerty"));
+ store.dispatch(actions.filterToggle(FILTERS.ERROR));
+ store.dispatch(actions.filterToggle(FILTERS.INFO));
+
+ const wrapper = mount(Provider({ store }, getFilterBar()));
+
+ const message = wrapper.find(".devtools-searchinput-summary");
+ expect(message.text()).toBe("3 hidden");
+ expect(message.prop("title")).toBe("3 items hidden by text filter");
+ });
+
+ it("does not display the number of hidden messages when there are no messages", () => {
+ const store = setupStore();
+ store.dispatch(actions.filterTextSet("qwerty"));
+ const wrapper = mount(Provider({ store }, getFilterBar()));
+
+ const toolbar = wrapper.find(".devtools-searchinput-summary");
+ expect(toolbar.exists()).toBeFalsy();
+ });
+
+ it("Displays a filter buttons bar on its own element in narrow displayMode", () => {
+ const store = setupStore();
+
+ const wrapper = mount(
+ Provider(
+ { store },
+ getFilterBar({
+ displayMode: FILTERBAR_DISPLAY_MODES.NARROW,
+ })
+ )
+ );
+
+ const secondaryBar = wrapper.find(".webconsole-filterbar-secondary");
+ expect(secondaryBar.length).toBe(1);
+
+ // Buttons are displayed
+ const filterBtn = props =>
+ FilterButton(
+ Object.assign(
+ {},
+ {
+ active: true,
+ dispatch: store.dispatch,
+ },
+ props
+ )
+ );
+
+ const buttons = [
+ filterBtn({ label: "Errors", filterKey: FILTERS.ERROR }),
+ filterBtn({ label: "Warnings", filterKey: FILTERS.WARN }),
+ filterBtn({ label: "Logs", filterKey: FILTERS.LOG }),
+ filterBtn({ label: "Info", filterKey: FILTERS.INFO }),
+ filterBtn({ label: "Debug", filterKey: FILTERS.DEBUG }),
+ dom.div({
+ className: "devtools-separator",
+ }),
+ filterBtn({
+ label: "CSS",
+ filterKey: "css",
+ active: false,
+ title:
+ "Stylesheets will be reparsed to check for errors. Refresh the page to also see errors from stylesheets modified from Javascript.",
+ }),
+ filterBtn({ label: "XHR", filterKey: "netxhr", active: false }),
+ filterBtn({ label: "Requests", filterKey: "net", active: false }),
+ ];
+
+ secondaryBar.children().forEach((child, index) => {
+ expect(child.html()).toEqual(shallow(buttons[index]).html());
+ });
+ });
+
+ it("fires MESSAGES_CLEAR action when clear button is clicked", () => {
+ const store = setupStore();
+ store.dispatch = sinon.spy();
+
+ const wrapper = mount(Provider({ store }, getFilterBar()));
+ wrapper.find(".devtools-clear-icon").simulate("click");
+ const call = store.dispatch.getCall(0);
+ expect(call.args[0]).toEqual({
+ type: MESSAGES_CLEAR,
+ });
+ });
+
+ it("sets filter text when text is typed", () => {
+ const store = setupStore();
+
+ const wrapper = mount(Provider({ store }, getFilterBar()));
+ const input = wrapper.find(".devtools-filterinput");
+ input.simulate("change", { target: { value: "a" } });
+ expect(store.getState().filters.text).toBe("a");
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/filter-button.test.js b/devtools/client/webconsole/test/node/components/filter-button.test.js
new file mode 100644
index 0000000000..1600f43312
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/filter-button.test.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const expect = require("expect");
+const { render } = require("enzyme");
+
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+
+const FilterButton = createFactory(
+ require("resource://devtools/client/webconsole/components/FilterBar/FilterButton.js")
+);
+const {
+ MESSAGE_LEVEL,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+describe("FilterButton component:", () => {
+ const props = {
+ active: true,
+ label: "Error",
+ filterKey: MESSAGE_LEVEL.ERROR,
+ };
+
+ it("displays as active when turned on", () => {
+ const wrapper = render(FilterButton(props));
+ expect(wrapper.is("button")).toBe(true);
+ expect(wrapper.hasClass("devtools-togglebutton")).toBe(true);
+ expect(wrapper.attr("data-category")).toBe("error");
+ expect(wrapper.attr("aria-pressed")).toBe("true");
+ expect(wrapper.text()).toBe("Error");
+ });
+
+ it("displays as inactive when turned off", () => {
+ const wrapper = render(FilterButton({ ...props, active: false }));
+ expect(wrapper.is("button")).toBe(true);
+ expect(wrapper.hasClass("devtools-togglebutton")).toBe(true);
+ expect(wrapper.attr("data-category")).toBe("error");
+ expect(wrapper.attr("aria-pressed")).toBe("false");
+ expect(wrapper.text()).toBe("Error");
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/filter-checkbox.test.js b/devtools/client/webconsole/test/node/components/filter-checkbox.test.js
new file mode 100644
index 0000000000..41fc65b712
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/filter-checkbox.test.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const expect = require("expect");
+const { render } = require("enzyme");
+
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+
+const FilterCheckbox = createFactory(
+ require("resource://devtools/client/webconsole/components/FilterBar/FilterCheckbox.js")
+);
+
+describe("FilterCheckbox component:", () => {
+ const props = {
+ label: "test label",
+ title: "test title",
+ checked: true,
+ onChange: () => {},
+ };
+
+ it("displays as checked", () => {
+ const wrapper = render(FilterCheckbox(props));
+ expect(wrapper.is("label")).toBe(true);
+ expect(wrapper.attr("title")).toBe("test title");
+ expect(wrapper.hasClass("filter-checkbox")).toBe(true);
+ expect(wrapper.html()).toBe('<input type="checkbox" checked>test label');
+ });
+
+ it("displays as unchecked", () => {
+ const wrapper = render(FilterCheckbox({ ...props, checked: false }));
+ expect(wrapper.is("label")).toBe(true);
+ expect(wrapper.attr("title")).toBe("test title");
+ expect(wrapper.hasClass("filter-checkbox")).toBe(true);
+ expect(wrapper.html()).toBe('<input type="checkbox">test label');
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/message-container.test.js b/devtools/client/webconsole/test/node/components/message-container.test.js
new file mode 100644
index 0000000000..303de1692e
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/message-container.test.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test utils.
+const expect = require("expect");
+const { render } = require("enzyme");
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+
+// Components under test.
+let {
+ MessageContainer,
+ getMessageComponent,
+} = require("resource://devtools/client/webconsole/components/Output/MessageContainer.js");
+MessageContainer = createFactory(MessageContainer);
+const ConsoleApiCall = require("resource://devtools/client/webconsole/components/Output/message-types/ConsoleApiCall.js");
+const CSSWarning = require("resource://devtools/client/webconsole/components/Output/message-types/CSSWarning.js");
+const EvaluationResult = require("resource://devtools/client/webconsole/components/Output/message-types/EvaluationResult.js");
+const PageError = require("resource://devtools/client/webconsole/components/Output/message-types/PageError.js");
+
+// Test fakes.
+const {
+ stubPreparedMessages,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+const serviceContainer = require("resource://devtools/client/webconsole/test/node/fixtures/serviceContainer.js");
+
+describe("MessageContainer component:", () => {
+ it("pipes data to children as expected", () => {
+ const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+ const rendered = render(
+ MessageContainer({
+ getMessage: () => message,
+ serviceContainer,
+ })
+ );
+
+ expect(rendered.text().includes("foobar")).toBe(true);
+ });
+ it("picks correct child component", () => {
+ const messageTypes = [
+ {
+ component: ConsoleApiCall,
+ message: stubPreparedMessages.get("console.log('foobar', 'test')"),
+ },
+ {
+ component: EvaluationResult,
+ message: stubPreparedMessages.get("new Date(0)"),
+ },
+ {
+ component: PageError,
+ message: stubPreparedMessages.get(
+ "ReferenceError: asdf is not defined"
+ ),
+ },
+ {
+ component: CSSWarning,
+ message: stubPreparedMessages.get(
+ "Unknown property ‘such-unknown-property’. Declaration dropped."
+ ),
+ },
+ ];
+
+ messageTypes.forEach(info => {
+ const { component, message } = info;
+ expect(getMessageComponent(message)).toBe(component);
+ });
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/message-icon.test.js b/devtools/client/webconsole/test/node/components/message-icon.test.js
new file mode 100644
index 0000000000..b5d35f0d46
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/message-icon.test.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const {
+ MESSAGE_LEVEL,
+ MESSAGE_TYPE,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+const expect = require("expect");
+const { render } = require("enzyme");
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const MessageIcon = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/MessageIcon.js")
+);
+
+describe("MessageIcon component:", () => {
+ it("renders icon based on level", () => {
+ const rendered = render(MessageIcon({ level: MESSAGE_LEVEL.ERROR }));
+ expect(rendered.hasClass("icon")).toBe(true);
+ expect(rendered.attr("title")).toBe("Error");
+ expect(rendered.attr("aria-live")).toBe("off");
+ });
+
+ it("renders logpoint items", () => {
+ const rendered = render(
+ MessageIcon({
+ level: MESSAGE_LEVEL.LOG,
+ type: "logPoint",
+ })
+ );
+ expect(rendered.hasClass("logpoint")).toBe(true);
+ });
+
+ it("renders evaluation expression items", () => {
+ const rendered = render(
+ MessageIcon({
+ level: MESSAGE_LEVEL.LOG,
+ type: MESSAGE_TYPE.COMMAND,
+ })
+ );
+ expect(rendered.hasClass("icon")).toBe(true);
+ expect(rendered.attr("title")).toBe("Evaluated code");
+ });
+
+ it("renders evaluation expression result items", () => {
+ const rendered = render(
+ MessageIcon({
+ level: MESSAGE_LEVEL.LOG,
+ type: MESSAGE_TYPE.RESULT,
+ })
+ );
+ expect(rendered.hasClass("icon")).toBe(true);
+ expect(rendered.attr("title")).toBe("Evaluation result");
+ });
+
+ it("renders icon with custom title", () => {
+ const expectedTitle = "Rendered with custom title";
+ const rendered = render(
+ MessageIcon({
+ level: MESSAGE_LEVEL.INFO,
+ type: "info",
+ title: expectedTitle,
+ })
+ );
+ expect(rendered.attr("title")).toBe(expectedTitle);
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/message-location.test.js b/devtools/client/webconsole/test/node/components/message-location.test.js
new file mode 100644
index 0000000000..a5bc41c143
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/message-location.test.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test utils.
+const expect = require("expect");
+const { mount } = require("enzyme");
+const sinon = require("sinon");
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const serviceContainer = require("resource://devtools/client/webconsole/test/node/fixtures/serviceContainer.js");
+
+let {
+ MessageContainer,
+} = require("resource://devtools/client/webconsole/components/Output/MessageContainer.js");
+MessageContainer = createFactory(MessageContainer);
+
+// Test fakes.
+const {
+ stubPreparedMessages,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+
+describe("Message - location element", () => {
+ it("Calls onViewSourceInDebugger when clicked", () => {
+ const onViewSourceInDebugger = sinon.spy();
+ const onViewSource = sinon.spy();
+
+ const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+ const wrapper = mount(
+ MessageContainer({
+ getMessage: () => message,
+ serviceContainer: Object.assign({}, serviceContainer, {
+ onViewSourceInDebugger,
+ onViewSource,
+ }),
+ })
+ );
+
+ // There should be the location
+ const locationLink = wrapper.find(`.message-location a`);
+ expect(locationLink.length).toBe(1);
+ expect(locationLink.text()).toBe("test-console-api.html:1:35");
+
+ locationLink.simulate("click");
+
+ expect(onViewSourceInDebugger.calledOnce).toBe(true);
+ expect(onViewSource.notCalled).toBe(true);
+ });
+
+ it("Calls onViewSource when clicked and onViewSourceInDebugger undefined", () => {
+ const onViewSource = sinon.spy();
+
+ const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+
+ const wrapper = mount(
+ MessageContainer({
+ getMessage: () => message,
+ serviceContainer: Object.assign({}, serviceContainer, {
+ onViewSource,
+ onViewSourceInDebugger: undefined,
+ }),
+ })
+ );
+
+ // There should be the location
+ const locationLink = wrapper.find(`.message-location a`);
+
+ locationLink.simulate("click");
+ expect(onViewSource.calledOnce).toBe(true);
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/message-repeat.test.js b/devtools/client/webconsole/test/node/components/message-repeat.test.js
new file mode 100644
index 0000000000..bfedbc4ef6
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/message-repeat.test.js
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const expect = require("expect");
+const { render } = require("enzyme");
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const MessageRepeat = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/MessageRepeat.js")
+);
+
+describe("MessageRepeat component:", () => {
+ it("renders repeated value correctly", () => {
+ const rendered = render(MessageRepeat({ repeat: 99 }));
+ expect(rendered.hasClass("message-repeats")).toBe(true);
+ expect(rendered.text()).toBe("99");
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/message-types-aria.test.js b/devtools/client/webconsole/test/node/components/message-types-aria.test.js
new file mode 100644
index 0000000000..dcabd7a572
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/message-types-aria.test.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test utils.
+const expect = require("expect");
+const { render } = require("enzyme");
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+
+// Components under test.
+const ConsoleApiCall = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/message-types/ConsoleApiCall.js")
+);
+const ConsoleCmd = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/message-types/ConsoleCommand.js")
+);
+const EvaluationResult = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/message-types/EvaluationResult.js")
+);
+
+const {
+ ConsoleCommand,
+} = require("resource://devtools/client/webconsole/types.js");
+
+// Test fakes.
+const {
+ stubPreparedMessages,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+const serviceContainer = require("resource://devtools/client/webconsole/test/node/fixtures/serviceContainer.js");
+
+describe("message types component ARIA:", () => {
+ describe("ConsoleAPICall", () => {
+ it("sets aria-live to polite", () => {
+ const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+ expect(wrapper.attr("aria-live")).toBe("polite");
+ });
+ });
+
+ describe("EvaluationResult", () => {
+ it("sets aria-live to polite", () => {
+ const message = stubPreparedMessages.get("asdf()");
+ const wrapper = render(EvaluationResult({ message, serviceContainer }));
+ expect(wrapper.attr("aria-live")).toBe("polite");
+ });
+ });
+
+ describe("ConsoleCommand", () => {
+ it("sets aria-live to off", () => {
+ const message = new ConsoleCommand({
+ messageText: `"simple"`,
+ });
+ const wrapper = render(ConsoleCmd({ message, serviceContainer }));
+ expect(wrapper.attr("aria-live")).toBe("off");
+ });
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/network-event-message.test.js b/devtools/client/webconsole/test/node/components/network-event-message.test.js
new file mode 100644
index 0000000000..6bdc395544
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/network-event-message.test.js
@@ -0,0 +1,154 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test utils.
+const expect = require("expect");
+const { render } = require("enzyme");
+
+// React
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+
+// Components under test.
+const NetworkEventMessage = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/message-types/NetworkEventMessage.js")
+);
+const {
+ INDENT_WIDTH,
+} = require("resource://devtools/client/webconsole/components/Output/MessageIndent.js");
+
+// Test fakes.
+const {
+ stubPreparedMessages,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+const serviceContainer = require("resource://devtools/client/webconsole/test/node/fixtures/serviceContainer.js");
+
+const EXPECTED_URL = "https://example.com/inexistent.html";
+const EXPECTED_STATUS = /\[HTTP\/\d\.\d \d+ [A-Za-z ]+ \d+ms\]/;
+
+describe("NetworkEventMessage component:", () => {
+ describe("GET request", () => {
+ it("renders as expected", () => {
+ const message = stubPreparedMessages.get("GET request");
+ const update = stubPreparedMessages.get("GET request update");
+ const wrapper = render(
+ NetworkEventMessage({
+ message,
+ serviceContainer,
+ timestampsVisible: true,
+ networkMessageUpdate: update,
+ })
+ );
+ const {
+ timestampString,
+ } = require("resource://devtools/client/webconsole/utils/l10n.js");
+
+ expect(wrapper.find(".timestamp").text()).toBe(
+ timestampString(message.timeStamp)
+ );
+ expect(wrapper.find(".message-body .method").text()).toBe("GET");
+ expect(wrapper.find(".message-body .xhr").length).toBe(0);
+ expect(wrapper.find(".message-body .url").length).toBe(1);
+ expect(wrapper.find(".message-body .url").text()).toBe(EXPECTED_URL);
+ expect(wrapper.find(".message-body .status").length).toBe(1);
+ expect(wrapper.find(".message-body .status").text()).toMatch(
+ EXPECTED_STATUS
+ );
+ });
+
+ it("does not have a timestamp when timestampsVisible prop is falsy", () => {
+ const message = stubPreparedMessages.get("GET request update");
+ const wrapper = render(
+ NetworkEventMessage({
+ message,
+ serviceContainer,
+ timestampsVisible: false,
+ })
+ );
+
+ expect(wrapper.find(".timestamp").length).toBe(0);
+ });
+
+ it("has the expected indent", () => {
+ const message = stubPreparedMessages.get("GET request");
+
+ const indent = 10;
+ let wrapper = render(
+ NetworkEventMessage({
+ message: Object.assign({}, message, { indent }),
+ serviceContainer,
+ })
+ );
+ expect(wrapper.prop("data-indent")).toBe(`${indent}`);
+ const indentEl = wrapper.find(".indent");
+ expect(indentEl.prop("style").width).toBe(`${indent * INDENT_WIDTH}px`);
+
+ wrapper = render(NetworkEventMessage({ message, serviceContainer }));
+ expect(wrapper.prop("data-indent")).toBe(`0`);
+ // there's no indent element where the indent is 0
+ expect(wrapper.find(".indent").length).toBe(0);
+ });
+ });
+
+ describe("XHR GET request", () => {
+ it("renders as expected", () => {
+ const message = stubPreparedMessages.get("XHR GET request");
+ const update = stubPreparedMessages.get("XHR GET request update");
+ const wrapper = render(
+ NetworkEventMessage({
+ message,
+ serviceContainer,
+ networkMessageUpdate: update,
+ })
+ );
+
+ expect(wrapper.find(".message-body .method").text()).toBe("GET");
+ expect(wrapper.find(".message-body .xhr").length).toBe(1);
+ expect(wrapper.find(".message-body .xhr").text()).toBe("XHR");
+ expect(wrapper.find(".message-body .url").text()).toBe(EXPECTED_URL);
+ expect(wrapper.find(".message-body .status").text()).toMatch(
+ EXPECTED_STATUS
+ );
+ });
+ });
+
+ describe("XHR POST request", () => {
+ it("renders as expected", () => {
+ const message = stubPreparedMessages.get("XHR POST request");
+ const update = stubPreparedMessages.get("XHR POST request update");
+ const wrapper = render(
+ NetworkEventMessage({
+ message,
+ serviceContainer,
+ networkMessageUpdate: update,
+ })
+ );
+
+ expect(wrapper.find(".message-body .method").text()).toBe("POST");
+ expect(wrapper.find(".message-body .xhr").length).toBe(1);
+ expect(wrapper.find(".message-body .xhr").text()).toBe("XHR");
+ expect(wrapper.find(".message-body .url").length).toBe(1);
+ expect(wrapper.find(".message-body .url").text()).toBe(EXPECTED_URL);
+ expect(wrapper.find(".message-body .status").length).toBe(1);
+ expect(wrapper.find(".message-body .status").text()).toMatch(
+ EXPECTED_STATUS
+ );
+ });
+ });
+
+ describe("is expandable", () => {
+ it("renders as expected", () => {
+ const message = stubPreparedMessages.get("XHR POST request");
+ const wrapper = render(
+ NetworkEventMessage({
+ message,
+ serviceContainer,
+ })
+ );
+
+ expect(wrapper.find(".message .theme-twisty")).toExist();
+ });
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/page-error.test.js b/devtools/client/webconsole/test/node/components/page-error.test.js
new file mode 100644
index 0000000000..e2489a7f34
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/page-error.test.js
@@ -0,0 +1,661 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test utils.
+const expect = require("expect");
+const { render, mount } = require("enzyme");
+const sinon = require("sinon");
+
+// React
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const Provider = createFactory(
+ require("resource://devtools/client/shared/vendor/react-redux.js").Provider
+);
+const {
+ formatErrorTextWithCausedBy,
+ setupStore,
+} = require("resource://devtools/client/webconsole/test/node/helpers.js");
+const {
+ prepareMessage,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+
+// Components under test.
+const PageError = require("resource://devtools/client/webconsole/components/Output/message-types/PageError.js");
+const {
+ MESSAGE_OPEN,
+ MESSAGE_CLOSE,
+} = require("resource://devtools/client/webconsole/constants.js");
+const {
+ INDENT_WIDTH,
+} = require("resource://devtools/client/webconsole/components/Output/MessageIndent.js");
+
+// Test fakes.
+const {
+ stubPackets,
+ stubPreparedMessages,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+const serviceContainer = require("resource://devtools/client/webconsole/test/node/fixtures/serviceContainer.js");
+
+describe("PageError component:", () => {
+ it("renders", () => {
+ const message = stubPreparedMessages.get(
+ "ReferenceError: asdf is not defined"
+ );
+ const wrapper = render(
+ PageError({
+ message,
+ serviceContainer,
+ timestampsVisible: true,
+ })
+ );
+ const {
+ timestampString,
+ } = require("resource://devtools/client/webconsole/utils/l10n.js");
+
+ expect(wrapper.find(".timestamp").text()).toBe(
+ timestampString(message.timeStamp)
+ );
+
+ expect(wrapper.find(".message-body").text()).toBe(
+ "Uncaught ReferenceError: asdf is not defined[Learn More]"
+ );
+
+ // The stacktrace should be closed by default.
+ const frameLinks = wrapper.find(`.stack-trace`);
+ expect(frameLinks.length).toBe(0);
+
+ // There should be the location.
+ const locationLink = wrapper.find(`.message-location`);
+ expect(locationLink.length).toBe(1);
+ // @TODO Will likely change. See bug 1307952
+ expect(locationLink.text()).toBe("test-console-api.html:3:5");
+ });
+
+ it("does not have a timestamp when timestampsVisible prop is falsy", () => {
+ const message = stubPreparedMessages.get(
+ "ReferenceError: asdf is not defined"
+ );
+ const wrapper = render(
+ PageError({
+ message,
+ serviceContainer,
+ timestampsVisible: false,
+ })
+ );
+
+ expect(wrapper.find(".timestamp").length).toBe(0);
+ });
+
+ it("renders an error with a longString exception message", () => {
+ const message = stubPreparedMessages.get("TypeError longString message");
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ const text = wrapper.find(".message-body").text();
+ expect(text.startsWith("Uncaught Error: Long error Long error")).toBe(true);
+ });
+
+ it("renders thrown empty string", () => {
+ const message = stubPreparedMessages.get(`throw ""`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe("Uncaught <empty string>");
+ });
+
+ it("renders thrown string", () => {
+ const message = stubPreparedMessages.get(`throw "tomato"`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught tomato`);
+ });
+
+ it("renders thrown boolean", () => {
+ const message = stubPreparedMessages.get(`throw false`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught false`);
+ });
+
+ it("renders thrown number ", () => {
+ const message = stubPreparedMessages.get(`throw 0`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught 0`);
+ });
+
+ it("renders thrown null", () => {
+ const message = stubPreparedMessages.get(`throw null`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught null`);
+ });
+
+ it("renders thrown undefined", () => {
+ const message = stubPreparedMessages.get(`throw undefined`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught undefined`);
+ });
+
+ it("renders thrown Symbol", () => {
+ const message = stubPreparedMessages.get(`throw Symbol`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught Symbol("potato")`);
+ });
+
+ it("renders thrown object", () => {
+ const message = stubPreparedMessages.get(`throw Object`);
+
+ // We need to wrap the PageError in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ PageError({ message, serviceContainer })
+ )
+ );
+
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught Object { vegetable: "cucumber" }`);
+ });
+
+ it("renders thrown error", () => {
+ const message = stubPreparedMessages.get(`throw Error Object`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught Error: pumpkin`);
+ });
+
+ it("renders thrown Error with custom name", () => {
+ const message = stubPreparedMessages.get(
+ `throw Error Object with custom name`
+ );
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught JuicyError: pineapple`);
+ });
+
+ it("renders thrown Error with error cause", () => {
+ const message = stubPreparedMessages.get(
+ `throw Error Object with error cause`
+ );
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe(
+ "Uncaught Error: something went wrong\nCaused by: SyntaxError: original error"
+ );
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("renders thrown Error with error cause chain", () => {
+ const message = stubPreparedMessages.get(
+ `throw Error Object with cause chain`
+ );
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe(
+ [
+ "Uncaught Error: err-d",
+ "Caused by: Error: err-c",
+ "Caused by: Error: err-b",
+ "Caused by: Error: err-a",
+ ].join("\n")
+ );
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("renders thrown Error with cyclical cause chain", () => {
+ const message = stubPreparedMessages.get(
+ `throw Error Object with cyclical cause chain`
+ );
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ // TODO: This is not how we should display cyclical cause chain, but we have it here
+ // to ensure it's displaying something that makes _some_ sense.
+ // This should be properly handled in Bug 1719605.
+ expect(text).toBe(
+ [
+ "Uncaught Error: err-b",
+ "Caused by: Error: err-a",
+ "Caused by: Error: err-b",
+ "Caused by: Error: err-a",
+ ].join("\n")
+ );
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("renders thrown Error with null cause", () => {
+ const message = stubPreparedMessages.get(
+ `throw Error Object with falsy cause`
+ );
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe("Uncaught Error: null cause\nCaused by: null");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("renders thrown Error with number cause", () => {
+ const message = stubPreparedMessages.get(
+ `throw Error Object with number cause`
+ );
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe("Uncaught Error: number cause\nCaused by: 0");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("renders thrown Error with string cause", () => {
+ const message = stubPreparedMessages.get(
+ `throw Error Object with string cause`
+ );
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe(
+ `Uncaught Error: string cause\nCaused by: "cause message"`
+ );
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("renders thrown Error with object cause", () => {
+ const message = stubPreparedMessages.get(
+ `throw Error Object with object cause`
+ );
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe("Uncaught Error: object cause\nCaused by: Object { … }");
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("renders uncaught rejected Promise with empty string", () => {
+ const message = stubPreparedMessages.get(`Promise reject ""`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe("Uncaught (in promise) <empty string>");
+ });
+
+ it("renders uncaught rejected Promise with string", () => {
+ const message = stubPreparedMessages.get(`Promise reject "tomato"`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught (in promise) tomato`);
+ });
+
+ it("renders uncaught rejected Promise with boolean", () => {
+ const message = stubPreparedMessages.get(`Promise reject false`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught (in promise) false`);
+ });
+
+ it("renders uncaught rejected Promise with number ", () => {
+ const message = stubPreparedMessages.get(`Promise reject 0`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught (in promise) 0`);
+ });
+
+ it("renders uncaught rejected Promise with null", () => {
+ const message = stubPreparedMessages.get(`Promise reject null`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught (in promise) null`);
+ });
+
+ it("renders uncaught rejected Promise with undefined", () => {
+ const message = stubPreparedMessages.get(`Promise reject undefined`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught (in promise) undefined`);
+ });
+
+ it("renders uncaught rejected Promise with Symbol", () => {
+ const message = stubPreparedMessages.get(`Promise reject Symbol`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught (in promise) Symbol("potato")`);
+ });
+
+ it("renders uncaught rejected Promise with object", () => {
+ const message = stubPreparedMessages.get(`Promise reject Object`);
+ // We need to wrap the PageError in a Provider in order for the
+ // ObjectInspector to work.
+ const wrapper = render(
+ Provider(
+ { store: setupStore() },
+ PageError({ message, serviceContainer })
+ )
+ );
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught (in promise) Object { vegetable: "cucumber" }`);
+ });
+
+ it("renders uncaught rejected Promise with error", () => {
+ const message = stubPreparedMessages.get(`Promise reject Error Object`);
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught (in promise) Error: pumpkin`);
+ });
+
+ it("renders uncaught rejected Promise with Error with custom name", () => {
+ const message = stubPreparedMessages.get(
+ `Promise reject Error Object with custom name`
+ );
+ const wrapper = render(PageError({ message, serviceContainer }));
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(`Uncaught (in promise) JuicyError: pineapple`);
+ });
+
+ it("renders uncaught rejected Promise with Error with cause", () => {
+ const message = stubPreparedMessages.get(
+ `Promise reject Error Object with error cause`
+ );
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ const text = formatErrorTextWithCausedBy(
+ wrapper.find(".message-body").text()
+ );
+ expect(text).toBe(
+ [
+ `Uncaught (in promise) Error: something went wrong`,
+ `Caused by: ReferenceError: unknownFunc is not defined`,
+ ].join("\n")
+ );
+ expect(wrapper.hasClass("error")).toBe(true);
+ });
+
+ it("renders URLs in message as actual, cropped, links", () => {
+ // Let's replace the packet data in order to mimick a pageError.
+ const packet = stubPackets.get("throw string with URL");
+
+ 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}`;
+
+ // We remove the exceptionDocURL to not have the "learn more" link.
+ packet.pageError.exceptionDocURL = null;
+
+ const message = prepareMessage(packet, { getNextId: () => "1" });
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ const text = wrapper.find(".message-body").text();
+ expect(text).toBe(
+ `Uncaught “${evilURL}“ is evil and “${badURL}“ is not good either`
+ );
+
+ // There should be 2 cropped links.
+ const links = wrapper.find(".message-body a.cropped-url");
+ expect(links.length).toBe(2);
+
+ expect(links.eq(0).attr("href")).toBe(evilURL);
+ expect(links.eq(0).attr("title")).toBe(evilURL);
+
+ expect(links.eq(1).attr("href")).toBe(badURL);
+ expect(links.eq(1).attr("title")).toBe(badURL);
+ });
+
+ it("displays a [Learn more] link", () => {
+ const store = setupStore();
+
+ const message = stubPreparedMessages.get(
+ "ReferenceError: asdf is not defined"
+ );
+
+ serviceContainer.openLink = sinon.spy();
+ const wrapper = mount(
+ Provider(
+ { store },
+ PageError({
+ message,
+ serviceContainer,
+ dispatch: () => {},
+ })
+ )
+ );
+
+ // There should be a [Learn more] link.
+ const url =
+ "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined";
+ const learnMore = wrapper.find(".learn-more-link");
+ expect(learnMore.length).toBe(1);
+ expect(learnMore.prop("title")).toBe(url);
+
+ learnMore.simulate("click");
+ const call = serviceContainer.openLink.getCall(0);
+ expect(call.args[0]).toEqual(message.exceptionDocURL);
+ });
+
+ // Unskip will happen in Bug 1529548.
+ it.skip("has a stacktrace which can be opened", () => {
+ const message = stubPreparedMessages.get(
+ "ReferenceError: asdf is not defined"
+ );
+ const wrapper = render(
+ PageError({ message, serviceContainer, open: true })
+ );
+
+ // There should be a collapse button.
+ expect(wrapper.find(".collapse-button[aria-expanded=true]").length).toBe(1);
+
+ // There should be five stacktrace items.
+ const frameLinks = wrapper.find(`.stack-trace span.frame-link`);
+ expect(frameLinks.length).toBe(5);
+ });
+
+ // Unskip will happen in Bug 1529548.
+ it.skip("toggle the stacktrace when the collapse button is clicked", () => {
+ const store = setupStore();
+ store.dispatch = sinon.spy();
+ const message = stubPreparedMessages.get(
+ "ReferenceError: asdf is not defined"
+ );
+
+ let wrapper = mount(
+ Provider(
+ { store },
+ PageError({
+ message,
+ open: true,
+ dispatch: store.dispatch,
+ serviceContainer,
+ })
+ )
+ );
+
+ wrapper.find(".collapse-button[aria-expanded='true']").simulate("click");
+ let call = store.dispatch.getCall(0);
+ expect(call.args[0]).toEqual({
+ id: message.id,
+ type: MESSAGE_CLOSE,
+ });
+
+ wrapper = mount(
+ Provider(
+ { store },
+ PageError({
+ message,
+ open: false,
+ dispatch: store.dispatch,
+ serviceContainer,
+ })
+ )
+ );
+ wrapper.find(".collapse-button[aria-expanded='false']").simulate("click");
+ call = store.dispatch.getCall(1);
+ expect(call.args[0]).toEqual({
+ id: message.id,
+ type: MESSAGE_OPEN,
+ });
+ });
+
+ it("has the expected indent", () => {
+ const message = stubPreparedMessages.get(
+ "ReferenceError: asdf is not defined"
+ );
+ const indent = 10;
+ let wrapper = render(
+ PageError({
+ message: Object.assign({}, message, { indent }),
+ serviceContainer,
+ })
+ );
+ expect(wrapper.prop("data-indent")).toBe(`${indent}`);
+ const indentEl = wrapper.find(".indent");
+ expect(indentEl.prop("style").width).toBe(`${indent * INDENT_WIDTH}px`);
+
+ wrapper = render(PageError({ message, serviceContainer }));
+ expect(wrapper.prop("data-indent")).toBe(`0`);
+ // there's no indent element where the indent is 0
+ expect(wrapper.find(".indent").length).toBe(0);
+ });
+
+ it("has empty error notes", () => {
+ const message = stubPreparedMessages.get(
+ "ReferenceError: asdf is not defined"
+ );
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ const notes = wrapper.find(".error-note");
+
+ expect(notes.length).toBe(0);
+ });
+
+ it("can show an error note", () => {
+ const origMessage = stubPreparedMessages.get(
+ "ReferenceError: asdf is not defined"
+ );
+ const message = Object.assign({}, origMessage, {
+ notes: [
+ {
+ messageBody: "test note",
+ frame: {
+ source: "https://example.com/test.js",
+ line: 2,
+ column: 6,
+ },
+ },
+ ],
+ });
+
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ const notes = wrapper.find(".error-note");
+ expect(notes.length).toBe(1);
+
+ const note = notes.eq(0);
+ expect(note.find(".message-body").text()).toBe("note: test note");
+
+ // There should be the location.
+ const locationLink = note.find(`.message-location`);
+ expect(locationLink.length).toBe(1);
+ expect(locationLink.text()).toBe("test.js:2:6");
+ });
+
+ it("can show multiple error notes", () => {
+ const origMessage = stubPreparedMessages.get(
+ "ReferenceError: asdf is not defined"
+ );
+ const message = Object.assign({}, origMessage, {
+ notes: [
+ {
+ messageBody: "test note 1",
+ frame: {
+ source: "https://example.com/test1.js",
+ line: 2,
+ column: 6,
+ },
+ },
+ {
+ messageBody: "test note 2",
+ frame: {
+ source: "https://example.com/test2.js",
+ line: 10,
+ column: 18,
+ },
+ },
+ {
+ messageBody: "test note 3",
+ frame: {
+ source: "https://example.com/test3.js",
+ line: 9,
+ column: 4,
+ },
+ },
+ ],
+ });
+
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ const notes = wrapper.find(".error-note");
+ expect(notes.length).toBe(3);
+
+ const note1 = notes.eq(0);
+ expect(note1.find(".message-body").text()).toBe("note: test note 1");
+
+ const locationLink1 = note1.find(`.message-location`);
+ expect(locationLink1.length).toBe(1);
+ expect(locationLink1.text()).toBe("test1.js:2:6");
+
+ const note2 = notes.eq(1);
+ expect(note2.find(".message-body").text()).toBe("note: test note 2");
+
+ const locationLink2 = note2.find(`.message-location`);
+ expect(locationLink2.length).toBe(1);
+ expect(locationLink2.text()).toBe("test2.js:10:18");
+
+ const note3 = notes.eq(2);
+ expect(note3.find(".message-body").text()).toBe("note: test note 3");
+
+ const locationLink3 = note3.find(`.message-location`);
+ expect(locationLink3.length).toBe(1);
+ expect(locationLink3.text()).toBe("test3.js:9:4");
+ });
+
+ it("displays error notes", () => {
+ const message = stubPreparedMessages.get(
+ "SyntaxError: redeclaration of let a"
+ );
+
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ const notes = wrapper.find(".error-note");
+ expect(notes.length).toBe(1);
+
+ const note = notes.eq(0);
+ expect(note.find(".message-body").text()).toBe(
+ "note: Previously declared at line 2, column 7"
+ );
+
+ // There should be the location.
+ const locationLink = note.find(`.message-location`);
+ expect(locationLink.length).toBe(1);
+ expect(locationLink.text()).toBe("test-console-api.html:2:7");
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/warning-group.test.js b/devtools/client/webconsole/test/node/components/warning-group.test.js
new file mode 100644
index 0000000000..12d1615d6c
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/warning-group.test.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test utils.
+const expect = require("expect");
+const { render } = require("enzyme");
+
+// Components under test.
+const WarningGroup = require("resource://devtools/client/webconsole/components/Output/message-types/WarningGroup.js");
+const {
+ MESSAGE_SOURCE,
+ MESSAGE_TYPE,
+} = require("resource://devtools/client/webconsole/constants.js");
+const {
+ ConsoleMessage,
+} = require("resource://devtools/client/webconsole/types.js");
+const {
+ createWarningGroupMessage,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+
+// Test fakes.
+const {
+ stubPreparedMessages,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+const serviceContainer = require("resource://devtools/client/webconsole/test/node/fixtures/serviceContainer.js");
+const mockMessage = ConsoleMessage({
+ messageText: "this is a warning group",
+ source: MESSAGE_SOURCE.CONSOLE_FRONTEND,
+ timeStamp: Date.now(),
+});
+
+describe("WarningGroup component:", () => {
+ it("renders", () => {
+ const wrapper = render(
+ WarningGroup({
+ message: mockMessage,
+ serviceContainer,
+ timestampsVisible: true,
+ badge: 42,
+ })
+ );
+
+ const {
+ timestampString,
+ } = require("resource://devtools/client/webconsole/utils/l10n.js");
+ expect(wrapper.find(".timestamp").text()).toBe(
+ timestampString(mockMessage.timeStamp)
+ );
+ expect(wrapper.find(".message-body").text()).toBe(
+ "this is a warning group 42"
+ );
+ expect(wrapper.find(".arrow[aria-expanded=false]")).toExist();
+ });
+
+ it("does have an expanded arrow when `open` prop is true", () => {
+ const wrapper = render(
+ WarningGroup({
+ message: mockMessage,
+ serviceContainer,
+ open: true,
+ })
+ );
+
+ expect(wrapper.find(".arrow[aria-expanded=true]")).toExist();
+ });
+
+ it("does not have a timestamp when timestampsVisible prop is falsy", () => {
+ const wrapper = render(
+ WarningGroup({
+ message: mockMessage,
+ serviceContainer,
+ timestampsVisible: false,
+ })
+ );
+
+ expect(wrapper.find(".timestamp").length).toBe(0);
+ });
+
+ it("renders Content Blocking Group message", () => {
+ const firstMessage = stubPreparedMessages.get(
+ "ReferenceError: asdf is not defined"
+ );
+ firstMessage.messageText =
+ "The resource at “https://evil.com” was blocked.";
+ firstMessage.category = "cookieBlockedPermission";
+ const type = MESSAGE_TYPE.CONTENT_BLOCKING_GROUP;
+ const message = createWarningGroupMessage(
+ `${firstMessage.type}-${firstMessage.innerWindowID}`,
+ type,
+ firstMessage
+ );
+
+ const wrapper = render(
+ WarningGroup({
+ message,
+ serviceContainer,
+ badge: 24,
+ })
+ );
+
+ expect(wrapper.find(".message-body").text()).toBe(
+ "The resource at “<URL>” was blocked. 24"
+ );
+ expect(wrapper.find(".arrow[aria-expanded=false]")).toExist();
+ });
+});
diff --git a/devtools/client/webconsole/test/node/components/webconsole-wrapper.test.js b/devtools/client/webconsole/test/node/components/webconsole-wrapper.test.js
new file mode 100644
index 0000000000..1ea44b7388
--- /dev/null
+++ b/devtools/client/webconsole/test/node/components/webconsole-wrapper.test.js
@@ -0,0 +1,148 @@
+/* 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 utils.
+const expect = require("expect");
+const {
+ stubPackets,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+const {
+ clonePacket,
+ getMessageAt,
+ getPrivatePacket,
+ getWebConsoleUiMock,
+} = require("resource://devtools/client/webconsole/test/node/helpers.js");
+
+const WebConsoleWrapper = require("resource://devtools/client/webconsole/webconsole-wrapper.js");
+const {
+ messagesAdd,
+} = require("resource://devtools/client/webconsole/actions/messages.js");
+
+async function getWebConsoleWrapper() {
+ const hud = {
+ currentTarget: { client: {}, getFront: () => {} },
+ getMappedExpression: () => {},
+ commands: {
+ objectCommand: {
+ releaseObjects: async frontsToRelease => {},
+ },
+ },
+ };
+ const webConsoleUi = getWebConsoleUiMock(hud);
+
+ const wcow = new WebConsoleWrapper(null, webConsoleUi, null, null);
+ await wcow.init();
+ return wcow;
+}
+
+describe("WebConsoleWrapper", () => {
+ it("clears queues when dispatchMessagesClear is called", async () => {
+ const ncow = await getWebConsoleWrapper();
+ ncow.queuedMessageAdds.push({ fakePacket: "message", data: {} });
+ ncow.queuedMessageUpdates.push({ fakePacket: "message-update", data: {} });
+ ncow.queuedRequestUpdates.push({ fakePacket: "request-update", data: {} });
+
+ ncow.dispatchMessagesClear();
+
+ expect(ncow.queuedMessageAdds.length).toBe(0);
+ expect(ncow.queuedMessageUpdates.length).toBe(0);
+ expect(ncow.queuedRequestUpdates.length).toBe(0);
+ });
+
+ it("removes private packets from message queue on dispatchPrivateMessagesClear", async () => {
+ const ncow = await getWebConsoleWrapper();
+
+ const publicLog = stubPackets.get("console.log('mymap')");
+ ncow.queuedMessageAdds.push(
+ getPrivatePacket("console.trace()"),
+ publicLog,
+ getPrivatePacket("XHR POST request")
+ );
+
+ ncow.dispatchPrivateMessagesClear();
+ expect(ncow.queuedMessageAdds).toEqual([publicLog]);
+ });
+
+ it("removes private packets from network update queue on dispatchPrivateMessagesClear", async () => {
+ const ncow = await getWebConsoleWrapper();
+ const publicLog = stubPackets.get("console.log('mymap')");
+ ncow.queuedMessageAdds.push(
+ getPrivatePacket("console.trace()"),
+ publicLog,
+ getPrivatePacket("XHR POST request")
+ );
+
+ const postId = "pid1";
+ const getId = "gid1";
+
+ // Add messages in the store to make sure that update to private requests are
+ // removed from the queue.
+ ncow
+ .getStore()
+ .dispatch(
+ messagesAdd([
+ stubPackets.get("GET request"),
+ { ...getPrivatePacket("XHR GET request"), actor: getId },
+ ])
+ );
+
+ // Add packet to the message queue to make sure that update to private requests are
+ // removed from the queue.
+ ncow.queuedMessageAdds.push({
+ ...getPrivatePacket("XHR POST request"),
+ actor: postId,
+ });
+
+ const publicNetworkUpdate = stubPackets.get("GET request update");
+ ncow.queuedMessageUpdates.push(
+ publicNetworkUpdate,
+ {
+ ...getPrivatePacket("XHR GET request update"),
+ actor: getId,
+ },
+ {
+ ...getPrivatePacket("XHR POST request update"),
+ actor: postId,
+ }
+ );
+
+ ncow.dispatchPrivateMessagesClear();
+
+ expect(ncow.queuedMessageUpdates.length).toBe(1);
+ expect(ncow.queuedMessageUpdates).toEqual([publicNetworkUpdate]);
+ });
+
+ it("removes private packets from network request queue on dispatchPrivateMessagesClear", async () => {
+ const ncow = await getWebConsoleWrapper();
+
+ const packet1 = clonePacket(stubPackets.get("GET request"));
+ const packet2 = clonePacket(getPrivatePacket("XHR GET request"));
+ const packet3 = clonePacket(getPrivatePacket("XHR POST request"));
+
+ // We need to reassign the timeStamp of the packet to guarantee the order.
+ packet1.timeStamp = packet1.timeStamp + 1;
+ packet2.timeStamp = packet2.timeStamp + 2;
+ packet3.timeStamp = packet3.timeStamp + 3;
+
+ ncow.getStore().dispatch(messagesAdd([packet1, packet2, packet3]));
+
+ const state = ncow.getStore().getState();
+ const publicId = getMessageAt(state, 0).id;
+ const privateXhrGetId = getMessageAt(state, 1).id;
+ const privateXhrPostId = getMessageAt(state, 2).id;
+ ncow.queuedRequestUpdates.push(
+ { id: publicId },
+ { id: privateXhrGetId },
+ { id: privateXhrPostId }
+ );
+ // ncow.queuedRequestUpdates.push({fakePacket: "request-update"});
+
+ ncow.dispatchPrivateMessagesClear();
+
+ expect(ncow.queuedRequestUpdates.length).toBe(1);
+ expect(ncow.queuedRequestUpdates).toEqual([{ id: publicId }]);
+ });
+});
diff --git a/devtools/client/webconsole/test/node/fixtures/DevToolsUtils.js b/devtools/client/webconsole/test/node/fixtures/DevToolsUtils.js
new file mode 100644
index 0000000000..5be367a60e
--- /dev/null
+++ b/devtools/client/webconsole/test/node/fixtures/DevToolsUtils.js
@@ -0,0 +1,8 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+module.exports = {
+ getTopWindow: () => ({}),
+};
diff --git a/devtools/client/webconsole/test/node/fixtures/WebConsoleUtils.js b/devtools/client/webconsole/test/node/fixtures/WebConsoleUtils.js
new file mode 100644
index 0000000000..1b5f134be3
--- /dev/null
+++ b/devtools/client/webconsole/test/node/fixtures/WebConsoleUtils.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const L10n = require("resource://devtools/client/webconsole/test/node/fixtures/L10n.js");
+
+const Utils = {
+ L10n,
+ supportsString(s) {
+ return s;
+ },
+};
+
+module.exports = {
+ Utils,
+};
diff --git a/devtools/client/webconsole/test/node/fixtures/async-storage.js b/devtools/client/webconsole/test/node/fixtures/async-storage.js
new file mode 100644
index 0000000000..35ed2f594f
--- /dev/null
+++ b/devtools/client/webconsole/test/node/fixtures/async-storage.js
@@ -0,0 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+module.exports = {
+ getItem: () => Promise.resolve(),
+ setItem: () => Promise.resolve(),
+ removeItem: () => Promise.resolve(),
+ clear: () => Promise.resolve(),
+ length: () => Promise.resolve(),
+ key: () => Promise.resolve(),
+};
diff --git a/devtools/client/webconsole/test/node/fixtures/serviceContainer.js b/devtools/client/webconsole/test/node/fixtures/serviceContainer.js
new file mode 100644
index 0000000000..d846204ef9
--- /dev/null
+++ b/devtools/client/webconsole/test/node/fixtures/serviceContainer.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+module.exports = {
+ attachRefToWebConsoleUI: () => {},
+ emitForTests: () => {},
+ onViewSourceInDebugger: () => {},
+ onViewSourceInStyleEditor: () => {},
+ openNetworkPanel: () => {},
+ resendNetworkRequest: () => {},
+ sourceMapURLService: {
+ subscribeByURL: () => {
+ return () => {};
+ },
+ subscribeByID: () => {
+ return () => {};
+ },
+ subscribeByLocation: () => {
+ return () => {};
+ },
+ },
+ openLink: () => {},
+ // eslint-disable-next-line react/display-name
+ createElement: tagName => document.createElement(tagName),
+};
diff --git a/devtools/client/webconsole/test/node/fixtures/stubs/browser_dummy.js b/devtools/client/webconsole/test/node/fixtures/stubs/browser_dummy.js
new file mode 100644
index 0000000000..8a9353cd7e
--- /dev/null
+++ b/devtools/client/webconsole/test/node/fixtures/stubs/browser_dummy.js
@@ -0,0 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This file is a fake test so we can have support files in the stubs.ini, which are then
+// referenced as support files in the webconsole mochitest ini file.
+
+"use strict";
+
+add_task(function() {
+ ok(true, "this is not a test");
+});
diff --git a/devtools/client/webconsole/test/node/fixtures/stubs/consoleApi.js b/devtools/client/webconsole/test/node/fixtures/stubs/consoleApi.js
new file mode 100644
index 0000000000..54d3414c74
--- /dev/null
+++ b/devtools/client/webconsole/test/node/fixtures/stubs/consoleApi.js
@@ -0,0 +1,1489 @@
+/* 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();
+rawPackets.set(`console.log('foobar', 'test')`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "foobar",
+ "test"
+ ],
+ "columnNumber": 35,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "log",
+ "lineNumber": 1,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source22",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.log(undefined)`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ {
+ "type": "undefined"
+ }
+ ],
+ "columnNumber": 35,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "log",
+ "lineNumber": 1,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source22",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.warn('danger, will robinson!')`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "danger, will robinson!"
+ ],
+ "columnNumber": 35,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "warn",
+ "lineNumber": 1,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source22",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.log(NaN)`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ {
+ "type": "NaN"
+ }
+ ],
+ "columnNumber": 35,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "log",
+ "lineNumber": 1,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source22",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.log(null)`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ {
+ "type": "null"
+ }
+ ],
+ "columnNumber": 35,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "log",
+ "lineNumber": 1,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source22",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.log('鼬')`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "鼬"
+ ],
+ "columnNumber": 35,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "log",
+ "lineNumber": 1,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source22",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.clear()`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [],
+ "columnNumber": 35,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "clear",
+ "lineNumber": 1,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source22",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.count('bar')`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "bar"
+ ],
+ "columnNumber": 35,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "count",
+ "lineNumber": 1,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source22",
+ "innerWindowID": 8589934593,
+ "counter": {
+ "count": 1,
+ "label": "bar"
+ }
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.assert(false, {message: 'foobar'})`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj30",
+ "class": "Object",
+ "ownPropertyLength": 1,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "message": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "foobar"
+ }
+ },
+ "ownPropertiesLength": 1
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj30"
+ }
+ ],
+ "columnNumber": 35,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "assert",
+ "lineNumber": 1,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source30",
+ "innerWindowID": 8589934593,
+ "stacktrace": [
+ {
+ "columnNumber": 35,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "functionName": "triggerPacket",
+ "lineNumber": 1,
+ "sourceId": "server0.conn0.child1/source30"
+ }
+ ]
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.log('úṇĩçödê țĕșť')`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "úṇĩçödê țĕșť"
+ ],
+ "columnNumber": 35,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "log",
+ "lineNumber": 1,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source22",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.dirxml(window)`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj33",
+ "class": "Window",
+ "ownPropertyLength": 818,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html"
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj33"
+ }
+ ],
+ "columnNumber": 35,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "dirxml",
+ "lineNumber": 1,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source22",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.log('myarray', ['red', 'green', 'blue'])`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "myarray",
+ {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj35",
+ "class": "Array",
+ "ownPropertyLength": 4,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 3,
+ "items": [
+ "red",
+ "green",
+ "blue"
+ ]
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj35"
+ }
+ ],
+ "columnNumber": 35,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "log",
+ "lineNumber": 1,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source22",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.log('myregex', /a.b.c/)`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "myregex",
+ {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj37",
+ "class": "RegExp",
+ "ownPropertyLength": 1,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "displayString": "/a.b.c/"
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj37"
+ }
+ ],
+ "columnNumber": 35,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "log",
+ "lineNumber": 1,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source22",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.table(['red', 'green', 'blue']);`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj39",
+ "class": "Array",
+ "ownPropertyLength": 4,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": null,
+ "ownProperties": {
+ "0": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "red"
+ },
+ "1": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "green"
+ },
+ "2": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "blue"
+ }
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj39"
+ }
+ ],
+ "columnNumber": 35,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "table",
+ "lineNumber": 1,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source22",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.log('myobject', {red: 'redValue', green: 'greenValue', blue: 'blueValue'});`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "myobject",
+ {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj41",
+ "class": "Object",
+ "ownPropertyLength": 3,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "red": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "redValue"
+ },
+ "green": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "greenValue"
+ },
+ "blue": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "blueValue"
+ }
+ },
+ "ownPropertiesLength": 3
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj41"
+ }
+ ],
+ "columnNumber": 35,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "log",
+ "lineNumber": 1,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source22",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.debug('debug message');`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "debug message"
+ ],
+ "columnNumber": 35,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "debug",
+ "lineNumber": 1,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source22",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.info('info message');`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "info message"
+ ],
+ "columnNumber": 35,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "info",
+ "lineNumber": 1,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source22",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.error('error message');`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "error message"
+ ],
+ "columnNumber": 35,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "error",
+ "lineNumber": 1,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source30",
+ "innerWindowID": 8589934593,
+ "stacktrace": [
+ {
+ "columnNumber": 35,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "functionName": "triggerPacket",
+ "lineNumber": 1,
+ "sourceId": "server0.conn0.child1/source30"
+ }
+ ]
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.log('mymap')`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "mymap",
+ {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj46",
+ "class": "Map",
+ "ownPropertyLength": 0,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "MapLike",
+ "size": 2,
+ "entries": [
+ [
+ "key1",
+ "value1"
+ ],
+ [
+ "key2",
+ "value2"
+ ]
+ ]
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj46"
+ }
+ ],
+ "columnNumber": 11,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "log",
+ "lineNumber": 5,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source46",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.log('myset')`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "myset",
+ {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj48",
+ "class": "Set",
+ "ownPropertyLength": 0,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 2,
+ "items": [
+ "a",
+ "b"
+ ]
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj48"
+ }
+ ],
+ "columnNumber": 11,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "log",
+ "lineNumber": 2,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source48",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.trace()`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [],
+ "columnNumber": 13,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "trace",
+ "lineNumber": 3,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source50",
+ "innerWindowID": 8589934593,
+ "stacktrace": [
+ {
+ "columnNumber": 13,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "functionName": "testStacktraceFiltering",
+ "lineNumber": 3,
+ "sourceId": "server0.conn0.child1/source50"
+ },
+ {
+ "columnNumber": 5,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "functionName": "foo",
+ "lineNumber": 6,
+ "sourceId": "server0.conn0.child1/source50"
+ },
+ {
+ "columnNumber": 3,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "functionName": "triggerPacket",
+ "lineNumber": 9,
+ "sourceId": "server0.conn0.child1/source50"
+ }
+ ]
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.trace('bar', {'foo': 'bar'}, [1,2,3])`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "bar",
+ {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj51",
+ "class": "Object",
+ "ownPropertyLength": 1,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "foo": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "bar"
+ }
+ },
+ "ownPropertiesLength": 1
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj51"
+ },
+ {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj52",
+ "class": "Array",
+ "ownPropertyLength": 4,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 3,
+ "items": [
+ 1,
+ 2,
+ 3
+ ]
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj52"
+ }
+ ],
+ "columnNumber": 13,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "trace",
+ "lineNumber": 3,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source51",
+ "innerWindowID": 8589934593,
+ "stacktrace": [
+ {
+ "columnNumber": 13,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "functionName": "testStacktraceWithLog",
+ "lineNumber": 3,
+ "sourceId": "server0.conn0.child1/source51"
+ },
+ {
+ "columnNumber": 5,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "functionName": "foo",
+ "lineNumber": 6,
+ "sourceId": "server0.conn0.child1/source50"
+ },
+ {
+ "columnNumber": 3,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "functionName": "triggerPacket",
+ "lineNumber": 9,
+ "sourceId": "server0.conn0.child1/source50"
+ }
+ ]
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.trace("%cHello%c|%cWorld")`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "Hello",
+ "|",
+ "World"
+ ],
+ "columnNumber": 13,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "trace",
+ "lineNumber": 2,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child3/source57",
+ "innerWindowID": 10737418241,
+ "stacktrace": [
+ {
+ "columnNumber": 13,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "functionName": "triggerPacket",
+ "lineNumber": 2,
+ "sourceId": "server0.conn0.child3/source57"
+ }
+ ],
+ "styles": [
+ "color:red",
+ "",
+ "color: blue"
+ ]
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.time('bar')`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "bar"
+ ],
+ "columnNumber": 11,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "time",
+ "lineNumber": 2,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source48",
+ "innerWindowID": 8589934593,
+ "timer": {
+ "name": "bar"
+ }
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`timerAlreadyExists`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "bar"
+ ],
+ "columnNumber": 11,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "time",
+ "lineNumber": 3,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source54",
+ "innerWindowID": 8589934593,
+ "timer": {
+ "error": "timerAlreadyExists",
+ "name": "bar"
+ }
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.timeLog('bar') - 1`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "bar"
+ ],
+ "columnNumber": 11,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "timeLog",
+ "lineNumber": 4,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source54",
+ "innerWindowID": 8589934593,
+ "timer": {
+ "duration": 4,
+ "name": "bar"
+ }
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.timeLog('bar') - 2`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "bar",
+ "second call",
+ {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj55",
+ "class": "Object",
+ "ownPropertyLength": 1,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "state": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": 1
+ }
+ },
+ "ownPropertiesLength": 1
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj55"
+ }
+ ],
+ "columnNumber": 11,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "timeLog",
+ "lineNumber": 5,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source46",
+ "innerWindowID": 8589934593,
+ "timer": {
+ "duration": 5,
+ "name": "bar"
+ }
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.timeEnd('bar')`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "bar"
+ ],
+ "columnNumber": 11,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "timeEnd",
+ "lineNumber": 6,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source54",
+ "innerWindowID": 8589934593,
+ "timer": {
+ "duration": 9,
+ "name": "bar"
+ }
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`timeEnd.timerDoesntExist`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "bar"
+ ],
+ "columnNumber": 11,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "timeEnd",
+ "lineNumber": 7,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source54",
+ "innerWindowID": 8589934593,
+ "timer": {
+ "error": "timerDoesntExist",
+ "name": "bar"
+ }
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`timeLog.timerDoesntExist`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "bar"
+ ],
+ "columnNumber": 11,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "timeLog",
+ "lineNumber": 8,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source54",
+ "innerWindowID": 8589934593,
+ "timer": {
+ "error": "timerDoesntExist",
+ "name": "bar"
+ }
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.table('bar')`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "bar"
+ ],
+ "columnNumber": 11,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "table",
+ "lineNumber": 2,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source48",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.table(['a', 'b', 'c'])`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj58",
+ "class": "Array",
+ "ownPropertyLength": 4,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": null,
+ "ownProperties": {
+ "0": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "a"
+ },
+ "1": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "b"
+ },
+ "2": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "c"
+ }
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj58"
+ }
+ ],
+ "columnNumber": 11,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "table",
+ "lineNumber": 2,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source48",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.group('bar')`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "bar"
+ ],
+ "columnNumber": 11,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "group",
+ "lineNumber": 2,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source48",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.groupEnd('bar')`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [],
+ "columnNumber": 11,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "groupEnd",
+ "lineNumber": 3,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source54",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.groupCollapsed('foo')`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "foo"
+ ],
+ "columnNumber": 11,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "groupCollapsed",
+ "lineNumber": 2,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source48",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.groupEnd('foo')`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [],
+ "columnNumber": 11,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "groupEnd",
+ "lineNumber": 3,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source54",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.group()`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [],
+ "columnNumber": 11,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "group",
+ "lineNumber": 2,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source48",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.groupEnd()`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [],
+ "columnNumber": 11,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "groupEnd",
+ "lineNumber": 3,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source54",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.log(%cfoobar)`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "foo",
+ "bar"
+ ],
+ "columnNumber": 11,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "log",
+ "lineNumber": 2,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source48",
+ "innerWindowID": 8589934593,
+ "styles": [
+ "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:url('https://example.com/test')"
+ ]
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.log("%cHello%c|%cWorld")`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "Hello",
+ "|",
+ "World"
+ ],
+ "columnNumber": 13,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "log",
+ "lineNumber": 2,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source63",
+ "innerWindowID": 8589934593,
+ "styles": [
+ "color:red",
+ "",
+ "color: blue"
+ ]
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.group(%cfoo%cbar)`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "foo",
+ "bar"
+ ],
+ "columnNumber": 11,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "group",
+ "lineNumber": 2,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source48",
+ "innerWindowID": 8589934593,
+ "styles": [
+ "color:blue;font-size:1.3em;background:url('https://example.com/test');position:absolute;top:10px",
+ "color:red;background:url('https://example.com/test')"
+ ]
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.groupEnd(%cfoo%cbar)`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [],
+ "columnNumber": 11,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "groupEnd",
+ "lineNumber": 6,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source54",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.groupCollapsed(%cfoo%cbaz)`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "foo",
+ "baz"
+ ],
+ "columnNumber": 11,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "groupCollapsed",
+ "lineNumber": 2,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source48",
+ "innerWindowID": 8589934593,
+ "styles": [
+ "color:blue;font-size:1.3em;background:url('https://example.com/test');position:absolute;top:10px",
+ "color:red;background:url('https://example.com/test')"
+ ]
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.groupEnd(%cfoo%cbaz)`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [],
+ "columnNumber": 11,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "groupEnd",
+ "lineNumber": 6,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source54",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.dir({C, M, Y, K})`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj67",
+ "class": "Object",
+ "ownPropertyLength": 4,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "cyan": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "C"
+ },
+ "magenta": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "M"
+ },
+ "yellow": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "Y"
+ },
+ "black": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "K"
+ }
+ },
+ "ownPropertiesLength": 4
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj67"
+ }
+ ],
+ "columnNumber": 35,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "dir",
+ "lineNumber": 1,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source22",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.count | default: 1`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "default"
+ ],
+ "columnNumber": 15,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "count",
+ "lineNumber": 2,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source68",
+ "innerWindowID": 8589934593,
+ "counter": {
+ "count": 1,
+ "label": "default"
+ }
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.count | default: 2`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "default"
+ ],
+ "columnNumber": 15,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "count",
+ "lineNumber": 3,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source68",
+ "innerWindowID": 8589934593,
+ "counter": {
+ "count": 2,
+ "label": "default"
+ }
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.count | test counter: 1`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "test counter"
+ ],
+ "columnNumber": 15,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "count",
+ "lineNumber": 4,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source68",
+ "innerWindowID": 8589934593,
+ "counter": {
+ "count": 1,
+ "label": "test counter"
+ }
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.count | test counter: 2`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "test counter"
+ ],
+ "columnNumber": 15,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "count",
+ "lineNumber": 5,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source68",
+ "innerWindowID": 8589934593,
+ "counter": {
+ "count": 2,
+ "label": "test counter"
+ }
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.count | default: 3`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "default"
+ ],
+ "columnNumber": 15,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "count",
+ "lineNumber": 6,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source68",
+ "innerWindowID": 8589934593,
+ "counter": {
+ "count": 3,
+ "label": "default"
+ }
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.count | clear`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [],
+ "columnNumber": 15,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "clear",
+ "lineNumber": 7,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source68",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.count | default: 4`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "default"
+ ],
+ "columnNumber": 15,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "count",
+ "lineNumber": 8,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source68",
+ "innerWindowID": 8589934593,
+ "counter": {
+ "count": 4,
+ "label": "default"
+ }
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.count | test counter: 3`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "test counter"
+ ],
+ "columnNumber": 15,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "count",
+ "lineNumber": 9,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source68",
+ "innerWindowID": 8589934593,
+ "counter": {
+ "count": 3,
+ "label": "test counter"
+ }
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.countReset | test counter: 0`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "test counter"
+ ],
+ "columnNumber": 15,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "countReset",
+ "lineNumber": 10,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source68",
+ "innerWindowID": 8589934593,
+ "counter": {
+ "count": 0,
+ "label": "test counter"
+ }
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.countReset | counterDoesntExist`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "test counter"
+ ],
+ "columnNumber": 15,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "countReset",
+ "lineNumber": 11,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source68",
+ "innerWindowID": 8589934593,
+ "counter": {
+ "error": "counterDoesntExist",
+ "label": "test counter"
+ }
+ },
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`console.log escaped characters`, {
+ "resourceType": "console-message",
+ "message": {
+ "arguments": [
+ "hello \nfrom \rthe \"string world!"
+ ],
+ "columnNumber": 35,
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "level": "log",
+ "lineNumber": 1,
+ "timeStamp": 1572867483805,
+ "sourceId": "server0.conn0.child1/source22",
+ "innerWindowID": 8589934593
+ },
+ "isAlreadyExistingResource": false
+});
+
+
+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 = ConsoleMessage(transformedPacket);
+ stubPreparedMessages.set(key, message);
+}
+
+module.exports = {
+ rawPackets,
+ stubPreparedMessages,
+ stubPackets,
+};
diff --git a/devtools/client/webconsole/test/node/fixtures/stubs/cssMessage.js b/devtools/client/webconsole/test/node/fixtures/stubs/cssMessage.js
new file mode 100644
index 0000000000..89a200839a
--- /dev/null
+++ b/devtools/client/webconsole/test/node/fixtures/stubs/cssMessage.js
@@ -0,0 +1,87 @@
+/* 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();
+rawPackets.set(`Unknown property ‘such-unknown-property’. Declaration dropped.`, {
+ "pageError": {
+ "errorMessage": "Unknown property ‘such-unknown-property’. Declaration dropped.",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/stub-generators/test-css-message.html",
+ "sourceId": null,
+ "lineText": "",
+ "lineNumber": 3,
+ "columnNumber": 27,
+ "category": "CSS Parser",
+ "innerWindowID": 8589934593,
+ "timeStamp": 1572867483805,
+ "warning": true,
+ "error": false,
+ "info": false,
+ "private": false,
+ "stacktrace": null,
+ "notes": null,
+ "chromeContext": false,
+ "isForwardedFromContentProcess": false
+ },
+ "resourceType": "css-message",
+ "cssSelectors": "p",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`Error in parsing value for ‘padding-top’. Declaration dropped.`, {
+ "pageError": {
+ "errorMessage": "Error in parsing value for ‘padding-top’. Declaration dropped.",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/stub-generators/test-css-message.html",
+ "sourceId": null,
+ "lineText": "",
+ "lineNumber": 3,
+ "columnNumber": 18,
+ "category": "CSS Parser",
+ "innerWindowID": 8589934593,
+ "timeStamp": 1572867483805,
+ "warning": true,
+ "error": false,
+ "info": false,
+ "private": false,
+ "stacktrace": null,
+ "notes": null,
+ "chromeContext": false,
+ "isForwardedFromContentProcess": false
+ },
+ "resourceType": "css-message",
+ "cssSelectors": "p",
+ "isAlreadyExistingResource": false
+});
+
+
+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 = ConsoleMessage(transformedPacket);
+ stubPreparedMessages.set(key, message);
+}
+
+module.exports = {
+ rawPackets,
+ stubPreparedMessages,
+ stubPackets,
+};
diff --git a/devtools/client/webconsole/test/node/fixtures/stubs/evaluationResult.js b/devtools/client/webconsole/test/node/fixtures/stubs/evaluationResult.js
new file mode 100644
index 0000000000..c69f73c5b5
--- /dev/null
+++ b/devtools/client/webconsole/test/node/fixtures/stubs/evaluationResult.js
@@ -0,0 +1,1431 @@
+/* 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();
+rawPackets.set(`new Date(0)`, {
+ "resultID": "1573832025018-0",
+ "hasException": false,
+ "input": "new Date(0)",
+ "result": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj20",
+ "class": "Date",
+ "ownPropertyLength": 0,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "timestamp": 0
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal12884901889/obj20"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`asdf()`, {
+ "resultID": "1573832025112-1",
+ "errorMessageName": "JSMSG_NOT_DEFINED",
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj22",
+ "class": "ReferenceError",
+ "ownPropertyLength": 4,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "ReferenceError",
+ "message": "asdf is not defined",
+ "stack": "@debugger eval code:1:1\n",
+ "fileName": "debugger eval code",
+ "lineNumber": 1,
+ "columnNumber": 1
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal12884901889/obj22"
+ },
+ "exceptionMessage": "ReferenceError: asdf is not defined",
+ "exceptionDocURL": "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default",
+ "exceptionStack": [
+ {
+ "filename": "debugger eval code",
+ "sourceId": "server0.conn0.child1/source24",
+ "lineNumber": 1,
+ "columnNumber": 1,
+ "functionName": null
+ }
+ ],
+ "hasException": true,
+ "frame": {
+ "source": "debugger eval code",
+ "line": 1,
+ "column": 1
+ },
+ "input": "asdf()",
+ "result": {
+ "type": "undefined"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`1 + @`, {
+ "resultID": "1573832025117-2",
+ "errorMessageName": "JSMSG_ILLEGAL_CHARACTER",
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj23",
+ "class": "SyntaxError",
+ "ownPropertyLength": 4,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "SyntaxError",
+ "message": "illegal character U+0040",
+ "stack": "",
+ "fileName": "debugger eval code",
+ "lineNumber": 1,
+ "columnNumber": 5
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal12884901889/obj23"
+ },
+ "exceptionMessage": "SyntaxError: illegal character U+0040",
+ "exceptionDocURL": "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Illegal_character?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default",
+ "hasException": true,
+ "frame": {
+ "source": "debugger eval code",
+ "line": 1,
+ "column": 5
+ },
+ "input": "1 + @",
+ "result": {
+ "type": "undefined"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`inspect({a: 1})`, {
+ "resultID": "1573832025122-3",
+ "hasException": false,
+ "helperResult": {
+ "type": "inspectObject",
+ "input": "inspect({a: 1})",
+ "object": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj25",
+ "class": "Object",
+ "ownPropertyLength": 1,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "a": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": 1
+ }
+ },
+ "ownPropertiesLength": 1
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal12884901889/obj25"
+ },
+ "forceExpandInConsole": false
+ },
+ "input": "inspect({a: 1})",
+ "result": {
+ "type": "undefined"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`undefined`, {
+ "resultID": "1573832025127-5",
+ "hasException": false,
+ "input": "undefined",
+ "result": {
+ "type": "undefined"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`longString message Error`, {
+ "resultID": "1573832025130-6",
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj28",
+ "class": "Error",
+ "ownPropertyLength": 4,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": {
+ "_grip": {
+ "type": "longString",
+ "actor": "server0.conn0.windowGlobal12884901889/longstractor29",
+ "length": 110000,
+ "initial": "Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error"
+ },
+ "actorID": "server0.conn0.windowGlobal12884901889/longstractor29"
+ },
+ "stack": "@debugger eval code:1:7\n",
+ "fileName": "debugger eval code",
+ "lineNumber": 1,
+ "columnNumber": 7
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal12884901889/obj28"
+ },
+ "exceptionMessage": {
+ "_grip": {
+ "type": "longString",
+ "actor": "server0.conn0.windowGlobal12884901889/longstractor30",
+ "length": 110007,
+ "initial": "Error: Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Lon"
+ },
+ "actorID": "server0.conn0.windowGlobal12884901889/longstractor30"
+ },
+ "exceptionStack": [
+ {
+ "filename": "debugger eval code",
+ "sourceId": "server0.conn0.child1/source31",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "functionName": null
+ }
+ ],
+ "hasException": true,
+ "frame": {
+ "source": "debugger eval code",
+ "sourceId": "server0.conn0.child1/source31",
+ "line": 1,
+ "column": 7
+ },
+ "input": "throw new Error(\"Long error \".repeat(10000))",
+ "result": {
+ "type": "undefined"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`eval throw ""`, {
+ "resultID": "1573832025134-7",
+ "exception": "",
+ "exceptionMessage": "",
+ "exceptionStack": [
+ {
+ "filename": "debugger eval code",
+ "sourceId": "server0.conn0.child1/source24",
+ "lineNumber": 1,
+ "columnNumber": 1,
+ "functionName": null
+ }
+ ],
+ "hasException": true,
+ "frame": {
+ "source": "debugger eval code",
+ "sourceId": "server0.conn0.child1/source24",
+ "line": 1,
+ "column": 1
+ },
+ "input": "throw \"\"",
+ "result": {
+ "type": "undefined"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`eval throw "tomato"`, {
+ "resultID": "1573832025137-8",
+ "exception": "tomato",
+ "exceptionMessage": "tomato",
+ "exceptionStack": [
+ {
+ "filename": "debugger eval code",
+ "sourceId": "server0.conn0.child1/source24",
+ "lineNumber": 1,
+ "columnNumber": 1,
+ "functionName": null
+ }
+ ],
+ "hasException": true,
+ "frame": {
+ "source": "debugger eval code",
+ "sourceId": "server0.conn0.child1/source24",
+ "line": 1,
+ "column": 1
+ },
+ "input": "throw \"tomato\"",
+ "result": {
+ "type": "undefined"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`eval throw false`, {
+ "resultID": "1588154002962-9",
+ "exception": false,
+ "exceptionMessage": "false",
+ "exceptionStack": [
+ {
+ "filename": "debugger eval code",
+ "sourceId": "server0.conn2.child2/source26",
+ "lineNumber": 1,
+ "columnNumber": 1,
+ "functionName": null
+ }
+ ],
+ "hasException": true,
+ "frame": {
+ "source": "debugger eval code",
+ "sourceId": "server0.conn2.child2/source26",
+ "line": 1,
+ "column": 1
+ },
+ "input": "throw false",
+ "result": {
+ "type": "undefined"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`eval throw 0`, {
+ "resultID": "1588154002979-10",
+ "exception": 0,
+ "exceptionMessage": "0",
+ "exceptionStack": [
+ {
+ "filename": "debugger eval code",
+ "sourceId": "server0.conn2.child2/source26",
+ "lineNumber": 1,
+ "columnNumber": 1,
+ "functionName": null
+ }
+ ],
+ "hasException": true,
+ "frame": {
+ "source": "debugger eval code",
+ "sourceId": "server0.conn2.child2/source26",
+ "line": 1,
+ "column": 1
+ },
+ "input": "throw 0",
+ "result": {
+ "type": "undefined"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`eval throw null`, {
+ "resultID": "1588154003064-11",
+ "exception": {
+ "type": "null"
+ },
+ "exceptionMessage": "null",
+ "exceptionStack": [
+ {
+ "filename": "debugger eval code",
+ "sourceId": "server0.conn2.child2/source26",
+ "lineNumber": 1,
+ "columnNumber": 1,
+ "functionName": null
+ }
+ ],
+ "hasException": true,
+ "frame": {
+ "source": "debugger eval code",
+ "sourceId": "server0.conn2.child2/source26",
+ "line": 1,
+ "column": 1
+ },
+ "input": "throw null",
+ "result": {
+ "type": "undefined"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`eval throw undefined`, {
+ "resultID": "1588154003073-12",
+ "exception": {
+ "type": "undefined"
+ },
+ "exceptionMessage": "undefined",
+ "exceptionStack": [
+ {
+ "filename": "debugger eval code",
+ "sourceId": "server0.conn2.child2/source26",
+ "lineNumber": 1,
+ "columnNumber": 1,
+ "functionName": null
+ }
+ ],
+ "hasException": true,
+ "frame": {
+ "source": "debugger eval code",
+ "sourceId": "server0.conn2.child2/source26",
+ "line": 1,
+ "column": 1
+ },
+ "input": "throw undefined",
+ "result": {
+ "type": "undefined"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`eval throw Symbol`, {
+ "resultID": "1588154003077-13",
+ "exception": {
+ "type": "symbol",
+ "actor": "server0.conn0.windowGlobal12884901889/symbol38",
+ "name": "potato"
+ },
+ "exceptionMessage": "Symbol(potato)",
+ "exceptionStack": [
+ {
+ "filename": "debugger eval code",
+ "sourceId": "server0.conn2.child2/source33",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "functionName": null
+ }
+ ],
+ "hasException": true,
+ "frame": {
+ "source": "debugger eval code",
+ "sourceId": "server0.conn2.child2/source33",
+ "line": 1,
+ "column": 7
+ },
+ "input": "throw Symbol(\"potato\")",
+ "result": {
+ "type": "undefined"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`eval throw Object`, {
+ "resultID": "1588154003082-14",
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj40",
+ "class": "Object",
+ "ownPropertyLength": 1,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "vegetable": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "cucumber"
+ }
+ },
+ "ownPropertiesLength": 1
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal12884901889/obj40"
+ },
+ "exceptionMessage": "[object Object]",
+ "exceptionStack": [
+ {
+ "filename": "debugger eval code",
+ "sourceId": "server0.conn2.child2/source26",
+ "lineNumber": 1,
+ "columnNumber": 1,
+ "functionName": null
+ }
+ ],
+ "hasException": true,
+ "frame": {
+ "source": "debugger eval code",
+ "sourceId": "server0.conn2.child2/source26",
+ "line": 1,
+ "column": 1
+ },
+ "input": "throw {vegetable: \"cucumber\"}",
+ "result": {
+ "type": "undefined"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`eval throw Error Object`, {
+ "resultID": "1588154003093-15",
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj42",
+ "class": "Error",
+ "ownPropertyLength": 4,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "pumpkin",
+ "stack": "@debugger eval code:1:7\n",
+ "fileName": "debugger eval code",
+ "lineNumber": 1,
+ "columnNumber": 7
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal12884901889/obj42"
+ },
+ "exceptionMessage": "Error: pumpkin",
+ "exceptionStack": [
+ {
+ "filename": "debugger eval code",
+ "sourceId": "server0.conn2.child2/source33",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "functionName": null
+ }
+ ],
+ "hasException": true,
+ "frame": {
+ "source": "debugger eval code",
+ "sourceId": "server0.conn2.child2/source33",
+ "line": 1,
+ "column": 7
+ },
+ "input": "throw new Error(\"pumpkin\")",
+ "result": {
+ "type": "undefined"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`eval throw Error Object with custom name`, {
+ "resultID": "1588154003097-16",
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj44",
+ "class": "Error",
+ "ownPropertyLength": 6,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "JuicyError",
+ "message": "pineapple",
+ "stack": "@debugger eval code:2:15\n",
+ "fileName": "debugger eval code",
+ "lineNumber": 2,
+ "columnNumber": 15
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal12884901889/obj44"
+ },
+ "exceptionMessage": "JuicyError: pineapple",
+ "exceptionStack": [
+ {
+ "filename": "debugger eval code",
+ "sourceId": "server0.conn2.child2/source49",
+ "lineNumber": 5,
+ "columnNumber": 5,
+ "functionName": null
+ }
+ ],
+ "hasException": true,
+ "frame": {
+ "source": "debugger eval code",
+ "sourceId": "server0.conn2.child2/source49",
+ "line": 5,
+ "column": 5
+ },
+ "input": "\n var err = new Error(\"pineapple\");\n err.name = \"JuicyError\";\n err.flavor = \"delicious\";\n throw err;\n ",
+ "result": {
+ "type": "undefined"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`eval throw Error Object with error cause`, {
+ "resultID": "1621589071246-16",
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj46",
+ "class": "Error",
+ "ownPropertyLength": 5,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "something went wrong",
+ "stack": "@debugger eval code:3:15\n",
+ "fileName": "debugger eval code",
+ "lineNumber": 3,
+ "columnNumber": 15,
+ "cause": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj47",
+ "class": "SyntaxError",
+ "ownPropertyLength": 4,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "SyntaxError",
+ "message": "original error",
+ "stack": "@debugger eval code:2:25\n",
+ "fileName": "debugger eval code",
+ "lineNumber": 2,
+ "columnNumber": 25
+ }
+ }
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal12884901889/obj46"
+ },
+ "exceptionMessage": "Error: something went wrong",
+ "exceptionStack": [
+ {
+ "filename": "debugger eval code",
+ "sourceId": "server0.conn0.child4/source49",
+ "lineNumber": 6,
+ "columnNumber": 5,
+ "functionName": null
+ }
+ ],
+ "hasException": true,
+ "frame": {
+ "source": "debugger eval code",
+ "sourceId": "server0.conn0.child4/source49",
+ "line": 6,
+ "column": 5
+ },
+ "input": "\n var originalError = new SyntaxError(\"original error\")\n var err = new Error(\"something went wrong\", {\n cause: originalError\n });\n throw err;\n ",
+ "result": {
+ "type": "undefined"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`eval throw Error Object with cause chain`, {
+ "resultID": "1621589071248-17",
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj49",
+ "class": "Error",
+ "ownPropertyLength": 5,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "err-d",
+ "stack": "@debugger eval code:5:16\n",
+ "fileName": "debugger eval code",
+ "lineNumber": 5,
+ "columnNumber": 16,
+ "cause": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj50",
+ "class": "Error",
+ "ownPropertyLength": 5,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "err-c",
+ "stack": "@debugger eval code:4:16\n",
+ "fileName": "debugger eval code",
+ "lineNumber": 4,
+ "columnNumber": 16,
+ "cause": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj51",
+ "class": "Error",
+ "ownPropertyLength": 5,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "err-b",
+ "stack": "@debugger eval code:3:16\n",
+ "fileName": "debugger eval code",
+ "lineNumber": 3,
+ "columnNumber": 16,
+ "cause": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj52",
+ "class": "Error",
+ "ownPropertyLength": 4,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "err-a",
+ "stack": "@debugger eval code:2:16\n",
+ "fileName": "debugger eval code",
+ "lineNumber": 2,
+ "columnNumber": 16
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal12884901889/obj49"
+ },
+ "exceptionMessage": "Error: err-d",
+ "exceptionStack": [
+ {
+ "filename": "debugger eval code",
+ "sourceId": "server0.conn0.child4/source49",
+ "lineNumber": 6,
+ "columnNumber": 5,
+ "functionName": null
+ }
+ ],
+ "hasException": true,
+ "frame": {
+ "source": "debugger eval code",
+ "sourceId": "server0.conn0.child4/source49",
+ "line": 6,
+ "column": 5
+ },
+ "input": "\n var errA = new Error(\"err-a\")\n var errB = new Error(\"err-b\", { cause: errA })\n var errC = new Error(\"err-c\", { cause: errB })\n var errD = new Error(\"err-d\", { cause: errC })\n throw errD;\n ",
+ "result": {
+ "type": "undefined"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`eval throw Error Object with cyclical cause chain`, {
+ "resultID": "1621589071250-18",
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj54",
+ "class": "Error",
+ "ownPropertyLength": 5,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "err-y",
+ "stack": "@debugger eval code:3:16\n",
+ "fileName": "debugger eval code",
+ "lineNumber": 3,
+ "columnNumber": 16,
+ "cause": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj55",
+ "class": "Error",
+ "ownPropertyLength": 5,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "err-x",
+ "stack": "@debugger eval code:2:16\n",
+ "fileName": "debugger eval code",
+ "lineNumber": 2,
+ "columnNumber": 16,
+ "cause": {
+ "type": "undefined"
+ }
+ }
+ }
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal12884901889/obj54"
+ },
+ "exceptionMessage": "Error: err-y",
+ "exceptionStack": [
+ {
+ "filename": "debugger eval code",
+ "sourceId": "server0.conn0.child4/source57",
+ "lineNumber": 4,
+ "columnNumber": 5,
+ "functionName": null
+ }
+ ],
+ "hasException": true,
+ "frame": {
+ "source": "debugger eval code",
+ "sourceId": "server0.conn0.child4/source57",
+ "line": 4,
+ "column": 5
+ },
+ "input": "\n var errX = new Error(\"err-x\", { cause: errY})\n var errY = new Error(\"err-y\", { cause: errX })\n throw errY;\n ",
+ "result": {
+ "type": "undefined"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`eval throw Error Object with falsy cause`, {
+ "resultID": "1621589071252-19",
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj57",
+ "class": "Error",
+ "ownPropertyLength": 5,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "false cause",
+ "stack": "@debugger eval code:1:7\n",
+ "fileName": "debugger eval code",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "cause": false
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal12884901889/obj57"
+ },
+ "exceptionMessage": "Error: false cause",
+ "exceptionStack": [
+ {
+ "filename": "debugger eval code",
+ "sourceId": "server0.conn0.child4/source31",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "functionName": null
+ }
+ ],
+ "hasException": true,
+ "frame": {
+ "source": "debugger eval code",
+ "sourceId": "server0.conn0.child4/source31",
+ "line": 1,
+ "column": 7
+ },
+ "input": "throw new Error(\"false cause\", { cause: false });",
+ "result": {
+ "type": "undefined"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`eval throw Error Object with null cause`, {
+ "resultID": "1622039251647-20",
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj59",
+ "class": "Error",
+ "ownPropertyLength": 5,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "null cause",
+ "stack": "@debugger eval code:1:7\n",
+ "fileName": "debugger eval code",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "cause": {
+ "type": "null"
+ }
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal12884901889/obj59"
+ },
+ "exceptionMessage": "Error: null cause",
+ "exceptionStack": [
+ {
+ "filename": "debugger eval code",
+ "sourceId": "server0.conn2.child4/source31",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "functionName": null
+ }
+ ],
+ "hasException": true,
+ "frame": {
+ "source": "debugger eval code",
+ "sourceId": "server0.conn2.child4/source31",
+ "line": 1,
+ "column": 7
+ },
+ "input": "throw new Error(\"null cause\", { cause: null });",
+ "result": {
+ "type": "undefined"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`eval throw Error Object with undefined cause`, {
+ "resultID": "1622039251649-21",
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj61",
+ "class": "Error",
+ "ownPropertyLength": 5,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "undefined cause",
+ "stack": "@debugger eval code:1:7\n",
+ "fileName": "debugger eval code",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "cause": {
+ "type": "undefined"
+ }
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal12884901889/obj61"
+ },
+ "exceptionMessage": "Error: undefined cause",
+ "exceptionStack": [
+ {
+ "filename": "debugger eval code",
+ "sourceId": "server0.conn2.child4/source31",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "functionName": null
+ }
+ ],
+ "hasException": true,
+ "frame": {
+ "source": "debugger eval code",
+ "sourceId": "server0.conn2.child4/source31",
+ "line": 1,
+ "column": 7
+ },
+ "input": "throw new Error(\"undefined cause\", { cause: undefined });",
+ "result": {
+ "type": "undefined"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`eval throw Error Object with number cause`, {
+ "resultID": "1621589071253-20",
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj63",
+ "class": "Error",
+ "ownPropertyLength": 5,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "number cause",
+ "stack": "@debugger eval code:1:7\n",
+ "fileName": "debugger eval code",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "cause": 0
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal12884901889/obj63"
+ },
+ "exceptionMessage": "Error: number cause",
+ "exceptionStack": [
+ {
+ "filename": "debugger eval code",
+ "sourceId": "server0.conn0.child4/source31",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "functionName": null
+ }
+ ],
+ "hasException": true,
+ "frame": {
+ "source": "debugger eval code",
+ "sourceId": "server0.conn0.child4/source31",
+ "line": 1,
+ "column": 7
+ },
+ "input": "throw new Error(\"number cause\", { cause: 0 });",
+ "result": {
+ "type": "undefined"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`eval throw Error Object with string cause`, {
+ "resultID": "1621589071255-21",
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj65",
+ "class": "Error",
+ "ownPropertyLength": 5,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "string cause",
+ "stack": "@debugger eval code:1:7\n",
+ "fileName": "debugger eval code",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "cause": "cause message"
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal12884901889/obj65"
+ },
+ "exceptionMessage": "Error: string cause",
+ "exceptionStack": [
+ {
+ "filename": "debugger eval code",
+ "sourceId": "server0.conn0.child4/source31",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "functionName": null
+ }
+ ],
+ "hasException": true,
+ "frame": {
+ "source": "debugger eval code",
+ "sourceId": "server0.conn0.child4/source31",
+ "line": 1,
+ "column": 7
+ },
+ "input": "throw new Error(\"string cause\", { cause: \"cause message\" });",
+ "result": {
+ "type": "undefined"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`eval throw Error Object with object cause`, {
+ "resultID": "1621589071256-22",
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj67",
+ "class": "Error",
+ "ownPropertyLength": 5,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "object cause",
+ "stack": "@debugger eval code:1:7\n",
+ "fileName": "debugger eval code",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "cause": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj68",
+ "class": "Object",
+ "ownPropertyLength": 2,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false
+ }
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal12884901889/obj67"
+ },
+ "exceptionMessage": "Error: object cause",
+ "exceptionStack": [
+ {
+ "filename": "debugger eval code",
+ "sourceId": "server0.conn0.child4/source31",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "functionName": null
+ }
+ ],
+ "hasException": true,
+ "frame": {
+ "source": "debugger eval code",
+ "sourceId": "server0.conn0.child4/source31",
+ "line": 1,
+ "column": 7
+ },
+ "input": "throw new Error(\"object cause\", { cause: { code: 234, message: \"ERR_234\"} });",
+ "result": {
+ "type": "undefined"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`eval pending promise`, {
+ "resultID": "1609858965386-17",
+ "hasException": false,
+ "input": "new Promise(() => {})",
+ "result": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj70",
+ "class": "Promise",
+ "ownPropertyLength": 0,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "<state>": {
+ "value": "pending"
+ }
+ },
+ "ownPropertiesLength": 1
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal12884901889/obj70"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`eval Promise.resolve`, {
+ "resultID": "1609858965388-18",
+ "hasException": false,
+ "input": "Promise.resolve(123)",
+ "result": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj72",
+ "class": "Promise",
+ "ownPropertyLength": 0,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "<state>": {
+ "value": "fulfilled"
+ },
+ "<value>": {
+ "value": 123
+ }
+ },
+ "ownPropertiesLength": 2
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal12884901889/obj72"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`eval Promise.reject`, {
+ "resultID": "1609858965389-19",
+ "hasException": false,
+ "input": "Promise.reject(\"ouch\")",
+ "result": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj74",
+ "class": "Promise",
+ "ownPropertyLength": 0,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "<state>": {
+ "value": "rejected"
+ },
+ "<reason>": {
+ "value": "ouch"
+ }
+ },
+ "ownPropertiesLength": 2
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal12884901889/obj74"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`eval resolved promise`, {
+ "resultID": "1609858965393-20",
+ "hasException": false,
+ "input": "Promise.resolve().then(() => 246)",
+ "result": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj76",
+ "class": "Promise",
+ "ownPropertyLength": 0,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "<state>": {
+ "value": "fulfilled"
+ },
+ "<value>": {
+ "value": 246
+ }
+ },
+ "ownPropertiesLength": 2
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal12884901889/obj76"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`eval rejected promise`, {
+ "resultID": "1609858965397-21",
+ "hasException": false,
+ "input": "Promise.resolve().then(() => a.b.c)",
+ "result": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj78",
+ "class": "Promise",
+ "ownPropertyLength": 0,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "<state>": {
+ "value": "rejected"
+ },
+ "<reason>": {
+ "value": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj79",
+ "class": "ReferenceError",
+ "ownPropertyLength": 4,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "ReferenceError",
+ "message": "a is not defined",
+ "stack": "@debugger eval code:1:30\npromise callback*@debugger eval code:1:19\n",
+ "fileName": "debugger eval code",
+ "lineNumber": 1,
+ "columnNumber": 30
+ }
+ }
+ }
+ },
+ "ownPropertiesLength": 2
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal12884901889/obj78"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+rawPackets.set(`eval rejected promise with Error`, {
+ "resultID": "1621589071278-28",
+ "hasException": false,
+ "input": "Promise.resolve().then(() => {\n try {\n a.b.c\n } catch(e) {\n throw new Error(\"something went wrong\", { cause: e })\n }\n })",
+ "result": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj82",
+ "class": "Promise",
+ "ownPropertyLength": 0,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "<state>": {
+ "value": "rejected"
+ },
+ "<reason>": {
+ "value": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj83",
+ "class": "Error",
+ "ownPropertyLength": 5,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "something went wrong",
+ "stack": "@debugger eval code:5:15\npromise callback*@debugger eval code:1:19\n",
+ "fileName": "debugger eval code",
+ "lineNumber": 5,
+ "columnNumber": 15,
+ "cause": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal12884901889/obj84",
+ "class": "ReferenceError",
+ "ownPropertyLength": 4,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "ReferenceError",
+ "message": "a is not defined",
+ "stack": "@debugger eval code:3:9\npromise callback*@debugger eval code:1:19\n",
+ "fileName": "debugger eval code",
+ "lineNumber": 3,
+ "columnNumber": 9
+ }
+ }
+ }
+ }
+ }
+ },
+ "ownPropertiesLength": 2
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal12884901889/obj82"
+ },
+ "startTime": 1572867483805,
+ "timestamp": 1572867483805
+});
+
+
+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 = ConsoleMessage(transformedPacket);
+ stubPreparedMessages.set(key, message);
+}
+
+module.exports = {
+ rawPackets,
+ stubPreparedMessages,
+ stubPackets,
+};
diff --git a/devtools/client/webconsole/test/node/fixtures/stubs/index.js b/devtools/client/webconsole/test/node/fixtures/stubs/index.js
new file mode 100644
index 0000000000..9e5de16308
--- /dev/null
+++ b/devtools/client/webconsole/test/node/fixtures/stubs/index.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const maps = [];
+
+[
+ "consoleApi",
+ "cssMessage",
+ "evaluationResult",
+ "networkEvent",
+ "pageError",
+ "platformMessage",
+].forEach(filename => {
+ maps[filename] = require(`./${filename}`);
+});
+
+// Combine all the maps into a single map.
+module.exports = {
+ stubPreparedMessages: new Map([
+ ...maps.consoleApi.stubPreparedMessages,
+ ...maps.cssMessage.stubPreparedMessages,
+ ...maps.evaluationResult.stubPreparedMessages,
+ ...maps.networkEvent.stubPreparedMessages,
+ ...maps.pageError.stubPreparedMessages,
+ ...maps.platformMessage.stubPreparedMessages,
+ ]),
+ stubPackets: new Map([
+ ...maps.consoleApi.stubPackets,
+ ...maps.cssMessage.stubPackets,
+ ...maps.evaluationResult.stubPackets,
+ ...maps.networkEvent.stubPackets,
+ ...maps.pageError.stubPackets,
+ ...maps.platformMessage.stubPackets,
+ ]),
+};
diff --git a/devtools/client/webconsole/test/node/fixtures/stubs/networkEvent.js b/devtools/client/webconsole/test/node/fixtures/stubs/networkEvent.js
new file mode 100644
index 0000000000..340c10ba65
--- /dev/null
+++ b/devtools/client/webconsole/test/node/fixtures/stubs/networkEvent.js
@@ -0,0 +1,269 @@
+/* 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();
+rawPackets.set(`GET request`, {
+ "resourceType": "network-event",
+ "timeStamp": 1572867483805,
+ "actor": "server0.conn0.netEvent4",
+ "startedDateTime": "2019-11-04T11:06:34.542Z",
+ "method": "GET",
+ "url": "https://example.com/inexistent.html",
+ "isXHR": false,
+ "cause": {
+ "type": "img",
+ "loadingDocumentUri": "https://example.com/browser/devtools/client/webconsole/test/browser/stub-generators/test-network-event.html",
+ "stacktraceAvailable": true,
+ "lastFrame": {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/stub-generators/test-network-event.html",
+ "lineNumber": 3,
+ "columnNumber": 1,
+ "functionName": "triggerPacket",
+ "asyncCause": null
+ }
+ },
+ "httpVersion": "HTTP/1.1",
+ "status": "404",
+ "statusText": "Not Found",
+ "remoteAddress": "127.0.0.1",
+ "remotePort": 4443,
+ "mimeType": "text/html; charset=utf-8",
+ "waitingTime": 2,
+ "contentSize": 418,
+ "transferredSize": 578,
+ "timings": {},
+ "private": false,
+ "isThirdPartyTrackingResource": false,
+ "referrerPolicy": "strict-origin-when-cross-origin",
+ "blockedReason": 0,
+ "totalTime": 2,
+ "securityState": "insecure",
+ "isRacing": false
+});
+
+rawPackets.set(`GET request update`, {
+ "resourceType": "network-event",
+ "timeStamp": 1572867483805,
+ "actor": "server0.conn0.netEvent4",
+ "startedDateTime": "2020-07-07T14:41:14.572Z",
+ "method": "GET",
+ "url": "https://example.com/inexistent.html",
+ "isXHR": false,
+ "cause": {
+ "type": "img",
+ "loadingDocumentUri": "https://example.com/browser/devtools/client/webconsole/test/browser/stub-generators/test-network-event.html",
+ "stacktraceAvailable": true,
+ "lastFrame": {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/stub-generators/test-network-event.html",
+ "lineNumber": 3,
+ "columnNumber": 1,
+ "functionName": "triggerPacket",
+ "asyncCause": null
+ }
+ },
+ "httpVersion": "HTTP/1.1",
+ "status": "404",
+ "statusText": "Not Found",
+ "remoteAddress": "127.0.0.1",
+ "remotePort": 4443,
+ "mimeType": "text/html; charset=utf-8",
+ "waitingTime": 2,
+ "contentSize": 418,
+ "transferredSize": 578,
+ "timings": {},
+ "private": false,
+ "isThirdPartyTrackingResource": false,
+ "referrerPolicy": "strict-origin-when-cross-origin",
+ "blockedReason": 0,
+ "totalTime": 3,
+ "securityState": "insecure",
+ "isRacing": false
+});
+
+rawPackets.set(`XHR GET request`, {
+ "resourceType": "network-event",
+ "timeStamp": 1572867483805,
+ "actor": "server0.conn0.netEvent23",
+ "startedDateTime": "2020-07-07T14:41:14.612Z",
+ "method": "GET",
+ "url": "https://example.com/inexistent.html",
+ "isXHR": true,
+ "cause": {
+ "type": "xhr",
+ "loadingDocumentUri": "https://example.com/browser/devtools/client/webconsole/test/browser/stub-generators/test-network-event.html",
+ "stacktraceAvailable": true,
+ "lastFrame": {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/stub-generators/test-network-event.html",
+ "lineNumber": 4,
+ "columnNumber": 5,
+ "functionName": "triggerPacket",
+ "asyncCause": null
+ }
+ },
+ "httpVersion": "HTTP/1.1",
+ "status": "404",
+ "statusText": "Not Found",
+ "remoteAddress": "127.0.0.1",
+ "remotePort": 4443,
+ "mimeType": "text/html; charset=utf-8",
+ "waitingTime": 1,
+ "contentSize": 418,
+ "transferredSize": 578,
+ "timings": {},
+ "private": false,
+ "isThirdPartyTrackingResource": false,
+ "referrerPolicy": "strict-origin-when-cross-origin",
+ "blockedReason": 0,
+ "totalTime": 1,
+ "securityState": "insecure",
+ "isRacing": false
+});
+
+rawPackets.set(`XHR GET request update`, {
+ "resourceType": "network-event",
+ "timeStamp": 1572867483805,
+ "actor": "server0.conn0.netEvent23",
+ "method": "GET",
+ "url": "https://example.com/inexistent.html",
+ "isXHR": true,
+ "cause": {
+ "type": "xhr",
+ "loadingDocumentUri": "https://example.com/browser/devtools/client/webconsole/test/browser/stub-generators/test-network-event.html",
+ "stacktraceAvailable": true,
+ "lastFrame": {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/stub-generators/test-network-event.html",
+ "lineNumber": 4,
+ "columnNumber": 5,
+ "functionName": "triggerPacket",
+ "asyncCause": null
+ }
+ },
+ "httpVersion": "HTTP/1.1",
+ "status": "404",
+ "statusText": "Not Found",
+ "remoteAddress": "127.0.0.1",
+ "remotePort": 4443,
+ "mimeType": "text/html; charset=utf-8",
+ "waitingTime": 1,
+ "contentSize": 418,
+ "transferredSize": 578,
+ "timings": {},
+ "private": false,
+ "isThirdPartyTrackingResource": false,
+ "referrerPolicy": "strict-origin-when-cross-origin",
+ "blockedReason": 0,
+ "totalTime": 1,
+ "securityState": "insecure",
+ "isRacing": false
+});
+
+rawPackets.set(`XHR POST request`, {
+ "resourceType": "network-event",
+ "timeStamp": 1572867483805,
+ "actor": "server0.conn0.netEvent42",
+ "startedDateTime": "2019-11-04T11:06:35.007Z",
+ "method": "POST",
+ "url": "https://example.com/inexistent.html",
+ "isXHR": true,
+ "cause": {
+ "type": "xhr",
+ "loadingDocumentUri": "https://example.com/browser/devtools/client/webconsole/test/browser/stub-generators/test-network-event.html",
+ "stacktraceAvailable": true,
+ "lastFrame": {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/stub-generators/test-network-event.html",
+ "lineNumber": 4,
+ "columnNumber": 5,
+ "functionName": "triggerPacket",
+ "asyncCause": null
+ }
+ },
+ "httpVersion": "HTTP/1.1",
+ "status": "404",
+ "statusText": "Not Found",
+ "remoteAddress": "127.0.0.1",
+ "remotePort": 4443,
+ "mimeType": "text/html; charset=utf-8",
+ "waitingTime": 2,
+ "contentSize": 418,
+ "transferredSize": 578,
+ "timings": {},
+ "private": false,
+ "isThirdPartyTrackingResource": false,
+ "referrerPolicy": "strict-origin-when-cross-origin",
+ "blockedReason": 0,
+ "totalTime": 1,
+ "securityState": "insecure",
+ "isRacing": false
+});
+
+rawPackets.set(`XHR POST request update`, {
+ "resourceType": "network-event",
+ "timeStamp": 1572867483805,
+ "actor": "server0.conn0.netEvent42",
+ "method": "POST",
+ "url": "https://example.com/inexistent.html",
+ "isXHR": true,
+ "cause": {
+ "type": "xhr",
+ "loadingDocumentUri": "https://example.com/browser/devtools/client/webconsole/test/browser/stub-generators/test-network-event.html",
+ "stacktraceAvailable": true,
+ "lastFrame": {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/stub-generators/test-network-event.html",
+ "lineNumber": 4,
+ "columnNumber": 5,
+ "functionName": "triggerPacket",
+ "asyncCause": null
+ }
+ },
+ "httpVersion": "HTTP/1.1",
+ "status": "404",
+ "statusText": "Not Found",
+ "remoteAddress": "127.0.0.1",
+ "remotePort": 4443,
+ "mimeType": "text/html; charset=utf-8",
+ "waitingTime": 2,
+ "contentSize": 418,
+ "transferredSize": 578,
+ "timings": {},
+ "private": false,
+ "isThirdPartyTrackingResource": false,
+ "referrerPolicy": "strict-origin-when-cross-origin",
+ "blockedReason": 0,
+ "totalTime": 2,
+ "securityState": "insecure",
+ "isRacing": false
+});
+
+
+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 = NetworkEventMessage(transformedPacket);
+ stubPreparedMessages.set(key, message);
+}
+
+module.exports = {
+ rawPackets,
+ stubPreparedMessages,
+ stubPackets,
+};
diff --git a/devtools/client/webconsole/test/node/fixtures/stubs/pageError.js b/devtools/client/webconsole/test/node/fixtures/stubs/pageError.js
new file mode 100644
index 0000000000..8f8da9a521
--- /dev/null
+++ b/devtools/client/webconsole/test/node/fixtures/stubs/pageError.js
@@ -0,0 +1,2482 @@
+/* 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();
+rawPackets.set(`ReferenceError: asdf is not defined`, {
+ "pageError": {
+ "errorMessage": "ReferenceError: asdf is not defined",
+ "errorMessageName": "JSMSG_NOT_DEFINED",
+ "exceptionDocURL": "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn0.child1/source22",
+ "lineText": "",
+ "lineNumber": 3,
+ "columnNumber": 5,
+ "category": "content javascript",
+ "innerWindowID": 8589934593,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn0.child1/source22",
+ "lineNumber": 3,
+ "columnNumber": 5,
+ "functionName": "bar"
+ },
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn0.child1/source22",
+ "lineNumber": 6,
+ "columnNumber": 5,
+ "functionName": "foo"
+ },
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn0.child1/source22",
+ "lineNumber": 9,
+ "columnNumber": 3,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": false,
+ "isForwardedFromContentProcess": false,
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj20",
+ "class": "ReferenceError",
+ "ownPropertyLength": 4,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "ReferenceError",
+ "message": "asdf is not defined",
+ "stack": "bar@https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html:3:5\nfoo@https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html:6:5\n@https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html:9:3\n",
+ "fileName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "lineNumber": 3,
+ "columnNumber": 5
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj20"
+ },
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`SyntaxError: redeclaration of let a`, {
+ "pageError": {
+ "errorMessage": "SyntaxError: redeclaration of let a",
+ "errorMessageName": "JSMSG_REDECLARED_VAR",
+ "exceptionDocURL": "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Redeclared_parameter?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": null,
+ "lineText": " let a, a;",
+ "lineNumber": 2,
+ "columnNumber": 10,
+ "category": "content javascript",
+ "innerWindowID": 8589934593,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": [
+ {
+ "messageBody": "Previously declared at line 2, column 7",
+ "frame": {
+ "source": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": null,
+ "line": 2,
+ "column": 7
+ }
+ }
+ ],
+ "chromeContext": false,
+ "isPromiseRejection": false,
+ "isForwardedFromContentProcess": false,
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj21",
+ "class": "SyntaxError",
+ "ownPropertyLength": 4,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "SyntaxError",
+ "message": "redeclaration of let a",
+ "stack": "",
+ "fileName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "lineNumber": 2,
+ "columnNumber": 10
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj21"
+ },
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`TypeError longString message`, {
+ "pageError": {
+ "errorMessage": {
+ "_grip": {
+ "type": "longString",
+ "actor": "server0.conn0.windowGlobal15032385537/longstractor23",
+ "length": 110007,
+ "initial": "Error: Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Lon"
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/longstractor23"
+ },
+ "errorMessageName": "",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn0.child1/source23",
+ "lineText": "",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "category": "content javascript",
+ "innerWindowID": 8589934593,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn0.child1/source23",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": false,
+ "isForwardedFromContentProcess": false,
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj24",
+ "class": "Error",
+ "ownPropertyLength": 4,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": {
+ "_grip": {
+ "type": "longString",
+ "actor": "server0.conn0.windowGlobal15032385537/longstractor25",
+ "length": 110000,
+ "initial": "Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error Long error"
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/longstractor25"
+ },
+ "stack": "@https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html:1:7\n",
+ "fileName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "lineNumber": 1,
+ "columnNumber": 7
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj24"
+ },
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`throw string with URL`, {
+ "pageError": {
+ "errorMessage": "uncaught exception: “https://evil.com/?aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa“ is evil and “https://not-so-evil.com/?aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa“ is not good either",
+ "errorMessageName": "JSMSG_UNCAUGHT_EXCEPTION",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn0.child2/source31",
+ "lineText": "",
+ "lineNumber": 1,
+ "columnNumber": 1,
+ "category": "content javascript",
+ "innerWindowID": 10737418241,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn0.child2/source31",
+ "lineNumber": 1,
+ "columnNumber": 1,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": false,
+ "isForwardedFromContentProcess": false,
+ "exception": "“https://evil.com/?aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa“ is evil and “https://not-so-evil.com/?aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa“ is not good either",
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`throw ""`, {
+ "pageError": {
+ "errorMessage": "uncaught exception: ",
+ "errorMessageName": "JSMSG_UNCAUGHT_EXCEPTION",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": null,
+ "lineText": "",
+ "lineNumber": 1,
+ "columnNumber": 1,
+ "category": "content javascript",
+ "innerWindowID": 8589934593,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn0.child1/source25",
+ "lineNumber": 1,
+ "columnNumber": 1,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": false,
+ "isForwardedFromContentProcess": false,
+ "exception": "",
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`throw "tomato"`, {
+ "pageError": {
+ "errorMessage": "uncaught exception: tomato",
+ "errorMessageName": "JSMSG_UNCAUGHT_EXCEPTION",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": null,
+ "lineText": "",
+ "lineNumber": 1,
+ "columnNumber": 1,
+ "category": "content javascript",
+ "innerWindowID": 8589934593,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn0.child1/source25",
+ "lineNumber": 1,
+ "columnNumber": 1,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": false,
+ "isForwardedFromContentProcess": false,
+ "exception": "tomato",
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`throw false`, {
+ "pageError": {
+ "errorMessage": "uncaught exception: false",
+ "errorMessageName": "JSMSG_UNCAUGHT_EXCEPTION",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn4.child2/source31",
+ "lineText": "",
+ "lineNumber": 1,
+ "columnNumber": 1,
+ "category": "content javascript",
+ "innerWindowID": 6442450946,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn4.child2/source31",
+ "lineNumber": 1,
+ "columnNumber": 1,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": false,
+ "isForwardedFromContentProcess": false,
+ "exception": false,
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`throw 0`, {
+ "pageError": {
+ "errorMessage": "uncaught exception: 0",
+ "errorMessageName": "JSMSG_UNCAUGHT_EXCEPTION",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn4.child2/source31",
+ "lineText": "",
+ "lineNumber": 1,
+ "columnNumber": 1,
+ "category": "content javascript",
+ "innerWindowID": 6442450946,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn4.child2/source31",
+ "lineNumber": 1,
+ "columnNumber": 1,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": false,
+ "isForwardedFromContentProcess": false,
+ "exception": 0,
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`throw null`, {
+ "pageError": {
+ "errorMessage": "uncaught exception: null",
+ "errorMessageName": "JSMSG_UNCAUGHT_EXCEPTION",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn4.child2/source31",
+ "lineText": "",
+ "lineNumber": 1,
+ "columnNumber": 1,
+ "category": "content javascript",
+ "innerWindowID": 6442450946,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn4.child2/source31",
+ "lineNumber": 1,
+ "columnNumber": 1,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": false,
+ "isForwardedFromContentProcess": false,
+ "exception": {
+ "type": "null"
+ },
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`throw undefined`, {
+ "pageError": {
+ "errorMessage": "uncaught exception: undefined",
+ "errorMessageName": "JSMSG_UNCAUGHT_EXCEPTION",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn4.child2/source31",
+ "lineText": "",
+ "lineNumber": 1,
+ "columnNumber": 1,
+ "category": "content javascript",
+ "innerWindowID": 6442450946,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn4.child2/source31",
+ "lineNumber": 1,
+ "columnNumber": 1,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": false,
+ "isForwardedFromContentProcess": false,
+ "exception": {
+ "type": "undefined"
+ },
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`throw Symbol`, {
+ "pageError": {
+ "errorMessage": "uncaught exception: Symbol(potato)",
+ "errorMessageName": "JSMSG_UNCAUGHT_EXCEPTION",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn4.child2/source27",
+ "lineText": "",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "category": "content javascript",
+ "innerWindowID": 6442450946,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn4.child2/source27",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": false,
+ "isForwardedFromContentProcess": false,
+ "exception": {
+ "type": "symbol",
+ "actor": "server0.conn0.windowGlobal15032385537/symbol34",
+ "name": "potato"
+ },
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`throw Object`, {
+ "pageError": {
+ "errorMessage": "uncaught exception: [object Object]",
+ "errorMessageName": "JSMSG_UNCAUGHT_EXCEPTION",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn4.child2/source31",
+ "lineText": "",
+ "lineNumber": 1,
+ "columnNumber": 1,
+ "category": "content javascript",
+ "innerWindowID": 6442450946,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn4.child2/source31",
+ "lineNumber": 1,
+ "columnNumber": 1,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": false,
+ "isForwardedFromContentProcess": false,
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj36",
+ "class": "Object",
+ "ownPropertyLength": 1,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "vegetable": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "cucumber"
+ }
+ },
+ "ownPropertiesLength": 1
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj36"
+ },
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`throw Error Object`, {
+ "pageError": {
+ "errorMessage": "Error: pumpkin",
+ "errorMessageName": "",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn4.child2/source41",
+ "lineText": "",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "category": "content javascript",
+ "innerWindowID": 6442450946,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn4.child2/source27",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": false,
+ "isForwardedFromContentProcess": false,
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj38",
+ "class": "Error",
+ "ownPropertyLength": 4,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "pumpkin",
+ "stack": "@https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html:1:7\n",
+ "fileName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "lineNumber": 1,
+ "columnNumber": 7
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj38"
+ },
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`throw Error Object with custom name`, {
+ "pageError": {
+ "errorMessage": "JuicyError: pineapple",
+ "errorMessageName": "",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn4.child2/source43",
+ "lineText": "",
+ "lineNumber": 2,
+ "columnNumber": 15,
+ "category": "content javascript",
+ "innerWindowID": 6442450946,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn4.child2/source43",
+ "lineNumber": 2,
+ "columnNumber": 15,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": false,
+ "isForwardedFromContentProcess": false,
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj40",
+ "class": "Error",
+ "ownPropertyLength": 6,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "JuicyError",
+ "message": "pineapple",
+ "stack": "@https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html:2:15\n",
+ "fileName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "lineNumber": 2,
+ "columnNumber": 15
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj40"
+ },
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`throw Error Object with error cause`, {
+ "pageError": {
+ "errorMessage": "Error: something went wrong",
+ "errorMessageName": "",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn0.child4/source45",
+ "lineText": "",
+ "lineNumber": 3,
+ "columnNumber": 15,
+ "category": "content javascript",
+ "innerWindowID": 2147483651,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn0.child4/source45",
+ "lineNumber": 3,
+ "columnNumber": 15,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": false,
+ "isForwardedFromContentProcess": false,
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj42",
+ "class": "Error",
+ "ownPropertyLength": 5,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "something went wrong",
+ "stack": "@https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html:3:15\n",
+ "fileName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "lineNumber": 3,
+ "columnNumber": 15,
+ "cause": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj43",
+ "class": "SyntaxError",
+ "ownPropertyLength": 4,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "SyntaxError",
+ "message": "original error",
+ "stack": "@https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html:2:25\n",
+ "fileName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "lineNumber": 2,
+ "columnNumber": 25
+ }
+ }
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj42"
+ },
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`throw Error Object with cause chain`, {
+ "pageError": {
+ "errorMessage": "Error: err-d",
+ "errorMessageName": "",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn0.child4/source48",
+ "lineText": "",
+ "lineNumber": 5,
+ "columnNumber": 13,
+ "category": "content javascript",
+ "innerWindowID": 2147483651,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn0.child4/source48",
+ "lineNumber": 5,
+ "columnNumber": 13,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": false,
+ "isForwardedFromContentProcess": false,
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj45",
+ "class": "Error",
+ "ownPropertyLength": 5,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "err-d",
+ "stack": "@https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html:5:13\n",
+ "fileName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "lineNumber": 5,
+ "columnNumber": 13,
+ "cause": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj46",
+ "class": "Error",
+ "ownPropertyLength": 5,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "err-c",
+ "stack": "@https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html:4:13\n",
+ "fileName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "lineNumber": 4,
+ "columnNumber": 13,
+ "cause": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj47",
+ "class": "Error",
+ "ownPropertyLength": 5,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "err-b",
+ "stack": "@https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html:3:13\n",
+ "fileName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "lineNumber": 3,
+ "columnNumber": 13,
+ "cause": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj48",
+ "class": "Error",
+ "ownPropertyLength": 4,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "err-a",
+ "stack": "@https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html:2:13\n",
+ "fileName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "lineNumber": 2,
+ "columnNumber": 13
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj45"
+ },
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`throw Error Object with cyclical cause chain`, {
+ "pageError": {
+ "errorMessage": "Error: err-b",
+ "errorMessageName": "",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn0.child4/source53",
+ "lineText": "",
+ "lineNumber": 3,
+ "columnNumber": 13,
+ "category": "content javascript",
+ "innerWindowID": 2147483651,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn0.child4/source48",
+ "lineNumber": 3,
+ "columnNumber": 13,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": false,
+ "isForwardedFromContentProcess": false,
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj50",
+ "class": "Error",
+ "ownPropertyLength": 5,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "err-b",
+ "stack": "@https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html:3:13\n",
+ "fileName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "lineNumber": 3,
+ "columnNumber": 13,
+ "cause": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj51",
+ "class": "Error",
+ "ownPropertyLength": 5,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "err-a",
+ "stack": "@https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html:2:13\n",
+ "fileName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "lineNumber": 2,
+ "columnNumber": 13,
+ "cause": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj52",
+ "class": "Error",
+ "ownPropertyLength": 5,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "err-b",
+ "stack": "@https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html:3:13\n",
+ "fileName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "lineNumber": 3,
+ "columnNumber": 13,
+ "cause": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj53",
+ "class": "Error",
+ "ownPropertyLength": 4,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "err-a",
+ "stack": "@https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html:2:13\n",
+ "fileName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "lineNumber": 2,
+ "columnNumber": 13
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj50"
+ },
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`throw Error Object with falsy cause`, {
+ "pageError": {
+ "errorMessage": "Error: null cause",
+ "errorMessageName": "",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn0.child4/source58",
+ "lineText": "",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "category": "content javascript",
+ "innerWindowID": 2147483651,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn0.child4/source26",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": false,
+ "isForwardedFromContentProcess": false,
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj55",
+ "class": "Error",
+ "ownPropertyLength": 5,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "null cause",
+ "stack": "@https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html:1:7\n",
+ "fileName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "cause": {
+ "type": "null"
+ }
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj55"
+ },
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`throw Error Object with number cause`, {
+ "pageError": {
+ "errorMessage": "Error: number cause",
+ "errorMessageName": "",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn0.child4/source60",
+ "lineText": "",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "category": "content javascript",
+ "innerWindowID": 2147483651,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn0.child4/source26",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": false,
+ "isForwardedFromContentProcess": false,
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj57",
+ "class": "Error",
+ "ownPropertyLength": 5,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "number cause",
+ "stack": "@https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html:1:7\n",
+ "fileName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "cause": 0
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj57"
+ },
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`throw Error Object with string cause`, {
+ "pageError": {
+ "errorMessage": "Error: string cause",
+ "errorMessageName": "",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn0.child4/source62",
+ "lineText": "",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "category": "content javascript",
+ "innerWindowID": 2147483651,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn0.child4/source26",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": false,
+ "isForwardedFromContentProcess": false,
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj59",
+ "class": "Error",
+ "ownPropertyLength": 5,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "string cause",
+ "stack": "@https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html:1:7\n",
+ "fileName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "cause": "cause message"
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj59"
+ },
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`throw Error Object with object cause`, {
+ "pageError": {
+ "errorMessage": "Error: object cause",
+ "errorMessageName": "",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn0.child4/source64",
+ "lineText": "",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "category": "content javascript",
+ "innerWindowID": 2147483651,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn0.child4/source26",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": false,
+ "isForwardedFromContentProcess": false,
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj61",
+ "class": "Error",
+ "ownPropertyLength": 5,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "object cause",
+ "stack": "@https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html:1:7\n",
+ "fileName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "lineNumber": 1,
+ "columnNumber": 7,
+ "cause": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj62",
+ "class": "Object",
+ "ownPropertyLength": 2,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false
+ }
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj61"
+ },
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`Promise reject ""`, {
+ "pageError": {
+ "errorMessage": "uncaught exception: ",
+ "errorMessageName": "JSMSG_UNCAUGHT_EXCEPTION",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": null,
+ "lineText": "",
+ "lineNumber": 1,
+ "columnNumber": 9,
+ "category": "content javascript",
+ "innerWindowID": 6442450946,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": null,
+ "lineNumber": 1,
+ "columnNumber": 9,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": true,
+ "isForwardedFromContentProcess": false,
+ "exception": "",
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`Promise reject "tomato"`, {
+ "pageError": {
+ "errorMessage": "uncaught exception: tomato",
+ "errorMessageName": "JSMSG_UNCAUGHT_EXCEPTION",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": null,
+ "lineText": "",
+ "lineNumber": 1,
+ "columnNumber": 9,
+ "category": "content javascript",
+ "innerWindowID": 6442450946,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": null,
+ "lineNumber": 1,
+ "columnNumber": 9,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": true,
+ "isForwardedFromContentProcess": false,
+ "exception": "tomato",
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`Promise reject false`, {
+ "pageError": {
+ "errorMessage": "uncaught exception: false",
+ "errorMessageName": "JSMSG_UNCAUGHT_EXCEPTION",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": null,
+ "lineText": "",
+ "lineNumber": 1,
+ "columnNumber": 9,
+ "category": "content javascript",
+ "innerWindowID": 6442450946,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": null,
+ "lineNumber": 1,
+ "columnNumber": 9,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": true,
+ "isForwardedFromContentProcess": false,
+ "exception": false,
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`Promise reject 0`, {
+ "pageError": {
+ "errorMessage": "uncaught exception: 0",
+ "errorMessageName": "JSMSG_UNCAUGHT_EXCEPTION",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": null,
+ "lineText": "",
+ "lineNumber": 1,
+ "columnNumber": 9,
+ "category": "content javascript",
+ "innerWindowID": 6442450946,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": null,
+ "lineNumber": 1,
+ "columnNumber": 9,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": true,
+ "isForwardedFromContentProcess": false,
+ "exception": 0,
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`Promise reject null`, {
+ "pageError": {
+ "errorMessage": "uncaught exception: null",
+ "errorMessageName": "JSMSG_UNCAUGHT_EXCEPTION",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": null,
+ "lineText": "",
+ "lineNumber": 1,
+ "columnNumber": 9,
+ "category": "content javascript",
+ "innerWindowID": 6442450946,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": null,
+ "lineNumber": 1,
+ "columnNumber": 9,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": true,
+ "isForwardedFromContentProcess": false,
+ "exception": {
+ "type": "null"
+ },
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`Promise reject undefined`, {
+ "pageError": {
+ "errorMessage": "uncaught exception: undefined",
+ "errorMessageName": "JSMSG_UNCAUGHT_EXCEPTION",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": null,
+ "lineText": "",
+ "lineNumber": 1,
+ "columnNumber": 9,
+ "category": "content javascript",
+ "innerWindowID": 6442450946,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": null,
+ "lineNumber": 1,
+ "columnNumber": 9,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": true,
+ "isForwardedFromContentProcess": false,
+ "exception": {
+ "type": "undefined"
+ },
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`Promise reject Symbol`, {
+ "pageError": {
+ "errorMessage": "uncaught exception: Symbol(potato)",
+ "errorMessageName": "JSMSG_UNCAUGHT_EXCEPTION",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": null,
+ "lineText": "",
+ "lineNumber": 1,
+ "columnNumber": 9,
+ "category": "content javascript",
+ "innerWindowID": 6442450946,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": null,
+ "lineNumber": 1,
+ "columnNumber": 9,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": true,
+ "isForwardedFromContentProcess": false,
+ "exception": {
+ "type": "symbol",
+ "actor": "server0.conn0.windowGlobal15032385537/symbol70",
+ "name": "potato"
+ },
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`Promise reject Object`, {
+ "pageError": {
+ "errorMessage": "uncaught exception: Object",
+ "errorMessageName": "JSMSG_UNCAUGHT_EXCEPTION",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": null,
+ "lineText": "",
+ "lineNumber": 1,
+ "columnNumber": 9,
+ "category": "content javascript",
+ "innerWindowID": 6442450946,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": null,
+ "lineNumber": 1,
+ "columnNumber": 9,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": true,
+ "isForwardedFromContentProcess": false,
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj72",
+ "class": "Object",
+ "ownPropertyLength": 1,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": false,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "vegetable": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "cucumber"
+ }
+ },
+ "ownPropertiesLength": 1
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj72"
+ },
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`Promise reject Error Object`, {
+ "pageError": {
+ "errorMessage": "Error: pumpkin",
+ "errorMessageName": "",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn4.child2/source55",
+ "lineText": "",
+ "lineNumber": 1,
+ "columnNumber": 16,
+ "category": "content javascript",
+ "innerWindowID": 6442450946,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": null,
+ "lineNumber": 1,
+ "columnNumber": 9,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": true,
+ "isForwardedFromContentProcess": false,
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj74",
+ "class": "Error",
+ "ownPropertyLength": 4,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "pumpkin",
+ "stack": "@https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html:1:16\n",
+ "fileName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "lineNumber": 1,
+ "columnNumber": 16
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj74"
+ },
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`Promise reject Error Object with custom name`, {
+ "pageError": {
+ "errorMessage": "JuicyError: pineapple",
+ "errorMessageName": "",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn4.child2/source57",
+ "lineText": "",
+ "lineNumber": 2,
+ "columnNumber": 15,
+ "category": "content javascript",
+ "innerWindowID": 6442450946,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": null,
+ "lineNumber": 5,
+ "columnNumber": 13,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": true,
+ "isForwardedFromContentProcess": false,
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj76",
+ "class": "Error",
+ "ownPropertyLength": 6,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "JuicyError",
+ "message": "pineapple",
+ "stack": "@https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html:2:15\n",
+ "fileName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "lineNumber": 2,
+ "columnNumber": 15
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj76"
+ },
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`Promise reject Error Object with error cause`, {
+ "pageError": {
+ "errorMessage": "Error: something went wrong",
+ "errorMessageName": "",
+ "sourceName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": "server0.conn0.child4/source81",
+ "lineText": "",
+ "lineNumber": 5,
+ "columnNumber": 15,
+ "category": "content javascript",
+ "innerWindowID": 2147483651,
+ "timeStamp": 1572867483805,
+ "warning": false,
+ "error": true,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": null,
+ "lineNumber": 5,
+ "columnNumber": 15,
+ "functionName": null
+ },
+ {
+ "filename": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "sourceId": null,
+ "lineNumber": 1,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "promise callback"
+ },
+ {
+ "filename": "resource://testing-common/content-task.js line 112 > eval",
+ "sourceId": null,
+ "lineNumber": 6,
+ "columnNumber": 29,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 113,
+ "columnNumber": 33,
+ "functionName": null
+ },
+ {
+ "filename": "resource://testing-common/content-task.js",
+ "sourceId": null,
+ "lineNumber": 66,
+ "columnNumber": 19,
+ "functionName": null,
+ "asyncCause": "MessageListener.receiveMessage"
+ }
+ ],
+ "notes": null,
+ "chromeContext": false,
+ "isPromiseRejection": true,
+ "isForwardedFromContentProcess": false,
+ "exception": {
+ "_grip": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj78",
+ "class": "Error",
+ "ownPropertyLength": 5,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "Error",
+ "message": "something went wrong",
+ "stack": "@https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html:5:15\npromise callback*@https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html:1:19\n",
+ "fileName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "lineNumber": 5,
+ "columnNumber": 15,
+ "cause": {
+ "type": "object",
+ "actor": "server0.conn0.windowGlobal15032385537/obj79",
+ "class": "ReferenceError",
+ "ownPropertyLength": 4,
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "isError": true,
+ "preview": {
+ "kind": "Error",
+ "name": "ReferenceError",
+ "message": "unknownFunc is not defined",
+ "stack": "@https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html:3:9\npromise callback*@https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html:1:19\n",
+ "fileName": "https://example.com/browser/devtools/client/webconsole/test/browser/test-console-api.html",
+ "lineNumber": 3,
+ "columnNumber": 9
+ }
+ }
+ }
+ },
+ "actorID": "server0.conn0.windowGlobal15032385537/obj78"
+ },
+ "hasException": true
+ },
+ "resourceType": "error-message",
+ "isAlreadyExistingResource": false
+});
+
+
+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 = ConsoleMessage(transformedPacket);
+ stubPreparedMessages.set(key, message);
+}
+
+module.exports = {
+ rawPackets,
+ stubPreparedMessages,
+ stubPackets,
+};
diff --git a/devtools/client/webconsole/test/node/fixtures/stubs/platformMessage.js b/devtools/client/webconsole/test/node/fixtures/stubs/platformMessage.js
new file mode 100644
index 0000000000..db2114d835
--- /dev/null
+++ b/devtools/client/webconsole/test/node/fixtures/stubs/platformMessage.js
@@ -0,0 +1,56 @@
+/* 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();
+rawPackets.set(`platform-simple-message`, {
+ "message": "foobar test",
+ "timeStamp": 1572867483805,
+ "resourceType": "platform-message",
+ "isAlreadyExistingResource": false
+});
+
+rawPackets.set(`platform-longString-message`, {
+ "message": {
+ "type": "longString",
+ "actor": "server0.conn0.longstractor28",
+ "length": 20002,
+ "initial": "a\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ },
+ "timeStamp": 1572867483805,
+ "resourceType": "platform-message",
+ "isAlreadyExistingResource": false
+});
+
+
+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 = ConsoleMessage(transformedPacket);
+ stubPreparedMessages.set(key, message);
+}
+
+module.exports = {
+ rawPackets,
+ stubPreparedMessages,
+ stubPackets,
+};
diff --git a/devtools/client/webconsole/test/node/fixtures/stubs/stubs.toml b/devtools/client/webconsole/test/node/fixtures/stubs/stubs.toml
new file mode 100644
index 0000000000..c40990700c
--- /dev/null
+++ b/devtools/client/webconsole/test/node/fixtures/stubs/stubs.toml
@@ -0,0 +1,15 @@
+[DEFAULT]
+tags = "devtools"
+subsuite = "devtools"
+support-files = [
+ "consoleApi.js",
+ "cssMessage.js",
+ "evaluationResult.js",
+ "index.js",
+ "networkEvent.js",
+ "pageError.js",
+ "platformMessage.js",
+]
+
+["browser_dummy.js"]
+skip-if = ["true"] #This is only here so we can expose the support files in other toml files.
diff --git a/devtools/client/webconsole/test/node/helpers.js b/devtools/client/webconsole/test/node/helpers.js
new file mode 100644
index 0000000000..d50a0a64ed
--- /dev/null
+++ b/devtools/client/webconsole/test/node/helpers.js
@@ -0,0 +1,188 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const reduxActions = require("resource://devtools/client/webconsole/actions/index.js");
+const {
+ configureStore,
+} = require("resource://devtools/client/webconsole/store.js");
+const {
+ IdGenerator,
+} = require("resource://devtools/client/webconsole/utils/id-generator.js");
+const {
+ stubPackets,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+const {
+ getMutableMessagesById,
+} = require("resource://devtools/client/webconsole/selectors/messages.js");
+const {
+ getPrefsService,
+} = require("resource://devtools/client/webconsole/utils/prefs.js");
+const prefsService = getPrefsService({});
+const { PREFS } = require("resource://devtools/client/webconsole/constants.js");
+const Telemetry = require("resource://devtools/client/shared/test-helpers/jest-fixtures/telemetry.js");
+const {
+ getSerializedPacket,
+ parsePacketAndCreateFronts,
+} = require("resource://devtools/client/webconsole/test/browser/stub-generator-helpers.js");
+
+/**
+ * Prepare actions for use in testing.
+ */
+function setupActions() {
+ // Some actions use dependency injection. This helps them avoid using state in
+ // a hard-to-test way. We need to inject stubbed versions of these dependencies.
+ const wrappedActions = Object.assign({}, reduxActions);
+
+ const idGenerator = new IdGenerator();
+ wrappedActions.messagesAdd = packets => {
+ return reduxActions.messagesAdd(packets, idGenerator);
+ };
+
+ return {
+ ...reduxActions,
+ messagesAdd: packets => reduxActions.messagesAdd(packets, idGenerator),
+ };
+}
+
+/**
+ * Prepare the store for use in testing.
+ */
+function setupStore(
+ input = [],
+ { storeOptions = {}, actions, webConsoleUI } = {}
+) {
+ if (!webConsoleUI) {
+ webConsoleUI = getWebConsoleUiMock();
+ }
+ const store = configureStore(webConsoleUI, {
+ ...storeOptions,
+ thunkArgs: { toolbox: {} },
+ telemetry: new Telemetry(),
+ });
+
+ // Add the messages from the input commands to the store.
+ const messagesAdd = actions ? actions.messagesAdd : reduxActions.messagesAdd;
+ store.dispatch(messagesAdd(input.map(cmd => stubPackets.get(cmd))));
+
+ return store;
+}
+
+/**
+ * Create deep copy of given packet object.
+ */
+function clonePacket(packet) {
+ const strPacket = getSerializedPacket(packet);
+ return parsePacketAndCreateFronts(JSON.parse(strPacket));
+}
+
+/**
+ * Return the message in the store at the given index.
+ *
+ * @param {object} state - The redux state of the console.
+ * @param {int} index - The index of the message in the map.
+ * @return {Message} - The message, or undefined if the index does not exists in the map.
+ */
+function getMessageAt(state, index) {
+ const messageMap = getMutableMessagesById(state);
+ return messageMap.get(state.messages.mutableMessagesOrder[index]);
+}
+
+/**
+ * Return the first message in the store.
+ *
+ * @param {object} state - The redux state of the console.
+ * @return {Message} - The last message, or undefined if there are no message in store.
+ */
+function getFirstMessage(state) {
+ return getMessageAt(state, 0);
+}
+
+/**
+ * Return the last message in the store.
+ *
+ * @param {object} state - The redux state of the console.
+ * @return {Message} - The last message, or undefined if there are no message in store.
+ */
+function getLastMessage(state) {
+ const lastIndex = state.messages.mutableMessagesOrder.length - 1;
+ return getMessageAt(state, lastIndex);
+}
+
+function getFiltersPrefs() {
+ return Object.values(PREFS.FILTER).reduce((res, pref) => {
+ res[pref] = prefsService.getBoolPref(pref);
+ return res;
+ }, {});
+}
+
+function clearPrefs() {
+ [
+ "devtools.hud.loglimit",
+ ...Object.values(PREFS.FILTER),
+ ...Object.values(PREFS.UI),
+ ].forEach(prefsService.clearUserPref);
+}
+
+function getPrivatePacket(key) {
+ const packet = clonePacket(stubPackets.get(key));
+ if (packet.message) {
+ packet.message.private = true;
+ } else if (packet.pageError) {
+ packet.pageError.private = true;
+ }
+ if (Object.getOwnPropertyNames(packet).includes("private")) {
+ packet.private = true;
+ }
+ return packet;
+}
+
+function getWebConsoleUiMock(hud) {
+ return {
+ emit: () => {},
+ emitForTests: () => {},
+ hud: {
+ // @backward-compat { version 123 } A new Objects Manager front has a new "releaseActors" method.
+ // Once 123 is release, supportsReleaseActors could be removed.
+ commands: {
+ client: {
+ mainRoot: {
+ supportsReleaseActors: true,
+ },
+ },
+ objectCommand: {
+ releaseObjects: async frontsToRelease => {},
+ },
+ },
+ ...hud,
+ },
+ clearNetworkRequests: () => {},
+ clearMessagesCache: () => {},
+ inspectObjectActor: () => {},
+ toolbox: {},
+ watchCssMessages: () => {},
+ };
+}
+
+function formatErrorTextWithCausedBy(text) {
+ // The component text does not append new line character before
+ // the "Caused by" label, so add it here to make the assertions
+ // look more legible
+ return text.replace(/Caused by/g, "\nCaused by");
+}
+
+module.exports = {
+ clearPrefs,
+ clonePacket,
+ formatErrorTextWithCausedBy,
+ getFiltersPrefs,
+ getFirstMessage,
+ getLastMessage,
+ getMessageAt,
+ getPrivatePacket,
+ getWebConsoleUiMock,
+ prefsService,
+ setupActions,
+ setupStore,
+};
diff --git a/devtools/client/webconsole/test/node/jest-setup.js b/devtools/client/webconsole/test/node/jest-setup.js
new file mode 100644
index 0000000000..a7f26960c1
--- /dev/null
+++ b/devtools/client/webconsole/test/node/jest-setup.js
@@ -0,0 +1,54 @@
+/* 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";
+
+/* global global, __dirname */
+
+const {
+ setMocksInGlobal,
+} = require("resource://devtools/client/shared/test-helpers/shared-node-helpers.js");
+setMocksInGlobal();
+
+// Configure enzyme with React 16 adapter.
+const Enzyme = require("enzyme");
+const Adapter = require("enzyme-adapter-react-16");
+Enzyme.configure({ adapter: new Adapter() });
+
+global.Components = { stack: { caller: "" }, Constructor: () => {} };
+
+if (!global.ResizeObserver) {
+ global.ResizeObserver = class ResizeObserver {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+ };
+}
+
+const mcRoot = `${__dirname}/../../../../../`;
+const { pref } = require(mcRoot +
+ "devtools/client/shared/test-helpers/jest-fixtures/Services");
+pref("devtools.debugger.remote-timeout", 10000);
+pref("devtools.hud.loglimit", 10000);
+pref("devtools.webconsole.filter.error", true);
+pref("devtools.webconsole.filter.warn", true);
+pref("devtools.webconsole.filter.info", true);
+pref("devtools.webconsole.filter.log", true);
+pref("devtools.webconsole.filter.debug", true);
+pref("devtools.webconsole.filter.css", false);
+pref("devtools.webconsole.filter.net", false);
+pref("devtools.webconsole.filter.netxhr", false);
+pref("devtools.webconsole.inputHistoryCount", 300);
+pref("devtools.webconsole.persistlog", false);
+pref("devtools.webconsole.timestampMessages", false);
+pref("devtools.webconsole.sidebarToggle", true);
+pref("devtools.webconsole.groupWarningMessages", false);
+pref("devtools.webconsole.input.editor", false);
+pref("devtools.webconsole.input.autocomplete", true);
+pref("devtools.webconsole.input.eagerEvaluation", true);
+pref("devtools.browserconsole.enableNetworkMonitoring", false);
+pref("devtools.webconsole.input.editorWidth", 800);
+pref("devtools.webconsole.input.editorOnboarding", true);
+pref("devtools.webconsole.input.context", false);
+pref("devtools.discovery.log", false);
diff --git a/devtools/client/webconsole/test/node/jest.config.js b/devtools/client/webconsole/test/node/jest.config.js
new file mode 100644
index 0000000000..40e277474d
--- /dev/null
+++ b/devtools/client/webconsole/test/node/jest.config.js
@@ -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/. */
+
+"use strict";
+
+/* global __dirname */
+const testHelpersDir = `${__dirname}/../../../shared/test-helpers`;
+const sharedJestConfig = require(`${testHelpersDir}/shared-jest.config`);
+
+module.exports = {
+ ...sharedJestConfig,
+ moduleNameMapper: {
+ ...sharedJestConfig.moduleNameMapper,
+ "^chrome://mochitests/content/browser/devtools/client/webconsole/test/browser/stub-generator-helpers.js": `${__dirname}/../../test/browser/stub-generator-helpers.js`,
+ "\\.css$": `${testHelpersDir}/jest-fixtures/empty-module`,
+ "\\.svg$": `${testHelpersDir}/jest-fixtures/svgMock.js`,
+ },
+ setupFiles: ["<rootDir>jest-setup.js"],
+ transform: {
+ "\\.[jt]sx?$": "babel-jest",
+ },
+};
diff --git a/devtools/client/webconsole/test/node/middleware/debounce.test.js b/devtools/client/webconsole/test/node/middleware/debounce.test.js
new file mode 100644
index 0000000000..56246286ce
--- /dev/null
+++ b/devtools/client/webconsole/test/node/middleware/debounce.test.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const expect = require("expect");
+
+const {
+ debounceActions,
+} = require("resource://devtools/client/shared/redux/middleware/debounce.js");
+
+describe("Debounce Middleware", () => {
+ let nextArgs = [];
+ const fakeStore = {};
+ const fakeNext = (...args) => {
+ nextArgs.push(args);
+ };
+
+ beforeEach(() => {
+ nextArgs = [];
+ });
+
+ it("should pass the intercepted action to next", () => {
+ const fakeAction = {
+ type: "FAKE_ACTION",
+ };
+
+ debounceActions()(fakeStore)(fakeNext)(fakeAction);
+
+ expect(nextArgs.length).toEqual(1);
+ expect(nextArgs[0]).toEqual([fakeAction]);
+ });
+
+ it("should debounce if specified", () => {
+ const fakeAction = {
+ type: "FAKE_ACTION",
+ meta: {
+ debounce: true,
+ },
+ };
+
+ const executed = debounceActions(1, 1)(fakeStore)(fakeNext)(fakeAction);
+ expect(nextArgs.length).toEqual(0);
+
+ return executed.then(() => {
+ expect(nextArgs.length).toEqual(1);
+ });
+ });
+
+ it("should have no effect if no timeout", () => {
+ const fakeAction = {
+ type: "FAKE_ACTION",
+ meta: {
+ debounce: true,
+ },
+ };
+
+ debounceActions()(fakeStore)(fakeNext)(fakeAction);
+ expect(nextArgs.length).toEqual(1);
+ expect(nextArgs[0]).toEqual([fakeAction]);
+ });
+});
diff --git a/devtools/client/webconsole/test/node/package.json b/devtools/client/webconsole/test/node/package.json
new file mode 100644
index 0000000000..d394afa26e
--- /dev/null
+++ b/devtools/client/webconsole/test/node/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "webconsole-tests",
+ "version": "0.0.1",
+ "scripts": {
+ "test": "TZ=Europe/Prague jest",
+ "test-ci": "TZ=Europe/Prague jest --json"
+ },
+ "dependencies": {
+ "@babel/core": "^7.20.12",
+ "@babel/plugin-proposal-class-properties": "7.18.6",
+ "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
+ "@babel/plugin-proposal-optional-chaining": "^7.20.7",
+ "@babel/plugin-proposal-private-methods": "^7.18.6",
+ "@babel/preset-env": "^7.15.6",
+ "@babel/preset-react": "7.14.5",
+ "@babel/register": "^7.18.9",
+ "babel-jest": "^23.0.0",
+ "babel-plugin-module-resolver": "^4.1.0",
+ "babel-plugin-transform-amd-to-commonjs": "1.4.0",
+ "enzyme": "^3.11.0",
+ "enzyme-adapter-react-16": "^1.15.7",
+ "expect": "^1.16.0",
+ "jest": "^24.6.0",
+ "react": "16.4.1",
+ "react-dom": "16.4.1",
+ "react-test-renderer": "16.4.1",
+ "sinon": "^1.17.5"
+ }
+}
diff --git a/devtools/client/webconsole/test/node/store/filters.test.js b/devtools/client/webconsole/test/node/store/filters.test.js
new file mode 100644
index 0000000000..eddf59e537
--- /dev/null
+++ b/devtools/client/webconsole/test/node/store/filters.test.js
@@ -0,0 +1,345 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const expect = require("expect");
+
+const actions = require("resource://devtools/client/webconsole/actions/index.js");
+const {
+ messagesAdd,
+} = require("resource://devtools/client/webconsole/actions/index.js");
+const {
+ ConsoleCommand,
+} = require("resource://devtools/client/webconsole/types.js");
+const {
+ getVisibleMessages,
+} = require("resource://devtools/client/webconsole/selectors/messages.js");
+const {
+ getAllFilters,
+} = require("resource://devtools/client/webconsole/selectors/filters.js");
+const {
+ setupStore,
+ getFiltersPrefs,
+} = require("resource://devtools/client/webconsole/test/node/helpers.js");
+const {
+ FILTERS,
+ PREFS,
+} = require("resource://devtools/client/webconsole/constants.js");
+const {
+ stubPackets,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+const {
+ stubPreparedMessages,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+
+describe("Filtering", () => {
+ let store;
+ let numMessages;
+ // Number of messages in prepareBaseStore which are not filtered out, i.e. Evaluation
+ // Results and console commands.
+ const numUnfilterableMessages = 2;
+
+ beforeEach(() => {
+ store = prepareBaseStore();
+ store.dispatch(actions.filtersClear());
+ numMessages = getVisibleMessages(store.getState()).length;
+ });
+
+ /**
+ * Tests for filter buttons in Console toolbar. The test switches off
+ * all filters and consequently tests one by one on the list of messages
+ * created in `prepareBaseStore` method.
+ */
+ describe("Level filter", () => {
+ beforeEach(() => {
+ // Switch off all filters (include those which are on by default).
+ store.dispatch(actions.filtersClear());
+ store.dispatch(actions.filterToggle(FILTERS.DEBUG));
+ store.dispatch(actions.filterToggle(FILTERS.ERROR));
+ store.dispatch(actions.filterToggle(FILTERS.INFO));
+ store.dispatch(actions.filterToggle(FILTERS.LOG));
+ store.dispatch(actions.filterToggle(FILTERS.WARN));
+
+ const messages = getVisibleMessages(store.getState());
+ expect(messages.length).toEqual(numUnfilterableMessages);
+ });
+
+ it("filters log messages", () => {
+ store.dispatch(actions.filterToggle(FILTERS.LOG));
+
+ const messages = getVisibleMessages(store.getState());
+ expect(messages.length).toEqual(numUnfilterableMessages + 6);
+ });
+
+ it("filters debug messages", () => {
+ store.dispatch(actions.filterToggle(FILTERS.DEBUG));
+
+ const messages = getVisibleMessages(store.getState());
+ expect(messages.length).toEqual(numUnfilterableMessages + 1);
+ });
+
+ it("filters info messages", () => {
+ store.dispatch(actions.filterToggle(FILTERS.INFO));
+
+ const messages = getVisibleMessages(store.getState());
+ expect(messages.length).toEqual(numUnfilterableMessages + 1);
+ });
+
+ it("filters warning messages", () => {
+ store.dispatch(actions.filterToggle(FILTERS.WARN));
+
+ const messages = getVisibleMessages(store.getState());
+ expect(messages.length).toEqual(numUnfilterableMessages + 1);
+ });
+
+ it("filters error messages", () => {
+ store.dispatch(actions.filterToggle(FILTERS.ERROR));
+
+ const messages = getVisibleMessages(store.getState());
+ expect(messages.length).toEqual(numUnfilterableMessages + 5);
+ });
+
+ it("filters css messages", () => {
+ const message = stubPreparedMessages.get(
+ "Unknown property ‘such-unknown-property’. Declaration dropped."
+ );
+ store.dispatch(messagesAdd([message]));
+
+ let messages = getVisibleMessages(store.getState());
+ expect(messages.length).toEqual(numUnfilterableMessages);
+
+ store.dispatch(actions.filterToggle("css"));
+ messages = getVisibleMessages(store.getState());
+ expect(messages.length).toEqual(numUnfilterableMessages + 1);
+ });
+
+ it("filters xhr messages", () => {
+ const message = stubPreparedMessages.get("XHR GET request");
+ store.dispatch(messagesAdd([message]));
+
+ let messages = getVisibleMessages(store.getState());
+ expect(messages.length).toEqual(numUnfilterableMessages);
+
+ store.dispatch(actions.filterToggle("netxhr"));
+ messages = getVisibleMessages(store.getState());
+ expect(messages.length).toEqual(numUnfilterableMessages + 1);
+ });
+
+ it("filters network messages", () => {
+ const message = stubPreparedMessages.get("GET request update");
+ store.dispatch(messagesAdd([message]));
+
+ let messages = getVisibleMessages(store.getState());
+ expect(messages.length).toEqual(numUnfilterableMessages);
+
+ store.dispatch(actions.filterToggle("net"));
+ messages = getVisibleMessages(store.getState());
+ expect(messages.length).toEqual(numUnfilterableMessages + 1);
+ });
+ });
+
+ describe("Text filter", () => {
+ it("set the expected property on the store", () => {
+ store.dispatch(actions.filterTextSet("danger"));
+ expect(getAllFilters(store.getState()).text).toEqual("danger");
+ });
+
+ it("matches on value grips", () => {
+ store.dispatch(actions.filterTextSet("danger"));
+ let messages = getVisibleMessages(store.getState());
+ expect(messages.length - numUnfilterableMessages).toEqual(1);
+
+ // Checks that trimming works.
+ store.dispatch(actions.filterTextSet(" danger "));
+ messages = getVisibleMessages(store.getState());
+ expect(messages.length - numUnfilterableMessages).toEqual(1);
+ });
+
+ it("matches unicode values", () => {
+ store.dispatch(actions.filterTextSet("鼬"));
+
+ const messages = getVisibleMessages(store.getState());
+ expect(messages.length - numUnfilterableMessages).toEqual(1);
+ });
+
+ it("matches locations", () => {
+ // Add a message with a different filename.
+ const locationMsg = Object.assign(
+ {},
+ stubPackets.get("console.log('foobar', 'test')")
+ );
+ locationMsg.message = Object.assign({}, locationMsg.message, {
+ filename: "search-location-test.js",
+ });
+ store.dispatch(messagesAdd([locationMsg]));
+
+ store.dispatch(actions.filterTextSet("search-location-test.js"));
+
+ const messages = getVisibleMessages(store.getState());
+ expect(messages.length - numUnfilterableMessages).toEqual(1);
+ });
+
+ it("matches stacktrace functionName", () => {
+ const traceMessage = stubPackets.get("console.trace()");
+ store.dispatch(messagesAdd([traceMessage]));
+
+ store.dispatch(actions.filterTextSet("testStacktraceFiltering"));
+
+ const messages = getVisibleMessages(store.getState());
+ expect(messages.length - numUnfilterableMessages).toEqual(1);
+ });
+
+ it("matches stacktrace location", () => {
+ const traceMessage = stubPackets.get("console.trace()");
+ traceMessage.message = Object.assign({}, traceMessage.message, {
+ filename: "search-location-test.js",
+ lineNumber: 85,
+ columnNumber: 13,
+ });
+
+ store.dispatch(messagesAdd([traceMessage]));
+
+ store.dispatch(actions.filterTextSet("search-location-test.js:85:13"));
+
+ const messages = getVisibleMessages(store.getState());
+ expect(messages.length - numUnfilterableMessages).toEqual(1);
+ });
+
+ it("matches prefixed log message", () => {
+ const stub = {
+ level: "debug",
+ filename: "resource:///modules/CustomizableUI.sys.mjs",
+ lineNumber: 181,
+ functionName: "initialize",
+ timeStamp: 1519311532912,
+ arguments: ["Initializing"],
+ prefix: "MyNicePrefix",
+ workerType: "none",
+ styles: [],
+ category: "webdev",
+ _type: "ConsoleAPI",
+ };
+ store.dispatch(messagesAdd([stub]));
+
+ store.dispatch(actions.filterTextSet("MyNice"));
+ let messages = getVisibleMessages(store.getState());
+ expect(messages.length - numUnfilterableMessages).toEqual(1);
+
+ store.dispatch(actions.filterTextSet("MyNicePrefix"));
+ messages = getVisibleMessages(store.getState());
+ expect(messages.length - numUnfilterableMessages).toEqual(1);
+
+ store.dispatch(actions.filterTextSet("MyNicePrefix:"));
+ messages = getVisibleMessages(store.getState());
+ expect(messages.length - numUnfilterableMessages).toEqual(1);
+ });
+
+ it("restores all messages once text is cleared", () => {
+ store.dispatch(actions.filterTextSet("danger"));
+ store.dispatch(actions.filterTextSet(""));
+
+ const messages = getVisibleMessages(store.getState());
+ expect(messages.length).toEqual(numMessages);
+ });
+ });
+
+ describe("Combined filters", () => {
+ // @TODO add test
+ it.todo("filters");
+ });
+});
+
+describe("Clear filters", () => {
+ it("clears all filters", () => {
+ const store = setupStore();
+
+ // Setup test case
+ store.dispatch(actions.filterToggle(FILTERS.ERROR));
+ store.dispatch(actions.filterToggle(FILTERS.CSS));
+ store.dispatch(actions.filterToggle(FILTERS.NET));
+ store.dispatch(actions.filterToggle(FILTERS.NETXHR));
+ store.dispatch(actions.filterTextSet("foobar"));
+
+ expect(getAllFilters(store.getState())).toEqual({
+ // default
+ [FILTERS.WARN]: true,
+ [FILTERS.LOG]: true,
+ [FILTERS.INFO]: true,
+ [FILTERS.DEBUG]: true,
+ // changed
+ [FILTERS.ERROR]: false,
+ [FILTERS.CSS]: true,
+ [FILTERS.NET]: true,
+ [FILTERS.NETXHR]: true,
+ [FILTERS.TEXT]: "foobar",
+ });
+ expect(getFiltersPrefs()).toEqual({
+ [PREFS.FILTER.WARN]: true,
+ [PREFS.FILTER.LOG]: true,
+ [PREFS.FILTER.INFO]: true,
+ [PREFS.FILTER.DEBUG]: true,
+ [PREFS.FILTER.ERROR]: false,
+ [PREFS.FILTER.CSS]: true,
+ [PREFS.FILTER.NET]: true,
+ [PREFS.FILTER.NETXHR]: true,
+ });
+
+ store.dispatch(actions.filtersClear());
+
+ expect(getAllFilters(store.getState())).toEqual({
+ [FILTERS.CSS]: false,
+ [FILTERS.DEBUG]: true,
+ [FILTERS.ERROR]: true,
+ [FILTERS.INFO]: true,
+ [FILTERS.LOG]: true,
+ [FILTERS.NET]: false,
+ [FILTERS.NETXHR]: false,
+ [FILTERS.WARN]: true,
+ [FILTERS.TEXT]: "",
+ });
+
+ expect(getFiltersPrefs()).toEqual({
+ [PREFS.FILTER.CSS]: false,
+ [PREFS.FILTER.DEBUG]: true,
+ [PREFS.FILTER.ERROR]: true,
+ [PREFS.FILTER.INFO]: true,
+ [PREFS.FILTER.LOG]: true,
+ [PREFS.FILTER.NET]: false,
+ [PREFS.FILTER.NETXHR]: false,
+ [PREFS.FILTER.WARN]: true,
+ });
+ });
+});
+
+function prepareBaseStore() {
+ const store = setupStore([
+ // Console API
+ "console.log('foobar', 'test')",
+ "console.warn('danger, will robinson!')",
+ "console.log(undefined)",
+ "console.count('bar')",
+ "console.log('鼬')",
+ // Evaluation Result - never filtered
+ "new Date(0)",
+ // PageError
+ "ReferenceError: asdf is not defined",
+ "TypeError longString message",
+ "console.debug('debug message');",
+ "console.info('info message');",
+ "console.error('error message');",
+ "console.table(['red', 'green', 'blue']);",
+ "console.assert(false, {message: 'foobar'})",
+ // This is a 404 request, it's displayed as an error
+ "GET request update",
+ "console.group('bar')",
+ "console.groupEnd()",
+ ]);
+
+ // Console Command - never filtered
+ store.dispatch(
+ messagesAdd([new ConsoleCommand({ messageText: `console.warn("x")` })])
+ );
+
+ return store;
+}
diff --git a/devtools/client/webconsole/test/node/store/hidden-messages.test.js b/devtools/client/webconsole/test/node/store/hidden-messages.test.js
new file mode 100644
index 0000000000..c135c9b304
--- /dev/null
+++ b/devtools/client/webconsole/test/node/store/hidden-messages.test.js
@@ -0,0 +1,199 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const expect = require("expect");
+
+const actions = require("resource://devtools/client/webconsole/actions/index.js");
+const {
+ getFilteredMessagesCount,
+} = require("resource://devtools/client/webconsole/selectors/messages.js");
+const {
+ setupStore,
+} = require("resource://devtools/client/webconsole/test/node/helpers.js");
+const {
+ FILTERS,
+} = require("resource://devtools/client/webconsole/constants.js");
+const {
+ stubPackets,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+
+describe("Filtering - Hidden messages", () => {
+ let store;
+
+ beforeEach(() => {
+ store = prepareBaseStore();
+ // Switch off all filters (include those which are on by default).
+ store.dispatch(actions.filtersClear());
+ store.dispatch(actions.filterToggle(FILTERS.DEBUG));
+ store.dispatch(actions.filterToggle(FILTERS.ERROR));
+ store.dispatch(actions.filterToggle(FILTERS.INFO));
+ store.dispatch(actions.filterToggle(FILTERS.LOG));
+ store.dispatch(actions.filterToggle(FILTERS.WARN));
+ });
+
+ it("has the expected numbers", () => {
+ const counter = getFilteredMessagesCount(store.getState());
+ expect(counter).toEqual(BASIC_TEST_CASE_FILTERED_MESSAGE_COUNT);
+ });
+
+ it("has the expected numbers when there is a text search", () => {
+ // "info" is disabled and the filter input only matches a warning message.
+ store.dispatch(actions.filtersClear());
+ store.dispatch(actions.filterToggle(FILTERS.INFO));
+ store.dispatch(actions.filterTextSet("danger, will robinson!"));
+
+ let counter = getFilteredMessagesCount(store.getState());
+ expect(counter).toEqual({
+ [FILTERS.ERROR]: 0,
+ [FILTERS.WARN]: 0,
+ [FILTERS.LOG]: 0,
+ [FILTERS.INFO]: 1,
+ [FILTERS.DEBUG]: 0,
+ [FILTERS.TEXT]: 9,
+ global: 10,
+ });
+
+ // Numbers update if the text search is cleared.
+ store.dispatch(actions.filterTextSet(""));
+ counter = getFilteredMessagesCount(store.getState());
+ expect(counter).toEqual({
+ [FILTERS.ERROR]: 0,
+ [FILTERS.WARN]: 0,
+ [FILTERS.LOG]: 0,
+ [FILTERS.INFO]: 1,
+ [FILTERS.DEBUG]: 0,
+ [FILTERS.TEXT]: 0,
+ global: 1,
+ });
+ });
+
+ it("has the expected numbers when there's a text search on disabled categories", () => {
+ store.dispatch(actions.filterTextSet("danger, will robinson!"));
+ let counter = getFilteredMessagesCount(store.getState());
+ expect(counter).toEqual({
+ [FILTERS.ERROR]: 3,
+ [FILTERS.WARN]: 1,
+ [FILTERS.LOG]: 5,
+ [FILTERS.INFO]: 1,
+ [FILTERS.DEBUG]: 1,
+ [FILTERS.TEXT]: 0,
+ global: 11,
+ });
+
+ // Numbers update if the text search is cleared.
+ store.dispatch(actions.filterTextSet(""));
+ counter = getFilteredMessagesCount(store.getState());
+ expect(counter).toEqual(BASIC_TEST_CASE_FILTERED_MESSAGE_COUNT);
+ });
+
+ it("updates when messages are added", () => {
+ const packets = MESSAGES.map(key => stubPackets.get(key));
+ store.dispatch(actions.messagesAdd(packets));
+
+ const counter = getFilteredMessagesCount(store.getState());
+ expect(counter).toEqual({
+ [FILTERS.ERROR]: 6,
+ [FILTERS.WARN]: 2,
+ [FILTERS.LOG]: 10,
+ [FILTERS.INFO]: 2,
+ [FILTERS.DEBUG]: 2,
+ [FILTERS.TEXT]: 0,
+ global: 22,
+ });
+ });
+
+ it("updates when filters are toggled", () => {
+ store.dispatch(actions.filterToggle(FILTERS.LOG));
+
+ let counter = getFilteredMessagesCount(store.getState());
+ expect(counter).toEqual(
+ Object.assign({}, BASIC_TEST_CASE_FILTERED_MESSAGE_COUNT, {
+ [FILTERS.LOG]: 0,
+ global: 6,
+ })
+ );
+
+ store.dispatch(actions.filterToggle(FILTERS.ERROR));
+
+ counter = getFilteredMessagesCount(store.getState());
+ expect(counter).toEqual(
+ Object.assign({}, BASIC_TEST_CASE_FILTERED_MESSAGE_COUNT, {
+ [FILTERS.ERROR]: 0,
+ [FILTERS.LOG]: 0,
+ global: 3,
+ })
+ );
+
+ store.dispatch(actions.filterToggle(FILTERS.LOG));
+ store.dispatch(actions.filterToggle(FILTERS.ERROR));
+ counter = getFilteredMessagesCount(store.getState());
+ expect(counter).toEqual(BASIC_TEST_CASE_FILTERED_MESSAGE_COUNT);
+ });
+
+ it("has the expected numbers after message clear", () => {
+ // Add a text search to make sure it is handled as well.
+ store.dispatch(actions.filterTextSet("danger, will robinson!"));
+ store.dispatch(actions.messagesClear());
+ const counter = getFilteredMessagesCount(store.getState());
+ expect(counter).toEqual({
+ [FILTERS.ERROR]: 0,
+ [FILTERS.WARN]: 0,
+ [FILTERS.LOG]: 0,
+ [FILTERS.INFO]: 0,
+ [FILTERS.DEBUG]: 0,
+ [FILTERS.TEXT]: 0,
+ global: 0,
+ });
+ });
+
+ it("has the expected numbers after filter clear", () => {
+ // Add a text search to make sure it is handled as well.
+ store.dispatch(actions.filterTextSet("danger, will robinson!"));
+ store.dispatch(actions.filtersClear());
+ const counter = getFilteredMessagesCount(store.getState());
+ expect(counter).toEqual({
+ [FILTERS.ERROR]: 0,
+ [FILTERS.WARN]: 0,
+ [FILTERS.LOG]: 0,
+ [FILTERS.INFO]: 0,
+ [FILTERS.DEBUG]: 0,
+ [FILTERS.TEXT]: 0,
+ global: 0,
+ });
+ });
+});
+
+const MESSAGES = [
+ // Error
+ "ReferenceError: asdf is not defined",
+ "console.error('error message');",
+ "console.assert(false, {message: 'foobar'})",
+ // Warning
+ "console.warn('danger, will robinson!')",
+ // Log
+ "console.log('foobar', 'test')",
+ "console.log(undefined)",
+ "console.count('bar')",
+ "console.log('鼬')",
+ "console.table(['red', 'green', 'blue']);",
+ // Info
+ "console.info('info message');",
+ // Debug
+ "console.debug('debug message');",
+];
+
+const BASIC_TEST_CASE_FILTERED_MESSAGE_COUNT = {
+ [FILTERS.ERROR]: 3,
+ [FILTERS.WARN]: 1,
+ [FILTERS.LOG]: 5,
+ [FILTERS.INFO]: 1,
+ [FILTERS.DEBUG]: 1,
+ [FILTERS.TEXT]: 0,
+ global: 11,
+};
+
+function prepareBaseStore() {
+ return setupStore(MESSAGES);
+}
diff --git a/devtools/client/webconsole/test/node/store/messages.test.js b/devtools/client/webconsole/test/node/store/messages.test.js
new file mode 100644
index 0000000000..d91325bcfa
--- /dev/null
+++ b/devtools/client/webconsole/test/node/store/messages.test.js
@@ -0,0 +1,1305 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const {
+ getAllMessagesUiById,
+ getAllCssMessagesMatchingElements,
+ getAllNetworkMessagesUpdateById,
+ getAllRepeatById,
+ getAllDisabledMessagesById,
+ getCurrentGroup,
+ getGroupsById,
+ getMutableMessagesById,
+ getVisibleMessages,
+} = require("resource://devtools/client/webconsole/selectors/messages.js");
+
+const {
+ clonePacket,
+ getFirstMessage,
+ getLastMessage,
+ getMessageAt,
+ setupActions,
+ setupStore,
+} = require("resource://devtools/client/webconsole/test/node/helpers.js");
+const {
+ stubPackets,
+ stubPreparedMessages,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+const {
+ MESSAGE_TYPE,
+ CSS_MESSAGE_ADD_MATCHING_ELEMENTS,
+} = require("resource://devtools/client/webconsole/constants.js");
+const {
+ createWarningGroupMessage,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+
+const expect = require("expect");
+
+describe("Message reducer:", () => {
+ let actions;
+
+ beforeAll(() => {
+ actions = setupActions();
+ });
+
+ describe("mutableMessagesById", () => {
+ it("adds a message to an empty store", () => {
+ const { dispatch, getState } = setupStore();
+
+ const packet = stubPackets.get("console.log('foobar', 'test')");
+ dispatch(actions.messagesAdd([packet]));
+
+ const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+
+ expect(getFirstMessage(getState())).toEqual(message);
+ });
+
+ it("increments repeat on a repeating log message", () => {
+ const key1 = "console.log('foobar', 'test')";
+ const { dispatch, getState } = setupStore([key1, key1], { actions });
+
+ const packet = clonePacket(stubPackets.get(key1));
+ const packet2 = clonePacket(packet);
+
+ // Repeat ID must be the same even if the timestamp is different.
+ packet.message.timeStamp = 1;
+ packet2.message.timeStamp = 2;
+ dispatch(actions.messagesAdd([packet, packet2]));
+
+ const messages = getMutableMessagesById(getState());
+
+ expect(messages.size).toBe(1);
+ const repeat = getAllRepeatById(getState());
+ expect(repeat[getFirstMessage(getState()).id]).toBe(4);
+ });
+
+ it("doesn't increment repeat on same log message with different locations", () => {
+ const key1 = "console.log('foobar', 'test')";
+ const { dispatch, getState } = setupStore();
+
+ const packet = clonePacket(stubPackets.get(key1));
+
+ // Dispatch original packet.
+ dispatch(actions.messagesAdd([packet]));
+
+ // Dispatch same packet with modified column number.
+ packet.message.columnNumber = packet.message.columnNumber + 1;
+ dispatch(actions.messagesAdd([packet]));
+
+ // Dispatch same packet with modified line number.
+ packet.message.lineNumber = packet.message.lineNumber + 1;
+ dispatch(actions.messagesAdd([packet]));
+
+ const messages = getMutableMessagesById(getState());
+
+ expect(messages.size).toBe(3);
+
+ const repeat = getAllRepeatById(getState());
+ expect(Object.keys(repeat).length).toBe(0);
+ });
+
+ it("increments repeat on a repeating css message", () => {
+ const key1 =
+ "Unknown property ‘such-unknown-property’. Declaration dropped.";
+ const { dispatch, getState } = setupStore([key1, key1]);
+
+ const packet = clonePacket(stubPackets.get(key1));
+
+ // Repeat ID must be the same even if the timestamp is different.
+ packet.pageError.timeStamp = 1;
+ dispatch(actions.messagesAdd([packet]));
+ packet.pageError.timeStamp = 2;
+ dispatch(actions.messagesAdd([packet]));
+
+ const messages = getMutableMessagesById(getState());
+
+ expect(messages.size).toBe(1);
+
+ const repeat = getAllRepeatById(getState());
+ expect(repeat[getFirstMessage(getState()).id]).toBe(4);
+ });
+
+ it("doesn't increment repeat on same css message with different locations", () => {
+ const key1 =
+ "Unknown property ‘such-unknown-property’. Declaration dropped.";
+ const { dispatch, getState } = setupStore();
+
+ const packet = clonePacket(stubPackets.get(key1));
+
+ // Dispatch original packet.
+ dispatch(actions.messagesAdd([packet]));
+
+ // Dispatch same packet with modified column number.
+ packet.pageError.columnNumber = packet.pageError.columnNumber + 1;
+ dispatch(actions.messagesAdd([packet]));
+
+ // Dispatch same packet with modified line number.
+ packet.pageError.lineNumber = packet.pageError.lineNumber + 1;
+ dispatch(actions.messagesAdd([packet]));
+
+ const messages = getMutableMessagesById(getState());
+
+ expect(messages.size).toBe(3);
+
+ const repeat = getAllRepeatById(getState());
+ expect(Object.keys(repeat).length).toBe(0);
+ });
+
+ it("increments repeat on a repeating error message", () => {
+ const key1 = "ReferenceError: asdf is not defined";
+ const { dispatch, getState } = setupStore([key1, key1]);
+
+ const packet = clonePacket(stubPackets.get(key1));
+
+ // Repeat ID must be the same even if the timestamp is different.
+ packet.pageError.timeStamp = 1;
+ dispatch(actions.messagesAdd([packet]));
+ packet.pageError.timeStamp = 2;
+ dispatch(actions.messagesAdd([packet]));
+
+ const messages = getMutableMessagesById(getState());
+
+ expect(messages.size).toBe(1);
+
+ const repeat = getAllRepeatById(getState());
+ expect(repeat[getFirstMessage(getState()).id]).toBe(4);
+ });
+
+ it("does not increment repeat after closing a group", () => {
+ const logKey = "console.log('foobar', 'test')";
+ const { getState } = setupStore([
+ logKey,
+ logKey,
+ "console.group('bar')",
+ logKey,
+ logKey,
+ logKey,
+ "console.groupEnd()",
+ logKey,
+ ]);
+
+ const messages = getMutableMessagesById(getState());
+
+ expect(messages.size).toBe(4);
+ const repeat = getAllRepeatById(getState());
+ expect(repeat[getFirstMessage(getState()).id]).toBe(2);
+ expect(repeat[getMessageAt(getState(), 2).id]).toBe(3);
+ expect(repeat[getLastMessage(getState()).id]).toBe(undefined);
+ });
+
+ it("doesn't increment undefined messages coming from different places", () => {
+ const { getState } = setupStore(["console.log(undefined)", "undefined"]);
+
+ const messages = getMutableMessagesById(getState());
+ expect(messages.size).toBe(2);
+
+ const repeat = getAllRepeatById(getState());
+ expect(Object.keys(repeat).length).toBe(0);
+ });
+
+ it("doesn't increment successive falsy but different messages", () => {
+ const { getState } = setupStore(
+ ["console.log(NaN)", "console.log(undefined)", "console.log(null)"],
+ { actions }
+ );
+
+ const messages = getMutableMessagesById(getState());
+ expect(messages.size).toBe(3);
+ const repeat = getAllRepeatById(getState());
+ expect(Object.keys(repeat).length).toBe(0);
+ });
+
+ it("increment falsy messages when expected", () => {
+ const { dispatch, getState } = setupStore();
+
+ const nanPacket = stubPackets.get("console.log(NaN)");
+ dispatch(actions.messagesAdd([nanPacket, nanPacket]));
+ let messages = getMutableMessagesById(getState());
+ expect(messages.size).toBe(1);
+ let repeat = getAllRepeatById(getState());
+ expect(repeat[getLastMessage(getState()).id]).toBe(2);
+
+ const undefinedPacket = stubPackets.get("console.log(undefined)");
+ dispatch(actions.messagesAdd([undefinedPacket, undefinedPacket]));
+ messages = getMutableMessagesById(getState());
+ expect(messages.size).toBe(2);
+ repeat = getAllRepeatById(getState());
+ expect(repeat[getLastMessage(getState()).id]).toBe(2);
+
+ const nullPacket = stubPackets.get("console.log(null)");
+ dispatch(actions.messagesAdd([nullPacket, nullPacket]));
+ messages = getMutableMessagesById(getState());
+ expect(messages.size).toBe(3);
+ repeat = getAllRepeatById(getState());
+ expect(repeat[getLastMessage(getState()).id]).toBe(2);
+ });
+
+ it("does not clobber a unique message", () => {
+ const key1 = "console.log('foobar', 'test')";
+ const { dispatch, getState } = setupStore([key1, key1]);
+
+ const packet = stubPackets.get(key1);
+ dispatch(actions.messagesAdd([packet]));
+
+ const packet2 = stubPackets.get("console.log(undefined)");
+ dispatch(actions.messagesAdd([packet2]));
+
+ const messages = getMutableMessagesById(getState());
+ expect(messages.size).toBe(2);
+
+ const repeat = getAllRepeatById(getState());
+ expect(repeat[getFirstMessage(getState()).id]).toBe(3);
+ expect(repeat[getLastMessage(getState()).id]).toBe(undefined);
+ });
+
+ it("does not increment repeat after adding similar warning group", () => {
+ const { dispatch, getState } = setupStore();
+
+ // Mocking a warning message that would create a warning group
+ const warningMessage = stubPreparedMessages.get(
+ "ReferenceError: asdf is not defined"
+ );
+ warningMessage.messageText =
+ "The resource at “https://evil.com” was blocked.";
+ warningMessage.category = "cookieBlockedPermission";
+
+ const type = MESSAGE_TYPE.CONTENT_BLOCKING_GROUP;
+ const firstMessageId = `${warningMessage.type}-${warningMessage.innerWindowID}`;
+ const message1 = createWarningGroupMessage(
+ firstMessageId,
+ type,
+ warningMessage
+ );
+ const secondMessageId = `${warningMessage.type}-${
+ warningMessage.innerWindowID + 10
+ }`;
+ const message2 = createWarningGroupMessage(
+ secondMessageId,
+ type,
+ warningMessage
+ );
+
+ dispatch(actions.messagesAdd([message1, message2]));
+
+ const messages = getMutableMessagesById(getState());
+ expect(messages.size).toBe(2);
+
+ const repeat = getAllRepeatById(getState());
+ expect(Object.keys(repeat).length).toBe(0);
+ });
+
+ it("adds a message in response to console.clear()", () => {
+ const { dispatch, getState } = setupStore([]);
+
+ dispatch(actions.messagesAdd([stubPackets.get("console.clear()")]));
+
+ const messages = getMutableMessagesById(getState());
+
+ expect(messages.size).toBe(1);
+ expect(getFirstMessage(getState()).parameters[0]).toBe(
+ "Console was cleared."
+ );
+ });
+
+ it("clears the messages list in response to MESSAGES_CLEAR action", () => {
+ const { dispatch, getState } = setupStore([
+ "console.log('foobar', 'test')",
+ "console.log('foobar', 'test')",
+ "console.log(undefined)",
+ "console.table(['red', 'green', 'blue']);",
+ "console.group('bar')",
+ ]);
+
+ dispatch(actions.messagesClear());
+
+ const state = getState();
+ expect(getMutableMessagesById(state).size).toBe(0);
+ expect(getVisibleMessages(state).length).toBe(0);
+ expect(getAllMessagesUiById(state).length).toBe(0);
+ expect(getGroupsById(state).size).toBe(0);
+ expect(getAllCssMessagesMatchingElements(state).size).toBe(0);
+ expect(getCurrentGroup(state)).toBe(null);
+ expect(getAllRepeatById(state)).toEqual({});
+ expect(state.messages.mutableMessagesOrder).toEqual([]);
+ });
+
+ it("cleans the repeatsById object when messages are pruned", () => {
+ const { dispatch, getState } = setupStore(
+ [
+ "console.log('foobar', 'test')",
+ "console.log('foobar', 'test')",
+ "console.log(undefined)",
+ "console.log(undefined)",
+ ],
+ {
+ actions,
+ storeOptions: {
+ logLimit: 2,
+ },
+ }
+ );
+
+ // Check that we have the expected data.
+ let repeats = getAllRepeatById(getState());
+ expect(Object.keys(repeats).length).toBe(2);
+ const lastMessageId = getLastMessage(getState()).id;
+
+ // This addition will prune the first message out of the store.
+ let packet = stubPackets.get("console.log('foobar', 'test')");
+ dispatch(actions.messagesAdd([packet]));
+
+ repeats = getAllRepeatById(getState());
+
+ // There should be only the data for the "undefined" message.
+ expect(Object.keys(repeats)).toEqual([lastMessageId]);
+ expect(Object.keys(repeats).length).toBe(1);
+ expect(repeats[lastMessageId]).toBe(2);
+
+ // This addition will prune the first message out of the store.
+ packet = stubPackets.get("console.log(undefined)");
+ dispatch(actions.messagesAdd([packet]));
+
+ // repeatById should now be empty.
+ expect(getAllRepeatById(getState())).toEqual({});
+ });
+
+ it("properly limits number of messages", () => {
+ const logLimit = 1000;
+ const { dispatch, getState } = setupStore([], {
+ storeOptions: {
+ logLimit,
+ },
+ });
+
+ const packet = clonePacket(stubPackets.get("console.log(undefined)"));
+
+ for (let i = 1; i <= logLimit + 2; i++) {
+ packet.message.arguments = [`message num ${i}`];
+ dispatch(actions.messagesAdd([packet]));
+ }
+
+ const messages = getMutableMessagesById(getState());
+ expect(messages.size).toBe(logLimit);
+ expect(getFirstMessage(getState()).parameters[0]).toBe(`message num 3`);
+ expect(getLastMessage(getState()).parameters[0]).toBe(
+ `message num ${logLimit + 2}`
+ );
+
+ const { mutableMessagesOrder } = getState().messages;
+ expect(mutableMessagesOrder.length).toBe(logLimit);
+ });
+
+ it("properly limits number of messages when there are nested groups", () => {
+ const logLimit = 1000;
+ const { dispatch, getState } = setupStore([], {
+ storeOptions: {
+ logLimit,
+ },
+ });
+
+ const packet = clonePacket(stubPackets.get("console.log(undefined)"));
+ const packetGroup = clonePacket(stubPackets.get("console.group('bar')"));
+ const packetGroupEnd = clonePacket(stubPackets.get("console.groupEnd()"));
+
+ packetGroup.message.arguments = [`group-1`];
+ dispatch(actions.messagesAdd([packetGroup]));
+ packetGroup.message.arguments = [`group-1-1`];
+ dispatch(actions.messagesAdd([packetGroup]));
+ packetGroup.message.arguments = [`group-1-1-1`];
+ dispatch(actions.messagesAdd([packetGroup]));
+ packet.message.arguments = [`message-in-group-1`];
+ dispatch(actions.messagesAdd([packet]));
+ packet.message.arguments = [`message-in-group-2`];
+ dispatch(actions.messagesAdd([packet]));
+ // Closing group-1-1-1
+ dispatch(actions.messagesAdd([packetGroupEnd]));
+ // Closing group-1-1
+ dispatch(actions.messagesAdd([packetGroupEnd]));
+ // Closing group-1
+ dispatch(actions.messagesAdd([packetGroupEnd]));
+
+ for (let i = 0; i < logLimit; i++) {
+ packet.message.arguments = [`message-${i}`];
+ dispatch(actions.messagesAdd([packet]));
+ }
+
+ const visibleMessages = getVisibleMessages(getState());
+ const messages = getMutableMessagesById(getState());
+ const { mutableMessagesOrder } = getState().messages;
+
+ expect(messages.size).toBe(logLimit);
+ expect(visibleMessages.length).toBe(logLimit);
+ expect(mutableMessagesOrder.length).toBe(logLimit);
+
+ expect(messages.get(visibleMessages[0]).parameters[0]).toBe(`message-0`);
+ expect(messages.get(visibleMessages[logLimit - 1]).parameters[0]).toBe(
+ `message-${logLimit - 1}`
+ );
+
+ // The groups were cleaned up.
+ const groups = getGroupsById(getState());
+ expect(groups.size).toBe(0);
+ });
+
+ it("properly limits number of groups", () => {
+ const logLimit = 100;
+ const { dispatch, getState } = setupStore([], {
+ storeOptions: { logLimit },
+ });
+
+ const packet = clonePacket(stubPackets.get("console.log(undefined)"));
+ const packetGroup = clonePacket(stubPackets.get("console.group('bar')"));
+ const packetGroupEnd = clonePacket(stubPackets.get("console.groupEnd()"));
+
+ for (let i = 0; i < logLimit + 2; i++) {
+ dispatch(actions.messagesAdd([packetGroup]));
+ packet.message.arguments = [`message-${i}-a`];
+ dispatch(actions.messagesAdd([packet]));
+ packet.message.arguments = [`message-${i}-b`];
+ dispatch(actions.messagesAdd([packet]));
+ dispatch(actions.messagesAdd([packetGroupEnd]));
+ }
+
+ const visibleMessages = getVisibleMessages(getState());
+ const messages = getMutableMessagesById(getState());
+ // We should have three times the logLimit since each group has one message inside.
+ expect(messages.size).toBe(logLimit * 3);
+
+ // We should have logLimit number of groups
+ const groups = getGroupsById(getState());
+ expect(groups.size).toBe(logLimit);
+
+ expect(messages.get(visibleMessages[1]).parameters[0]).toBe(
+ `message-2-a`
+ );
+ expect(getLastMessage(getState()).parameters[0]).toBe(
+ `message-${logLimit + 1}-b`
+ );
+ });
+
+ it("properly limits number of collapsed groups", () => {
+ const logLimit = 100;
+ const { dispatch, getState } = setupStore([], {
+ storeOptions: { logLimit },
+ });
+
+ const packet = clonePacket(stubPackets.get("console.log(undefined)"));
+ const packetGroupCollapsed = clonePacket(
+ stubPackets.get("console.groupCollapsed('foo')")
+ );
+ const packetGroupEnd = clonePacket(stubPackets.get("console.groupEnd()"));
+
+ for (let i = 0; i < logLimit + 2; i++) {
+ packetGroupCollapsed.message.arguments = [`group-${i}`];
+ dispatch(actions.messagesAdd([packetGroupCollapsed]));
+ packet.message.arguments = [`message-${i}-a`];
+ dispatch(actions.messagesAdd([packet]));
+ packet.message.arguments = [`message-${i}-b`];
+ dispatch(actions.messagesAdd([packet]));
+ dispatch(actions.messagesAdd([packetGroupEnd]));
+ }
+
+ const messages = getMutableMessagesById(getState());
+ // We should have three times the logLimit since each group has two message inside.
+ expect(messages.size).toBe(logLimit * 3);
+
+ // We should have logLimit number of groups
+ const groups = getGroupsById(getState());
+ expect(groups.size).toBe(logLimit);
+
+ expect(getFirstMessage(getState()).parameters[0]).toBe(`group-2`);
+ expect(getLastMessage(getState()).parameters[0]).toBe(
+ `message-${logLimit + 1}-b`
+ );
+
+ const visibleMessages = getVisibleMessages(getState());
+ expect(visibleMessages.length).toBe(logLimit);
+ const lastVisibleMessageId = visibleMessages[visibleMessages.length - 1];
+ expect(messages.get(lastVisibleMessageId).parameters[0]).toBe(
+ `group-${logLimit + 1}`
+ );
+ });
+
+ it("does not add null messages to the store", () => {
+ const { dispatch, getState } = setupStore();
+
+ const message = stubPackets.get("console.time('bar')");
+ dispatch(actions.messagesAdd([message]));
+
+ const messages = getMutableMessagesById(getState());
+ expect(messages.size).toBe(0);
+ });
+
+ it("adds console.table call with unsupported type as console.log", () => {
+ const { dispatch, getState } = setupStore();
+
+ const packet = stubPackets.get("console.table('bar')");
+ dispatch(actions.messagesAdd([packet]));
+
+ const tableMessage = getLastMessage(getState());
+ expect(tableMessage.level).toEqual(MESSAGE_TYPE.LOG);
+ });
+
+ it("adds console.group messages to the store", () => {
+ const { dispatch, getState } = setupStore();
+
+ const message = stubPackets.get("console.group('bar')");
+ dispatch(actions.messagesAdd([message]));
+
+ const messages = getMutableMessagesById(getState());
+ expect(messages.size).toBe(1);
+ });
+
+ it("adds messages in console.group to the store", () => {
+ const { dispatch, getState } = setupStore();
+
+ const groupPacket = stubPackets.get("console.group('bar')");
+ const groupEndPacket = stubPackets.get("console.groupEnd('bar')");
+ const logPacket = stubPackets.get("console.log('foobar', 'test')");
+
+ const packets = [
+ groupPacket,
+ logPacket,
+ groupPacket,
+ groupPacket,
+ logPacket,
+ groupEndPacket,
+ logPacket,
+ groupEndPacket,
+ logPacket,
+ groupEndPacket,
+ logPacket,
+ ];
+ dispatch(actions.messagesAdd(packets));
+
+ // Here is what we should have (8 messages)
+ // ▼ bar
+ // | foobar test
+ // | ▼ bar
+ // | | ▼ bar
+ // | | | foobar test
+ // | | foobar test
+ // | foobar test
+ // foobar test
+
+ const isNotGroupEnd = p => p !== groupEndPacket;
+ const messageCount = packets.filter(isNotGroupEnd).length;
+
+ const messages = getMutableMessagesById(getState());
+ const visibleMessages = getVisibleMessages(getState());
+ expect(messages.size).toBe(messageCount);
+ expect(visibleMessages.length).toBe(messageCount);
+ });
+
+ it("sets groupId property as expected", () => {
+ const { dispatch, getState } = setupStore();
+
+ dispatch(
+ actions.messagesAdd([
+ stubPackets.get("console.group('bar')"),
+ stubPackets.get("console.log('foobar', 'test')"),
+ ])
+ );
+
+ const messages = getMutableMessagesById(getState());
+ expect(messages.size).toBe(2);
+ expect(getLastMessage(getState()).groupId).toBe(
+ getFirstMessage(getState()).id
+ );
+ });
+
+ it("does not display console.groupEnd messages to the store", () => {
+ const { dispatch, getState } = setupStore();
+
+ const message = stubPackets.get("console.groupEnd('bar')");
+ dispatch(actions.messagesAdd([message]));
+
+ const messages = getMutableMessagesById(getState());
+ expect(messages.size).toBe(0);
+ });
+
+ it("filters out message added after a console.groupCollapsed message", () => {
+ const { dispatch, getState } = setupStore();
+
+ dispatch(
+ actions.messagesAdd([
+ stubPackets.get("console.groupCollapsed('foo')"),
+ stubPackets.get("console.log('foobar', 'test')"),
+ ])
+ );
+
+ const messages = getVisibleMessages(getState());
+ expect(messages.length).toBe(1);
+ });
+
+ it("adds console.dirxml call as console.log", () => {
+ const { dispatch, getState } = setupStore();
+
+ const packet = stubPackets.get("console.dirxml(window)");
+ dispatch(actions.messagesAdd([packet]));
+
+ const dirxmlMessage = getLastMessage(getState());
+ expect(dirxmlMessage.level).toEqual(MESSAGE_TYPE.LOG);
+ });
+
+ it("does not throw when adding incomplete console.count packet", () => {
+ const { dispatch, getState } = setupStore();
+ const packet = clonePacket(stubPackets.get(`console.count('bar')`));
+
+ // Remove counter information to mimick packet we receive in the browser console.
+ delete packet.message.counter;
+
+ dispatch(actions.messagesAdd([packet]));
+ // The message should not be added to the state.
+ expect(getMutableMessagesById(getState()).size).toBe(0);
+ });
+ });
+
+ describe("mutableMessagesOrder", () => {
+ it("adds a message to an empty store", () => {
+ const { dispatch, getState } = setupStore();
+
+ const packet = stubPackets.get("console.log('foobar', 'test')");
+ dispatch(actions.messagesAdd([packet]));
+
+ const { mutableMessagesOrder } = getState().messages;
+ expect(mutableMessagesOrder.length).toBe(1);
+ expect(mutableMessagesOrder[0]).toBe(
+ // Don't get getMessageIndexAt/getFirstMessage since it relies on mutableMessagesOrder
+ [...getMutableMessagesById(getState()).keys()][0]
+ );
+ });
+
+ it("reorder messages", () => {
+ const { dispatch, getState } = setupStore();
+
+ const naNpacket = stubPackets.get("console.log(NaN)");
+ dispatch(actions.messagesAdd([naNpacket]));
+
+ // Add a message that has a shorter timestamp than the previous one, and thus, should
+ // be displayed before
+ const nullPacket = clonePacket(stubPackets.get("console.log(null)"));
+ nullPacket.message.timeStamp = naNpacket.message.timeStamp - 10;
+ dispatch(actions.messagesAdd([nullPacket]));
+
+ // Add a message that should be display between the 2 previous messages
+ const undefinedPacket = clonePacket(
+ stubPackets.get("console.log(undefined)")
+ );
+ undefinedPacket.message.timeStamp = naNpacket.message.timeStamp - 5;
+ dispatch(actions.messagesAdd([undefinedPacket]));
+
+ const { mutableMessagesOrder } = getState().messages;
+ const [nanMessage, nullMessage, undefinedMessage] = [
+ ...getMutableMessagesById(getState()).values(),
+ ];
+ const visibleMessages = getVisibleMessages(getState());
+
+ // Checking that messages in the Map are the expected ones
+ expect(nanMessage.parameters[0].type).toBe("NaN");
+ expect(nullMessage.parameters[0].type).toBe("null");
+ expect(undefinedMessage.parameters[0].type).toBe("undefined");
+
+ // Check that mutableMessagesOrder has the message ids in the chronological order
+ expect(mutableMessagesOrder).toEqual([
+ nullMessage.id,
+ undefinedMessage.id,
+ nanMessage.id,
+ ]);
+
+ // Since we didn't filtered anything, visibleMessages should be similar to mutableMessagesOrder
+ expect(mutableMessagesOrder).toEqual(visibleMessages);
+
+ // Check that visibleMessages is computed from mutableMessagesOrder when filtering
+ dispatch(actions.filterToggle("log"));
+ expect(getVisibleMessages(getState())).toEqual([]);
+ dispatch(actions.filterToggle("log"));
+ expect(getVisibleMessages(getState())).toEqual([
+ nullMessage.id,
+ undefinedMessage.id,
+ nanMessage.id,
+ ]);
+ });
+ });
+
+ describe("expandedMessageIds", () => {
+ it("opens console.trace messages when they are added", () => {
+ const { dispatch, getState } = setupStore();
+
+ const message = stubPackets.get("console.trace()");
+ dispatch(actions.messagesAdd([message]));
+
+ const expanded = getAllMessagesUiById(getState());
+ expect(expanded.length).toBe(1);
+ expect(expanded[0]).toBe(getFirstMessage(getState()).id);
+ });
+
+ it("clears the messages UI list in response to MESSAGES_CLEAR action", () => {
+ const { dispatch, getState } = setupStore([
+ "console.log('foobar', 'test')",
+ "console.log(undefined)",
+ ]);
+
+ const traceMessage = stubPackets.get("console.trace()");
+ dispatch(actions.messagesAdd([traceMessage]));
+
+ dispatch(actions.messagesClear());
+
+ const expanded = getAllMessagesUiById(getState());
+ expect(expanded.length).toBe(0);
+ });
+
+ it("cleans the messages UI list when messages are pruned", () => {
+ const { dispatch, getState } = setupStore(
+ ["console.trace()", "console.log(undefined)", "console.trace()"],
+ {
+ storeOptions: {
+ logLimit: 3,
+ },
+ }
+ );
+
+ // Check that we have the expected data.
+ let expanded = getAllMessagesUiById(getState());
+ expect(expanded.length).toBe(2);
+ expect(expanded[0]).toBe(getFirstMessage(getState()).id);
+ const lastMessageId = getLastMessage(getState()).id;
+ expect(expanded[expanded.length - 1]).toBe(lastMessageId);
+
+ // This addition will prune the first message out of the store.
+ let packet = stubPackets.get("console.log(undefined)");
+ dispatch(actions.messagesAdd([packet]));
+
+ expanded = getAllMessagesUiById(getState());
+
+ // There should be only the id of the last console.trace message.
+ expect(expanded.length).toBe(1);
+ expect(expanded[0]).toBe(lastMessageId);
+
+ // These additions will prune the last console.trace message out of the store.
+ packet = stubPackets.get("console.log('foobar', 'test')");
+ dispatch(actions.messagesAdd([packet]));
+ packet = stubPackets.get("console.log(undefined)");
+ dispatch(actions.messagesAdd([packet]));
+
+ // expandedMessageIds should now be empty.
+ expect(getAllMessagesUiById(getState()).length).toBe(0);
+ });
+
+ it("opens console.group messages when they are added", () => {
+ const { dispatch, getState } = setupStore();
+
+ const message = stubPackets.get("console.group('bar')");
+ dispatch(actions.messagesAdd([message]));
+
+ const expanded = getAllMessagesUiById(getState());
+ expect(expanded.length).toBe(1);
+ expect(expanded[0]).toBe(getFirstMessage(getState()).id);
+ });
+
+ it("does not open console.groupCollapsed messages when they are added", () => {
+ const { dispatch, getState } = setupStore();
+
+ const message = stubPackets.get("console.groupCollapsed('foo')");
+ dispatch(actions.messagesAdd([message]));
+
+ const expanded = getAllMessagesUiById(getState());
+ expect(expanded.length).toBe(0);
+ });
+
+ it("reacts to messageClose/messageOpen actions on console.group", () => {
+ const { dispatch, getState } = setupStore(["console.group('bar')"]);
+ const firstMessageId = getFirstMessage(getState()).id;
+
+ let expanded = getAllMessagesUiById(getState());
+ expect(expanded.length).toBe(1);
+ expect(expanded[0]).toBe(firstMessageId);
+
+ dispatch(actions.messageClose(firstMessageId));
+
+ expanded = getAllMessagesUiById(getState());
+ expect(expanded.length).toBe(0);
+
+ dispatch(actions.messageOpen(firstMessageId));
+
+ expanded = getAllMessagesUiById(getState());
+ expect(expanded.length).toBe(1);
+ expect(expanded[0]).toBe(firstMessageId);
+ });
+
+ it("reacts to messageClose/messageOpen actions on exception", () => {
+ const { dispatch, getState } = setupStore([
+ "ReferenceError: asdf is not defined",
+ ]);
+ const firstMessageId = getFirstMessage(getState()).id;
+
+ let expanded = getAllMessagesUiById(getState());
+ expect(expanded.length).toBe(0);
+
+ dispatch(actions.messageOpen(firstMessageId));
+
+ expanded = getAllMessagesUiById(getState());
+ expect(expanded.length).toBe(1);
+ expect(expanded[0]).toBe(firstMessageId);
+
+ dispatch(actions.messageClose(firstMessageId));
+
+ expanded = getAllMessagesUiById(getState());
+ expect(expanded.length).toBe(0);
+ });
+ });
+
+ describe("currentGroup", () => {
+ it("sets the currentGroup when console.group message is added", () => {
+ const { dispatch, getState } = setupStore();
+
+ const packet = stubPackets.get("console.group('bar')");
+ dispatch(actions.messagesAdd([packet]));
+
+ const currentGroup = getCurrentGroup(getState());
+ expect(currentGroup).toBe(getFirstMessage(getState()).id);
+ });
+
+ it("sets currentGroup to expected value when console.groupEnd is added", () => {
+ const { dispatch, getState } = setupStore([
+ "console.group('bar')",
+ "console.groupCollapsed('foo')",
+ "console.group('bar')",
+ "console.groupEnd('bar')",
+ ]);
+
+ let currentGroup = getCurrentGroup(getState());
+ expect(currentGroup).toBe(getMessageAt(getState(), 1).id);
+
+ const endFooPacket = stubPackets.get("console.groupEnd('foo')");
+ dispatch(actions.messagesAdd([endFooPacket]));
+ currentGroup = getCurrentGroup(getState());
+ expect(currentGroup).toBe(getFirstMessage(getState()).id);
+
+ const endBarPacket = stubPackets.get("console.groupEnd('bar')");
+ dispatch(actions.messagesAdd([endBarPacket]));
+ currentGroup = getCurrentGroup(getState());
+ expect(currentGroup).toBe(null);
+ });
+
+ it("resets the currentGroup to null in response to MESSAGES_CLEAR action", () => {
+ const { dispatch, getState } = setupStore(["console.group('bar')"]);
+
+ dispatch(actions.messagesClear());
+
+ const currentGroup = getCurrentGroup(getState());
+ expect(currentGroup).toBe(null);
+ });
+ });
+
+ describe("groupsById", () => {
+ it("adds the group with expected array when console.group message is added", () => {
+ const { dispatch, getState } = setupStore();
+
+ const barPacket = stubPackets.get("console.group('bar')");
+ dispatch(actions.messagesAdd([barPacket]));
+
+ let groupsById = getGroupsById(getState());
+ expect(groupsById.size).toBe(1);
+ expect(groupsById.has(getFirstMessage(getState()).id)).toBe(true);
+ expect(groupsById.get(getFirstMessage(getState()).id)).toEqual([]);
+
+ const fooPacket = stubPackets.get("console.groupCollapsed('foo')");
+ dispatch(actions.messagesAdd([fooPacket]));
+
+ groupsById = getGroupsById(getState());
+ expect(groupsById.size).toBe(2);
+ expect(groupsById.has(getLastMessage(getState()).id)).toBe(true);
+ expect(groupsById.get(getLastMessage(getState()).id)).toEqual([
+ getFirstMessage(getState()).id,
+ ]);
+ });
+
+ it("resets groupsById in response to MESSAGES_CLEAR action", () => {
+ const { dispatch, getState } = setupStore([
+ "console.group('bar')",
+ "console.groupCollapsed('foo')",
+ ]);
+
+ let groupsById = getGroupsById(getState());
+ expect(groupsById.size).toBe(2);
+
+ dispatch(actions.messagesClear());
+
+ groupsById = getGroupsById(getState());
+ expect(groupsById.size).toBe(0);
+ });
+
+ it("cleans the groupsById property when messages are pruned", () => {
+ const { dispatch, getState } = setupStore(
+ [
+ "console.group('bar')",
+ "console.group()",
+ "console.groupEnd()",
+ "console.groupEnd('bar')",
+ "console.group('bar')",
+ "console.groupEnd('bar')",
+ "console.log('foobar', 'test')",
+ ],
+ {
+ actions,
+ storeOptions: {
+ logLimit: 3,
+ },
+ }
+ );
+
+ /*
+ * Here is the initial state of the console:
+ * ▶︎ bar
+ * ▶︎ noLabel
+ * ▶︎ bar
+ * foobar test
+ */
+
+ // Check that we have the expected data.
+ let groupsById = getGroupsById(getState());
+ expect(groupsById.size).toBe(3);
+
+ // This addition will prune the first group (and its child group) out of the store.
+ /*
+ * ▶︎ bar
+ * foobar test
+ * undefined
+ */
+ let packet = stubPackets.get("console.log(undefined)");
+ dispatch(actions.messagesAdd([packet]));
+
+ groupsById = getGroupsById(getState());
+
+ // There should be only the id of the last console.group message.
+ expect(groupsById.size).toBe(1);
+
+ // This additions will prune the last group message out of the store.
+ /*
+ * foobar test
+ * undefined
+ * foobar test
+ */
+ packet = stubPackets.get("console.log('foobar', 'test')");
+ dispatch(actions.messagesAdd([packet]));
+
+ // groupsById should now be empty.
+ expect(getGroupsById(getState()).size).toBe(0);
+ });
+ });
+
+ describe("networkMessagesUpdateById", () => {
+ it("adds the network update message when network update action is called", () => {
+ const { dispatch, getState } = setupStore();
+
+ let packet = clonePacket(stubPackets.get("GET request"));
+ let updatePacket = clonePacket(stubPackets.get("GET request update"));
+
+ packet.actor = "message1";
+ updatePacket.actor = "message1";
+ dispatch(actions.messagesAdd([packet]));
+ dispatch(actions.networkMessageUpdates([updatePacket], null));
+
+ let networkUpdates = getAllNetworkMessagesUpdateById(getState());
+ expect(Object.keys(networkUpdates)).toEqual(["message1"]);
+
+ packet = clonePacket(stubPackets.get("GET request"));
+ updatePacket = stubPackets.get("XHR GET request update");
+ packet.actor = "message2";
+ updatePacket.actor = "message2";
+ dispatch(actions.messagesAdd([packet]));
+ dispatch(actions.networkMessageUpdates([updatePacket], null));
+
+ networkUpdates = getAllNetworkMessagesUpdateById(getState());
+ expect(Object.keys(networkUpdates)).toEqual(["message1", "message2"]);
+ });
+
+ it("resets networkMessagesUpdateById in response to MESSAGES_CLEAR action", () => {
+ const { dispatch, getState } = setupStore(["XHR GET request"]);
+
+ const updatePacket = stubPackets.get("XHR GET request update");
+ dispatch(actions.networkMessageUpdates([updatePacket], null));
+
+ let networkUpdates = getAllNetworkMessagesUpdateById(getState());
+ expect(!!Object.keys(networkUpdates).length).toBe(true);
+
+ dispatch(actions.messagesClear());
+
+ networkUpdates = getAllNetworkMessagesUpdateById(getState());
+ expect(Object.keys(networkUpdates).length).toBe(0);
+ });
+
+ it("cleans the networkMessagesUpdateById property when messages are pruned", () => {
+ const { dispatch, getState } = setupStore([], {
+ storeOptions: {
+ logLimit: 3,
+ },
+ });
+
+ // Add 3 network messages and their updates
+ let packet = clonePacket(stubPackets.get("XHR GET request"));
+ const updatePacket = clonePacket(
+ stubPackets.get("XHR GET request update")
+ );
+ packet.actor = "message1";
+ updatePacket.actor = "message1";
+ dispatch(actions.messagesAdd([packet]));
+ dispatch(actions.networkMessageUpdates([updatePacket], null));
+
+ packet.actor = "message2";
+ updatePacket.actor = "message2";
+ dispatch(actions.messagesAdd([packet]));
+ dispatch(actions.networkMessageUpdates([updatePacket], null));
+
+ packet.actor = "message3";
+ updatePacket.actor = "message3";
+ dispatch(actions.messagesAdd([packet]));
+ dispatch(actions.networkMessageUpdates([updatePacket], null));
+
+ // Check that we have the expected data.
+ const messages = getMutableMessagesById(getState());
+ const [
+ firstNetworkMessageId,
+ secondNetworkMessageId,
+ thirdNetworkMessageId,
+ ] = [...messages.keys()];
+
+ let networkUpdates = getAllNetworkMessagesUpdateById(getState());
+ expect(Object.keys(networkUpdates)).toEqual([
+ firstNetworkMessageId,
+ secondNetworkMessageId,
+ thirdNetworkMessageId,
+ ]);
+
+ // This addition will remove the first network message.
+ packet = stubPackets.get("console.log(undefined)");
+ dispatch(actions.messagesAdd([packet]));
+
+ networkUpdates = getAllNetworkMessagesUpdateById(getState());
+ expect(Object.keys(networkUpdates)).toEqual([
+ secondNetworkMessageId,
+ thirdNetworkMessageId,
+ ]);
+
+ // This addition will remove the second network message.
+ packet = stubPackets.get("console.log('foobar', 'test')");
+ dispatch(actions.messagesAdd([packet]));
+
+ networkUpdates = getAllNetworkMessagesUpdateById(getState());
+ expect(Object.keys(networkUpdates)).toEqual([thirdNetworkMessageId]);
+
+ // This addition will remove the last network message.
+ packet = stubPackets.get("console.log(undefined)");
+ dispatch(actions.messagesAdd([packet]));
+
+ // networkMessageUpdateById should now be empty.
+ networkUpdates = getAllNetworkMessagesUpdateById(getState());
+ expect(Object.keys(networkUpdates)).toEqual([]);
+ });
+ });
+
+ describe("cssMessagesMatchingElements", () => {
+ it("resets cssMessagesMatchingElements in response to MESSAGES_CLEAR action", () => {
+ const { dispatch, getState } = setupStore([
+ `Unknown property ‘such-unknown-property’. Declaration dropped.`,
+ ]);
+
+ const data = Symbol();
+ dispatch({
+ type: CSS_MESSAGE_ADD_MATCHING_ELEMENTS,
+ id: getFirstMessage(getState()).id,
+ elements: data,
+ });
+
+ const matchingElements = getAllCssMessagesMatchingElements(getState());
+ expect(matchingElements.size).toBe(1);
+ expect(matchingElements.get(getFirstMessage(getState()).id)).toBe(data);
+
+ dispatch(actions.messagesClear());
+
+ expect(getAllCssMessagesMatchingElements(getState()).size).toBe(0);
+ });
+
+ it("cleans the cssMessagesMatchingElements property when messages are pruned", () => {
+ const { dispatch, getState } = setupStore([], {
+ storeOptions: {
+ logLimit: 2,
+ },
+ });
+
+ // Add 2 css warnings message and their associated data.
+ dispatch(
+ actions.messagesAdd([
+ stubPackets.get(
+ `Unknown property ‘such-unknown-property’. Declaration dropped.`
+ ),
+ ])
+ );
+ dispatch(
+ actions.messagesAdd([
+ stubPackets.get(
+ `Error in parsing value for ‘padding-top’. Declaration dropped.`
+ ),
+ ])
+ );
+
+ const messages = getMutableMessagesById(getState());
+
+ const data1 = Symbol();
+ const data2 = Symbol();
+ const [id1, id2] = [...messages.keys()];
+
+ dispatch({
+ type: CSS_MESSAGE_ADD_MATCHING_ELEMENTS,
+ id: id1,
+ elements: data1,
+ });
+ dispatch({
+ type: CSS_MESSAGE_ADD_MATCHING_ELEMENTS,
+ id: id2,
+ elements: data2,
+ });
+
+ let matchingElements = getAllCssMessagesMatchingElements(getState());
+ expect(matchingElements.size).toBe(2);
+
+ // This addition will remove the first css warning.
+ dispatch(
+ actions.messagesAdd([stubPackets.get("console.log(undefined)")])
+ );
+
+ matchingElements = getAllCssMessagesMatchingElements(getState());
+ expect(matchingElements.size).toBe(1);
+ expect(matchingElements.get(id2)).toBe(data2);
+
+ // This addition will remove the second css warning.
+ dispatch(
+ actions.messagesAdd([stubPackets.get("console.log('foobar', 'test')")])
+ );
+
+ expect(getAllCssMessagesMatchingElements(getState()).size).toBe(0);
+ });
+ });
+
+ describe("messagesAdd", () => {
+ it("still log repeated message over logLimit, but only repeated ones", () => {
+ // Log two distinct messages
+ const key1 = "console.log('foobar', 'test')";
+ const key2 = "console.log(undefined)";
+ const { dispatch, getState } = setupStore([key1, key2], {
+ storeOptions: {
+ logLimit: 2,
+ },
+ });
+
+ // Then repeat the last one two times and log the first one again
+ const packet1 = clonePacket(stubPackets.get(key2));
+ const packet2 = clonePacket(stubPackets.get(key2));
+ const packet3 = clonePacket(stubPackets.get(key1));
+
+ // Repeat ID must be the same even if the timestamp is different.
+ packet1.message.timeStamp = packet1.message.timeStamp + 1;
+ packet2.message.timeStamp = packet2.message.timeStamp + 2;
+ packet3.message.timeStamp = packet3.message.timeStamp + 3;
+ dispatch(actions.messagesAdd([packet1, packet2, packet3]));
+
+ // There is still only two messages being logged,
+ const messages = getMutableMessagesById(getState());
+ expect(messages.size).toBe(2);
+
+ // the second one being repeated 3 times
+ const repeat = getAllRepeatById(getState());
+ expect(repeat[getFirstMessage(getState()).id]).toBe(3);
+ expect(repeat[getLastMessage(getState()).id]).toBe(undefined);
+ });
+ });
+
+ describe("messageRemove", () => {
+ it("removes the message from the store", () => {
+ const { dispatch, getState } = setupStore([
+ "console.trace()",
+ "console.log(undefined)",
+ "console.trace()",
+ "console.log(undefined)",
+ ]);
+
+ let expanded = getAllMessagesUiById(getState());
+ expect(expanded.length).toBe(2);
+
+ const secondTraceMessage = getMessageAt(getState(), 2);
+ dispatch(actions.messageRemove(secondTraceMessage.id));
+
+ const messages = getMutableMessagesById(getState());
+ const { mutableMessagesOrder } = getState().messages;
+ // The messages was removed
+ expect(messages.size).toBe(3);
+ expect(mutableMessagesOrder.length).toBe(3);
+
+ // Its id was removed from the messagesUI property as well
+ expanded = getAllMessagesUiById(getState());
+ expect(expanded.length).toBe(1);
+ expect(expanded.includes(secondTraceMessage.id)).toBeFalsy();
+ });
+ });
+
+ describe("disabledMessagesById", () => {
+ it("adds messages ids to disabledMessagesById when message disable action is called", () => {
+ const { dispatch, getState } = setupStore();
+
+ dispatch(actions.messagesDisable(["message1", "message2"]));
+
+ const disabledMessages = getAllDisabledMessagesById(getState());
+ expect(disabledMessages).toEqual(["message1", "message2"]);
+ });
+
+ it("clears disabledMessagesById in response to MESSAGES_CLEAR action", () => {
+ const { dispatch, getState } = setupStore();
+
+ dispatch(actions.messagesDisable(["message1", "message2"]));
+
+ let disabledMessages = getAllDisabledMessagesById(getState());
+ expect(disabledMessages).toEqual(["message1", "message2"]);
+
+ dispatch(actions.messagesClear());
+
+ disabledMessages = getAllDisabledMessagesById(getState());
+ expect(disabledMessages).toEqual([]);
+ });
+
+ it("remove message id from disabledMessagesById when the message is removed", () => {
+ const { dispatch, getState } = setupStore(
+ [
+ "console.log('foobar', 'test')",
+ "XHR GET request update",
+ "console.log(undefined)",
+ ],
+ {
+ actions,
+ storeOptions: {
+ logLimit: 3,
+ },
+ }
+ );
+
+ // This is `console.log('foobar', 'test'`
+ const firstMessageId = getMessageAt(getState(), 0).id;
+ // This is for `XHR GET request update`
+ const secondMessageId = getMessageAt(getState(), 1).id;
+
+ dispatch(actions.messagesDisable([firstMessageId, secondMessageId]));
+
+ let disabledMessages = getAllDisabledMessagesById(getState());
+ expect(disabledMessages).toEqual([firstMessageId, secondMessageId]);
+
+ // Adding a new message should prune the first(oldest) message and should
+ // remove its id from the disabled messages list.
+ const packet = stubPackets.get("GET request");
+ dispatch(actions.messagesAdd([packet]));
+
+ disabledMessages = getAllDisabledMessagesById(getState());
+ expect(disabledMessages).toEqual([secondMessageId]);
+ });
+ });
+});
diff --git a/devtools/client/webconsole/test/node/store/network-messages.test.js b/devtools/client/webconsole/test/node/store/network-messages.test.js
new file mode 100644
index 0000000000..1daba02f2d
--- /dev/null
+++ b/devtools/client/webconsole/test/node/store/network-messages.test.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const {
+ getAllNetworkMessagesUpdateById,
+} = require("resource://devtools/client/webconsole/selectors/messages.js");
+const {
+ setupActions,
+ setupStore,
+ clonePacket,
+} = require("resource://devtools/client/webconsole/test/node/helpers.js");
+const {
+ stubPackets,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+
+const expect = require("expect");
+
+describe("Network message reducer:", () => {
+ let actions;
+ let getState;
+ let dispatch;
+
+ beforeAll(() => {
+ actions = setupActions();
+ });
+
+ beforeEach(() => {
+ const store = setupStore();
+
+ getState = store.getState;
+ dispatch = store.dispatch;
+
+ const packet = clonePacket(stubPackets.get("GET request"));
+ const updatePacket = clonePacket(stubPackets.get("GET request update"));
+
+ packet.actor = "message1";
+ updatePacket.actor = "message1";
+ dispatch(actions.messagesAdd([packet]));
+ dispatch(actions.networkMessageUpdates([updatePacket], null));
+ });
+
+ describe("networkMessagesUpdateById", () => {
+ it("adds fetched HTTP request headers", () => {
+ const headers = {
+ headers: [],
+ };
+
+ dispatch(
+ actions.networkUpdateRequests([
+ {
+ id: "message1",
+ data: {
+ requestHeaders: headers,
+ },
+ },
+ ])
+ );
+
+ const networkUpdates = getAllNetworkMessagesUpdateById(getState());
+ expect(networkUpdates.message1.requestHeaders).toBe(headers);
+ });
+
+ it("makes sure multiple HTTP updates of same request does not override", () => {
+ dispatch(
+ actions.networkUpdateRequests([
+ {
+ id: "message1",
+ data: {
+ stacktrace: [{}],
+ },
+ },
+ {
+ id: "message1",
+ data: {
+ requestHeaders: { headers: [] },
+ },
+ },
+ ])
+ );
+
+ const networkUpdates = getAllNetworkMessagesUpdateById(getState());
+ expect(networkUpdates.message1.requestHeaders).toNotBe(undefined);
+ expect(networkUpdates.message1.stacktrace).toNotBe(undefined);
+ });
+
+ it("adds fetched HTTP security info", () => {
+ const securityInfo = {
+ state: "insecure",
+ };
+
+ dispatch(
+ actions.networkUpdateRequests([
+ {
+ id: "message1",
+ data: {
+ securityInfo,
+ },
+ },
+ ])
+ );
+
+ const networkUpdates = getAllNetworkMessagesUpdateById(getState());
+ expect(networkUpdates.message1.securityInfo).toBe(securityInfo);
+ expect(networkUpdates.message1.securityState).toBe("insecure");
+ });
+
+ it("adds fetched HTTP post data", () => {
+ const uploadHeaders = Symbol();
+ const requestPostData = {
+ postData: {
+ text: "",
+ },
+ uploadHeaders,
+ };
+
+ dispatch(
+ actions.networkUpdateRequests([
+ {
+ id: "message1",
+ data: {
+ requestPostData,
+ },
+ },
+ ])
+ );
+
+ const { message1 } = getAllNetworkMessagesUpdateById(getState());
+ expect(message1.requestPostData).toBe(requestPostData);
+ expect(message1.requestHeadersFromUploadStream).toBe(uploadHeaders);
+ });
+ });
+});
diff --git a/devtools/client/webconsole/test/node/store/private-messages.test.js b/devtools/client/webconsole/test/node/store/private-messages.test.js
new file mode 100644
index 0000000000..f040770436
--- /dev/null
+++ b/devtools/client/webconsole/test/node/store/private-messages.test.js
@@ -0,0 +1,252 @@
+/* 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 {
+ getAllMessagesUiById,
+ getAllCssMessagesMatchingElements,
+ getAllNetworkMessagesUpdateById,
+ getAllRepeatById,
+ getCurrentGroup,
+ getGroupsById,
+ getMutableMessagesById,
+ getVisibleMessages,
+} = require("resource://devtools/client/webconsole/selectors/messages.js");
+const {
+ getFirstMessage,
+ getLastMessage,
+ getPrivatePacket,
+ getWebConsoleUiMock,
+ setupActions,
+ setupStore,
+} = require("resource://devtools/client/webconsole/test/node/helpers.js");
+const {
+ stubPackets,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+const {
+ CSS_MESSAGE_ADD_MATCHING_ELEMENTS,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+const expect = require("expect");
+
+describe("private messages", () => {
+ let actions;
+ beforeAll(() => {
+ actions = setupActions();
+ });
+
+ it("removes private messages on PRIVATE_MESSAGES_CLEAR action", () => {
+ const { dispatch, getState } = setupStore();
+
+ dispatch(
+ actions.messagesAdd([
+ getPrivatePacket("console.trace()"),
+ stubPackets.get("console.log('mymap')"),
+ getPrivatePacket("console.log(undefined)"),
+ getPrivatePacket("GET request"),
+ ])
+ );
+
+ let state = getState();
+ const messages = getMutableMessagesById(state);
+ expect(messages.size).toBe(4);
+
+ dispatch(actions.privateMessagesClear());
+
+ state = getState();
+ expect(getMutableMessagesById(state).size).toBe(1);
+ expect(getVisibleMessages(state).length).toBe(1);
+ });
+
+ it("cleans messagesUiById on PRIVATE_MESSAGES_CLEAR action", () => {
+ const { dispatch, getState } = setupStore();
+
+ dispatch(
+ actions.messagesAdd([
+ getPrivatePacket("console.trace()"),
+ stubPackets.get("console.trace()"),
+ ])
+ );
+
+ let state = getState();
+ expect(getAllMessagesUiById(state).length).toBe(2);
+
+ dispatch(actions.privateMessagesClear());
+
+ state = getState();
+ expect(getAllMessagesUiById(state).length).toBe(1);
+ });
+
+ it("cleans repeatsById on PRIVATE_MESSAGES_CLEAR action", () => {
+ const { dispatch, getState } = setupStore();
+
+ dispatch(
+ actions.messagesAdd([
+ getPrivatePacket("console.log(undefined)"),
+ getPrivatePacket("console.log(undefined)"),
+ stubPackets.get("console.log(undefined)"),
+ stubPackets.get("console.log(undefined)"),
+ ])
+ );
+
+ let state = getState();
+ expect(getAllRepeatById(state)).toEqual({
+ [getFirstMessage(state).id]: 2,
+ [getLastMessage(state).id]: 2,
+ });
+
+ dispatch(actions.privateMessagesClear());
+
+ state = getState();
+ expect(Object.keys(getAllRepeatById(state)).length).toBe(1);
+ expect(getAllRepeatById(state)).toEqual({
+ [getFirstMessage(state).id]: 2,
+ });
+ });
+
+ it("cleans cssMessagesMatchingElements on PRIVATE_MESSAGES_CLEAR action", () => {
+ const { dispatch, getState } = setupStore();
+
+ dispatch(
+ actions.messagesAdd([
+ getPrivatePacket(
+ `Unknown property ‘such-unknown-property’. Declaration dropped.`
+ ),
+ stubPackets.get(
+ `Error in parsing value for ‘padding-top’. Declaration dropped.`
+ ),
+ ])
+ );
+
+ const privateData = Symbol("privateData");
+ const publicData = Symbol("publicData");
+
+ dispatch({
+ type: CSS_MESSAGE_ADD_MATCHING_ELEMENTS,
+ id: getFirstMessage(getState()).id,
+ elements: privateData,
+ });
+
+ dispatch({
+ type: CSS_MESSAGE_ADD_MATCHING_ELEMENTS,
+ id: getLastMessage(getState()).id,
+ elements: publicData,
+ });
+
+ let state = getState();
+ expect(getAllCssMessagesMatchingElements(state).size).toBe(2);
+
+ dispatch(actions.privateMessagesClear());
+
+ state = getState();
+ expect(getAllCssMessagesMatchingElements(state).size).toBe(1);
+ expect(
+ getAllCssMessagesMatchingElements(state).get(
+ getFirstMessage(getState()).id
+ )
+ ).toBe(publicData);
+ });
+
+ it("cleans group properties on PRIVATE_MESSAGES_CLEAR action", () => {
+ const { dispatch, getState } = setupStore();
+ dispatch(
+ actions.messagesAdd([
+ stubPackets.get("console.group()"),
+ getPrivatePacket("console.group()"),
+ ])
+ );
+
+ let state = getState();
+ const publicMessageId = getFirstMessage(state).id;
+ const privateMessageId = getLastMessage(state).id;
+ expect(getCurrentGroup(state)).toBe(privateMessageId);
+ expect(getGroupsById(state).size).toBe(2);
+
+ dispatch(actions.privateMessagesClear());
+
+ state = getState();
+ expect(getGroupsById(state).size).toBe(1);
+ expect(getGroupsById(state).has(publicMessageId)).toBe(true);
+ expect(getCurrentGroup(state)).toBe(publicMessageId);
+ });
+
+ it("cleans networkMessagesUpdateById on PRIVATE_MESSAGES_CLEAR action", () => {
+ const { dispatch, getState } = setupStore();
+
+ const publicActor = "network/public";
+ const privateActor = "network/private";
+ const publicPacket = {
+ ...stubPackets.get("GET request"),
+ actor: publicActor,
+ };
+ const privatePacket = {
+ ...getPrivatePacket("XHR GET request"),
+ actor: privateActor,
+ };
+
+ // We need to reassign the timeStamp of the packet to guarantee the order.
+ publicPacket.timeStamp = publicPacket.timeStamp + 1;
+ privatePacket.timeStamp = privatePacket.timeStamp + 2;
+
+ dispatch(actions.messagesAdd([publicPacket, privatePacket]));
+
+ let networkUpdates = getAllNetworkMessagesUpdateById(getState());
+ expect(Object.keys(networkUpdates)).toEqual([publicActor, privateActor]);
+
+ dispatch(actions.privateMessagesClear());
+
+ networkUpdates = getAllNetworkMessagesUpdateById(getState());
+ expect(Object.keys(networkUpdates)).toEqual([publicActor]);
+ });
+
+ it("releases private backend actors on PRIVATE_MESSAGES_CLEAR action", () => {
+ const releasedActors = [];
+ const { dispatch, getState } = setupStore([], {
+ webConsoleUI: getWebConsoleUiMock({
+ commands: {
+ client: {
+ mainRoot: {
+ supportsReleaseActors: true,
+ },
+ },
+ objectCommand: {
+ releaseObjects: async frontsToRelease => {
+ for (const front of frontsToRelease) {
+ releasedActors.push(front.actorID);
+ }
+ },
+ },
+ },
+ }),
+ });
+ const mockFrontRelease = function () {
+ releasedActors.push(this.actorID);
+ };
+
+ const publicPacket = stubPackets.get(
+ "console.log('myarray', ['red', 'green', 'blue'])"
+ );
+ const privatePacket = getPrivatePacket("console.log('mymap')");
+
+ publicPacket.message.arguments[1].release = mockFrontRelease;
+ privatePacket.message.arguments[1].release = mockFrontRelease;
+
+ // Add a log message.
+ dispatch(actions.messagesAdd([publicPacket, privatePacket]));
+
+ const firstMessage = getFirstMessage(getState());
+ const firstMessageActor = firstMessage.parameters[1].actorID;
+
+ const lastMessage = getLastMessage(getState());
+ const lastMessageActor = lastMessage.parameters[1].actorID;
+
+ // Kick-off the actor release.
+ dispatch(actions.privateMessagesClear());
+
+ expect(releasedActors.length).toBe(1);
+ expect(releasedActors).toInclude(lastMessageActor);
+ expect(releasedActors).toNotInclude(firstMessageActor);
+ });
+});
diff --git a/devtools/client/webconsole/test/node/store/release-actors.test.js b/devtools/client/webconsole/test/node/store/release-actors.test.js
new file mode 100644
index 0000000000..28339653b7
--- /dev/null
+++ b/devtools/client/webconsole/test/node/store/release-actors.test.js
@@ -0,0 +1,203 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const {
+ getFirstMessage,
+ setupActions,
+ setupStore,
+ getWebConsoleUiMock,
+} = require("resource://devtools/client/webconsole/test/node/helpers.js");
+
+const {
+ stubPackets,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+const expect = require("expect");
+
+describe("Release actor enhancer:", () => {
+ let actions;
+
+ beforeAll(() => {
+ actions = setupActions();
+ });
+
+ describe("release", () => {
+ it("releases backend actors when limit reached adding a single message", () => {
+ const logLimit = 100;
+ const releasedActors = [];
+
+ const { dispatch, getState } = setupStore([], {
+ storeOptions: { logLimit },
+ webConsoleUI: getWebConsoleUiMock({
+ commands: {
+ client: {
+ mainRoot: {
+ supportsReleaseActors: true,
+ },
+ },
+ objectCommand: {
+ releaseObjects: async frontsToRelease => {
+ for (const front of frontsToRelease) {
+ releasedActors.push(front.actorID);
+ }
+ },
+ },
+ },
+ }),
+ });
+
+ // Add a log message.
+ const packet = stubPackets.get(
+ "console.log('myarray', ['red', 'green', 'blue'])"
+ );
+ dispatch(actions.messagesAdd([packet]));
+
+ const firstMessage = getFirstMessage(getState());
+ const firstMessageActor = firstMessage.parameters[1].actorID;
+
+ // Add an evaluation result message (see Bug 1408321).
+ const evaluationResultPacket = stubPackets.get("new Date(0)");
+ dispatch(actions.messagesAdd([evaluationResultPacket]));
+ const secondMessageActor = evaluationResultPacket.result.actorID;
+
+ const logCount = logLimit + 1;
+ const assertPacket = stubPackets.get(
+ "console.assert(false, {message: 'foobar'})"
+ );
+ const thirdMessageActor = assertPacket.message.arguments[0].actorID;
+
+ for (let i = 1; i <= logCount; i++) {
+ assertPacket.message.arguments.push(`message num ${i}`);
+ dispatch(actions.messagesAdd([assertPacket]));
+ }
+
+ expect(releasedActors.length).toBe(3);
+ expect(releasedActors).toInclude(firstMessageActor);
+ expect(releasedActors).toInclude(secondMessageActor);
+ expect(releasedActors).toInclude(thirdMessageActor);
+ });
+
+ it("releases backend actors when limit reached adding multiple messages", () => {
+ const logLimit = 100;
+ const releasedActors = [];
+ const { dispatch, getState } = setupStore([], {
+ storeOptions: { logLimit },
+ webConsoleUI: getWebConsoleUiMock({
+ commands: {
+ client: {
+ mainRoot: {
+ supportsReleaseActors: true,
+ },
+ },
+ objectCommand: {
+ releaseObjects: async frontsToRelease => {
+ for (const front of frontsToRelease) {
+ releasedActors.push(front.actorID);
+ }
+ },
+ },
+ },
+ }),
+ });
+
+ // Add a log message.
+ const logPacket = stubPackets.get(
+ "console.log('myarray', ['red', 'green', 'blue'])"
+ );
+ dispatch(actions.messagesAdd([logPacket]));
+
+ const firstMessage = getFirstMessage(getState());
+ const firstMessageActor = firstMessage.parameters[1].actorID;
+
+ // Add an evaluation result message (see Bug 1408321).
+ const evaluationResultPacket = stubPackets.get("new Date(0)");
+ dispatch(actions.messagesAdd([evaluationResultPacket]));
+ const secondMessageActor = evaluationResultPacket.result.actorID;
+
+ // Add an assertion message.
+ const assertPacket = stubPackets.get(
+ "console.assert(false, {message: 'foobar'})"
+ );
+ dispatch(actions.messagesAdd([assertPacket]));
+ const thirdMessageActor = assertPacket.message.arguments[0].actorID;
+
+ // Add ${logLimit} messages so we prune the ones we added before.
+ const packets = [];
+ // Alternate between 2 packets so we don't trigger the repeat message mechanism.
+ const oddPacket = stubPackets.get("console.log(undefined)");
+ const evenPacket = stubPackets.get("console.log('foobar', 'test')");
+ for (let i = 0; i < logLimit; i++) {
+ const packet = i % 2 === 0 ? evenPacket : oddPacket;
+ packets.push(packet);
+ }
+
+ // Add all the packets at once. This will prune the first 3 messages.
+ dispatch(actions.messagesAdd(packets));
+
+ expect(releasedActors.length).toBe(3);
+ expect(releasedActors).toInclude(firstMessageActor);
+ expect(releasedActors).toInclude(secondMessageActor);
+ expect(releasedActors).toInclude(thirdMessageActor);
+ });
+
+ it("properly releases backend actors after clear", () => {
+ const releasedActors = [];
+ const { dispatch, getState } = setupStore([], {
+ webConsoleUI: getWebConsoleUiMock({
+ commands: {
+ client: {
+ mainRoot: {
+ supportsReleaseActors: true,
+ },
+ },
+ objectCommand: {
+ releaseObjects: async frontsToRelease => {
+ for (const front of frontsToRelease) {
+ releasedActors.push(front.actorID);
+ }
+ },
+ },
+ },
+ }),
+ });
+
+ // Add a log message.
+ const logPacket = stubPackets.get(
+ "console.log('myarray', ['red', 'green', 'blue'])"
+ );
+ dispatch(actions.messagesAdd([logPacket]));
+
+ const firstMessage = getFirstMessage(getState());
+ const firstMessageActor = firstMessage.parameters[1].actorID;
+
+ // Add an assertion message.
+ const assertPacket = stubPackets.get(
+ "console.assert(false, {message: 'foobar'})"
+ );
+ dispatch(actions.messagesAdd([assertPacket]));
+ const secondMessageActor = assertPacket.message.arguments[0].actorID;
+
+ // Add an evaluation result message (see Bug 1408321).
+ const evaluationResultPacket = stubPackets.get("new Date(0)");
+ dispatch(actions.messagesAdd([evaluationResultPacket]));
+ const thirdMessageActor = evaluationResultPacket.result.actorID;
+
+ // Add a message with a long string messageText property.
+ const longStringPacket = stubPackets.get("TypeError longString message");
+ dispatch(actions.messagesAdd([longStringPacket]));
+ const fourthMessageActor =
+ longStringPacket.pageError.errorMessage.actorID;
+ const fifthMessageActor = longStringPacket.pageError.exception.actorID;
+
+ // Kick-off the actor release.
+ dispatch(actions.messagesClear());
+
+ expect(releasedActors.length).toBe(5);
+ expect(releasedActors).toInclude(firstMessageActor);
+ expect(releasedActors).toInclude(secondMessageActor);
+ expect(releasedActors).toInclude(thirdMessageActor);
+ expect(releasedActors).toInclude(fourthMessageActor);
+ expect(releasedActors).toInclude(fifthMessageActor);
+ });
+ });
+});
diff --git a/devtools/client/webconsole/test/node/store/search.test.js b/devtools/client/webconsole/test/node/store/search.test.js
new file mode 100644
index 0000000000..7a0f7ebe22
--- /dev/null
+++ b/devtools/client/webconsole/test/node/store/search.test.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const expect = require("expect");
+
+const actions = require("resource://devtools/client/webconsole/actions/index.js");
+const {
+ getVisibleMessages,
+} = require("resource://devtools/client/webconsole/selectors/messages.js");
+const {
+ setupStore,
+} = require("resource://devtools/client/webconsole/test/node/helpers.js");
+
+describe("Searching in grips", () => {
+ let store;
+
+ beforeEach(() => {
+ store = prepareBaseStore();
+ store.dispatch(actions.filtersClear());
+ });
+
+ describe("Search in table & array & object props", () => {
+ it("matches on value grips", () => {
+ store.dispatch(actions.filterTextSet("red"));
+ expect(getVisibleMessages(store.getState()).length).toEqual(3);
+ });
+ });
+
+ describe("Search in object value", () => {
+ it("matches on value grips", () => {
+ store.dispatch(actions.filterTextSet("redValue"));
+ expect(getVisibleMessages(store.getState()).length).toEqual(1);
+ });
+ });
+
+ describe("Search in regex", () => {
+ it("matches on value grips", () => {
+ store.dispatch(actions.filterTextSet("a.b.c"));
+ expect(getVisibleMessages(store.getState()).length).toEqual(1);
+ });
+ });
+
+ describe("Search in map values", () => {
+ it("matches on value grips", () => {
+ store.dispatch(actions.filterTextSet("value1"));
+ expect(getVisibleMessages(store.getState()).length).toEqual(1);
+ });
+ });
+
+ describe("Search in map keys", () => {
+ it("matches on value grips", () => {
+ store.dispatch(actions.filterTextSet("key1"));
+ expect(getVisibleMessages(store.getState()).length).toEqual(1);
+ });
+ });
+
+ describe("Search in text", () => {
+ it("matches on value grips", () => {
+ store.dispatch(actions.filterTextSet("myobj"));
+ expect(getVisibleMessages(store.getState()).length).toEqual(1);
+ });
+ });
+
+ describe("Search in logs with net messages", () => {
+ it("matches on network messages", () => {
+ store.dispatch(actions.filterToggle("net"));
+ store.dispatch(actions.filterTextSet("get"));
+ expect(getVisibleMessages(store.getState()).length).toEqual(1);
+ });
+ });
+
+ describe("Search in frame", () => {
+ it("matches on file name", () => {
+ store.dispatch(actions.filterTextSet("test-console-api.html:1:35"));
+ expect(getVisibleMessages(store.getState()).length).toEqual(7);
+ });
+
+ it("do not match on full url", () => {
+ store.dispatch(
+ actions.filterTextSet("https://example.com/browser/devtools")
+ );
+ expect(getVisibleMessages(store.getState()).length).toEqual(0);
+ });
+ });
+
+ describe("Reverse search", () => {
+ it("reverse matches on value grips", () => {
+ store.dispatch(actions.filterTextSet("-red"));
+ expect(getVisibleMessages(store.getState()).length).toEqual(6);
+ });
+
+ it("reverse matches on file name", () => {
+ store.dispatch(actions.filterTextSet("-test-console-api.html:1:35"));
+ expect(getVisibleMessages(store.getState()).length).toEqual(2);
+ });
+ });
+});
+
+function prepareBaseStore() {
+ const store = setupStore([
+ "console.log('foobar', 'test')",
+ "console.warn('danger, will robinson!')",
+ "console.table(['red', 'green', 'blue']);",
+ "console.count('bar')",
+ "console.log('myarray', ['red', 'green', 'blue'])",
+ "console.log('myregex', /a.b.c/)",
+ "console.log('mymap')",
+ "console.log('myobject', {red: 'redValue', green: 'greenValue', blue: 'blueValue'});",
+ "GET request update",
+ ]);
+
+ return store;
+}
diff --git a/devtools/client/webconsole/test/node/store/ui.test.js b/devtools/client/webconsole/test/node/store/ui.test.js
new file mode 100644
index 0000000000..2786fb9a0f
--- /dev/null
+++ b/devtools/client/webconsole/test/node/store/ui.test.js
@@ -0,0 +1,119 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const expect = require("expect");
+
+const actions = require("resource://devtools/client/webconsole/actions/index.js");
+const {
+ setupStore,
+ getFirstMessage,
+ getLastMessage,
+} = require("resource://devtools/client/webconsole/test/node/helpers.js");
+const {
+ stubPackets,
+ stubPreparedMessages,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+
+describe("Testing UI", () => {
+ let store;
+
+ beforeEach(() => {
+ store = setupStore();
+ });
+
+ describe("Toggle sidebar", () => {
+ it("sidebar is toggled on and off", () => {
+ const packet = stubPackets.get("inspect({a: 1})");
+ const message = stubPreparedMessages.get("inspect({a: 1})");
+ store.dispatch(actions.messagesAdd([packet]));
+
+ const { actorID } = message.parameters[0];
+ const messageId = getFirstMessage(store.getState()).id;
+ store.dispatch(actions.showMessageObjectInSidebar(actorID, messageId));
+
+ expect(store.getState().ui.sidebarVisible).toEqual(true);
+ store.dispatch(actions.sidebarClose());
+ expect(store.getState().ui.sidebarVisible).toEqual(false);
+ });
+ });
+
+ describe("Hide sidebar on clear", () => {
+ it("sidebar is hidden on clear", () => {
+ const packet = stubPackets.get("inspect({a: 1})");
+ const message = stubPreparedMessages.get("inspect({a: 1})");
+ store.dispatch(actions.messagesAdd([packet]));
+
+ const { actorID } = message.parameters[0];
+ const messageId = getFirstMessage(store.getState()).id;
+ store.dispatch(actions.showMessageObjectInSidebar(actorID, messageId));
+
+ expect(store.getState().ui.sidebarVisible).toEqual(true);
+ store.dispatch(actions.messagesClear());
+ expect(store.getState().ui.sidebarVisible).toEqual(false);
+ store.dispatch(actions.messagesClear());
+ expect(store.getState().ui.sidebarVisible).toEqual(false);
+ });
+ });
+
+ describe("Show object in sidebar", () => {
+ it("sidebar is shown with correct object", () => {
+ const packet = stubPackets.get("inspect({a: 1})");
+ const message = stubPreparedMessages.get("inspect({a: 1})");
+ store.dispatch(actions.messagesAdd([packet]));
+
+ const { actorID } = message.parameters[0];
+ const messageId = getFirstMessage(store.getState()).id;
+ store.dispatch(actions.showMessageObjectInSidebar(actorID, messageId));
+
+ expect(store.getState().ui.sidebarVisible).toEqual(true);
+ expect(store.getState().ui.frontInSidebar).toEqual(message.parameters[0]);
+ });
+
+ it("sidebar is not updated for the same object", () => {
+ const packet = stubPackets.get("inspect({a: 1})");
+ const message = stubPreparedMessages.get("inspect({a: 1})");
+ store.dispatch(actions.messagesAdd([packet]));
+
+ const { actorID } = message.parameters[0];
+ const messageId = getFirstMessage(store.getState()).id;
+ store.dispatch(actions.showMessageObjectInSidebar(actorID, messageId));
+
+ expect(store.getState().ui.sidebarVisible).toEqual(true);
+ expect(store.getState().ui.frontInSidebar).toEqual(message.parameters[0]);
+ const state = store.getState().ui;
+
+ store.dispatch(actions.showMessageObjectInSidebar(actorID, messageId));
+ expect(store.getState().ui).toEqual(state);
+ });
+
+ it("sidebar shown and updated for new object", () => {
+ const packet = stubPackets.get("inspect({a: 1})");
+ const message = stubPreparedMessages.get("inspect({a: 1})");
+ store.dispatch(actions.messagesAdd([packet]));
+
+ const { actorID } = message.parameters[0];
+ const messageId = getFirstMessage(store.getState()).id;
+ store.dispatch(actions.showMessageObjectInSidebar(actorID, messageId));
+
+ expect(store.getState().ui.sidebarVisible).toEqual(true);
+ expect(store.getState().ui.frontInSidebar).toEqual(message.parameters[0]);
+
+ const newPacket = stubPackets.get("new Date(0)");
+ const newMessage = stubPreparedMessages.get("new Date(0)");
+ store.dispatch(actions.messagesAdd([newPacket]));
+
+ const newActorID = newMessage.parameters[0].actorID;
+ const newMessageId = getLastMessage(store.getState()).id;
+ store.dispatch(
+ actions.showMessageObjectInSidebar(newActorID, newMessageId)
+ );
+
+ expect(store.getState().ui.sidebarVisible).toEqual(true);
+ expect(store.getState().ui.frontInSidebar).toEqual(
+ newMessage.parameters[0]
+ );
+ });
+ });
+});
diff --git a/devtools/client/webconsole/test/node/utils/areMessagesSimilar.test.js b/devtools/client/webconsole/test/node/utils/areMessagesSimilar.test.js
new file mode 100644
index 0000000000..23d3bae986
--- /dev/null
+++ b/devtools/client/webconsole/test/node/utils/areMessagesSimilar.test.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const {
+ areMessagesSimilar,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+const {
+ stubPreparedMessages,
+} = require("resource://devtools/client/webconsole/test/node/fixtures/stubs/index.js");
+
+const expect = require("expect");
+
+describe("areMessagesSimilar:", () => {
+ it("returns true for duplicated messages", () => {
+ const baseMessage = stubPreparedMessages.get(
+ "console.log('foobar', 'test')"
+ );
+
+ // Repeat ID must be the same even if the timestamp is different.
+ const message1 = Object.assign({}, baseMessage, { timeStamp: 1 });
+ const message2 = Object.assign({}, baseMessage, { timeStamp: 2 });
+
+ expect(areMessagesSimilar(message1, message2)).toEqual(true);
+ });
+
+ it("returns false for different messages", () => {
+ const message1 = stubPreparedMessages.get("console.log('foobar', 'test')");
+ const message2 = Object.assign({}, message1, {
+ parameters: ["funny", "monkey"],
+ });
+ expect(areMessagesSimilar(message1, message2)).toEqual(false);
+ });
+
+ it("returns false for messages with different severities", () => {
+ const message1 = stubPreparedMessages.get("console.log('foobar', 'test')");
+ const message2 = Object.assign({}, message1, { level: "error" });
+ expect(areMessagesSimilar(message1, message2)).toEqual(false);
+ });
+
+ it("return false for messages with different falsy values", () => {
+ const messageNaN = stubPreparedMessages.get("console.log(NaN)");
+ const messageUnd = stubPreparedMessages.get("console.log(undefined)");
+ const messageNul = stubPreparedMessages.get("console.log(null)");
+
+ expect(areMessagesSimilar(messageNaN, messageUnd)).toEqual(false);
+ expect(areMessagesSimilar(messageUnd, messageNul)).toEqual(false);
+ expect(areMessagesSimilar(messageNul, messageNaN)).toEqual(false);
+ });
+});
diff --git a/devtools/client/webconsole/test/node/yarn.lock b/devtools/client/webconsole/test/node/yarn.lock
new file mode 100644
index 0000000000..b6b1249f54
--- /dev/null
+++ b/devtools/client/webconsole/test/node/yarn.lock
@@ -0,0 +1,6057 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@ampproject/remapping@^2.1.0":
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d"
+ integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==
+ dependencies:
+ "@jridgewell/gen-mapping" "^0.1.0"
+ "@jridgewell/trace-mapping" "^0.3.9"
+
+"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a"
+ integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==
+ dependencies:
+ "@babel/highlight" "^7.18.6"
+
+"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.1", "@babel/compat-data@^7.20.5":
+ version "7.20.10"
+ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.10.tgz#9d92fa81b87542fff50e848ed585b4212c1d34ec"
+ integrity sha512-sEnuDPpOJR/fcafHMjpcpGN5M2jbUGUHwmuWKM/YdPzeEDJg8bgmbcWQFUfE32MQjti1koACvoPVsDe8Uq+idg==
+
+"@babel/core@^7.1.0", "@babel/core@^7.20.12":
+ version "7.20.12"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.12.tgz#7930db57443c6714ad216953d1356dac0eb8496d"
+ integrity sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==
+ dependencies:
+ "@ampproject/remapping" "^2.1.0"
+ "@babel/code-frame" "^7.18.6"
+ "@babel/generator" "^7.20.7"
+ "@babel/helper-compilation-targets" "^7.20.7"
+ "@babel/helper-module-transforms" "^7.20.11"
+ "@babel/helpers" "^7.20.7"
+ "@babel/parser" "^7.20.7"
+ "@babel/template" "^7.20.7"
+ "@babel/traverse" "^7.20.12"
+ "@babel/types" "^7.20.7"
+ convert-source-map "^1.7.0"
+ debug "^4.1.0"
+ gensync "^1.0.0-beta.2"
+ json5 "^2.2.2"
+ semver "^6.3.0"
+
+"@babel/generator@^7.20.7", "@babel/generator@^7.4.0":
+ version "7.20.7"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.7.tgz#f8ef57c8242665c5929fe2e8d82ba75460187b4a"
+ integrity sha512-7wqMOJq8doJMZmP4ApXTzLxSr7+oO2jroJURrVEp6XShrQUObV8Tq/D0NCcoYg2uHqUrjzO0zwBjoYzelxK+sw==
+ dependencies:
+ "@babel/types" "^7.20.7"
+ "@jridgewell/gen-mapping" "^0.3.2"
+ jsesc "^2.5.1"
+
+"@babel/helper-annotate-as-pure@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb"
+ integrity sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==
+ dependencies:
+ "@babel/types" "^7.18.6"
+
+"@babel/helper-builder-binary-assignment-operator-visitor@^7.18.6":
+ version "7.18.9"
+ resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz#acd4edfd7a566d1d51ea975dff38fd52906981bb"
+ integrity sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==
+ dependencies:
+ "@babel/helper-explode-assignable-expression" "^7.18.6"
+ "@babel/types" "^7.18.9"
+
+"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.0", "@babel/helper-compilation-targets@^7.20.7":
+ version "7.20.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz#a6cd33e93629f5eb473b021aac05df62c4cd09bb"
+ integrity sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==
+ dependencies:
+ "@babel/compat-data" "^7.20.5"
+ "@babel/helper-validator-option" "^7.18.6"
+ browserslist "^4.21.3"
+ lru-cache "^5.1.1"
+ semver "^6.3.0"
+
+"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.20.5", "@babel/helper-create-class-features-plugin@^7.20.7":
+ version "7.20.12"
+ resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.12.tgz#4349b928e79be05ed2d1643b20b99bb87c503819"
+ integrity sha512-9OunRkbT0JQcednL0UFvbfXpAsUXiGjUk0a7sN8fUXX7Mue79cUSMjHGDRRi/Vz9vYlpIhLV5fMD5dKoMhhsNQ==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.18.6"
+ "@babel/helper-environment-visitor" "^7.18.9"
+ "@babel/helper-function-name" "^7.19.0"
+ "@babel/helper-member-expression-to-functions" "^7.20.7"
+ "@babel/helper-optimise-call-expression" "^7.18.6"
+ "@babel/helper-replace-supers" "^7.20.7"
+ "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0"
+ "@babel/helper-split-export-declaration" "^7.18.6"
+
+"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.20.5":
+ version "7.20.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.20.5.tgz#5ea79b59962a09ec2acf20a963a01ab4d076ccca"
+ integrity sha512-m68B1lkg3XDGX5yCvGO0kPx3v9WIYLnzjKfPcQiwntEQa5ZeRkPmo2X/ISJc8qxWGfwUr+kvZAeEzAwLec2r2w==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.18.6"
+ regexpu-core "^5.2.1"
+
+"@babel/helper-define-polyfill-provider@^0.3.3":
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz#8612e55be5d51f0cd1f36b4a5a83924e89884b7a"
+ integrity sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==
+ dependencies:
+ "@babel/helper-compilation-targets" "^7.17.7"
+ "@babel/helper-plugin-utils" "^7.16.7"
+ debug "^4.1.1"
+ lodash.debounce "^4.0.8"
+ resolve "^1.14.2"
+ semver "^6.1.2"
+
+"@babel/helper-environment-visitor@^7.18.9":
+ version "7.18.9"
+ resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be"
+ integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==
+
+"@babel/helper-explode-assignable-expression@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096"
+ integrity sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==
+ dependencies:
+ "@babel/types" "^7.18.6"
+
+"@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.19.0":
+ version "7.19.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz#941574ed5390682e872e52d3f38ce9d1bef4648c"
+ integrity sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==
+ dependencies:
+ "@babel/template" "^7.18.10"
+ "@babel/types" "^7.19.0"
+
+"@babel/helper-hoist-variables@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678"
+ integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==
+ dependencies:
+ "@babel/types" "^7.18.6"
+
+"@babel/helper-member-expression-to-functions@^7.20.7":
+ version "7.20.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.20.7.tgz#a6f26e919582275a93c3aa6594756d71b0bb7f05"
+ integrity sha512-9J0CxJLq315fEdi4s7xK5TQaNYjZw+nDVpVqr1axNGKzdrdwYBD5b4uKv3n75aABG0rCCTK8Im8Ww7eYfMrZgw==
+ dependencies:
+ "@babel/types" "^7.20.7"
+
+"@babel/helper-module-imports@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e"
+ integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==
+ dependencies:
+ "@babel/types" "^7.18.6"
+
+"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.20.11":
+ version "7.20.11"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz#df4c7af713c557938c50ea3ad0117a7944b2f1b0"
+ integrity sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg==
+ dependencies:
+ "@babel/helper-environment-visitor" "^7.18.9"
+ "@babel/helper-module-imports" "^7.18.6"
+ "@babel/helper-simple-access" "^7.20.2"
+ "@babel/helper-split-export-declaration" "^7.18.6"
+ "@babel/helper-validator-identifier" "^7.19.1"
+ "@babel/template" "^7.20.7"
+ "@babel/traverse" "^7.20.10"
+ "@babel/types" "^7.20.7"
+
+"@babel/helper-optimise-call-expression@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz#9369aa943ee7da47edab2cb4e838acf09d290ffe"
+ integrity sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==
+ dependencies:
+ "@babel/types" "^7.18.6"
+
+"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.8.3":
+ version "7.20.2"
+ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629"
+ integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==
+
+"@babel/helper-plugin-utils@^7.8.0":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz#9ea293be19babc0f52ff8ca88b34c3611b208670"
+ integrity sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==
+
+"@babel/helper-remap-async-to-generator@^7.18.9":
+ version "7.18.9"
+ resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz#997458a0e3357080e54e1d79ec347f8a8cd28519"
+ integrity sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.18.6"
+ "@babel/helper-environment-visitor" "^7.18.9"
+ "@babel/helper-wrap-function" "^7.18.9"
+ "@babel/types" "^7.18.9"
+
+"@babel/helper-replace-supers@^7.18.6", "@babel/helper-replace-supers@^7.20.7":
+ version "7.20.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz#243ecd2724d2071532b2c8ad2f0f9f083bcae331"
+ integrity sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==
+ dependencies:
+ "@babel/helper-environment-visitor" "^7.18.9"
+ "@babel/helper-member-expression-to-functions" "^7.20.7"
+ "@babel/helper-optimise-call-expression" "^7.18.6"
+ "@babel/template" "^7.20.7"
+ "@babel/traverse" "^7.20.7"
+ "@babel/types" "^7.20.7"
+
+"@babel/helper-simple-access@^7.20.2":
+ version "7.20.2"
+ resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz#0ab452687fe0c2cfb1e2b9e0015de07fc2d62dd9"
+ integrity sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==
+ dependencies:
+ "@babel/types" "^7.20.2"
+
+"@babel/helper-skip-transparent-expression-wrappers@^7.20.0":
+ version "7.20.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz#fbe4c52f60518cab8140d77101f0e63a8a230684"
+ integrity sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==
+ dependencies:
+ "@babel/types" "^7.20.0"
+
+"@babel/helper-split-export-declaration@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075"
+ integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==
+ dependencies:
+ "@babel/types" "^7.18.6"
+
+"@babel/helper-string-parser@^7.19.4":
+ version "7.19.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63"
+ integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==
+
+"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1":
+ version "7.19.1"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2"
+ integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==
+
+"@babel/helper-validator-option@^7.14.5", "@babel/helper-validator-option@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8"
+ integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==
+
+"@babel/helper-wrap-function@^7.18.9":
+ version "7.20.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz#75e2d84d499a0ab3b31c33bcfe59d6b8a45f62e3"
+ integrity sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==
+ dependencies:
+ "@babel/helper-function-name" "^7.19.0"
+ "@babel/template" "^7.18.10"
+ "@babel/traverse" "^7.20.5"
+ "@babel/types" "^7.20.5"
+
+"@babel/helpers@^7.20.7":
+ version "7.20.7"
+ resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.7.tgz#04502ff0feecc9f20ecfaad120a18f011a8e6dce"
+ integrity sha512-PBPjs5BppzsGaxHQCDKnZ6Gd9s6xl8bBCluz3vEInLGRJmnZan4F6BYCeqtyXqkk4W5IlPmjK4JlOuZkpJ3xZA==
+ dependencies:
+ "@babel/template" "^7.20.7"
+ "@babel/traverse" "^7.20.7"
+ "@babel/types" "^7.20.7"
+
+"@babel/highlight@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf"
+ integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==
+ dependencies:
+ "@babel/helper-validator-identifier" "^7.18.6"
+ chalk "^2.0.0"
+ js-tokens "^4.0.0"
+
+"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.4.3":
+ version "7.20.7"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.7.tgz#66fe23b3c8569220817d5feb8b9dcdc95bb4f71b"
+ integrity sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg==
+
+"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2"
+ integrity sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.18.6"
+
+"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.18.9":
+ version "7.20.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz#d9c85589258539a22a901033853101a6198d4ef1"
+ integrity sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.20.2"
+ "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0"
+ "@babel/plugin-proposal-optional-chaining" "^7.20.7"
+
+"@babel/plugin-proposal-async-generator-functions@^7.20.1":
+ version "7.20.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz#bfb7276d2d573cb67ba379984a2334e262ba5326"
+ integrity sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==
+ dependencies:
+ "@babel/helper-environment-visitor" "^7.18.9"
+ "@babel/helper-plugin-utils" "^7.20.2"
+ "@babel/helper-remap-async-to-generator" "^7.18.9"
+ "@babel/plugin-syntax-async-generators" "^7.8.4"
+
+"@babel/plugin-proposal-class-properties@7.18.6", "@babel/plugin-proposal-class-properties@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz#b110f59741895f7ec21a6fff696ec46265c446a3"
+ integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==
+ dependencies:
+ "@babel/helper-create-class-features-plugin" "^7.18.6"
+ "@babel/helper-plugin-utils" "^7.18.6"
+
+"@babel/plugin-proposal-class-static-block@^7.18.6":
+ version "7.20.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.20.7.tgz#92592e9029b13b15be0f7ce6a7aedc2879ca45a7"
+ integrity sha512-AveGOoi9DAjUYYuUAG//Ig69GlazLnoyzMw68VCDux+c1tsnnH/OkYcpz/5xzMkEFC6UxjR5Gw1c+iY2wOGVeQ==
+ dependencies:
+ "@babel/helper-create-class-features-plugin" "^7.20.7"
+ "@babel/helper-plugin-utils" "^7.20.2"
+ "@babel/plugin-syntax-class-static-block" "^7.14.5"
+
+"@babel/plugin-proposal-dynamic-import@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz#72bcf8d408799f547d759298c3c27c7e7faa4d94"
+ integrity sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.18.6"
+ "@babel/plugin-syntax-dynamic-import" "^7.8.3"
+
+"@babel/plugin-proposal-export-namespace-from@^7.18.9":
+ version "7.18.9"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz#5f7313ab348cdb19d590145f9247540e94761203"
+ integrity sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.18.9"
+ "@babel/plugin-syntax-export-namespace-from" "^7.8.3"
+
+"@babel/plugin-proposal-json-strings@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz#7e8788c1811c393aff762817e7dbf1ebd0c05f0b"
+ integrity sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.18.6"
+ "@babel/plugin-syntax-json-strings" "^7.8.3"
+
+"@babel/plugin-proposal-logical-assignment-operators@^7.18.9":
+ version "7.20.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz#dfbcaa8f7b4d37b51e8bfb46d94a5aea2bb89d83"
+ integrity sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.20.2"
+ "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4"
+
+"@babel/plugin-proposal-nullish-coalescing-operator@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz#fdd940a99a740e577d6c753ab6fbb43fdb9467e1"
+ integrity sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.18.6"
+ "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3"
+
+"@babel/plugin-proposal-numeric-separator@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz#899b14fbafe87f053d2c5ff05b36029c62e13c75"
+ integrity sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.18.6"
+ "@babel/plugin-syntax-numeric-separator" "^7.10.4"
+
+"@babel/plugin-proposal-object-rest-spread@^7.20.2":
+ version "7.20.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz#aa662940ef425779c75534a5c41e9d936edc390a"
+ integrity sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==
+ dependencies:
+ "@babel/compat-data" "^7.20.5"
+ "@babel/helper-compilation-targets" "^7.20.7"
+ "@babel/helper-plugin-utils" "^7.20.2"
+ "@babel/plugin-syntax-object-rest-spread" "^7.8.3"
+ "@babel/plugin-transform-parameters" "^7.20.7"
+
+"@babel/plugin-proposal-optional-catch-binding@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz#f9400d0e6a3ea93ba9ef70b09e72dd6da638a2cb"
+ integrity sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.18.6"
+ "@babel/plugin-syntax-optional-catch-binding" "^7.8.3"
+
+"@babel/plugin-proposal-optional-chaining@^7.18.9", "@babel/plugin-proposal-optional-chaining@^7.20.7":
+ version "7.20.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.20.7.tgz#49f2b372519ab31728cc14115bb0998b15bfda55"
+ integrity sha512-T+A7b1kfjtRM51ssoOfS1+wbyCVqorfyZhT99TvxxLMirPShD8CzKMRepMlCBGM5RpHMbn8s+5MMHnPstJH6mQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.20.2"
+ "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0"
+ "@babel/plugin-syntax-optional-chaining" "^7.8.3"
+
+"@babel/plugin-proposal-private-methods@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz#5209de7d213457548a98436fa2882f52f4be6bea"
+ integrity sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==
+ dependencies:
+ "@babel/helper-create-class-features-plugin" "^7.18.6"
+ "@babel/helper-plugin-utils" "^7.18.6"
+
+"@babel/plugin-proposal-private-property-in-object@^7.18.6":
+ version "7.20.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.20.5.tgz#309c7668f2263f1c711aa399b5a9a6291eef6135"
+ integrity sha512-Vq7b9dUA12ByzB4EjQTPo25sFhY+08pQDBSZRtUAkj7lb7jahaHR5igera16QZ+3my1nYR4dKsNdYj5IjPHilQ==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.18.6"
+ "@babel/helper-create-class-features-plugin" "^7.20.5"
+ "@babel/helper-plugin-utils" "^7.20.2"
+ "@babel/plugin-syntax-private-property-in-object" "^7.14.5"
+
+"@babel/plugin-proposal-unicode-property-regex@^7.18.6", "@babel/plugin-proposal-unicode-property-regex@^7.4.4":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz#af613d2cd5e643643b65cded64207b15c85cb78e"
+ integrity sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==
+ dependencies:
+ "@babel/helper-create-regexp-features-plugin" "^7.18.6"
+ "@babel/helper-plugin-utils" "^7.18.6"
+
+"@babel/plugin-syntax-async-generators@^7.8.4":
+ version "7.8.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
+ integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-class-properties@^7.12.13":
+ version "7.12.13"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10"
+ integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.12.13"
+
+"@babel/plugin-syntax-class-static-block@^7.14.5":
+ version "7.14.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406"
+ integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.14.5"
+
+"@babel/plugin-syntax-dynamic-import@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3"
+ integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-export-namespace-from@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a"
+ integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-syntax-import-assertions@^7.20.0":
+ version "7.20.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz#bb50e0d4bea0957235390641209394e87bdb9cc4"
+ integrity sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.19.0"
+
+"@babel/plugin-syntax-json-strings@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a"
+ integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-jsx@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0"
+ integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.18.6"
+
+"@babel/plugin-syntax-logical-assignment-operators@^7.10.4":
+ version "7.10.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699"
+ integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.10.4"
+
+"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9"
+ integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-numeric-separator@^7.10.4":
+ version "7.10.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97"
+ integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.10.4"
+
+"@babel/plugin-syntax-object-rest-spread@^7.0.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871"
+ integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-optional-catch-binding@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1"
+ integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-optional-chaining@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a"
+ integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-private-property-in-object@^7.14.5":
+ version "7.14.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad"
+ integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.14.5"
+
+"@babel/plugin-syntax-top-level-await@^7.14.5":
+ version "7.14.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c"
+ integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.14.5"
+
+"@babel/plugin-transform-arrow-functions@^7.18.6":
+ version "7.20.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz#bea332b0e8b2dab3dafe55a163d8227531ab0551"
+ integrity sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.20.2"
+
+"@babel/plugin-transform-async-to-generator@^7.18.6":
+ version "7.20.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz#dfee18623c8cb31deb796aa3ca84dda9cea94354"
+ integrity sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==
+ dependencies:
+ "@babel/helper-module-imports" "^7.18.6"
+ "@babel/helper-plugin-utils" "^7.20.2"
+ "@babel/helper-remap-async-to-generator" "^7.18.9"
+
+"@babel/plugin-transform-block-scoped-functions@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz#9187bf4ba302635b9d70d986ad70f038726216a8"
+ integrity sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.18.6"
+
+"@babel/plugin-transform-block-scoping@^7.20.2":
+ version "7.20.11"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.20.11.tgz#9f5a3424bd112a3f32fe0cf9364fbb155cff262a"
+ integrity sha512-tA4N427a7fjf1P0/2I4ScsHGc5jcHPbb30xMbaTke2gxDuWpUfXDuX1FEymJwKk4tuGUvGcejAR6HdZVqmmPyw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.20.2"
+
+"@babel/plugin-transform-classes@^7.20.2":
+ version "7.20.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.20.7.tgz#f438216f094f6bb31dc266ebfab8ff05aecad073"
+ integrity sha512-LWYbsiXTPKl+oBlXUGlwNlJZetXD5Am+CyBdqhPsDVjM9Jc8jwBJFrKhHf900Kfk2eZG1y9MAG3UNajol7A4VQ==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.18.6"
+ "@babel/helper-compilation-targets" "^7.20.7"
+ "@babel/helper-environment-visitor" "^7.18.9"
+ "@babel/helper-function-name" "^7.19.0"
+ "@babel/helper-optimise-call-expression" "^7.18.6"
+ "@babel/helper-plugin-utils" "^7.20.2"
+ "@babel/helper-replace-supers" "^7.20.7"
+ "@babel/helper-split-export-declaration" "^7.18.6"
+ globals "^11.1.0"
+
+"@babel/plugin-transform-computed-properties@^7.18.9":
+ version "7.20.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz#704cc2fd155d1c996551db8276d55b9d46e4d0aa"
+ integrity sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.20.2"
+ "@babel/template" "^7.20.7"
+
+"@babel/plugin-transform-destructuring@^7.20.2":
+ version "7.20.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.7.tgz#8bda578f71620c7de7c93af590154ba331415454"
+ integrity sha512-Xwg403sRrZb81IVB79ZPqNQME23yhugYVqgTxAhT99h485F4f+GMELFhhOsscDUB7HCswepKeCKLn/GZvUKoBA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.20.2"
+
+"@babel/plugin-transform-dotall-regex@^7.18.6", "@babel/plugin-transform-dotall-regex@^7.4.4":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz#b286b3e7aae6c7b861e45bed0a2fafd6b1a4fef8"
+ integrity sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==
+ dependencies:
+ "@babel/helper-create-regexp-features-plugin" "^7.18.6"
+ "@babel/helper-plugin-utils" "^7.18.6"
+
+"@babel/plugin-transform-duplicate-keys@^7.18.9":
+ version "7.18.9"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz#687f15ee3cdad6d85191eb2a372c4528eaa0ae0e"
+ integrity sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.18.9"
+
+"@babel/plugin-transform-exponentiation-operator@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz#421c705f4521888c65e91fdd1af951bfefd4dacd"
+ integrity sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==
+ dependencies:
+ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.18.6"
+ "@babel/helper-plugin-utils" "^7.18.6"
+
+"@babel/plugin-transform-for-of@^7.18.8":
+ version "7.18.8"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz#6ef8a50b244eb6a0bdbad0c7c61877e4e30097c1"
+ integrity sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.18.6"
+
+"@babel/plugin-transform-function-name@^7.18.9":
+ version "7.18.9"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz#cc354f8234e62968946c61a46d6365440fc764e0"
+ integrity sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==
+ dependencies:
+ "@babel/helper-compilation-targets" "^7.18.9"
+ "@babel/helper-function-name" "^7.18.9"
+ "@babel/helper-plugin-utils" "^7.18.9"
+
+"@babel/plugin-transform-literals@^7.18.9":
+ version "7.18.9"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz#72796fdbef80e56fba3c6a699d54f0de557444bc"
+ integrity sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.18.9"
+
+"@babel/plugin-transform-member-expression-literals@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz#ac9fdc1a118620ac49b7e7a5d2dc177a1bfee88e"
+ integrity sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.18.6"
+
+"@babel/plugin-transform-modules-amd@^7.19.6":
+ version "7.20.11"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz#3daccca8e4cc309f03c3a0c4b41dc4b26f55214a"
+ integrity sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==
+ dependencies:
+ "@babel/helper-module-transforms" "^7.20.11"
+ "@babel/helper-plugin-utils" "^7.20.2"
+
+"@babel/plugin-transform-modules-commonjs@^7.19.6":
+ version "7.20.11"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.20.11.tgz#8cb23010869bf7669fd4b3098598b6b2be6dc607"
+ integrity sha512-S8e1f7WQ7cimJQ51JkAaDrEtohVEitXjgCGAS2N8S31Y42E+kWwfSz83LYz57QdBm7q9diARVqanIaH2oVgQnw==
+ dependencies:
+ "@babel/helper-module-transforms" "^7.20.11"
+ "@babel/helper-plugin-utils" "^7.20.2"
+ "@babel/helper-simple-access" "^7.20.2"
+
+"@babel/plugin-transform-modules-systemjs@^7.19.6":
+ version "7.20.11"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz#467ec6bba6b6a50634eea61c9c232654d8a4696e"
+ integrity sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==
+ dependencies:
+ "@babel/helper-hoist-variables" "^7.18.6"
+ "@babel/helper-module-transforms" "^7.20.11"
+ "@babel/helper-plugin-utils" "^7.20.2"
+ "@babel/helper-validator-identifier" "^7.19.1"
+
+"@babel/plugin-transform-modules-umd@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz#81d3832d6034b75b54e62821ba58f28ed0aab4b9"
+ integrity sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==
+ dependencies:
+ "@babel/helper-module-transforms" "^7.18.6"
+ "@babel/helper-plugin-utils" "^7.18.6"
+
+"@babel/plugin-transform-named-capturing-groups-regex@^7.19.1":
+ version "7.20.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz#626298dd62ea51d452c3be58b285d23195ba69a8"
+ integrity sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==
+ dependencies:
+ "@babel/helper-create-regexp-features-plugin" "^7.20.5"
+ "@babel/helper-plugin-utils" "^7.20.2"
+
+"@babel/plugin-transform-new-target@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz#d128f376ae200477f37c4ddfcc722a8a1b3246a8"
+ integrity sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.18.6"
+
+"@babel/plugin-transform-object-super@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz#fb3c6ccdd15939b6ff7939944b51971ddc35912c"
+ integrity sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.18.6"
+ "@babel/helper-replace-supers" "^7.18.6"
+
+"@babel/plugin-transform-parameters@^7.20.1", "@babel/plugin-transform-parameters@^7.20.7":
+ version "7.20.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.7.tgz#0ee349e9d1bc96e78e3b37a7af423a4078a7083f"
+ integrity sha512-WiWBIkeHKVOSYPO0pWkxGPfKeWrCJyD3NJ53+Lrp/QMSZbsVPovrVl2aWZ19D/LTVnaDv5Ap7GJ/B2CTOZdrfA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.20.2"
+
+"@babel/plugin-transform-property-literals@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz#e22498903a483448e94e032e9bbb9c5ccbfc93a3"
+ integrity sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.18.6"
+
+"@babel/plugin-transform-react-display-name@^7.14.5":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.18.6.tgz#8b1125f919ef36ebdfff061d664e266c666b9415"
+ integrity sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.18.6"
+
+"@babel/plugin-transform-react-jsx-development@^7.14.5":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.18.6.tgz#dbe5c972811e49c7405b630e4d0d2e1380c0ddc5"
+ integrity sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==
+ dependencies:
+ "@babel/plugin-transform-react-jsx" "^7.18.6"
+
+"@babel/plugin-transform-react-jsx@^7.14.5", "@babel/plugin-transform-react-jsx@^7.18.6":
+ version "7.20.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.20.7.tgz#025d85a1935fd7e19dfdcb1b1d4df34d4da484f7"
+ integrity sha512-Tfq7qqD+tRj3EoDhY00nn2uP2hsRxgYGi5mLQ5TimKav0a9Lrpd4deE+fcLXU8zFYRjlKPHZhpCvfEA6qnBxqQ==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.18.6"
+ "@babel/helper-module-imports" "^7.18.6"
+ "@babel/helper-plugin-utils" "^7.20.2"
+ "@babel/plugin-syntax-jsx" "^7.18.6"
+ "@babel/types" "^7.20.7"
+
+"@babel/plugin-transform-react-pure-annotations@^7.14.5":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.18.6.tgz#561af267f19f3e5d59291f9950fd7b9663d0d844"
+ integrity sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.18.6"
+ "@babel/helper-plugin-utils" "^7.18.6"
+
+"@babel/plugin-transform-regenerator@^7.18.6":
+ version "7.20.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz#57cda588c7ffb7f4f8483cc83bdcea02a907f04d"
+ integrity sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.20.2"
+ regenerator-transform "^0.15.1"
+
+"@babel/plugin-transform-reserved-words@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz#b1abd8ebf8edaa5f7fe6bbb8d2133d23b6a6f76a"
+ integrity sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.18.6"
+
+"@babel/plugin-transform-shorthand-properties@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz#6d6df7983d67b195289be24909e3f12a8f664dc9"
+ integrity sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.18.6"
+
+"@babel/plugin-transform-spread@^7.19.0":
+ version "7.20.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz#c2d83e0b99d3bf83e07b11995ee24bf7ca09401e"
+ integrity sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.20.2"
+ "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0"
+
+"@babel/plugin-transform-sticky-regex@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz#c6706eb2b1524028e317720339583ad0f444adcc"
+ integrity sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.18.6"
+
+"@babel/plugin-transform-template-literals@^7.18.9":
+ version "7.18.9"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz#04ec6f10acdaa81846689d63fae117dd9c243a5e"
+ integrity sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.18.9"
+
+"@babel/plugin-transform-typeof-symbol@^7.18.9":
+ version "7.18.9"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz#c8cea68263e45addcd6afc9091429f80925762c0"
+ integrity sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.18.9"
+
+"@babel/plugin-transform-unicode-escapes@^7.18.10":
+ version "7.18.10"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz#1ecfb0eda83d09bbcb77c09970c2dd55832aa246"
+ integrity sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.18.9"
+
+"@babel/plugin-transform-unicode-regex@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz#194317225d8c201bbae103364ffe9e2cea36cdca"
+ integrity sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==
+ dependencies:
+ "@babel/helper-create-regexp-features-plugin" "^7.18.6"
+ "@babel/helper-plugin-utils" "^7.18.6"
+
+"@babel/preset-env@^7.15.6":
+ version "7.20.2"
+ resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.20.2.tgz#9b1642aa47bb9f43a86f9630011780dab7f86506"
+ integrity sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==
+ dependencies:
+ "@babel/compat-data" "^7.20.1"
+ "@babel/helper-compilation-targets" "^7.20.0"
+ "@babel/helper-plugin-utils" "^7.20.2"
+ "@babel/helper-validator-option" "^7.18.6"
+ "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.18.6"
+ "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.18.9"
+ "@babel/plugin-proposal-async-generator-functions" "^7.20.1"
+ "@babel/plugin-proposal-class-properties" "^7.18.6"
+ "@babel/plugin-proposal-class-static-block" "^7.18.6"
+ "@babel/plugin-proposal-dynamic-import" "^7.18.6"
+ "@babel/plugin-proposal-export-namespace-from" "^7.18.9"
+ "@babel/plugin-proposal-json-strings" "^7.18.6"
+ "@babel/plugin-proposal-logical-assignment-operators" "^7.18.9"
+ "@babel/plugin-proposal-nullish-coalescing-operator" "^7.18.6"
+ "@babel/plugin-proposal-numeric-separator" "^7.18.6"
+ "@babel/plugin-proposal-object-rest-spread" "^7.20.2"
+ "@babel/plugin-proposal-optional-catch-binding" "^7.18.6"
+ "@babel/plugin-proposal-optional-chaining" "^7.18.9"
+ "@babel/plugin-proposal-private-methods" "^7.18.6"
+ "@babel/plugin-proposal-private-property-in-object" "^7.18.6"
+ "@babel/plugin-proposal-unicode-property-regex" "^7.18.6"
+ "@babel/plugin-syntax-async-generators" "^7.8.4"
+ "@babel/plugin-syntax-class-properties" "^7.12.13"
+ "@babel/plugin-syntax-class-static-block" "^7.14.5"
+ "@babel/plugin-syntax-dynamic-import" "^7.8.3"
+ "@babel/plugin-syntax-export-namespace-from" "^7.8.3"
+ "@babel/plugin-syntax-import-assertions" "^7.20.0"
+ "@babel/plugin-syntax-json-strings" "^7.8.3"
+ "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4"
+ "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3"
+ "@babel/plugin-syntax-numeric-separator" "^7.10.4"
+ "@babel/plugin-syntax-object-rest-spread" "^7.8.3"
+ "@babel/plugin-syntax-optional-catch-binding" "^7.8.3"
+ "@babel/plugin-syntax-optional-chaining" "^7.8.3"
+ "@babel/plugin-syntax-private-property-in-object" "^7.14.5"
+ "@babel/plugin-syntax-top-level-await" "^7.14.5"
+ "@babel/plugin-transform-arrow-functions" "^7.18.6"
+ "@babel/plugin-transform-async-to-generator" "^7.18.6"
+ "@babel/plugin-transform-block-scoped-functions" "^7.18.6"
+ "@babel/plugin-transform-block-scoping" "^7.20.2"
+ "@babel/plugin-transform-classes" "^7.20.2"
+ "@babel/plugin-transform-computed-properties" "^7.18.9"
+ "@babel/plugin-transform-destructuring" "^7.20.2"
+ "@babel/plugin-transform-dotall-regex" "^7.18.6"
+ "@babel/plugin-transform-duplicate-keys" "^7.18.9"
+ "@babel/plugin-transform-exponentiation-operator" "^7.18.6"
+ "@babel/plugin-transform-for-of" "^7.18.8"
+ "@babel/plugin-transform-function-name" "^7.18.9"
+ "@babel/plugin-transform-literals" "^7.18.9"
+ "@babel/plugin-transform-member-expression-literals" "^7.18.6"
+ "@babel/plugin-transform-modules-amd" "^7.19.6"
+ "@babel/plugin-transform-modules-commonjs" "^7.19.6"
+ "@babel/plugin-transform-modules-systemjs" "^7.19.6"
+ "@babel/plugin-transform-modules-umd" "^7.18.6"
+ "@babel/plugin-transform-named-capturing-groups-regex" "^7.19.1"
+ "@babel/plugin-transform-new-target" "^7.18.6"
+ "@babel/plugin-transform-object-super" "^7.18.6"
+ "@babel/plugin-transform-parameters" "^7.20.1"
+ "@babel/plugin-transform-property-literals" "^7.18.6"
+ "@babel/plugin-transform-regenerator" "^7.18.6"
+ "@babel/plugin-transform-reserved-words" "^7.18.6"
+ "@babel/plugin-transform-shorthand-properties" "^7.18.6"
+ "@babel/plugin-transform-spread" "^7.19.0"
+ "@babel/plugin-transform-sticky-regex" "^7.18.6"
+ "@babel/plugin-transform-template-literals" "^7.18.9"
+ "@babel/plugin-transform-typeof-symbol" "^7.18.9"
+ "@babel/plugin-transform-unicode-escapes" "^7.18.10"
+ "@babel/plugin-transform-unicode-regex" "^7.18.6"
+ "@babel/preset-modules" "^0.1.5"
+ "@babel/types" "^7.20.2"
+ babel-plugin-polyfill-corejs2 "^0.3.3"
+ babel-plugin-polyfill-corejs3 "^0.6.0"
+ babel-plugin-polyfill-regenerator "^0.4.1"
+ core-js-compat "^3.25.1"
+ semver "^6.3.0"
+
+"@babel/preset-modules@^0.1.5":
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz#ef939d6e7f268827e1841638dc6ff95515e115d9"
+ integrity sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/plugin-proposal-unicode-property-regex" "^7.4.4"
+ "@babel/plugin-transform-dotall-regex" "^7.4.4"
+ "@babel/types" "^7.4.4"
+ esutils "^2.0.2"
+
+"@babel/preset-react@7.14.5":
+ version "7.14.5"
+ resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.14.5.tgz#0fbb769513f899c2c56f3a882fa79673c2d4ab3c"
+ integrity sha512-XFxBkjyObLvBaAvkx1Ie95Iaq4S/GUEIrejyrntQ/VCMKUYvKLoyKxOBzJ2kjA3b6rC9/KL6KXfDC2GqvLiNqQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.14.5"
+ "@babel/helper-validator-option" "^7.14.5"
+ "@babel/plugin-transform-react-display-name" "^7.14.5"
+ "@babel/plugin-transform-react-jsx" "^7.14.5"
+ "@babel/plugin-transform-react-jsx-development" "^7.14.5"
+ "@babel/plugin-transform-react-pure-annotations" "^7.14.5"
+
+"@babel/register@^7.18.9":
+ version "7.18.9"
+ resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.18.9.tgz#1888b24bc28d5cc41c412feb015e9ff6b96e439c"
+ integrity sha512-ZlbnXDcNYHMR25ITwwNKT88JiaukkdVj/nG7r3wnuXkOTHc60Uy05PwMCPre0hSkY68E6zK3xz+vUJSP2jWmcw==
+ dependencies:
+ clone-deep "^4.0.1"
+ find-cache-dir "^2.0.0"
+ make-dir "^2.1.0"
+ pirates "^4.0.5"
+ source-map-support "^0.5.16"
+
+"@babel/runtime@^7.8.4":
+ version "7.20.7"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.7.tgz#fcb41a5a70550e04a7b708037c7c32f7f356d8fd"
+ integrity sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==
+ dependencies:
+ regenerator-runtime "^0.13.11"
+
+"@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.4.0":
+ version "7.20.7"
+ resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8"
+ integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==
+ dependencies:
+ "@babel/code-frame" "^7.18.6"
+ "@babel/parser" "^7.20.7"
+ "@babel/types" "^7.20.7"
+
+"@babel/traverse@^7.1.0", "@babel/traverse@^7.20.10", "@babel/traverse@^7.20.12", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.4.3":
+ version "7.20.12"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.12.tgz#7f0f787b3a67ca4475adef1f56cb94f6abd4a4b5"
+ integrity sha512-MsIbFN0u+raeja38qboyF8TIT7K0BFzz/Yd/77ta4MsUsmP2RAnidIlwq7d5HFQrH/OZJecGV6B71C4zAgpoSQ==
+ dependencies:
+ "@babel/code-frame" "^7.18.6"
+ "@babel/generator" "^7.20.7"
+ "@babel/helper-environment-visitor" "^7.18.9"
+ "@babel/helper-function-name" "^7.19.0"
+ "@babel/helper-hoist-variables" "^7.18.6"
+ "@babel/helper-split-export-declaration" "^7.18.6"
+ "@babel/parser" "^7.20.7"
+ "@babel/types" "^7.20.7"
+ debug "^4.1.0"
+ globals "^11.1.0"
+
+"@babel/types@^7.0.0", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4":
+ version "7.20.7"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.7.tgz#54ec75e252318423fc07fb644dc6a58a64c09b7f"
+ integrity sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==
+ dependencies:
+ "@babel/helper-string-parser" "^7.19.4"
+ "@babel/helper-validator-identifier" "^7.19.1"
+ to-fast-properties "^2.0.0"
+
+"@cnakazawa/watch@^1.0.3":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a"
+ integrity sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==
+ dependencies:
+ exec-sh "^0.3.2"
+ minimist "^1.2.0"
+
+"@jest/console@^24.7.1", "@jest/console@^24.9.0":
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.9.0.tgz#79b1bc06fb74a8cfb01cbdedf945584b1b9707f0"
+ integrity sha512-Zuj6b8TnKXi3q4ymac8EQfc3ea/uhLeCGThFqXeC8H9/raaH8ARPUTdId+XyGd03Z4In0/VjD2OYFcBF09fNLQ==
+ dependencies:
+ "@jest/source-map" "^24.9.0"
+ chalk "^2.0.1"
+ slash "^2.0.0"
+
+"@jest/core@^24.9.0":
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/@jest/core/-/core-24.9.0.tgz#2ceccd0b93181f9c4850e74f2a9ad43d351369c4"
+ integrity sha512-Fogg3s4wlAr1VX7q+rhV9RVnUv5tD7VuWfYy1+whMiWUrvl7U3QJSJyWcDio9Lq2prqYsZaeTv2Rz24pWGkJ2A==
+ dependencies:
+ "@jest/console" "^24.7.1"
+ "@jest/reporters" "^24.9.0"
+ "@jest/test-result" "^24.9.0"
+ "@jest/transform" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ ansi-escapes "^3.0.0"
+ chalk "^2.0.1"
+ exit "^0.1.2"
+ graceful-fs "^4.1.15"
+ jest-changed-files "^24.9.0"
+ jest-config "^24.9.0"
+ jest-haste-map "^24.9.0"
+ jest-message-util "^24.9.0"
+ jest-regex-util "^24.3.0"
+ jest-resolve "^24.9.0"
+ jest-resolve-dependencies "^24.9.0"
+ jest-runner "^24.9.0"
+ jest-runtime "^24.9.0"
+ jest-snapshot "^24.9.0"
+ jest-util "^24.9.0"
+ jest-validate "^24.9.0"
+ jest-watcher "^24.9.0"
+ micromatch "^3.1.10"
+ p-each-series "^1.0.0"
+ realpath-native "^1.1.0"
+ rimraf "^2.5.4"
+ slash "^2.0.0"
+ strip-ansi "^5.0.0"
+
+"@jest/environment@^24.9.0":
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-24.9.0.tgz#21e3afa2d65c0586cbd6cbefe208bafade44ab18"
+ integrity sha512-5A1QluTPhvdIPFYnO3sZC3smkNeXPVELz7ikPbhUj0bQjB07EoE9qtLrem14ZUYWdVayYbsjVwIiL4WBIMV4aQ==
+ dependencies:
+ "@jest/fake-timers" "^24.9.0"
+ "@jest/transform" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ jest-mock "^24.9.0"
+
+"@jest/fake-timers@^24.9.0":
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-24.9.0.tgz#ba3e6bf0eecd09a636049896434d306636540c93"
+ integrity sha512-eWQcNa2YSwzXWIMC5KufBh3oWRIijrQFROsIqt6v/NS9Io/gknw1jsAC9c+ih/RQX4A3O7SeWAhQeN0goKhT9A==
+ dependencies:
+ "@jest/types" "^24.9.0"
+ jest-message-util "^24.9.0"
+ jest-mock "^24.9.0"
+
+"@jest/reporters@^24.9.0":
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-24.9.0.tgz#86660eff8e2b9661d042a8e98a028b8d631a5b43"
+ integrity sha512-mu4X0yjaHrffOsWmVLzitKmmmWSQ3GGuefgNscUSWNiUNcEOSEQk9k3pERKEQVBb0Cnn88+UESIsZEMH3o88Gw==
+ dependencies:
+ "@jest/environment" "^24.9.0"
+ "@jest/test-result" "^24.9.0"
+ "@jest/transform" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ chalk "^2.0.1"
+ exit "^0.1.2"
+ glob "^7.1.2"
+ istanbul-lib-coverage "^2.0.2"
+ istanbul-lib-instrument "^3.0.1"
+ istanbul-lib-report "^2.0.4"
+ istanbul-lib-source-maps "^3.0.1"
+ istanbul-reports "^2.2.6"
+ jest-haste-map "^24.9.0"
+ jest-resolve "^24.9.0"
+ jest-runtime "^24.9.0"
+ jest-util "^24.9.0"
+ jest-worker "^24.6.0"
+ node-notifier "^5.4.2"
+ slash "^2.0.0"
+ source-map "^0.6.0"
+ string-length "^2.0.0"
+
+"@jest/source-map@^24.3.0", "@jest/source-map@^24.9.0":
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-24.9.0.tgz#0e263a94430be4b41da683ccc1e6bffe2a191714"
+ integrity sha512-/Xw7xGlsZb4MJzNDgB7PW5crou5JqWiBQaz6xyPd3ArOg2nfn/PunV8+olXbbEZzNl591o5rWKE9BRDaFAuIBg==
+ dependencies:
+ callsites "^3.0.0"
+ graceful-fs "^4.1.15"
+ source-map "^0.6.0"
+
+"@jest/test-result@^24.9.0":
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-24.9.0.tgz#11796e8aa9dbf88ea025757b3152595ad06ba0ca"
+ integrity sha512-XEFrHbBonBJ8dGp2JmF8kP/nQI/ImPpygKHwQ/SY+es59Z3L5PI4Qb9TQQMAEeYsThG1xF0k6tmG0tIKATNiiA==
+ dependencies:
+ "@jest/console" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ "@types/istanbul-lib-coverage" "^2.0.0"
+
+"@jest/test-sequencer@^24.9.0":
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-24.9.0.tgz#f8f334f35b625a4f2f355f2fe7e6036dad2e6b31"
+ integrity sha512-6qqsU4o0kW1dvA95qfNog8v8gkRN9ph6Lz7r96IvZpHdNipP2cBcb07J1Z45mz/VIS01OHJ3pY8T5fUY38tg4A==
+ dependencies:
+ "@jest/test-result" "^24.9.0"
+ jest-haste-map "^24.9.0"
+ jest-runner "^24.9.0"
+ jest-runtime "^24.9.0"
+
+"@jest/transform@^24.9.0":
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-24.9.0.tgz#4ae2768b296553fadab09e9ec119543c90b16c56"
+ integrity sha512-TcQUmyNRxV94S0QpMOnZl0++6RMiqpbH/ZMccFB/amku6Uwvyb1cjYX7xkp5nGNkbX4QPH/FcB6q1HBTHynLmQ==
+ dependencies:
+ "@babel/core" "^7.1.0"
+ "@jest/types" "^24.9.0"
+ babel-plugin-istanbul "^5.1.0"
+ chalk "^2.0.1"
+ convert-source-map "^1.4.0"
+ fast-json-stable-stringify "^2.0.0"
+ graceful-fs "^4.1.15"
+ jest-haste-map "^24.9.0"
+ jest-regex-util "^24.9.0"
+ jest-util "^24.9.0"
+ micromatch "^3.1.10"
+ pirates "^4.0.1"
+ realpath-native "^1.1.0"
+ slash "^2.0.0"
+ source-map "^0.6.1"
+ write-file-atomic "2.4.1"
+
+"@jest/types@^24.9.0":
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.9.0.tgz#63cb26cb7500d069e5a389441a7c6ab5e909fc59"
+ integrity sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==
+ dependencies:
+ "@types/istanbul-lib-coverage" "^2.0.0"
+ "@types/istanbul-reports" "^1.1.1"
+ "@types/yargs" "^13.0.0"
+
+"@jridgewell/gen-mapping@^0.1.0":
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996"
+ integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==
+ dependencies:
+ "@jridgewell/set-array" "^1.0.0"
+ "@jridgewell/sourcemap-codec" "^1.4.10"
+
+"@jridgewell/gen-mapping@^0.3.2":
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9"
+ integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==
+ dependencies:
+ "@jridgewell/set-array" "^1.0.1"
+ "@jridgewell/sourcemap-codec" "^1.4.10"
+ "@jridgewell/trace-mapping" "^0.3.9"
+
+"@jridgewell/resolve-uri@3.1.0":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
+ integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
+
+"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
+ integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
+
+"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10":
+ version "1.4.14"
+ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
+ integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
+
+"@jridgewell/trace-mapping@^0.3.9":
+ version "0.3.17"
+ resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985"
+ integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==
+ dependencies:
+ "@jridgewell/resolve-uri" "3.1.0"
+ "@jridgewell/sourcemap-codec" "1.4.14"
+
+"@types/babel__core@^7.1.0":
+ version "7.1.20"
+ resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.20.tgz#e168cdd612c92a2d335029ed62ac94c95b362359"
+ integrity sha512-PVb6Bg2QuscZ30FvOU7z4guG6c926D9YRvOxEaelzndpMsvP+YM74Q/dAFASpg2l6+XLalxSGxcq/lrgYWZtyQ==
+ dependencies:
+ "@babel/parser" "^7.1.0"
+ "@babel/types" "^7.0.0"
+ "@types/babel__generator" "*"
+ "@types/babel__template" "*"
+ "@types/babel__traverse" "*"
+
+"@types/babel__generator@*":
+ version "7.6.4"
+ resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.4.tgz#1f20ce4c5b1990b37900b63f050182d28c2439b7"
+ integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@types/babel__template@*":
+ version "7.4.1"
+ resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.1.tgz#3d1a48fd9d6c0edfd56f2ff578daed48f36c8969"
+ integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==
+ dependencies:
+ "@babel/parser" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6":
+ version "7.18.3"
+ resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.3.tgz#dfc508a85781e5698d5b33443416b6268c4b3e8d"
+ integrity sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==
+ dependencies:
+ "@babel/types" "^7.3.0"
+
+"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0":
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44"
+ integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==
+
+"@types/istanbul-lib-report@*":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686"
+ integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==
+ dependencies:
+ "@types/istanbul-lib-coverage" "*"
+
+"@types/istanbul-reports@^1.1.1":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz#e875cc689e47bce549ec81f3df5e6f6f11cfaeb2"
+ integrity sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==
+ dependencies:
+ "@types/istanbul-lib-coverage" "*"
+ "@types/istanbul-lib-report" "*"
+
+"@types/node@*":
+ version "13.9.2"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-13.9.2.tgz#ace1880c03594cc3e80206d96847157d8e7fa349"
+ integrity sha512-bnoqK579sAYrQbp73wwglccjJ4sfRdKU7WNEZ5FW4K2U6Kc0/eZ5kvXG0JKsEKFB50zrFmfFt52/cvBbZa7eXg==
+
+"@types/stack-utils@^1.0.1":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
+ integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
+
+"@types/yargs-parser@*":
+ version "21.0.0"
+ resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b"
+ integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==
+
+"@types/yargs@^13.0.0":
+ version "13.0.12"
+ resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-13.0.12.tgz#d895a88c703b78af0465a9de88aa92c61430b092"
+ integrity sha512-qCxJE1qgz2y0hA4pIxjBR+PelCH0U5CK1XJXFwCNqfmliatKp47UCXXE9Dyk1OXBDLvsCF57TqQEJaeLfDYEOQ==
+ dependencies:
+ "@types/yargs-parser" "*"
+
+abab@^2.0.0:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
+ integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==
+
+acorn-globals@^4.1.0:
+ version "4.3.4"
+ resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7"
+ integrity sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==
+ dependencies:
+ acorn "^6.0.1"
+ acorn-walk "^6.0.1"
+
+acorn-walk@^6.0.1:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c"
+ integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==
+
+acorn@^5.5.3:
+ version "5.7.4"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e"
+ integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==
+
+acorn@^6.0.1:
+ version "6.4.2"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
+ integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==
+
+airbnb-prop-types@^2.16.0:
+ version "2.16.0"
+ resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz#b96274cefa1abb14f623f804173ee97c13971dc2"
+ integrity sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg==
+ dependencies:
+ array.prototype.find "^2.1.1"
+ function.prototype.name "^1.1.2"
+ is-regex "^1.1.0"
+ object-is "^1.1.2"
+ object.assign "^4.1.0"
+ object.entries "^1.1.2"
+ prop-types "^15.7.2"
+ prop-types-exact "^1.2.0"
+ react-is "^16.13.1"
+
+ajv@^6.5.5:
+ version "6.12.0"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7"
+ integrity sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==
+ dependencies:
+ fast-deep-equal "^3.1.1"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.4.1"
+ uri-js "^4.2.2"
+
+ansi-escapes@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
+ integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==
+
+ansi-regex@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+ integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==
+
+ansi-regex@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.1.tgz#123d6479e92ad45ad897d4054e3c7ca7db4944e1"
+ integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==
+
+ansi-regex@^4.0.0, ansi-regex@^4.1.0:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed"
+ integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==
+
+ansi-styles@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
+ integrity sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==
+
+ansi-styles@^3.2.0, ansi-styles@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+ integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
+ dependencies:
+ color-convert "^1.9.0"
+
+anymatch@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
+ integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==
+ dependencies:
+ micromatch "^3.1.4"
+ normalize-path "^2.1.1"
+
+arr-diff@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf"
+ integrity sha512-dtXTVMkh6VkEEA7OhXnN1Ecb8aAGFdZ1LFxtOCoqj4qkyOJMt7+qs6Ahdy6p/NQCPYsRSXXivhSB/J5E9jmYKA==
+ dependencies:
+ arr-flatten "^1.0.1"
+
+arr-diff@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
+ integrity sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==
+
+arr-flatten@^1.0.1, arr-flatten@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
+ integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==
+
+arr-union@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
+ integrity sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==
+
+array-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
+ integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=
+
+array-filter@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83"
+ integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=
+
+array-unique@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53"
+ integrity sha512-G2n5bG5fSUCpnsXz4+8FUkYsGPkNfLn9YvS66U5qbTIXI2Ynnlo4Bi42bWv+omKUCqz+ejzfClwne0alJWJPhg==
+
+array-unique@^0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
+ integrity sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==
+
+array.prototype.find@^2.1.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.2.1.tgz#769b8182a0b535c3d76ac025abab98ba1e12467b"
+ integrity sha512-I2ri5Z9uMpMvnsNrHre9l3PaX+z9D0/z6F7Yt2u15q7wt0I62g5kX6xUKR1SJiefgG+u2/gJUmM8B47XRvQR6w==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.4"
+ es-abstract "^1.20.4"
+ es-shim-unscopables "^1.0.0"
+
+array.prototype.flat@^1.2.3:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz#0de82b426b0318dbfdb940089e38b043d37f6c7b"
+ integrity sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.17.0-next.1"
+
+array.prototype.reduce@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/array.prototype.reduce/-/array.prototype.reduce-1.0.5.tgz#6b20b0daa9d9734dd6bc7ea66b5bbce395471eac"
+ integrity sha512-kDdugMl7id9COE8R7MHF5jWk7Dqt/fs4Pv+JXoICnYwqpjjjbUurz6w5fT5IG6brLdJhv6/VoHB0H7oyIBXd+Q==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.4"
+ es-abstract "^1.20.4"
+ es-array-method-boxes-properly "^1.0.0"
+ is-string "^1.0.7"
+
+arrify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
+ integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==
+
+asap@~2.0.3:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
+ integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==
+
+asn1@~0.2.3:
+ version "0.2.4"
+ resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
+ integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==
+ dependencies:
+ safer-buffer "~2.1.0"
+
+assert-plus@1.0.0, assert-plus@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+ integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
+
+assign-symbols@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
+ integrity sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==
+
+astral-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
+ integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==
+
+async-limiter@~1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
+ integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
+
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+ integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
+
+atob@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
+ integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
+
+available-typed-arrays@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
+ integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
+
+aws-sign2@~0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
+ integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
+
+aws4@^1.8.0:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e"
+ integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==
+
+babel-code-frame@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
+ integrity sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g==
+ dependencies:
+ chalk "^1.1.3"
+ esutils "^2.0.2"
+ js-tokens "^3.0.2"
+
+babel-generator@^6.18.0:
+ version "6.26.1"
+ resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90"
+ integrity sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==
+ dependencies:
+ babel-messages "^6.23.0"
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ detect-indent "^4.0.0"
+ jsesc "^1.3.0"
+ lodash "^4.17.4"
+ source-map "^0.5.7"
+ trim-right "^1.0.1"
+
+babel-jest@^23.0.0:
+ version "23.6.0"
+ resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-23.6.0.tgz#a644232366557a2240a0c083da6b25786185a2f1"
+ integrity sha512-lqKGG6LYXYu+DQh/slrQ8nxXQkEkhugdXsU6St7GmhVS7Ilc/22ArwqXNJrf0QaOBjZB0360qZMwXqDYQHXaew==
+ dependencies:
+ babel-plugin-istanbul "^4.1.6"
+ babel-preset-jest "^23.2.0"
+
+babel-jest@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-24.9.0.tgz#3fc327cb8467b89d14d7bc70e315104a783ccd54"
+ integrity sha512-ntuddfyiN+EhMw58PTNL1ph4C9rECiQXjI4nMMBKBaNjXvqLdkXpPRcMSr4iyBrJg/+wz9brFUD6RhOAT6r4Iw==
+ dependencies:
+ "@jest/transform" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ "@types/babel__core" "^7.1.0"
+ babel-plugin-istanbul "^5.1.0"
+ babel-preset-jest "^24.9.0"
+ chalk "^2.4.2"
+ slash "^2.0.0"
+
+babel-messages@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e"
+ integrity sha512-Bl3ZiA+LjqaMtNYopA9TYE9HP1tQ+E5dLxE0XrAzcIJeK2UqF0/EaqXwBn9esd4UmTfEab+P+UYQ1GnioFIb/w==
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-istanbul@^4.1.6:
+ version "4.1.6"
+ resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz#36c59b2192efce81c5b378321b74175add1c9a45"
+ integrity sha512-PWP9FQ1AhZhS01T/4qLSKoHGY/xvkZdVBGlKM/HuxxS3+sC66HhTNR7+MpbO/so/cz/wY94MeSWJuP1hXIPfwQ==
+ dependencies:
+ babel-plugin-syntax-object-rest-spread "^6.13.0"
+ find-up "^2.1.0"
+ istanbul-lib-instrument "^1.10.1"
+ test-exclude "^4.2.1"
+
+babel-plugin-istanbul@^5.1.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz#df4ade83d897a92df069c4d9a25cf2671293c854"
+ integrity sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ find-up "^3.0.0"
+ istanbul-lib-instrument "^3.3.0"
+ test-exclude "^5.2.3"
+
+babel-plugin-jest-hoist@^23.2.0:
+ version "23.2.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-23.2.0.tgz#e61fae05a1ca8801aadee57a6d66b8cefaf44167"
+ integrity sha512-N0MlMjZtahXK0yb0K3V9hWPrq5e7tThbghvDr0k3X75UuOOqwsWW6mk8XHD2QvEC0Ca9dLIfTgNU36TeJD6Hnw==
+
+babel-plugin-jest-hoist@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.9.0.tgz#4f837091eb407e01447c8843cbec546d0002d756"
+ integrity sha512-2EMA2P8Vp7lG0RAzr4HXqtYwacfMErOuv1U3wrvxHX6rD1sV6xS3WXG3r8TRQ2r6w8OhvSdWt+z41hQNwNm3Xw==
+ dependencies:
+ "@types/babel__traverse" "^7.0.6"
+
+babel-plugin-module-resolver@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-module-resolver/-/babel-plugin-module-resolver-4.1.0.tgz#22a4f32f7441727ec1fbf4967b863e1e3e9f33e2"
+ integrity sha512-MlX10UDheRr3lb3P0WcaIdtCSRlxdQsB1sBqL7W0raF070bGl1HQQq5K3T2vf2XAYie+ww+5AKC/WrkjRO2knA==
+ dependencies:
+ find-babel-config "^1.2.0"
+ glob "^7.1.6"
+ pkg-up "^3.1.0"
+ reselect "^4.0.0"
+ resolve "^1.13.1"
+
+babel-plugin-polyfill-corejs2@^0.3.3:
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz#5d1bd3836d0a19e1b84bbf2d9640ccb6f951c122"
+ integrity sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==
+ dependencies:
+ "@babel/compat-data" "^7.17.7"
+ "@babel/helper-define-polyfill-provider" "^0.3.3"
+ semver "^6.1.1"
+
+babel-plugin-polyfill-corejs3@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz#56ad88237137eade485a71b52f72dbed57c6230a"
+ integrity sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==
+ dependencies:
+ "@babel/helper-define-polyfill-provider" "^0.3.3"
+ core-js-compat "^3.25.1"
+
+babel-plugin-polyfill-regenerator@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz#390f91c38d90473592ed43351e801a9d3e0fd747"
+ integrity sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==
+ dependencies:
+ "@babel/helper-define-polyfill-provider" "^0.3.3"
+
+babel-plugin-syntax-object-rest-spread@^6.13.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
+ integrity sha512-C4Aq+GaAj83pRQ0EFgTvw5YO6T3Qz2KGrNRwIj9mSoNHVvdZY4KO2uA6HNtNXCw993iSZnckY1aLW8nOi8i4+w==
+
+babel-plugin-transform-amd-to-commonjs@1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-amd-to-commonjs/-/babel-plugin-transform-amd-to-commonjs-1.4.0.tgz#d9bc5003eaa26dbdd4e854e453f84903852af2ca"
+ integrity sha512-Xx0kYPn0LPyms+8n2KLn9yd2R5XMb2P1sNe4qn64/UQY5F2KFYlhhhyYUNm/BThfODAzl7rbaOsEfpU2M8iDKQ==
+
+babel-preset-jest@^23.2.0:
+ version "23.2.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-23.2.0.tgz#8ec7a03a138f001a1a8fb1e8113652bf1a55da46"
+ integrity sha512-AdfWwc0PYvDtwr009yyVNh72Ev68os7SsPmOFVX7zSA+STXuk5CV2iMVazZU01bEoHCSwTkgv4E4HOOcODPkPg==
+ dependencies:
+ babel-plugin-jest-hoist "^23.2.0"
+ babel-plugin-syntax-object-rest-spread "^6.13.0"
+
+babel-preset-jest@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-24.9.0.tgz#192b521e2217fb1d1f67cf73f70c336650ad3cdc"
+ integrity sha512-izTUuhE4TMfTRPF92fFwD2QfdXaZW08qvWTFCI51V8rW5x00UuPgc3ajRoWofXOuxjfcOM5zzSYsQS3H8KGCAg==
+ dependencies:
+ "@babel/plugin-syntax-object-rest-spread" "^7.0.0"
+ babel-plugin-jest-hoist "^24.9.0"
+
+babel-runtime@^6.22.0, babel-runtime@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
+ integrity sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==
+ dependencies:
+ core-js "^2.4.0"
+ regenerator-runtime "^0.11.0"
+
+babel-template@^6.16.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02"
+ integrity sha512-PCOcLFW7/eazGUKIoqH97sO9A2UYMahsn/yRQ7uOk37iutwjq7ODtcTNF+iFDSHNfkctqsLRjLP7URnOx0T1fg==
+ dependencies:
+ babel-runtime "^6.26.0"
+ babel-traverse "^6.26.0"
+ babel-types "^6.26.0"
+ babylon "^6.18.0"
+ lodash "^4.17.4"
+
+babel-traverse@^6.18.0, babel-traverse@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee"
+ integrity sha512-iSxeXx7apsjCHe9c7n8VtRXGzI2Bk1rBSOJgCCjfyXb6v1aCqE1KSEpq/8SXuVN8Ka/Rh1WDTF0MDzkvTA4MIA==
+ dependencies:
+ babel-code-frame "^6.26.0"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ babylon "^6.18.0"
+ debug "^2.6.8"
+ globals "^9.18.0"
+ invariant "^2.2.2"
+ lodash "^4.17.4"
+
+babel-types@^6.18.0, babel-types@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
+ integrity sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g==
+ dependencies:
+ babel-runtime "^6.26.0"
+ esutils "^2.0.2"
+ lodash "^4.17.4"
+ to-fast-properties "^1.0.3"
+
+babylon@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
+ integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==
+
+balanced-match@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+ integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
+
+base@^0.11.1:
+ version "0.11.2"
+ resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
+ integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==
+ dependencies:
+ cache-base "^1.0.1"
+ class-utils "^0.3.5"
+ component-emitter "^1.2.1"
+ define-property "^1.0.0"
+ isobject "^3.0.1"
+ mixin-deep "^1.2.0"
+ pascalcase "^0.1.1"
+
+bcrypt-pbkdf@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
+ integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=
+ dependencies:
+ tweetnacl "^0.14.3"
+
+bindings@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
+ integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
+ dependencies:
+ file-uri-to-path "1.0.0"
+
+boolbase@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+ integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
+
+brace-expansion@^1.1.7:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+ integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+braces@^1.8.2:
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7"
+ integrity sha512-xU7bpz2ytJl1bH9cgIurjpg/n8Gohy9GTw81heDYLJQ4RU60dlyJsa+atVF2pI0yMMvKxI9HkKwjePCj5XI1hw==
+ dependencies:
+ expand-range "^1.8.1"
+ preserve "^0.2.0"
+ repeat-element "^1.1.2"
+
+braces@^2.3.1:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
+ integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==
+ dependencies:
+ arr-flatten "^1.1.0"
+ array-unique "^0.3.2"
+ extend-shallow "^2.0.1"
+ fill-range "^4.0.0"
+ isobject "^3.0.1"
+ repeat-element "^1.1.2"
+ snapdragon "^0.8.1"
+ snapdragon-node "^2.0.1"
+ split-string "^3.0.2"
+ to-regex "^3.0.1"
+
+browser-process-hrtime@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"
+ integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==
+
+browser-resolve@^1.11.3:
+ version "1.11.3"
+ resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6"
+ integrity sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==
+ dependencies:
+ resolve "1.1.7"
+
+browserslist@^4.21.3, browserslist@^4.21.4:
+ version "4.21.4"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.4.tgz#e7496bbc67b9e39dd0f98565feccdcb0d4ff6987"
+ integrity sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==
+ dependencies:
+ caniuse-lite "^1.0.30001400"
+ electron-to-chromium "^1.4.251"
+ node-releases "^2.0.6"
+ update-browserslist-db "^1.0.9"
+
+bser@2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05"
+ integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==
+ dependencies:
+ node-int64 "^0.4.0"
+
+buffer-from@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
+ integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
+
+cache-base@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
+ integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==
+ dependencies:
+ collection-visit "^1.0.0"
+ component-emitter "^1.2.1"
+ get-value "^2.0.6"
+ has-value "^1.0.0"
+ isobject "^3.0.1"
+ set-value "^2.0.0"
+ to-object-path "^0.3.0"
+ union-value "^1.0.0"
+ unset-value "^1.0.0"
+
+call-bind@^1.0.0, call-bind@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
+ integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
+ dependencies:
+ function-bind "^1.1.1"
+ get-intrinsic "^1.0.2"
+
+callsites@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
+ integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
+
+camelcase@^5.0.0, camelcase@^5.3.1:
+ version "5.3.1"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
+ integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
+
+caniuse-lite@^1.0.30001400:
+ version "1.0.30001445"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001445.tgz#cf2d4eb93f2bcdf0310de9dd6d18be271bc0b447"
+ integrity sha512-8sdQIdMztYmzfTMO6KfLny878Ln9c2M0fc7EH60IjlP4Dc4PiCy7K2Vl3ITmWgOyPgVQKa5x+UP/KqFsxj4mBg==
+
+capture-exit@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4"
+ integrity sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==
+ dependencies:
+ rsvp "^4.8.4"
+
+caseless@~0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+ integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
+
+chalk@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
+ integrity sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==
+ dependencies:
+ ansi-styles "^2.2.1"
+ escape-string-regexp "^1.0.2"
+ has-ansi "^2.0.0"
+ strip-ansi "^3.0.0"
+ supports-color "^2.0.0"
+
+chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.2:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+ integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
+ dependencies:
+ ansi-styles "^3.2.1"
+ escape-string-regexp "^1.0.5"
+ supports-color "^5.3.0"
+
+cheerio@^1.0.0-rc.3:
+ version "1.0.0-rc.3"
+ resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6"
+ integrity sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA==
+ dependencies:
+ css-select "~1.2.0"
+ dom-serializer "~0.1.1"
+ entities "~1.1.1"
+ htmlparser2 "^3.9.1"
+ lodash "^4.15.0"
+ parse5 "^3.0.1"
+
+ci-info@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
+ integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
+
+class-utils@^0.3.5:
+ version "0.3.6"
+ resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
+ integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==
+ dependencies:
+ arr-union "^3.1.0"
+ define-property "^0.2.5"
+ isobject "^3.0.0"
+ static-extend "^0.1.1"
+
+cliui@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5"
+ integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==
+ dependencies:
+ string-width "^3.1.0"
+ strip-ansi "^5.2.0"
+ wrap-ansi "^5.1.0"
+
+clone-deep@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
+ integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==
+ dependencies:
+ is-plain-object "^2.0.4"
+ kind-of "^6.0.2"
+ shallow-clone "^3.0.0"
+
+co@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+ integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==
+
+collection-visit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
+ integrity sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==
+ dependencies:
+ map-visit "^1.0.0"
+ object-visit "^1.0.0"
+
+color-convert@^1.9.0:
+ version "1.9.3"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+ integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
+ dependencies:
+ color-name "1.1.3"
+
+color-name@1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+ integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
+
+combined-stream@^1.0.6, combined-stream@~1.0.6:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+ integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+ dependencies:
+ delayed-stream "~1.0.0"
+
+commander@^2.19.0:
+ version "2.20.3"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
+ integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
+
+commondir@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
+ integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
+
+component-emitter@^1.2.1:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
+ integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+ integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
+
+convert-source-map@^1.4.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f"
+ integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==
+
+convert-source-map@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
+ integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==
+ dependencies:
+ safe-buffer "~5.1.1"
+
+copy-descriptor@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
+ integrity sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==
+
+core-js-compat@^3.25.1:
+ version "3.27.1"
+ resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.27.1.tgz#b5695eb25c602d72b1d30cbfba3cb7e5e4cf0a67"
+ integrity sha512-Dg91JFeCDA17FKnneN7oCMz4BkQ4TcffkgHP4OWwp9yx3pi7ubqMDXXSacfNak1PQqjc95skyt+YBLHQJnkJwA==
+ dependencies:
+ browserslist "^4.21.4"
+
+core-js@^1.0.0:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
+ integrity sha512-ZiPp9pZlgxpWRu0M+YWbm6+aQ84XEfH1JRXvfOc/fILWI0VKhLC2LX13X1NYq4fULzLMq7Hfh43CSo2/aIaUPA==
+
+core-js@^2.4.0:
+ version "2.6.11"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
+ integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==
+
+core-util-is@1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+ integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
+
+cross-spawn@^6.0.0:
+ version "6.0.5"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
+ integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
+ dependencies:
+ nice-try "^1.0.4"
+ path-key "^2.0.1"
+ semver "^5.5.0"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
+
+css-select@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
+ integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=
+ dependencies:
+ boolbase "~1.0.0"
+ css-what "2.1"
+ domutils "1.5.1"
+ nth-check "~1.0.1"
+
+css-what@2.1:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
+ integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
+
+cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0":
+ version "0.3.8"
+ resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a"
+ integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==
+
+cssstyle@^1.0.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.4.0.tgz#9d31328229d3c565c61e586b02041a28fccdccf1"
+ integrity sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==
+ dependencies:
+ cssom "0.3.x"
+
+dashdash@^1.12.0:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
+ integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=
+ dependencies:
+ assert-plus "^1.0.0"
+
+data-urls@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe"
+ integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==
+ dependencies:
+ abab "^2.0.0"
+ whatwg-mimetype "^2.2.0"
+ whatwg-url "^7.0.0"
+
+debug@^2.2.0, debug@^2.3.3, debug@^2.6.8:
+ version "2.6.9"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+ integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
+ dependencies:
+ ms "2.0.0"
+
+debug@^4.1.0:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
+ integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
+ dependencies:
+ ms "^2.1.1"
+
+debug@^4.1.1:
+ version "4.3.4"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
+ integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
+ dependencies:
+ ms "2.1.2"
+
+decamelize@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+ integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
+
+decode-uri-component@^0.2.0:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9"
+ integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==
+
+deep-is@~0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
+ integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
+
+define-properties@^1.1.2, define-properties@^1.1.3, define-properties@~1.1.2:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
+ integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
+ dependencies:
+ object-keys "^1.0.12"
+
+define-properties@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1"
+ integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==
+ dependencies:
+ has-property-descriptors "^1.0.0"
+ object-keys "^1.1.1"
+
+define-property@^0.2.5:
+ version "0.2.5"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116"
+ integrity sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==
+ dependencies:
+ is-descriptor "^0.1.0"
+
+define-property@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6"
+ integrity sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==
+ dependencies:
+ is-descriptor "^1.0.0"
+
+define-property@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d"
+ integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==
+ dependencies:
+ is-descriptor "^1.0.2"
+ isobject "^3.0.1"
+
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+ integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
+
+detect-indent@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208"
+ integrity sha512-BDKtmHlOzwI7iRuEkhzsnPoi5ypEhWAJB5RvHWe1kMr06js3uK5B3734i3ui5Yd+wOJV1cpE4JnivPD283GU/A==
+ dependencies:
+ repeating "^2.0.0"
+
+detect-newline@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2"
+ integrity sha512-CwffZFvlJffUg9zZA0uqrjQayUTC8ob94pnr5sFwaVv3IOmkfUHcWH+jXaQK3askE51Cqe8/9Ql/0uXNwqZ8Zg==
+
+diff-sequences@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5"
+ integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==
+
+discontinuous-range@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a"
+ integrity sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=
+
+dom-serializer@0:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
+ integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==
+ dependencies:
+ domelementtype "^2.0.1"
+ entities "^2.0.0"
+
+dom-serializer@~0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0"
+ integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==
+ dependencies:
+ domelementtype "^1.3.0"
+ entities "^1.1.1"
+
+domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
+ integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
+
+domelementtype@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d"
+ integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==
+
+domexception@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
+ integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==
+ dependencies:
+ webidl-conversions "^4.0.2"
+
+domhandler@^2.3.0:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
+ integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==
+ dependencies:
+ domelementtype "1"
+
+domutils@1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
+ integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=
+ dependencies:
+ dom-serializer "0"
+ domelementtype "1"
+
+domutils@^1.5.1:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
+ integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==
+ dependencies:
+ dom-serializer "0"
+ domelementtype "1"
+
+ecc-jsbn@~0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
+ integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=
+ dependencies:
+ jsbn "~0.1.0"
+ safer-buffer "^2.1.0"
+
+electron-to-chromium@^1.4.251:
+ version "1.4.284"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592"
+ integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==
+
+emoji-regex@^7.0.1:
+ version "7.0.3"
+ resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
+ integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
+
+encoding@^0.1.11:
+ version "0.1.13"
+ resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9"
+ integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==
+ dependencies:
+ iconv-lite "^0.6.2"
+
+end-of-stream@^1.1.0:
+ version "1.4.4"
+ resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
+ integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
+ dependencies:
+ once "^1.4.0"
+
+entities@^1.1.1, entities@~1.1.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
+ integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
+
+entities@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4"
+ integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==
+
+enzyme-adapter-react-16@^1.15.7:
+ version "1.15.7"
+ resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.7.tgz#a737e6d8e2c147e9da5acf957755be7634f76201"
+ integrity sha512-LtjKgvlTc/H7adyQcj+aq0P0H07LDL480WQl1gU512IUyaDo/sbOaNDdZsJXYW2XaoPqrLLE9KbZS+X2z6BASw==
+ dependencies:
+ enzyme-adapter-utils "^1.14.1"
+ enzyme-shallow-equal "^1.0.5"
+ has "^1.0.3"
+ object.assign "^4.1.4"
+ object.values "^1.1.5"
+ prop-types "^15.8.1"
+ react-is "^16.13.1"
+ react-test-renderer "^16.0.0-0"
+ semver "^5.7.0"
+
+enzyme-adapter-utils@^1.14.1:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.14.1.tgz#f30db15dafc22e0ccd44f5acc8d93be29218cdcf"
+ integrity sha512-JZgMPF1QOI7IzBj24EZoDpaeG/p8Os7WeBZWTJydpsH7JRStc7jYbHE4CmNQaLqazaGFyLM8ALWA3IIZvxW3PQ==
+ dependencies:
+ airbnb-prop-types "^2.16.0"
+ function.prototype.name "^1.1.5"
+ has "^1.0.3"
+ object.assign "^4.1.4"
+ object.fromentries "^2.0.5"
+ prop-types "^15.8.1"
+ semver "^5.7.1"
+
+enzyme-shallow-equal@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.1.tgz#7afe03db3801c9b76de8440694096412a8d9d49e"
+ integrity sha512-hGA3i1so8OrYOZSM9whlkNmVHOicJpsjgTzC+wn2JMJXhq1oO4kA4bJ5MsfzSIcC71aLDKzJ6gZpIxrqt3QTAQ==
+ dependencies:
+ has "^1.0.3"
+ object-is "^1.0.2"
+
+enzyme-shallow-equal@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.5.tgz#5528a897a6ad2bdc417c7221a7db682cd01711ba"
+ integrity sha512-i6cwm7hN630JXenxxJFBKzgLC3hMTafFQXflvzHgPmDhOBhxUWDe8AeRv1qp2/uWJ2Y8z5yLWMzmAfkTOiOCZg==
+ dependencies:
+ has "^1.0.3"
+ object-is "^1.1.5"
+
+enzyme@^3.11.0:
+ version "3.11.0"
+ resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.11.0.tgz#71d680c580fe9349f6f5ac6c775bc3e6b7a79c28"
+ integrity sha512-Dw8/Gs4vRjxY6/6i9wU0V+utmQO9kvh9XLnz3LIudviOnVYDEe2ec+0k+NQoMamn1VrjKgCUOWj5jG/5M5M0Qw==
+ dependencies:
+ array.prototype.flat "^1.2.3"
+ cheerio "^1.0.0-rc.3"
+ enzyme-shallow-equal "^1.0.1"
+ function.prototype.name "^1.1.2"
+ has "^1.0.3"
+ html-element-map "^1.2.0"
+ is-boolean-object "^1.0.1"
+ is-callable "^1.1.5"
+ is-number-object "^1.0.4"
+ is-regex "^1.0.5"
+ is-string "^1.0.5"
+ is-subset "^0.1.1"
+ lodash.escape "^4.0.1"
+ lodash.isequal "^4.5.0"
+ object-inspect "^1.7.0"
+ object-is "^1.0.2"
+ object.assign "^4.1.0"
+ object.entries "^1.1.1"
+ object.values "^1.1.1"
+ raf "^3.4.1"
+ rst-selector-parser "^2.2.3"
+ string.prototype.trim "^1.2.1"
+
+error-ex@^1.2.0, error-ex@^1.3.1:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
+ integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
+ dependencies:
+ is-arrayish "^0.2.1"
+
+es-abstract@^1.17.0-next.1, es-abstract@^1.17.4:
+ version "1.17.4"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.4.tgz#e3aedf19706b20e7c2594c35fc0d57605a79e184"
+ integrity sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==
+ dependencies:
+ es-to-primitive "^1.2.1"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+ has-symbols "^1.0.1"
+ is-callable "^1.1.5"
+ is-regex "^1.0.5"
+ object-inspect "^1.7.0"
+ object-keys "^1.1.1"
+ object.assign "^4.1.0"
+ string.prototype.trimleft "^2.1.1"
+ string.prototype.trimright "^2.1.1"
+
+es-abstract@^1.19.0, es-abstract@^1.20.4:
+ version "1.21.1"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.21.1.tgz#e6105a099967c08377830a0c9cb589d570dd86c6"
+ integrity sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg==
+ dependencies:
+ available-typed-arrays "^1.0.5"
+ call-bind "^1.0.2"
+ es-set-tostringtag "^2.0.1"
+ es-to-primitive "^1.2.1"
+ function-bind "^1.1.1"
+ function.prototype.name "^1.1.5"
+ get-intrinsic "^1.1.3"
+ get-symbol-description "^1.0.0"
+ globalthis "^1.0.3"
+ gopd "^1.0.1"
+ has "^1.0.3"
+ has-property-descriptors "^1.0.0"
+ has-proto "^1.0.1"
+ has-symbols "^1.0.3"
+ internal-slot "^1.0.4"
+ is-array-buffer "^3.0.1"
+ is-callable "^1.2.7"
+ is-negative-zero "^2.0.2"
+ is-regex "^1.1.4"
+ is-shared-array-buffer "^1.0.2"
+ is-string "^1.0.7"
+ is-typed-array "^1.1.10"
+ is-weakref "^1.0.2"
+ object-inspect "^1.12.2"
+ object-keys "^1.1.1"
+ object.assign "^4.1.4"
+ regexp.prototype.flags "^1.4.3"
+ safe-regex-test "^1.0.0"
+ string.prototype.trimend "^1.0.6"
+ string.prototype.trimstart "^1.0.6"
+ typed-array-length "^1.0.4"
+ unbox-primitive "^1.0.2"
+ which-typed-array "^1.1.9"
+
+es-array-method-boxes-properly@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e"
+ integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==
+
+es-get-iterator@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8"
+ integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==
+ dependencies:
+ es-abstract "^1.17.4"
+ has-symbols "^1.0.1"
+ is-arguments "^1.0.4"
+ is-map "^2.0.1"
+ is-set "^2.0.1"
+ is-string "^1.0.5"
+ isarray "^2.0.5"
+
+es-set-tostringtag@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8"
+ integrity sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==
+ dependencies:
+ get-intrinsic "^1.1.3"
+ has "^1.0.3"
+ has-tostringtag "^1.0.0"
+
+es-shim-unscopables@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241"
+ integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==
+ dependencies:
+ has "^1.0.3"
+
+es-to-primitive@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
+ integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
+ dependencies:
+ is-callable "^1.1.4"
+ is-date-object "^1.0.1"
+ is-symbol "^1.0.2"
+
+escalade@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
+ integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
+
+escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+ integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
+
+escape-string-regexp@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
+ integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
+
+escodegen@^1.9.1:
+ version "1.14.3"
+ resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503"
+ integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==
+ dependencies:
+ esprima "^4.0.1"
+ estraverse "^4.2.0"
+ esutils "^2.0.2"
+ optionator "^0.8.1"
+ optionalDependencies:
+ source-map "~0.6.1"
+
+esprima@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
+ integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
+
+estraverse@^4.2.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
+ integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
+
+esutils@^2.0.2:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
+ integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
+
+exec-sh@^0.3.2:
+ version "0.3.6"
+ resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.6.tgz#ff264f9e325519a60cb5e273692943483cca63bc"
+ integrity sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==
+
+execa@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
+ integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
+ dependencies:
+ cross-spawn "^6.0.0"
+ get-stream "^4.0.0"
+ is-stream "^1.1.0"
+ npm-run-path "^2.0.0"
+ p-finally "^1.0.0"
+ signal-exit "^3.0.0"
+ strip-eof "^1.0.0"
+
+exit@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
+ integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==
+
+expand-brackets@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b"
+ integrity sha512-hxx03P2dJxss6ceIeri9cmYOT4SRs3Zk3afZwWpOsRqLqprhTR8u++SlC+sFGsQr7WGFPdMF7Gjc1njDLDK6UA==
+ dependencies:
+ is-posix-bracket "^0.1.0"
+
+expand-brackets@^2.1.4:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
+ integrity sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==
+ dependencies:
+ debug "^2.3.3"
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ posix-character-classes "^0.1.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+expand-range@^1.8.1:
+ version "1.8.2"
+ resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337"
+ integrity sha512-AFASGfIlnIbkKPQwX1yHaDjFvh/1gyKJODme52V6IORh69uEYgZp0o9C+qsIGNVEiuuhQU0CSSl++Rlegg1qvA==
+ dependencies:
+ fill-range "^2.1.0"
+
+expect@^1.16.0:
+ version "1.20.2"
+ resolved "https://registry.yarnpkg.com/expect/-/expect-1.20.2.tgz#d458fe4c56004036bae3232416a3f6361f04f965"
+ integrity sha1-1Fj+TFYAQDa64yMkFqP2Nh8E+WU=
+ dependencies:
+ define-properties "~1.1.2"
+ has "^1.0.1"
+ is-equal "^1.5.1"
+ is-regex "^1.0.3"
+ object-inspect "^1.1.0"
+ object-keys "^1.0.9"
+ tmatch "^2.0.1"
+
+expect@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/expect/-/expect-24.9.0.tgz#b75165b4817074fa4a157794f46fe9f1ba15b6ca"
+ integrity sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q==
+ dependencies:
+ "@jest/types" "^24.9.0"
+ ansi-styles "^3.2.0"
+ jest-get-type "^24.9.0"
+ jest-matcher-utils "^24.9.0"
+ jest-message-util "^24.9.0"
+ jest-regex-util "^24.9.0"
+
+extend-shallow@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
+ integrity sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==
+ dependencies:
+ is-extendable "^0.1.0"
+
+extend-shallow@^3.0.0, extend-shallow@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8"
+ integrity sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==
+ dependencies:
+ assign-symbols "^1.0.0"
+ is-extendable "^1.0.1"
+
+extend@~3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
+ integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
+
+extglob@^0.3.1:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1"
+ integrity sha512-1FOj1LOwn42TMrruOHGt18HemVnbwAmAak7krWk+wa93KXxGbK+2jpezm+ytJYDaBX0/SPLZFHKM7m+tKobWGg==
+ dependencies:
+ is-extglob "^1.0.0"
+
+extglob@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
+ integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==
+ dependencies:
+ array-unique "^0.3.2"
+ define-property "^1.0.0"
+ expand-brackets "^2.1.4"
+ extend-shallow "^2.0.1"
+ fragment-cache "^0.2.1"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+extsprintf@1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
+ integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=
+
+extsprintf@^1.2.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
+ integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
+
+fast-deep-equal@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4"
+ integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==
+
+fast-json-stable-stringify@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
+ integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
+
+fast-levenshtein@~2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+ integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
+
+fb-watchman@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c"
+ integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==
+ dependencies:
+ bser "2.1.1"
+
+fbjs@^0.8.16:
+ version "0.8.18"
+ resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.18.tgz#9835e0addb9aca2eff53295cd79ca1cfc7c9662a"
+ integrity sha512-EQaWFK+fEPSoibjNy8IxUtaFOMXcWsY0JaVrQoZR9zC8N2Ygf9iDITPWjUTVIax95b6I742JFLqASHfsag/vKA==
+ dependencies:
+ core-js "^1.0.0"
+ isomorphic-fetch "^2.1.1"
+ loose-envify "^1.0.0"
+ object-assign "^4.1.0"
+ promise "^7.1.1"
+ setimmediate "^1.0.5"
+ ua-parser-js "^0.7.30"
+
+file-uri-to-path@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
+ integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
+
+filename-regex@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26"
+ integrity sha512-BTCqyBaWBTsauvnHiE8i562+EdJj+oUpkqWp2R1iCoR8f6oo8STRu3of7WJJ0TqWtxN50a5YFpzYK4Jj9esYfQ==
+
+fill-range@^2.1.0:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.4.tgz#eb1e773abb056dcd8df2bfdf6af59b8b3a936565"
+ integrity sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==
+ dependencies:
+ is-number "^2.1.0"
+ isobject "^2.0.0"
+ randomatic "^3.0.0"
+ repeat-element "^1.1.2"
+ repeat-string "^1.5.2"
+
+fill-range@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
+ integrity sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-number "^3.0.0"
+ repeat-string "^1.6.1"
+ to-regex-range "^2.1.0"
+
+find-babel-config@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/find-babel-config/-/find-babel-config-1.2.0.tgz#a9b7b317eb5b9860cda9d54740a8c8337a2283a2"
+ integrity sha512-jB2CHJeqy6a820ssiqwrKMeyC6nNdmrcgkKWJWmpoxpE8RKciYJXCcXRq1h2AzCo5I5BJeN2tkGEO3hLTuePRA==
+ dependencies:
+ json5 "^0.5.1"
+ path-exists "^3.0.0"
+
+find-cache-dir@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7"
+ integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==
+ dependencies:
+ commondir "^1.0.1"
+ make-dir "^2.0.0"
+ pkg-dir "^3.0.0"
+
+find-up@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
+ integrity sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==
+ dependencies:
+ path-exists "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+find-up@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
+ integrity sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==
+ dependencies:
+ locate-path "^2.0.0"
+
+find-up@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
+ integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
+ dependencies:
+ locate-path "^3.0.0"
+
+for-each@^0.3.3:
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
+ integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==
+ dependencies:
+ is-callable "^1.1.3"
+
+for-in@^1.0.1, for-in@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+ integrity sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==
+
+for-own@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce"
+ integrity sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==
+ dependencies:
+ for-in "^1.0.1"
+
+forever-agent@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
+ integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
+
+form-data@~2.3.2:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
+ integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.6"
+ mime-types "^2.1.12"
+
+formatio@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.1.1.tgz#5ed3ccd636551097383465d996199100e86161e9"
+ integrity sha1-XtPM1jZVEJc4NGXZlhmRAOhhYek=
+ dependencies:
+ samsam "~1.1"
+
+fragment-cache@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
+ integrity sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==
+ dependencies:
+ map-cache "^0.2.2"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+ integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
+
+fsevents@^1.2.7:
+ version "1.2.13"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38"
+ integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==
+ dependencies:
+ bindings "^1.5.0"
+ nan "^2.12.1"
+
+function-bind@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+ integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+function.prototype.name@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.2.tgz#5cdf79d7c05db401591dfde83e3b70c5123e9a45"
+ integrity sha512-C8A+LlHBJjB2AdcRPorc5JvJ5VUoWlXdEHLOJdCI7kjHEtGTpHQUiqMvCIKUwIsGwZX2jZJy761AXsn356bJQg==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.17.0-next.1"
+ functions-have-names "^1.2.0"
+
+function.prototype.name@^1.1.5:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621"
+ integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.3"
+ es-abstract "^1.19.0"
+ functions-have-names "^1.2.2"
+
+functions-have-names@^1.2.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.1.tgz#a981ac397fa0c9964551402cdc5533d7a4d52f91"
+ integrity sha512-j48B/ZI7VKs3sgeI2cZp7WXWmZXu7Iq5pl5/vptV5N2mq+DGFuS/ulaDjtaoLpYzuD6u8UgrUKHfgo7fDTSiBA==
+
+functions-have-names@^1.2.2:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
+ integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
+
+gensync@^1.0.0-beta.2:
+ version "1.0.0-beta.2"
+ resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
+ integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
+
+get-caller-file@^2.0.1:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
+ integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
+
+get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385"
+ integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==
+ dependencies:
+ function-bind "^1.1.1"
+ has "^1.0.3"
+ has-symbols "^1.0.3"
+
+get-stream@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
+ integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
+ dependencies:
+ pump "^3.0.0"
+
+get-symbol-description@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6"
+ integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==
+ dependencies:
+ call-bind "^1.0.2"
+ get-intrinsic "^1.1.1"
+
+get-value@^2.0.3, get-value@^2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
+ integrity sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==
+
+getpass@^0.1.1:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
+ integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=
+ dependencies:
+ assert-plus "^1.0.0"
+
+glob-base@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
+ integrity sha512-ab1S1g1EbO7YzauaJLkgLp7DZVAqj9M/dvKlTt8DkXA2tiOIcSMrlVI2J1RZyB5iJVccEscjGn+kpOG9788MHA==
+ dependencies:
+ glob-parent "^2.0.0"
+ is-glob "^2.0.0"
+
+glob-parent@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28"
+ integrity sha512-JDYOvfxio/t42HKdxkAYaCiBN7oYiuxykOxKxdaUW5Qn0zaYN3gRQWolrwdnf0shM9/EP0ebuuTmyoXNr1cC5w==
+ dependencies:
+ is-glob "^2.0.0"
+
+glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.6:
+ version "7.2.3"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
+ integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.1.1"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+globals@^11.1.0:
+ version "11.12.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
+ integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
+
+globals@^9.18.0:
+ version "9.18.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
+ integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==
+
+globalthis@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf"
+ integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==
+ dependencies:
+ define-properties "^1.1.3"
+
+gopd@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
+ integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==
+ dependencies:
+ get-intrinsic "^1.1.3"
+
+graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2:
+ version "4.2.10"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
+ integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
+
+growly@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
+ integrity sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==
+
+har-schema@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
+ integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
+
+har-validator@~5.1.3:
+ version "5.1.3"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
+ integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
+ dependencies:
+ ajv "^6.5.5"
+ har-schema "^2.0.0"
+
+has-ansi@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
+ integrity sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==
+ dependencies:
+ ansi-regex "^2.0.0"
+
+has-bigints@^1.0.1, has-bigints@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"
+ integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==
+
+has-flag@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+ integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
+
+has-property-descriptors@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861"
+ integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==
+ dependencies:
+ get-intrinsic "^1.1.1"
+
+has-proto@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0"
+ integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==
+
+has-symbols@^1.0.0, has-symbols@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
+ integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
+
+has-symbols@^1.0.2, has-symbols@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
+ integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
+
+has-tostringtag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25"
+ integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==
+ dependencies:
+ has-symbols "^1.0.2"
+
+has-value@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
+ integrity sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==
+ dependencies:
+ get-value "^2.0.3"
+ has-values "^0.1.4"
+ isobject "^2.0.0"
+
+has-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177"
+ integrity sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==
+ dependencies:
+ get-value "^2.0.6"
+ has-values "^1.0.0"
+ isobject "^3.0.0"
+
+has-values@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771"
+ integrity sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==
+
+has-values@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f"
+ integrity sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==
+ dependencies:
+ is-number "^3.0.0"
+ kind-of "^4.0.0"
+
+has@^1.0.1, has@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+ integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+ dependencies:
+ function-bind "^1.1.1"
+
+hosted-git-info@^2.1.4:
+ version "2.8.9"
+ resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
+ integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
+
+html-element-map@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/html-element-map/-/html-element-map-1.2.0.tgz#dfbb09efe882806af63d990cf6db37993f099f22"
+ integrity sha512-0uXq8HsuG1v2TmQ8QkIhzbrqeskE4kn52Q18QJ9iAA/SnHoEKXWiUxHQtclRsCFWEUD2So34X+0+pZZu862nnw==
+ dependencies:
+ array-filter "^1.0.0"
+
+html-encoding-sniffer@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8"
+ integrity sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==
+ dependencies:
+ whatwg-encoding "^1.0.1"
+
+html-escaper@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
+ integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
+
+htmlparser2@^3.9.1:
+ version "3.10.1"
+ resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
+ integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
+ dependencies:
+ domelementtype "^1.3.1"
+ domhandler "^2.3.0"
+ domutils "^1.5.1"
+ entities "^1.1.1"
+ inherits "^2.0.1"
+ readable-stream "^3.1.1"
+
+http-signature@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
+ integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=
+ dependencies:
+ assert-plus "^1.0.0"
+ jsprim "^1.2.2"
+ sshpk "^1.7.0"
+
+iconv-lite@0.4.24:
+ version "0.4.24"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
+ integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3"
+
+iconv-lite@^0.6.2:
+ version "0.6.3"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
+ integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3.0.0"
+
+import-local@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d"
+ integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==
+ dependencies:
+ pkg-dir "^3.0.0"
+ resolve-cwd "^2.0.0"
+
+imurmurhash@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+ integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2, inherits@^2.0.1, inherits@^2.0.3:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+ integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+internal-slot@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.4.tgz#8551e7baf74a7a6ba5f749cfb16aa60722f0d6f3"
+ integrity sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==
+ dependencies:
+ get-intrinsic "^1.1.3"
+ has "^1.0.3"
+ side-channel "^1.0.4"
+
+invariant@^2.2.2, invariant@^2.2.4:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
+ integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
+ dependencies:
+ loose-envify "^1.0.0"
+
+is-accessor-descriptor@^0.1.6:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
+ integrity sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==
+ dependencies:
+ kind-of "^3.0.2"
+
+is-accessor-descriptor@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656"
+ integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==
+ dependencies:
+ kind-of "^6.0.0"
+
+is-arguments@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3"
+ integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==
+
+is-array-buffer@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.1.tgz#deb1db4fcae48308d54ef2442706c0393997052a"
+ integrity sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ==
+ dependencies:
+ call-bind "^1.0.2"
+ get-intrinsic "^1.1.3"
+ is-typed-array "^1.1.10"
+
+is-arrayish@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+ integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==
+
+is-arrow-function@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/is-arrow-function/-/is-arrow-function-2.0.3.tgz#29be2c2d8d9450852b8bbafb635ba7b8d8e87ec2"
+ integrity sha1-Kb4sLY2UUIUri7r7Y1unuNjofsI=
+ dependencies:
+ is-callable "^1.0.4"
+
+is-bigint@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4"
+ integrity sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g==
+
+is-bigint@^1.0.1:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3"
+ integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==
+ dependencies:
+ has-bigints "^1.0.1"
+
+is-boolean-object@^1.0.0, is-boolean-object@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e"
+ integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ==
+
+is-boolean-object@^1.1.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719"
+ integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==
+ dependencies:
+ call-bind "^1.0.2"
+ has-tostringtag "^1.0.0"
+
+is-buffer@^1.1.5:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
+ integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
+
+is-callable@^1.0.4, is-callable@^1.1.4, is-callable@^1.1.5:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab"
+ integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==
+
+is-callable@^1.1.3, is-callable@^1.2.7:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055"
+ integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==
+
+is-ci@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
+ integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==
+ dependencies:
+ ci-info "^2.0.0"
+
+is-core-module@^2.9.0:
+ version "2.11.0"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144"
+ integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==
+ dependencies:
+ has "^1.0.3"
+
+is-data-descriptor@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
+ integrity sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==
+ dependencies:
+ kind-of "^3.0.2"
+
+is-data-descriptor@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7"
+ integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==
+ dependencies:
+ kind-of "^6.0.0"
+
+is-date-object@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
+ integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==
+
+is-descriptor@^0.1.0:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
+ integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==
+ dependencies:
+ is-accessor-descriptor "^0.1.6"
+ is-data-descriptor "^0.1.4"
+ kind-of "^5.0.0"
+
+is-descriptor@^1.0.0, is-descriptor@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec"
+ integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==
+ dependencies:
+ is-accessor-descriptor "^1.0.0"
+ is-data-descriptor "^1.0.0"
+ kind-of "^6.0.2"
+
+is-dotfile@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
+ integrity sha512-9YclgOGtN/f8zx0Pr4FQYMdibBiTaH3sn52vjYip4ZSf6C4/6RfTEZ+MR4GvKhCxdPh21Bg42/WL55f6KSnKpg==
+
+is-equal-shallow@^0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534"
+ integrity sha512-0EygVC5qPvIyb+gSz7zdD5/AAoS6Qrx1e//6N4yv4oNm30kqvdmG66oZFWVlQHUWe5OjP08FuTw2IdT0EOTcYA==
+ dependencies:
+ is-primitive "^2.0.0"
+
+is-equal@^1.5.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/is-equal/-/is-equal-1.6.1.tgz#74fafde5060fcaf187041c05f11f0b9f020bb9b3"
+ integrity sha512-3/79QTolnfNFrxQAvqH8M+O01uGWsVq54BUPG2mXQH7zi4BE/0TY+fmA444t8xSBvIwyNMvsTmCZ5ViVDlqPJg==
+ dependencies:
+ es-get-iterator "^1.0.1"
+ functions-have-names "^1.2.0"
+ has "^1.0.3"
+ is-arrow-function "^2.0.3"
+ is-bigint "^1.0.0"
+ is-boolean-object "^1.0.0"
+ is-callable "^1.1.4"
+ is-date-object "^1.0.1"
+ is-generator-function "^1.0.7"
+ is-number-object "^1.0.3"
+ is-regex "^1.0.4"
+ is-string "^1.0.4"
+ is-symbol "^1.0.3"
+ isarray "^2.0.5"
+ object-inspect "^1.7.0"
+ object.entries "^1.1.0"
+ which-boxed-primitive "^1.0.1"
+ which-collection "^1.0.0"
+
+is-extendable@^0.1.0, is-extendable@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
+ integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==
+
+is-extendable@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4"
+ integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==
+ dependencies:
+ is-plain-object "^2.0.4"
+
+is-extglob@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0"
+ integrity sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==
+
+is-finite@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3"
+ integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==
+
+is-fullwidth-code-point@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+ integrity sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==
+
+is-generator-fn@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118"
+ integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==
+
+is-generator-function@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522"
+ integrity sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==
+
+is-glob@^2.0.0, is-glob@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863"
+ integrity sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==
+ dependencies:
+ is-extglob "^1.0.0"
+
+is-map@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1"
+ integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==
+
+is-negative-zero@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150"
+ integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==
+
+is-number-object@^1.0.3, is-number-object@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197"
+ integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==
+
+is-number@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f"
+ integrity sha512-QUzH43Gfb9+5yckcrSA0VBDwEtDUchrk4F6tfJZQuNzDJbEDB9cZNzSfXGQ1jqmdDY/kl41lUOWM9syA8z8jlg==
+ dependencies:
+ kind-of "^3.0.2"
+
+is-number@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
+ integrity sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==
+ dependencies:
+ kind-of "^3.0.2"
+
+is-number@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff"
+ integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==
+
+is-plain-object@^2.0.3, is-plain-object@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
+ integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
+ dependencies:
+ isobject "^3.0.1"
+
+is-posix-bracket@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4"
+ integrity sha512-Yu68oeXJ7LeWNmZ3Zov/xg/oDBnBK2RNxwYY1ilNJX+tKKZqgPK+qOn/Gs9jEu66KDY9Netf5XLKNGzas/vPfQ==
+
+is-primitive@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575"
+ integrity sha512-N3w1tFaRfk3UrPfqeRyD+GYDASU3W5VinKhlORy8EWVf/sIdDL9GAcew85XmktCfH+ngG7SRXEVDoO18WMdB/Q==
+
+is-regex@^1.0.3, is-regex@^1.0.4, is-regex@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae"
+ integrity sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==
+ dependencies:
+ has "^1.0.3"
+
+is-regex@^1.1.0, is-regex@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
+ integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==
+ dependencies:
+ call-bind "^1.0.2"
+ has-tostringtag "^1.0.0"
+
+is-set@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43"
+ integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==
+
+is-shared-array-buffer@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79"
+ integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==
+ dependencies:
+ call-bind "^1.0.2"
+
+is-stream@^1.0.1, is-stream@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
+ integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==
+
+is-string@^1.0.4, is-string@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
+ integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
+
+is-string@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd"
+ integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==
+ dependencies:
+ has-tostringtag "^1.0.0"
+
+is-subset@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6"
+ integrity sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=
+
+is-symbol@^1.0.2, is-symbol@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
+ integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==
+ dependencies:
+ has-symbols "^1.0.1"
+
+is-typed-array@^1.1.10, is-typed-array@^1.1.9:
+ version "1.1.10"
+ resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.10.tgz#36a5b5cb4189b575d1a3e4b08536bfb485801e3f"
+ integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==
+ dependencies:
+ available-typed-arrays "^1.0.5"
+ call-bind "^1.0.2"
+ for-each "^0.3.3"
+ gopd "^1.0.1"
+ has-tostringtag "^1.0.0"
+
+is-typedarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+ integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
+
+is-utf8@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
+ integrity sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==
+
+is-weakmap@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2"
+ integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==
+
+is-weakref@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2"
+ integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==
+ dependencies:
+ call-bind "^1.0.2"
+
+is-weakset@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83"
+ integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw==
+
+is-windows@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
+ integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
+
+is-wsl@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
+ integrity sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==
+
+isarray@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+ integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
+
+isarray@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
+ integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+ integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
+
+isobject@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
+ integrity sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==
+ dependencies:
+ isarray "1.0.0"
+
+isobject@^3.0.0, isobject@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+ integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==
+
+isomorphic-fetch@^2.1.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
+ integrity sha512-9c4TNAKYXM5PRyVcwUZrF3W09nQ+sO7+jydgs4ZGW9dhsLG2VOlISJABombdQqQRXCwuYG3sYV/puGf5rp0qmA==
+ dependencies:
+ node-fetch "^1.0.1"
+ whatwg-fetch ">=0.10.0"
+
+isstream@~0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+ integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
+
+istanbul-lib-coverage@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz#ccf7edcd0a0bb9b8f729feeb0930470f9af664f0"
+ integrity sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ==
+
+istanbul-lib-coverage@^2.0.2, istanbul-lib-coverage@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49"
+ integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==
+
+istanbul-lib-instrument@^1.10.1:
+ version "1.10.2"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz#1f55ed10ac3c47f2bdddd5307935126754d0a9ca"
+ integrity sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A==
+ dependencies:
+ babel-generator "^6.18.0"
+ babel-template "^6.16.0"
+ babel-traverse "^6.18.0"
+ babel-types "^6.18.0"
+ babylon "^6.18.0"
+ istanbul-lib-coverage "^1.2.1"
+ semver "^5.3.0"
+
+istanbul-lib-instrument@^3.0.1, istanbul-lib-instrument@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz#a5f63d91f0bbc0c3e479ef4c5de027335ec6d630"
+ integrity sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==
+ dependencies:
+ "@babel/generator" "^7.4.0"
+ "@babel/parser" "^7.4.3"
+ "@babel/template" "^7.4.0"
+ "@babel/traverse" "^7.4.3"
+ "@babel/types" "^7.4.0"
+ istanbul-lib-coverage "^2.0.5"
+ semver "^6.0.0"
+
+istanbul-lib-report@^2.0.4:
+ version "2.0.8"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz#5a8113cd746d43c4889eba36ab10e7d50c9b4f33"
+ integrity sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==
+ dependencies:
+ istanbul-lib-coverage "^2.0.5"
+ make-dir "^2.1.0"
+ supports-color "^6.1.0"
+
+istanbul-lib-source-maps@^3.0.1:
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz#284997c48211752ec486253da97e3879defba8c8"
+ integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==
+ dependencies:
+ debug "^4.1.1"
+ istanbul-lib-coverage "^2.0.5"
+ make-dir "^2.1.0"
+ rimraf "^2.6.3"
+ source-map "^0.6.1"
+
+istanbul-reports@^2.2.6:
+ version "2.2.7"
+ resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-2.2.7.tgz#5d939f6237d7b48393cc0959eab40cd4fd056931"
+ integrity sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg==
+ dependencies:
+ html-escaper "^2.0.0"
+
+jest-changed-files@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.9.0.tgz#08d8c15eb79a7fa3fc98269bc14b451ee82f8039"
+ integrity sha512-6aTWpe2mHF0DhL28WjdkO8LyGjs3zItPET4bMSeXU6T3ub4FPMw+mcOcbdGXQOAfmLcxofD23/5Bl9Z4AkFwqg==
+ dependencies:
+ "@jest/types" "^24.9.0"
+ execa "^1.0.0"
+ throat "^4.0.0"
+
+jest-cli@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-24.9.0.tgz#ad2de62d07472d419c6abc301fc432b98b10d2af"
+ integrity sha512-+VLRKyitT3BWoMeSUIHRxV/2g8y9gw91Jh5z2UmXZzkZKpbC08CSehVxgHUwTpy+HwGcns/tqafQDJW7imYvGg==
+ dependencies:
+ "@jest/core" "^24.9.0"
+ "@jest/test-result" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ chalk "^2.0.1"
+ exit "^0.1.2"
+ import-local "^2.0.0"
+ is-ci "^2.0.0"
+ jest-config "^24.9.0"
+ jest-util "^24.9.0"
+ jest-validate "^24.9.0"
+ prompts "^2.0.1"
+ realpath-native "^1.1.0"
+ yargs "^13.3.0"
+
+jest-config@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-24.9.0.tgz#fb1bbc60c73a46af03590719efa4825e6e4dd1b5"
+ integrity sha512-RATtQJtVYQrp7fvWg6f5y3pEFj9I+H8sWw4aKxnDZ96mob5i5SD6ZEGWgMLXQ4LE8UurrjbdlLWdUeo+28QpfQ==
+ dependencies:
+ "@babel/core" "^7.1.0"
+ "@jest/test-sequencer" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ babel-jest "^24.9.0"
+ chalk "^2.0.1"
+ glob "^7.1.1"
+ jest-environment-jsdom "^24.9.0"
+ jest-environment-node "^24.9.0"
+ jest-get-type "^24.9.0"
+ jest-jasmine2 "^24.9.0"
+ jest-regex-util "^24.3.0"
+ jest-resolve "^24.9.0"
+ jest-util "^24.9.0"
+ jest-validate "^24.9.0"
+ micromatch "^3.1.10"
+ pretty-format "^24.9.0"
+ realpath-native "^1.1.0"
+
+jest-diff@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-24.9.0.tgz#931b7d0d5778a1baf7452cb816e325e3724055da"
+ integrity sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==
+ dependencies:
+ chalk "^2.0.1"
+ diff-sequences "^24.9.0"
+ jest-get-type "^24.9.0"
+ pretty-format "^24.9.0"
+
+jest-docblock@^24.3.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.9.0.tgz#7970201802ba560e1c4092cc25cbedf5af5a8ce2"
+ integrity sha512-F1DjdpDMJMA1cN6He0FNYNZlo3yYmOtRUnktrT9Q37njYzC5WEaDdmbynIgy0L/IvXvvgsG8OsqhLPXTpfmZAA==
+ dependencies:
+ detect-newline "^2.1.0"
+
+jest-each@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-24.9.0.tgz#eb2da602e2a610898dbc5f1f6df3ba86b55f8b05"
+ integrity sha512-ONi0R4BvW45cw8s2Lrx8YgbeXL1oCQ/wIDwmsM3CqM/nlblNCPmnC3IPQlMbRFZu3wKdQ2U8BqM6lh3LJ5Bsog==
+ dependencies:
+ "@jest/types" "^24.9.0"
+ chalk "^2.0.1"
+ jest-get-type "^24.9.0"
+ jest-util "^24.9.0"
+ pretty-format "^24.9.0"
+
+jest-environment-jsdom@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-24.9.0.tgz#4b0806c7fc94f95edb369a69cc2778eec2b7375b"
+ integrity sha512-Zv9FV9NBRzLuALXjvRijO2351DRQeLYXtpD4xNvfoVFw21IOKNhZAEUKcbiEtjTkm2GsJ3boMVgkaR7rN8qetA==
+ dependencies:
+ "@jest/environment" "^24.9.0"
+ "@jest/fake-timers" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ jest-mock "^24.9.0"
+ jest-util "^24.9.0"
+ jsdom "^11.5.1"
+
+jest-environment-node@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-24.9.0.tgz#333d2d2796f9687f2aeebf0742b519f33c1cbfd3"
+ integrity sha512-6d4V2f4nxzIzwendo27Tr0aFm+IXWa0XEUnaH6nU0FMaozxovt+sfRvh4J47wL1OvF83I3SSTu0XK+i4Bqe7uA==
+ dependencies:
+ "@jest/environment" "^24.9.0"
+ "@jest/fake-timers" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ jest-mock "^24.9.0"
+ jest-util "^24.9.0"
+
+jest-get-type@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e"
+ integrity sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==
+
+jest-haste-map@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.9.0.tgz#b38a5d64274934e21fa417ae9a9fbeb77ceaac7d"
+ integrity sha512-kfVFmsuWui2Sj1Rp1AJ4D9HqJwE4uwTlS/vO+eRUaMmd54BFpli2XhMQnPC2k4cHFVbB2Q2C+jtI1AGLgEnCjQ==
+ dependencies:
+ "@jest/types" "^24.9.0"
+ anymatch "^2.0.0"
+ fb-watchman "^2.0.0"
+ graceful-fs "^4.1.15"
+ invariant "^2.2.4"
+ jest-serializer "^24.9.0"
+ jest-util "^24.9.0"
+ jest-worker "^24.9.0"
+ micromatch "^3.1.10"
+ sane "^4.0.3"
+ walker "^1.0.7"
+ optionalDependencies:
+ fsevents "^1.2.7"
+
+jest-jasmine2@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-24.9.0.tgz#1f7b1bd3242c1774e62acabb3646d96afc3be6a0"
+ integrity sha512-Cq7vkAgaYKp+PsX+2/JbTarrk0DmNhsEtqBXNwUHkdlbrTBLtMJINADf2mf5FkowNsq8evbPc07/qFO0AdKTzw==
+ dependencies:
+ "@babel/traverse" "^7.1.0"
+ "@jest/environment" "^24.9.0"
+ "@jest/test-result" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ chalk "^2.0.1"
+ co "^4.6.0"
+ expect "^24.9.0"
+ is-generator-fn "^2.0.0"
+ jest-each "^24.9.0"
+ jest-matcher-utils "^24.9.0"
+ jest-message-util "^24.9.0"
+ jest-runtime "^24.9.0"
+ jest-snapshot "^24.9.0"
+ jest-util "^24.9.0"
+ pretty-format "^24.9.0"
+ throat "^4.0.0"
+
+jest-leak-detector@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-24.9.0.tgz#b665dea7c77100c5c4f7dfcb153b65cf07dcf96a"
+ integrity sha512-tYkFIDsiKTGwb2FG1w8hX9V0aUb2ot8zY/2nFg087dUageonw1zrLMP4W6zsRO59dPkTSKie+D4rhMuP9nRmrA==
+ dependencies:
+ jest-get-type "^24.9.0"
+ pretty-format "^24.9.0"
+
+jest-matcher-utils@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz#f5b3661d5e628dffe6dd65251dfdae0e87c3a073"
+ integrity sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA==
+ dependencies:
+ chalk "^2.0.1"
+ jest-diff "^24.9.0"
+ jest-get-type "^24.9.0"
+ pretty-format "^24.9.0"
+
+jest-message-util@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-24.9.0.tgz#527f54a1e380f5e202a8d1149b0ec872f43119e3"
+ integrity sha512-oCj8FiZ3U0hTP4aSui87P4L4jC37BtQwUMqk+zk/b11FR19BJDeZsZAvIHutWnmtw7r85UmR3CEWZ0HWU2mAlw==
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ "@jest/test-result" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ "@types/stack-utils" "^1.0.1"
+ chalk "^2.0.1"
+ micromatch "^3.1.10"
+ slash "^2.0.0"
+ stack-utils "^1.0.1"
+
+jest-mock@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-24.9.0.tgz#c22835541ee379b908673ad51087a2185c13f1c6"
+ integrity sha512-3BEYN5WbSq9wd+SyLDES7AHnjH9A/ROBwmz7l2y+ol+NtSFO8DYiEBzoO1CeFc9a8DYy10EO4dDFVv/wN3zl1w==
+ dependencies:
+ "@jest/types" "^24.9.0"
+
+jest-pnp-resolver@^1.2.1:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e"
+ integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==
+
+jest-regex-util@^24.3.0, jest-regex-util@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-24.9.0.tgz#c13fb3380bde22bf6575432c493ea8fe37965636"
+ integrity sha512-05Cmb6CuxaA+Ys6fjr3PhvV3bGQmO+2p2La4hFbU+W5uOc479f7FdLXUWXw4pYMAhhSZIuKHwSXSu6CsSBAXQA==
+
+jest-resolve-dependencies@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-24.9.0.tgz#ad055198959c4cfba8a4f066c673a3f0786507ab"
+ integrity sha512-Fm7b6AlWnYhT0BXy4hXpactHIqER7erNgIsIozDXWl5dVm+k8XdGVe1oTg1JyaFnOxarMEbax3wyRJqGP2Pq+g==
+ dependencies:
+ "@jest/types" "^24.9.0"
+ jest-regex-util "^24.3.0"
+ jest-snapshot "^24.9.0"
+
+jest-resolve@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-24.9.0.tgz#dff04c7687af34c4dd7e524892d9cf77e5d17321"
+ integrity sha512-TaLeLVL1l08YFZAt3zaPtjiVvyy4oSA6CRe+0AFPPVX3Q/VI0giIWWoAvoS5L96vj9Dqxj4fB5p2qrHCmTU/MQ==
+ dependencies:
+ "@jest/types" "^24.9.0"
+ browser-resolve "^1.11.3"
+ chalk "^2.0.1"
+ jest-pnp-resolver "^1.2.1"
+ realpath-native "^1.1.0"
+
+jest-runner@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-24.9.0.tgz#574fafdbd54455c2b34b4bdf4365a23857fcdf42"
+ integrity sha512-KksJQyI3/0mhcfspnxxEOBueGrd5E4vV7ADQLT9ESaCzz02WnbdbKWIf5Mkaucoaj7obQckYPVX6JJhgUcoWWg==
+ dependencies:
+ "@jest/console" "^24.7.1"
+ "@jest/environment" "^24.9.0"
+ "@jest/test-result" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ chalk "^2.4.2"
+ exit "^0.1.2"
+ graceful-fs "^4.1.15"
+ jest-config "^24.9.0"
+ jest-docblock "^24.3.0"
+ jest-haste-map "^24.9.0"
+ jest-jasmine2 "^24.9.0"
+ jest-leak-detector "^24.9.0"
+ jest-message-util "^24.9.0"
+ jest-resolve "^24.9.0"
+ jest-runtime "^24.9.0"
+ jest-util "^24.9.0"
+ jest-worker "^24.6.0"
+ source-map-support "^0.5.6"
+ throat "^4.0.0"
+
+jest-runtime@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-24.9.0.tgz#9f14583af6a4f7314a6a9d9f0226e1a781c8e4ac"
+ integrity sha512-8oNqgnmF3v2J6PVRM2Jfuj8oX3syKmaynlDMMKQ4iyzbQzIG6th5ub/lM2bCMTmoTKM3ykcUYI2Pw9xwNtjMnw==
+ dependencies:
+ "@jest/console" "^24.7.1"
+ "@jest/environment" "^24.9.0"
+ "@jest/source-map" "^24.3.0"
+ "@jest/transform" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ "@types/yargs" "^13.0.0"
+ chalk "^2.0.1"
+ exit "^0.1.2"
+ glob "^7.1.3"
+ graceful-fs "^4.1.15"
+ jest-config "^24.9.0"
+ jest-haste-map "^24.9.0"
+ jest-message-util "^24.9.0"
+ jest-mock "^24.9.0"
+ jest-regex-util "^24.3.0"
+ jest-resolve "^24.9.0"
+ jest-snapshot "^24.9.0"
+ jest-util "^24.9.0"
+ jest-validate "^24.9.0"
+ realpath-native "^1.1.0"
+ slash "^2.0.0"
+ strip-bom "^3.0.0"
+ yargs "^13.3.0"
+
+jest-serializer@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-24.9.0.tgz#e6d7d7ef96d31e8b9079a714754c5d5c58288e73"
+ integrity sha512-DxYipDr8OvfrKH3Kel6NdED3OXxjvxXZ1uIY2I9OFbGg+vUkkg7AGvi65qbhbWNPvDckXmzMPbK3u3HaDO49bQ==
+
+jest-snapshot@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-24.9.0.tgz#ec8e9ca4f2ec0c5c87ae8f925cf97497b0e951ba"
+ integrity sha512-uI/rszGSs73xCM0l+up7O7a40o90cnrk429LOiK3aeTvfC0HHmldbd81/B7Ix81KSFe1lwkbl7GnBGG4UfuDew==
+ dependencies:
+ "@babel/types" "^7.0.0"
+ "@jest/types" "^24.9.0"
+ chalk "^2.0.1"
+ expect "^24.9.0"
+ jest-diff "^24.9.0"
+ jest-get-type "^24.9.0"
+ jest-matcher-utils "^24.9.0"
+ jest-message-util "^24.9.0"
+ jest-resolve "^24.9.0"
+ mkdirp "^0.5.1"
+ natural-compare "^1.4.0"
+ pretty-format "^24.9.0"
+ semver "^6.2.0"
+
+jest-util@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-24.9.0.tgz#7396814e48536d2e85a37de3e4c431d7cb140162"
+ integrity sha512-x+cZU8VRmOJxbA1K5oDBdxQmdq0OIdADarLxk0Mq+3XS4jgvhG/oKGWcIDCtPG0HgjxOYvF+ilPJQsAyXfbNOg==
+ dependencies:
+ "@jest/console" "^24.9.0"
+ "@jest/fake-timers" "^24.9.0"
+ "@jest/source-map" "^24.9.0"
+ "@jest/test-result" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ callsites "^3.0.0"
+ chalk "^2.0.1"
+ graceful-fs "^4.1.15"
+ is-ci "^2.0.0"
+ mkdirp "^0.5.1"
+ slash "^2.0.0"
+ source-map "^0.6.0"
+
+jest-validate@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-24.9.0.tgz#0775c55360d173cd854e40180756d4ff52def8ab"
+ integrity sha512-HPIt6C5ACwiqSiwi+OfSSHbK8sG7akG8eATl+IPKaeIjtPOeBUd/g3J7DghugzxrGjI93qS/+RPKe1H6PqvhRQ==
+ dependencies:
+ "@jest/types" "^24.9.0"
+ camelcase "^5.3.1"
+ chalk "^2.0.1"
+ jest-get-type "^24.9.0"
+ leven "^3.1.0"
+ pretty-format "^24.9.0"
+
+jest-watcher@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-24.9.0.tgz#4b56e5d1ceff005f5b88e528dc9afc8dd4ed2b3b"
+ integrity sha512-+/fLOfKPXXYJDYlks62/4R4GoT+GU1tYZed99JSCOsmzkkF7727RqKrjNAxtfO4YpGv11wybgRvCjR73lK2GZw==
+ dependencies:
+ "@jest/test-result" "^24.9.0"
+ "@jest/types" "^24.9.0"
+ "@types/yargs" "^13.0.0"
+ ansi-escapes "^3.0.0"
+ chalk "^2.0.1"
+ jest-util "^24.9.0"
+ string-length "^2.0.0"
+
+jest-worker@^24.6.0, jest-worker@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.9.0.tgz#5dbfdb5b2d322e98567898238a9697bcce67b3e5"
+ integrity sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==
+ dependencies:
+ merge-stream "^2.0.0"
+ supports-color "^6.1.0"
+
+jest@^24.6.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/jest/-/jest-24.9.0.tgz#987d290c05a08b52c56188c1002e368edb007171"
+ integrity sha512-YvkBL1Zm7d2B1+h5fHEOdyjCG+sGMz4f8D86/0HiqJ6MB4MnDc8FgP5vdWsGnemOQro7lnYo8UakZ3+5A0jxGw==
+ dependencies:
+ import-local "^2.0.0"
+ jest-cli "^24.9.0"
+
+"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+ integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+js-tokens@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
+ integrity sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==
+
+jsbn@~0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+ integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
+
+jsdom@^11.5.1:
+ version "11.12.0"
+ resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8"
+ integrity sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==
+ dependencies:
+ abab "^2.0.0"
+ acorn "^5.5.3"
+ acorn-globals "^4.1.0"
+ array-equal "^1.0.0"
+ cssom ">= 0.3.2 < 0.4.0"
+ cssstyle "^1.0.0"
+ data-urls "^1.0.0"
+ domexception "^1.0.1"
+ escodegen "^1.9.1"
+ html-encoding-sniffer "^1.0.2"
+ left-pad "^1.3.0"
+ nwsapi "^2.0.7"
+ parse5 "4.0.0"
+ pn "^1.1.0"
+ request "^2.87.0"
+ request-promise-native "^1.0.5"
+ sax "^1.2.4"
+ symbol-tree "^3.2.2"
+ tough-cookie "^2.3.4"
+ w3c-hr-time "^1.0.1"
+ webidl-conversions "^4.0.2"
+ whatwg-encoding "^1.0.3"
+ whatwg-mimetype "^2.1.0"
+ whatwg-url "^6.4.1"
+ ws "^5.2.0"
+ xml-name-validator "^3.0.0"
+
+jsesc@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
+ integrity sha512-Mke0DA0QjUWuJlhsE0ZPPhYiJkRap642SmI/4ztCFaUs6V2AiH1sfecc+57NgaryfAA2VR3v6O+CSjC1jZJKOA==
+
+jsesc@^2.5.1:
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
+ integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
+
+jsesc@~0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
+ integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==
+
+json-parse-better-errors@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
+ integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
+
+json-schema-traverse@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+ integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+
+json-schema@0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
+ integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
+
+json-stringify-safe@~5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+ integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
+
+json5@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
+ integrity sha512-4xrs1aW+6N5DalkqSVA8fxh458CXvR99WU8WLKmq4v8eWAL86Xo3BVqyd3SkA9wEVjCMqyvvRRkshAdOnBp5rw==
+
+json5@^2.2.2:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
+ integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
+
+jsprim@^1.2.2:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
+ integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=
+ dependencies:
+ assert-plus "1.0.0"
+ extsprintf "1.3.0"
+ json-schema "0.2.3"
+ verror "1.10.0"
+
+kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+ integrity sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57"
+ integrity sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^5.0.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
+ integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==
+
+kind-of@^6.0.0, kind-of@^6.0.2:
+ version "6.0.3"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
+ integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
+
+kleur@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
+ integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
+
+left-pad@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e"
+ integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==
+
+leven@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
+ integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==
+
+levn@~0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
+ integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=
+ dependencies:
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+
+load-json-file@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
+ integrity sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^2.2.0"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+ strip-bom "^2.0.0"
+
+load-json-file@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
+ integrity sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^4.0.0"
+ pify "^3.0.0"
+ strip-bom "^3.0.0"
+
+locate-path@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
+ integrity sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==
+ dependencies:
+ p-locate "^2.0.0"
+ path-exists "^3.0.0"
+
+locate-path@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
+ integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
+ dependencies:
+ p-locate "^3.0.0"
+ path-exists "^3.0.0"
+
+lodash.debounce@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
+ integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
+
+lodash.escape@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98"
+ integrity sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg=
+
+lodash.flattendeep@^4.4.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
+ integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=
+
+lodash.isequal@^4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
+ integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
+
+lodash.sortby@^4.7.0:
+ version "4.7.0"
+ resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
+ integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==
+
+lodash@^4.15.0:
+ version "4.17.15"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
+ integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
+
+lodash@^4.17.19:
+ version "4.17.20"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
+ integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
+
+lodash@^4.17.4:
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+ integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
+lolex@1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.3.2.tgz#7c3da62ffcb30f0f5a80a2566ca24e45d8a01f31"
+ integrity sha1-fD2mL/yzDw9agKJWbKJORdigHzE=
+
+loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
+ integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
+ dependencies:
+ js-tokens "^3.0.0 || ^4.0.0"
+
+lru-cache@^5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
+ integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
+ dependencies:
+ yallist "^3.0.2"
+
+make-dir@^2.0.0, make-dir@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
+ integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==
+ dependencies:
+ pify "^4.0.1"
+ semver "^5.6.0"
+
+makeerror@1.0.12:
+ version "1.0.12"
+ resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a"
+ integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==
+ dependencies:
+ tmpl "1.0.5"
+
+map-cache@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
+ integrity sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==
+
+map-visit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
+ integrity sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==
+ dependencies:
+ object-visit "^1.0.0"
+
+math-random@^1.0.1:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c"
+ integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==
+
+merge-stream@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
+ integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
+
+micromatch@^2.3.11:
+ version "2.3.11"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
+ integrity sha512-LnU2XFEk9xxSJ6rfgAry/ty5qwUTyHYOBU0g4R6tIw5ljwgGIBmiKhRWLw5NpMOnrgUNcDJ4WMp8rl3sYVHLNA==
+ dependencies:
+ arr-diff "^2.0.0"
+ array-unique "^0.2.1"
+ braces "^1.8.2"
+ expand-brackets "^0.1.4"
+ extglob "^0.3.1"
+ filename-regex "^2.0.0"
+ is-extglob "^1.0.0"
+ is-glob "^2.0.1"
+ kind-of "^3.0.2"
+ normalize-path "^2.0.1"
+ object.omit "^2.0.0"
+ parse-glob "^3.0.4"
+ regex-cache "^0.4.2"
+
+micromatch@^3.1.10, micromatch@^3.1.4:
+ version "3.1.10"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
+ integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ braces "^2.3.1"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ extglob "^2.0.4"
+ fragment-cache "^0.2.1"
+ kind-of "^6.0.2"
+ nanomatch "^1.2.9"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.2"
+
+mime-db@1.43.0:
+ version "1.43.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58"
+ integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==
+
+mime-types@^2.1.12, mime-types@~2.1.19:
+ version "2.1.26"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06"
+ integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==
+ dependencies:
+ mime-db "1.43.0"
+
+minimatch@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+ integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimatch@^3.1.1:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
+ integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.6:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
+ integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
+
+mixin-deep@^1.2.0:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
+ integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==
+ dependencies:
+ for-in "^1.0.2"
+ is-extendable "^1.0.1"
+
+mkdirp@^0.5.1:
+ version "0.5.6"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
+ integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
+ dependencies:
+ minimist "^1.2.6"
+
+moo@^0.5.0:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.1.tgz#7aae7f384b9b09f620b6abf6f74ebbcd1b65dbc4"
+ integrity sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w==
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+ integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
+
+ms@2.1.2, ms@^2.1.1:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+ integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+nan@^2.12.1:
+ version "2.17.0"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
+ integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
+
+nanomatch@^1.2.9:
+ version "1.2.13"
+ resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
+ integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ fragment-cache "^0.2.1"
+ is-windows "^1.0.2"
+ kind-of "^6.0.2"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+natural-compare@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+ integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
+
+nearley@^2.7.10:
+ version "2.19.1"
+ resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.19.1.tgz#4af4006e16645ff800e9f993c3af039857d9dbdc"
+ integrity sha512-xq47GIUGXxU9vQg7g/y1o1xuKnkO7ev4nRWqftmQrLkfnE/FjRqDaGOUakM8XHPn/6pW3bGjU2wgoJyId90rqg==
+ dependencies:
+ commander "^2.19.0"
+ moo "^0.5.0"
+ railroad-diagrams "^1.0.0"
+ randexp "0.4.6"
+ semver "^5.4.1"
+
+nice-try@^1.0.4:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
+ integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+
+node-fetch@^1.0.1:
+ version "1.7.3"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
+ integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==
+ dependencies:
+ encoding "^0.1.11"
+ is-stream "^1.0.1"
+
+node-int64@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
+ integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==
+
+node-notifier@^5.4.2:
+ version "5.4.5"
+ resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.4.5.tgz#0cbc1a2b0f658493b4025775a13ad938e96091ef"
+ integrity sha512-tVbHs7DyTLtzOiN78izLA85zRqB9NvEXkAf014Vx3jtSvn/xBl6bR8ZYifj+dFcFrKI21huSQgJZ6ZtL3B4HfQ==
+ dependencies:
+ growly "^1.3.0"
+ is-wsl "^1.1.0"
+ semver "^5.5.0"
+ shellwords "^0.1.1"
+ which "^1.3.0"
+
+node-releases@^2.0.6:
+ version "2.0.8"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.8.tgz#0f349cdc8fcfa39a92ac0be9bc48b7706292b9ae"
+ integrity sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==
+
+normalize-package-data@^2.3.2:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
+ integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
+ dependencies:
+ hosted-git-info "^2.1.4"
+ resolve "^1.10.0"
+ semver "2 || 3 || 4 || 5"
+ validate-npm-package-license "^3.0.1"
+
+normalize-path@^2.0.1, normalize-path@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
+ integrity sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==
+ dependencies:
+ remove-trailing-separator "^1.0.1"
+
+npm-run-path@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
+ integrity sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==
+ dependencies:
+ path-key "^2.0.0"
+
+nth-check@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
+ integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==
+ dependencies:
+ boolbase "~1.0.0"
+
+nwsapi@^2.0.7:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.2.tgz#e5418863e7905df67d51ec95938d67bf801f0bb0"
+ integrity sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==
+
+oauth-sign@~0.9.0:
+ version "0.9.0"
+ resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
+ integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
+
+object-assign@^4.1.0, object-assign@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+ integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
+
+object-copy@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
+ integrity sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==
+ dependencies:
+ copy-descriptor "^0.1.0"
+ define-property "^0.2.5"
+ kind-of "^3.0.3"
+
+object-inspect@^1.1.0, object-inspect@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
+ integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==
+
+object-inspect@^1.12.2, object-inspect@^1.9.0:
+ version "1.12.3"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9"
+ integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==
+
+object-is@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.2.tgz#6b80eb84fe451498f65007982f035a5b445edec4"
+ integrity sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ==
+
+object-is@^1.1.2, object-is@^1.1.5:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac"
+ integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.3"
+
+object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.0.9, object-keys@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
+ integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
+
+object-visit@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
+ integrity sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==
+ dependencies:
+ isobject "^3.0.0"
+
+object.assign@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
+ integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==
+ dependencies:
+ define-properties "^1.1.2"
+ function-bind "^1.1.1"
+ has-symbols "^1.0.0"
+ object-keys "^1.0.11"
+
+object.assign@^4.1.4:
+ version "4.1.4"
+ resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f"
+ integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.4"
+ has-symbols "^1.0.3"
+ object-keys "^1.1.1"
+
+object.entries@^1.1.0, object.entries@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.1.tgz#ee1cf04153de02bb093fec33683900f57ce5399b"
+ integrity sha512-ilqR7BgdyZetJutmDPfXCDffGa0/Yzl2ivVNpbx/g4UeWrCdRnFDUBrKJGLhGieRHDATnyZXWBeCb29k9CJysQ==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.17.0-next.1"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+
+object.entries@^1.1.2:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.6.tgz#9737d0e5b8291edd340a3e3264bb8a3b00d5fa23"
+ integrity sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.4"
+ es-abstract "^1.20.4"
+
+object.fromentries@^2.0.5:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.6.tgz#cdb04da08c539cffa912dcd368b886e0904bfa73"
+ integrity sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.4"
+ es-abstract "^1.20.4"
+
+object.getownpropertydescriptors@^2.1.1:
+ version "2.1.5"
+ resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.5.tgz#db5a9002489b64eef903df81d6623c07e5b4b4d3"
+ integrity sha512-yDNzckpM6ntyQiGTik1fKV1DcVDRS+w8bvpWNCBanvH5LfRX9O8WTHqQzG4RZwRAM4I0oU7TV11Lj5v0g20ibw==
+ dependencies:
+ array.prototype.reduce "^1.0.5"
+ call-bind "^1.0.2"
+ define-properties "^1.1.4"
+ es-abstract "^1.20.4"
+
+object.omit@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa"
+ integrity sha512-UiAM5mhmIuKLsOvrL+B0U2d1hXHF3bFYWIuH1LMpuV2EJEHG1Ntz06PgLEHjm6VFd87NpH8rastvPoyv6UW2fA==
+ dependencies:
+ for-own "^0.1.4"
+ is-extendable "^0.1.1"
+
+object.pick@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
+ integrity sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==
+ dependencies:
+ isobject "^3.0.1"
+
+object.values@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e"
+ integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.17.0-next.1"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+
+object.values@^1.1.5:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.6.tgz#4abbaa71eba47d63589d402856f908243eea9b1d"
+ integrity sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.4"
+ es-abstract "^1.20.4"
+
+once@^1.3.0, once@^1.3.1, once@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
+ dependencies:
+ wrappy "1"
+
+optionator@^0.8.1:
+ version "0.8.3"
+ resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
+ integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
+ dependencies:
+ deep-is "~0.1.3"
+ fast-levenshtein "~2.0.6"
+ levn "~0.3.0"
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+ word-wrap "~1.2.3"
+
+p-each-series@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-1.0.0.tgz#930f3d12dd1f50e7434457a22cd6f04ac6ad7f71"
+ integrity sha512-J/e9xiZZQNrt+958FFzJ+auItsBGq+UrQ7nE89AUP7UOTtjHnkISANXLdayhVzh538UnLMCSlf13lFfRIAKQOA==
+ dependencies:
+ p-reduce "^1.0.0"
+
+p-finally@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+ integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==
+
+p-limit@^1.1.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
+ integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==
+ dependencies:
+ p-try "^1.0.0"
+
+p-limit@^2.0.0:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e"
+ integrity sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==
+ dependencies:
+ p-try "^2.0.0"
+
+p-locate@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
+ integrity sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==
+ dependencies:
+ p-limit "^1.1.0"
+
+p-locate@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
+ integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
+ dependencies:
+ p-limit "^2.0.0"
+
+p-reduce@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa"
+ integrity sha512-3Tx1T3oM1xO/Y8Gj0sWyE78EIJZ+t+aEmXUdvQgvGmSMri7aPTHoovbXEreWKkL5j21Er60XAWLTzKbAKYOujQ==
+
+p-try@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
+ integrity sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==
+
+p-try@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+ integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+
+parse-glob@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c"
+ integrity sha512-FC5TeK0AwXzq3tUBFtH74naWkPQCEWs4K+xMxWZBlKDWu0bVHXGZa+KKqxKidd7xwhdZ19ZNuF2uO1M/r196HA==
+ dependencies:
+ glob-base "^0.3.0"
+ is-dotfile "^1.0.0"
+ is-extglob "^1.0.0"
+ is-glob "^2.0.0"
+
+parse-json@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
+ integrity sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==
+ dependencies:
+ error-ex "^1.2.0"
+
+parse-json@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
+ integrity sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==
+ dependencies:
+ error-ex "^1.3.1"
+ json-parse-better-errors "^1.0.1"
+
+parse5@4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608"
+ integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==
+
+parse5@^3.0.1:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c"
+ integrity sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==
+ dependencies:
+ "@types/node" "*"
+
+pascalcase@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
+ integrity sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==
+
+path-exists@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b"
+ integrity sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==
+ dependencies:
+ pinkie-promise "^2.0.0"
+
+path-exists@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+ integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
+
+path-is-absolute@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+ integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
+
+path-key@^2.0.0, path-key@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+ integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==
+
+path-parse@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+ integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+path-type@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
+ integrity sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==
+ dependencies:
+ graceful-fs "^4.1.2"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+path-type@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
+ integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==
+ dependencies:
+ pify "^3.0.0"
+
+performance-now@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+ integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
+
+picocolors@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
+ integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+
+pify@^2.0.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+ integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==
+
+pify@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
+ integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==
+
+pify@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
+ integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
+
+pinkie-promise@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
+ integrity sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==
+ dependencies:
+ pinkie "^2.0.0"
+
+pinkie@^2.0.0:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
+ integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==
+
+pirates@^4.0.1, pirates@^4.0.5:
+ version "4.0.5"
+ resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b"
+ integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==
+
+pkg-dir@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3"
+ integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==
+ dependencies:
+ find-up "^3.0.0"
+
+pkg-up@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5"
+ integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==
+ dependencies:
+ find-up "^3.0.0"
+
+pn@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
+ integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==
+
+posix-character-classes@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
+ integrity sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==
+
+prelude-ls@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
+ integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
+
+preserve@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
+ integrity sha512-s/46sYeylUfHNjI+sA/78FAHlmIuKqI9wNnzEOGehAlUUYeObv5C2mOinXBjyUyWmJ2SfcS2/ydApH4hTF4WXQ==
+
+pretty-format@^24.9.0:
+ version "24.9.0"
+ resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9"
+ integrity sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==
+ dependencies:
+ "@jest/types" "^24.9.0"
+ ansi-regex "^4.0.0"
+ ansi-styles "^3.2.0"
+ react-is "^16.8.4"
+
+promise@^7.1.1:
+ version "7.3.1"
+ resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
+ integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==
+ dependencies:
+ asap "~2.0.3"
+
+prompts@^2.0.1:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069"
+ integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==
+ dependencies:
+ kleur "^3.0.3"
+ sisteransi "^1.0.5"
+
+prop-types-exact@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/prop-types-exact/-/prop-types-exact-1.2.0.tgz#825d6be46094663848237e3925a98c6e944e9869"
+ integrity sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA==
+ dependencies:
+ has "^1.0.3"
+ object.assign "^4.1.0"
+ reflect.ownkeys "^0.2.0"
+
+prop-types@^15.6.0, prop-types@^15.8.1:
+ version "15.8.1"
+ resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
+ integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
+ dependencies:
+ loose-envify "^1.4.0"
+ object-assign "^4.1.1"
+ react-is "^16.13.1"
+
+prop-types@^15.6.2, prop-types@^15.7.2:
+ version "15.7.2"
+ resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
+ integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
+ dependencies:
+ loose-envify "^1.4.0"
+ object-assign "^4.1.1"
+ react-is "^16.8.1"
+
+psl@^1.1.28:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c"
+ integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==
+
+pump@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
+ integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
+ dependencies:
+ end-of-stream "^1.1.0"
+ once "^1.3.1"
+
+punycode@^2.1.0, punycode@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+ integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+
+qs@~6.5.2:
+ version "6.5.2"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
+ integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
+
+raf@^3.4.1:
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
+ integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
+ dependencies:
+ performance-now "^2.1.0"
+
+railroad-diagrams@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e"
+ integrity sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=
+
+randexp@0.4.6:
+ version "0.4.6"
+ resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3"
+ integrity sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==
+ dependencies:
+ discontinuous-range "1.0.0"
+ ret "~0.1.10"
+
+randomatic@^3.0.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed"
+ integrity sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==
+ dependencies:
+ is-number "^4.0.0"
+ kind-of "^6.0.0"
+ math-random "^1.0.1"
+
+react-dom@16.4.1:
+ version "16.4.1"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.1.tgz#7f8b0223b3a5fbe205116c56deb85de32685dad6"
+ integrity sha512-1Gin+wghF/7gl4Cqcvr1DxFX2Osz7ugxSwl6gBqCMpdrxHjIFUS7GYxrFftZ9Ln44FHw0JxCFD9YtZsrbR5/4A==
+ dependencies:
+ fbjs "^0.8.16"
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+ prop-types "^15.6.0"
+
+react-is@^16.13.1, react-is@^16.4.1, react-is@^16.8.4:
+ version "16.13.1"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
+ integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
+
+react-is@^16.8.1, react-is@^16.8.6:
+ version "16.13.0"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.0.tgz#0f37c3613c34fe6b37cd7f763a0d6293ab15c527"
+ integrity sha512-GFMtL0vHkiBv9HluwNZTggSn/sCyEt9n02aM0dSAjGGyqyNlAyftYm4phPxdvCigG15JreC5biwxCgTAJZ7yAA==
+
+react-test-renderer@16.4.1:
+ version "16.4.1"
+ resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.4.1.tgz#f2fb30c2c7b517db6e5b10ed20bb6b0a7ccd8d70"
+ integrity sha512-wyyiPxRZOTpKnNIgUBOB6xPLTpIzwcQMIURhZvzUqZzezvHjaGNsDPBhMac5fIY3Jf5NuKxoGvV64zDSOECPPQ==
+ dependencies:
+ fbjs "^0.8.16"
+ object-assign "^4.1.1"
+ prop-types "^15.6.0"
+ react-is "^16.4.1"
+
+react-test-renderer@^16.0.0-0:
+ version "16.13.0"
+ resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.13.0.tgz#39ba3bf72cedc8210c3f81983f0bb061b14a3014"
+ integrity sha512-NQ2S9gdMUa7rgPGpKGyMcwl1d6D9MCF0lftdI3kts6kkiX+qvpC955jNjAZXlIDTjnN9jwFI8A8XhRh/9v0spA==
+ dependencies:
+ object-assign "^4.1.1"
+ prop-types "^15.6.2"
+ react-is "^16.8.6"
+ scheduler "^0.19.0"
+
+react@16.4.1:
+ version "16.4.1"
+ resolved "https://registry.yarnpkg.com/react/-/react-16.4.1.tgz#de51ba5764b5dbcd1f9079037b862bd26b82fe32"
+ integrity sha512-3GEs0giKp6E0Oh/Y9ZC60CmYgUPnp7voH9fbjWsvXtYFb4EWtgQub0ADSq0sJR0BbHc4FThLLtzlcFaFXIorwg==
+ dependencies:
+ fbjs "^0.8.16"
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+ prop-types "^15.6.0"
+
+read-pkg-up@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
+ integrity sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==
+ dependencies:
+ find-up "^1.0.0"
+ read-pkg "^1.0.0"
+
+read-pkg-up@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-4.0.0.tgz#1b221c6088ba7799601c808f91161c66e58f8978"
+ integrity sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==
+ dependencies:
+ find-up "^3.0.0"
+ read-pkg "^3.0.0"
+
+read-pkg@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
+ integrity sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==
+ dependencies:
+ load-json-file "^1.0.0"
+ normalize-package-data "^2.3.2"
+ path-type "^1.0.0"
+
+read-pkg@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
+ integrity sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==
+ dependencies:
+ load-json-file "^4.0.0"
+ normalize-package-data "^2.3.2"
+ path-type "^3.0.0"
+
+readable-stream@^3.1.1:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
+ integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
+ dependencies:
+ inherits "^2.0.3"
+ string_decoder "^1.1.1"
+ util-deprecate "^1.0.1"
+
+realpath-native@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c"
+ integrity sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==
+ dependencies:
+ util.promisify "^1.0.0"
+
+reflect.ownkeys@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"
+ integrity sha1-dJrO7H8/34tj+SegSAnpDFwLNGA=
+
+regenerate-unicode-properties@^10.1.0:
+ version "10.1.0"
+ resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c"
+ integrity sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==
+ dependencies:
+ regenerate "^1.4.2"
+
+regenerate@^1.4.2:
+ version "1.4.2"
+ resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
+ integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
+
+regenerator-runtime@^0.11.0:
+ version "0.11.1"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
+ integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
+
+regenerator-runtime@^0.13.11:
+ version "0.13.11"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
+ integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
+
+regenerator-transform@^0.15.1:
+ version "0.15.1"
+ resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.1.tgz#f6c4e99fc1b4591f780db2586328e4d9a9d8dc56"
+ integrity sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==
+ dependencies:
+ "@babel/runtime" "^7.8.4"
+
+regex-cache@^0.4.2:
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd"
+ integrity sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==
+ dependencies:
+ is-equal-shallow "^0.1.3"
+
+regex-not@^1.0.0, regex-not@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
+ integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==
+ dependencies:
+ extend-shallow "^3.0.2"
+ safe-regex "^1.1.0"
+
+regexp.prototype.flags@^1.4.3:
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac"
+ integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.3"
+ functions-have-names "^1.2.2"
+
+regexpu-core@^5.2.1:
+ version "5.2.2"
+ resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.2.2.tgz#3e4e5d12103b64748711c3aad69934d7718e75fc"
+ integrity sha512-T0+1Zp2wjF/juXMrMxHxidqGYn8U4R+zleSJhX9tQ1PUsS8a9UtYfbsF9LdiVgNX3kiX8RNaKM42nfSgvFJjmw==
+ dependencies:
+ regenerate "^1.4.2"
+ regenerate-unicode-properties "^10.1.0"
+ regjsgen "^0.7.1"
+ regjsparser "^0.9.1"
+ unicode-match-property-ecmascript "^2.0.0"
+ unicode-match-property-value-ecmascript "^2.1.0"
+
+regjsgen@^0.7.1:
+ version "0.7.1"
+ resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.7.1.tgz#ee5ef30e18d3f09b7c369b76e7c2373ed25546f6"
+ integrity sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA==
+
+regjsparser@^0.9.1:
+ version "0.9.1"
+ resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709"
+ integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==
+ dependencies:
+ jsesc "~0.5.0"
+
+remove-trailing-separator@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
+ integrity sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==
+
+repeat-element@^1.1.2:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9"
+ integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==
+
+repeat-string@^1.5.2, repeat-string@^1.6.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+ integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==
+
+repeating@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda"
+ integrity sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==
+ dependencies:
+ is-finite "^1.0.0"
+
+request-promise-core@1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f"
+ integrity sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==
+ dependencies:
+ lodash "^4.17.19"
+
+request-promise-native@^1.0.5:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.9.tgz#e407120526a5efdc9a39b28a5679bf47b9d9dc28"
+ integrity sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==
+ dependencies:
+ request-promise-core "1.1.4"
+ stealthy-require "^1.1.1"
+ tough-cookie "^2.3.3"
+
+request@^2.87.0:
+ version "2.88.2"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
+ integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
+ dependencies:
+ aws-sign2 "~0.7.0"
+ aws4 "^1.8.0"
+ caseless "~0.12.0"
+ combined-stream "~1.0.6"
+ extend "~3.0.2"
+ forever-agent "~0.6.1"
+ form-data "~2.3.2"
+ har-validator "~5.1.3"
+ http-signature "~1.2.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.19"
+ oauth-sign "~0.9.0"
+ performance-now "^2.1.0"
+ qs "~6.5.2"
+ safe-buffer "^5.1.2"
+ tough-cookie "~2.5.0"
+ tunnel-agent "^0.6.0"
+ uuid "^3.3.2"
+
+require-directory@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+ integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
+
+require-main-filename@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
+ integrity sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==
+
+require-main-filename@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
+ integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
+
+reselect@^4.0.0:
+ version "4.1.7"
+ resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.7.tgz#56480d9ff3d3188970ee2b76527bd94a95567a42"
+ integrity sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A==
+
+resolve-cwd@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
+ integrity sha512-ccu8zQTrzVr954472aUVPLEcB3YpKSYR3cg/3lo1okzobPBM+1INXBbBZlDbnI/hbEocnf8j0QVo43hQKrbchg==
+ dependencies:
+ resolve-from "^3.0.0"
+
+resolve-from@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
+ integrity sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==
+
+resolve-url@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
+ integrity sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==
+
+resolve@1.1.7:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
+ integrity sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg==
+
+resolve@^1.10.0, resolve@^1.13.1, resolve@^1.14.2:
+ version "1.22.1"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
+ integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
+ dependencies:
+ is-core-module "^2.9.0"
+ path-parse "^1.0.7"
+ supports-preserve-symlinks-flag "^1.0.0"
+
+ret@~0.1.10:
+ version "0.1.15"
+ resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
+ integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
+
+rimraf@^2.5.4, rimraf@^2.6.3:
+ version "2.7.1"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
+ integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
+ dependencies:
+ glob "^7.1.3"
+
+rst-selector-parser@^2.2.3:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91"
+ integrity sha1-gbIw6i/MYGbInjRy3nlChdmwPZE=
+ dependencies:
+ lodash.flattendeep "^4.4.0"
+ nearley "^2.7.10"
+
+rsvp@^4.8.4:
+ version "4.8.5"
+ resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
+ integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==
+
+safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
+ integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
+
+safe-buffer@~5.1.1:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+ integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
+safe-regex-test@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295"
+ integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==
+ dependencies:
+ call-bind "^1.0.2"
+ get-intrinsic "^1.1.3"
+ is-regex "^1.1.4"
+
+safe-regex@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
+ integrity sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==
+ dependencies:
+ ret "~0.1.10"
+
+"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+ integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+samsam@1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567"
+ integrity sha1-vsEf3IOp/aBjQBIQ5AF2wwJNFWc=
+
+samsam@~1.1:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.3.tgz#9f5087419b4d091f232571e7fa52e90b0f552621"
+ integrity sha1-n1CHQZtNCR8jJXHn+lLpCw9VJiE=
+
+sane@^4.0.3:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded"
+ integrity sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==
+ dependencies:
+ "@cnakazawa/watch" "^1.0.3"
+ anymatch "^2.0.0"
+ capture-exit "^2.0.0"
+ exec-sh "^0.3.2"
+ execa "^1.0.0"
+ fb-watchman "^2.0.0"
+ micromatch "^3.1.4"
+ minimist "^1.1.1"
+ walker "~1.0.5"
+
+sax@^1.2.4:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
+ integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
+
+scheduler@^0.19.0:
+ version "0.19.0"
+ resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.0.tgz#a715d56302de403df742f4a9be11975b32f5698d"
+ integrity sha512-xowbVaTPe9r7y7RUejcK73/j8tt2jfiyTednOvHbA8JoClvMYCp+r8QegLwK/n8zWQAtZb1fFnER4XLBZXrCxA==
+ dependencies:
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+
+"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1:
+ version "5.7.1"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
+ integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
+
+semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0:
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
+ integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
+
+set-blocking@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+ integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
+
+set-value@^2.0.0, set-value@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"
+ integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-extendable "^0.1.1"
+ is-plain-object "^2.0.3"
+ split-string "^3.0.1"
+
+setimmediate@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
+ integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==
+
+shallow-clone@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
+ integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==
+ dependencies:
+ kind-of "^6.0.2"
+
+shebang-command@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+ integrity sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==
+ dependencies:
+ shebang-regex "^1.0.0"
+
+shebang-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+ integrity sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==
+
+shellwords@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
+ integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
+
+side-channel@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
+ integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==
+ dependencies:
+ call-bind "^1.0.0"
+ get-intrinsic "^1.0.2"
+ object-inspect "^1.9.0"
+
+signal-exit@^3.0.0, signal-exit@^3.0.2:
+ version "3.0.7"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
+ integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
+
+sinon@^1.17.5:
+ version "1.17.7"
+ resolved "https://registry.yarnpkg.com/sinon/-/sinon-1.17.7.tgz#4542a4f49ba0c45c05eb2e9dd9d203e2b8efe0bf"
+ integrity sha1-RUKk9JugxFwF6y6d2dID4rjv4L8=
+ dependencies:
+ formatio "1.1.1"
+ lolex "1.3.2"
+ samsam "1.1.2"
+ util ">=0.10.3 <1"
+
+sisteransi@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
+ integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==
+
+slash@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
+ integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==
+
+snapdragon-node@^2.0.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
+ integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==
+ dependencies:
+ define-property "^1.0.0"
+ isobject "^3.0.0"
+ snapdragon-util "^3.0.1"
+
+snapdragon-util@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2"
+ integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==
+ dependencies:
+ kind-of "^3.2.0"
+
+snapdragon@^0.8.1:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d"
+ integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==
+ dependencies:
+ base "^0.11.1"
+ debug "^2.2.0"
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ map-cache "^0.2.2"
+ source-map "^0.5.6"
+ source-map-resolve "^0.5.0"
+ use "^3.1.0"
+
+source-map-resolve@^0.5.0:
+ version "0.5.3"
+ resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
+ integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==
+ dependencies:
+ atob "^2.1.2"
+ decode-uri-component "^0.2.0"
+ resolve-url "^0.2.1"
+ source-map-url "^0.4.0"
+ urix "^0.1.0"
+
+source-map-support@^0.5.16:
+ version "0.5.16"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042"
+ integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==
+ dependencies:
+ buffer-from "^1.0.0"
+ source-map "^0.6.0"
+
+source-map-support@^0.5.6:
+ version "0.5.21"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
+ integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
+ dependencies:
+ buffer-from "^1.0.0"
+ source-map "^0.6.0"
+
+source-map-url@^0.4.0:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56"
+ integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==
+
+source-map@^0.5.6, source-map@^0.5.7:
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
+ integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==
+
+source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+ integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+spdx-correct@^3.0.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
+ integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==
+ dependencies:
+ spdx-expression-parse "^3.0.0"
+ spdx-license-ids "^3.0.0"
+
+spdx-exceptions@^2.1.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d"
+ integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==
+
+spdx-expression-parse@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679"
+ integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==
+ dependencies:
+ spdx-exceptions "^2.1.0"
+ spdx-license-ids "^3.0.0"
+
+spdx-license-ids@^3.0.0:
+ version "3.0.12"
+ resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz#69077835abe2710b65f03969898b6637b505a779"
+ integrity sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==
+
+split-string@^3.0.1, split-string@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
+ integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==
+ dependencies:
+ extend-shallow "^3.0.0"
+
+sshpk@^1.7.0:
+ version "1.16.1"
+ resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
+ integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==
+ dependencies:
+ asn1 "~0.2.3"
+ assert-plus "^1.0.0"
+ bcrypt-pbkdf "^1.0.0"
+ dashdash "^1.12.0"
+ ecc-jsbn "~0.1.1"
+ getpass "^0.1.1"
+ jsbn "~0.1.0"
+ safer-buffer "^2.0.2"
+ tweetnacl "~0.14.0"
+
+stack-utils@^1.0.1:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.5.tgz#a19b0b01947e0029c8e451d5d61a498f5bb1471b"
+ integrity sha512-KZiTzuV3CnSnSvgMRrARVCj+Ht7rMbauGDK0LdVFRGyenwdylpajAp4Q0i6SX8rEmbTpMMf6ryq2gb8pPq2WgQ==
+ dependencies:
+ escape-string-regexp "^2.0.0"
+
+static-extend@^0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
+ integrity sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==
+ dependencies:
+ define-property "^0.2.5"
+ object-copy "^0.1.0"
+
+stealthy-require@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
+ integrity sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==
+
+string-length@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
+ integrity sha512-Qka42GGrS8Mm3SZ+7cH8UXiIWI867/b/Z/feQSpQx/rbfB8UGknGEZVaUQMOUVj+soY6NpWAxily63HI1OckVQ==
+ dependencies:
+ astral-regex "^1.0.0"
+ strip-ansi "^4.0.0"
+
+string-width@^3.0.0, string-width@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
+ integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
+ dependencies:
+ emoji-regex "^7.0.1"
+ is-fullwidth-code-point "^2.0.0"
+ strip-ansi "^5.1.0"
+
+string.prototype.trim@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.1.tgz#141233dff32c82bfad80684d7e5f0869ee0fb782"
+ integrity sha512-MjGFEeqixw47dAMFMtgUro/I0+wNqZB5GKXGt1fFr24u3TzDXCPu7J9Buppzoe3r/LqkSDLDDJzE15RGWDGAVw==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.17.0-next.1"
+ function-bind "^1.1.1"
+
+string.prototype.trimend@^1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533"
+ integrity sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.4"
+ es-abstract "^1.20.4"
+
+string.prototype.trimleft@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz#9bdb8ac6abd6d602b17a4ed321870d2f8dcefc74"
+ integrity sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==
+ dependencies:
+ define-properties "^1.1.3"
+ function-bind "^1.1.1"
+
+string.prototype.trimright@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz#440314b15996c866ce8a0341894d45186200c5d9"
+ integrity sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==
+ dependencies:
+ define-properties "^1.1.3"
+ function-bind "^1.1.1"
+
+string.prototype.trimstart@^1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4"
+ integrity sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.4"
+ es-abstract "^1.20.4"
+
+string_decoder@^1.1.1:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
+ integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+ dependencies:
+ safe-buffer "~5.2.0"
+
+strip-ansi@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+ integrity sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==
+ dependencies:
+ ansi-regex "^2.0.0"
+
+strip-ansi@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
+ integrity sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==
+ dependencies:
+ ansi-regex "^3.0.0"
+
+strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
+ integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
+ dependencies:
+ ansi-regex "^4.1.0"
+
+strip-bom@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
+ integrity sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==
+ dependencies:
+ is-utf8 "^0.2.0"
+
+strip-bom@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+ integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==
+
+strip-eof@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
+ integrity sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==
+
+supports-color@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
+ integrity sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==
+
+supports-color@^5.3.0:
+ version "5.5.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+ integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
+ dependencies:
+ has-flag "^3.0.0"
+
+supports-color@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
+ integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
+ dependencies:
+ has-flag "^3.0.0"
+
+supports-preserve-symlinks-flag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
+ integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
+symbol-tree@^3.2.2:
+ version "3.2.4"
+ resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
+ integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
+
+test-exclude@^4.2.1:
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.2.3.tgz#a9a5e64474e4398339245a0a769ad7c2f4a97c20"
+ integrity sha512-SYbXgY64PT+4GAL2ocI3HwPa4Q4TBKm0cwAVeKOt/Aoc0gSpNRjJX8w0pA1LMKZ3LBmd8pYBqApFNQLII9kavA==
+ dependencies:
+ arrify "^1.0.1"
+ micromatch "^2.3.11"
+ object-assign "^4.1.0"
+ read-pkg-up "^1.0.1"
+ require-main-filename "^1.0.1"
+
+test-exclude@^5.2.3:
+ version "5.2.3"
+ resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-5.2.3.tgz#c3d3e1e311eb7ee405e092dac10aefd09091eac0"
+ integrity sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==
+ dependencies:
+ glob "^7.1.3"
+ minimatch "^3.0.4"
+ read-pkg-up "^4.0.0"
+ require-main-filename "^2.0.0"
+
+throat@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a"
+ integrity sha512-wCVxLDcFxw7ujDxaeJC6nfl2XfHJNYs8yUYJnvMgtPEFlttP9tHSfRUv2vBe6C4hkVFPWoP1P6ZccbYjmSEkKA==
+
+tmatch@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/tmatch/-/tmatch-2.0.1.tgz#0c56246f33f30da1b8d3d72895abaf16660f38cf"
+ integrity sha1-DFYkbzPzDaG409colauvFmYPOM8=
+
+tmpl@1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"
+ integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==
+
+to-fast-properties@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
+ integrity sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og==
+
+to-fast-properties@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
+ integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=
+
+to-object-path@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af"
+ integrity sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==
+ dependencies:
+ kind-of "^3.0.2"
+
+to-regex-range@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38"
+ integrity sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==
+ dependencies:
+ is-number "^3.0.0"
+ repeat-string "^1.6.1"
+
+to-regex@^3.0.1, to-regex@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
+ integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==
+ dependencies:
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ regex-not "^1.0.2"
+ safe-regex "^1.1.0"
+
+tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@~2.5.0:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
+ integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
+ dependencies:
+ psl "^1.1.28"
+ punycode "^2.1.1"
+
+tr46@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
+ integrity sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==
+ dependencies:
+ punycode "^2.1.0"
+
+trim-right@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
+ integrity sha512-WZGXGstmCWgeevgTL54hrCuw1dyMQIzWy7ZfqRJfSmJZBwklI15egmQytFP6bPidmw3M8d5yEowl1niq4vmqZw==
+
+tunnel-agent@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+ integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=
+ dependencies:
+ safe-buffer "^5.0.1"
+
+tweetnacl@^0.14.3, tweetnacl@~0.14.0:
+ version "0.14.5"
+ resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
+ integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
+
+type-check@~0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
+ integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=
+ dependencies:
+ prelude-ls "~1.1.2"
+
+typed-array-length@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb"
+ integrity sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==
+ dependencies:
+ call-bind "^1.0.2"
+ for-each "^0.3.3"
+ is-typed-array "^1.1.9"
+
+ua-parser-js@^0.7.30:
+ version "0.7.32"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.32.tgz#cd8c639cdca949e30fa68c44b7813ef13e36d211"
+ integrity sha512-f9BESNVhzlhEFf2CHMSj40NWOjYPl1YKYbrvIr/hFTDEmLq7SRbWvm7FcdcpCYT95zrOhC7gZSxjdnnTpBcwVw==
+
+unbox-primitive@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"
+ integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==
+ dependencies:
+ call-bind "^1.0.2"
+ has-bigints "^1.0.2"
+ has-symbols "^1.0.3"
+ which-boxed-primitive "^1.0.2"
+
+unicode-canonical-property-names-ecmascript@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc"
+ integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==
+
+unicode-match-property-ecmascript@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3"
+ integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==
+ dependencies:
+ unicode-canonical-property-names-ecmascript "^2.0.0"
+ unicode-property-aliases-ecmascript "^2.0.0"
+
+unicode-match-property-value-ecmascript@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0"
+ integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==
+
+unicode-property-aliases-ecmascript@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd"
+ integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==
+
+union-value@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
+ integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==
+ dependencies:
+ arr-union "^3.1.0"
+ get-value "^2.0.6"
+ is-extendable "^0.1.1"
+ set-value "^2.0.1"
+
+unset-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"
+ integrity sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==
+ dependencies:
+ has-value "^0.3.1"
+ isobject "^3.0.0"
+
+update-browserslist-db@^1.0.9:
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3"
+ integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==
+ dependencies:
+ escalade "^3.1.1"
+ picocolors "^1.0.0"
+
+uri-js@^4.2.2:
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
+ integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==
+ dependencies:
+ punycode "^2.1.0"
+
+urix@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
+ integrity sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==
+
+use@^3.1.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
+ integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
+
+util-deprecate@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+ integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+
+util.promisify@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.1.1.tgz#77832f57ced2c9478174149cae9b96e9918cd54b"
+ integrity sha512-/s3UsZUrIfa6xDhr7zZhnE9SLQ5RIXyYfiVnMMyMDzOc8WhWN4Nbh36H842OyurKbCDAesZOJaVyvmSl6fhGQw==
+ dependencies:
+ call-bind "^1.0.0"
+ define-properties "^1.1.3"
+ for-each "^0.3.3"
+ has-symbols "^1.0.1"
+ object.getownpropertydescriptors "^2.1.1"
+
+"util@>=0.10.3 <1":
+ version "0.12.2"
+ resolved "https://registry.yarnpkg.com/util/-/util-0.12.2.tgz#54adb634c9e7c748707af2bf5a8c7ab640cbba2b"
+ integrity sha512-XE+MkWQvglYa+IOfBt5UFG93EmncEMP23UqpgDvVZVFBPxwmkK10QRp6pgU4xICPnWRf/t0zPv4noYSUq9gqUQ==
+ dependencies:
+ inherits "^2.0.3"
+ is-arguments "^1.0.4"
+ is-generator-function "^1.0.7"
+ safe-buffer "^5.1.2"
+
+uuid@^3.3.2:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
+ integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
+
+validate-npm-package-license@^3.0.1:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
+ integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==
+ dependencies:
+ spdx-correct "^3.0.0"
+ spdx-expression-parse "^3.0.0"
+
+verror@1.10.0:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
+ integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=
+ dependencies:
+ assert-plus "^1.0.0"
+ core-util-is "1.0.2"
+ extsprintf "^1.2.0"
+
+w3c-hr-time@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
+ integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==
+ dependencies:
+ browser-process-hrtime "^1.0.0"
+
+walker@^1.0.7, walker@~1.0.5:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f"
+ integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==
+ dependencies:
+ makeerror "1.0.12"
+
+webidl-conversions@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
+ integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
+
+whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
+ integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==
+ dependencies:
+ iconv-lite "0.4.24"
+
+whatwg-fetch@>=0.10.0:
+ version "3.6.2"
+ resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c"
+ integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==
+
+whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
+ integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
+
+whatwg-url@^6.4.1:
+ version "6.5.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8"
+ integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==
+ dependencies:
+ lodash.sortby "^4.7.0"
+ tr46 "^1.0.1"
+ webidl-conversions "^4.0.2"
+
+whatwg-url@^7.0.0:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"
+ integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==
+ dependencies:
+ lodash.sortby "^4.7.0"
+ tr46 "^1.0.1"
+ webidl-conversions "^4.0.2"
+
+which-boxed-primitive@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1"
+ integrity sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ==
+ dependencies:
+ is-bigint "^1.0.0"
+ is-boolean-object "^1.0.0"
+ is-number-object "^1.0.3"
+ is-string "^1.0.4"
+ is-symbol "^1.0.2"
+
+which-boxed-primitive@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
+ integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==
+ dependencies:
+ is-bigint "^1.0.1"
+ is-boolean-object "^1.1.0"
+ is-number-object "^1.0.4"
+ is-string "^1.0.5"
+ is-symbol "^1.0.3"
+
+which-collection@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906"
+ integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==
+ dependencies:
+ is-map "^2.0.1"
+ is-set "^2.0.1"
+ is-weakmap "^2.0.1"
+ is-weakset "^2.0.1"
+
+which-module@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
+ integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==
+
+which-typed-array@^1.1.9:
+ version "1.1.9"
+ resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6"
+ integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==
+ dependencies:
+ available-typed-arrays "^1.0.5"
+ call-bind "^1.0.2"
+ for-each "^0.3.3"
+ gopd "^1.0.1"
+ has-tostringtag "^1.0.0"
+ is-typed-array "^1.1.10"
+
+which@^1.2.9, which@^1.3.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+ integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+ dependencies:
+ isexe "^2.0.0"
+
+word-wrap@~1.2.3:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
+ integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
+
+wrap-ansi@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"
+ integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==
+ dependencies:
+ ansi-styles "^3.2.0"
+ string-width "^3.0.0"
+ strip-ansi "^5.0.0"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+ integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
+
+write-file-atomic@2.4.1:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.1.tgz#d0b05463c188ae804396fd5ab2a370062af87529"
+ integrity sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg==
+ dependencies:
+ graceful-fs "^4.1.11"
+ imurmurhash "^0.1.4"
+ signal-exit "^3.0.2"
+
+ws@^5.2.0:
+ version "5.2.3"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.3.tgz#05541053414921bc29c63bee14b8b0dd50b07b3d"
+ integrity sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA==
+ dependencies:
+ async-limiter "~1.0.0"
+
+xml-name-validator@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
+ integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
+
+y18n@^4.0.0:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
+ integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
+
+yallist@^3.0.2:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
+ integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
+
+yargs-parser@^13.1.2:
+ version "13.1.2"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"
+ integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==
+ dependencies:
+ camelcase "^5.0.0"
+ decamelize "^1.2.0"
+
+yargs@^13.3.0:
+ version "13.3.2"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"
+ integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==
+ dependencies:
+ cliui "^5.0.0"
+ find-up "^3.0.0"
+ get-caller-file "^2.0.1"
+ require-directory "^2.1.1"
+ require-main-filename "^2.0.0"
+ set-blocking "^2.0.0"
+ string-width "^3.0.0"
+ which-module "^2.0.0"
+ y18n "^4.0.0"
+ yargs-parser "^13.1.2"
diff --git a/devtools/client/webconsole/test/xpcshell/.eslintrc.js b/devtools/client/webconsole/test/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..8611c174f5
--- /dev/null
+++ b/devtools/client/webconsole/test/xpcshell/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the common devtools xpcshell eslintrc config.
+ extends: "../../../../.eslintrc.xpcshell.js",
+};
diff --git a/devtools/client/webconsole/test/xpcshell/test_webconsole_l10n.js b/devtools/client/webconsole/test/xpcshell/test_webconsole_l10n.js
new file mode 100644
index 0000000000..3b4208c206
--- /dev/null
+++ b/devtools/client/webconsole/test/xpcshell/test_webconsole_l10n.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+
+function run_test() {
+ const TEST_TIMESTAMP = 12345678;
+ const WCUL10n = require("resource://devtools/client/webconsole/utils/l10n.js");
+ const date = new Date(TEST_TIMESTAMP);
+ const localizedString = WCUL10n.timestampString(TEST_TIMESTAMP);
+ ok(
+ localizedString.includes(date.getHours()),
+ "the localized timestamp contains the hours"
+ );
+ ok(
+ localizedString.includes(date.getMinutes()),
+ "the localized timestamp contains the minutes"
+ );
+ ok(
+ localizedString.includes(date.getSeconds()),
+ "the localized timestamp contains the seconds"
+ );
+ ok(
+ localizedString.includes(date.getMilliseconds()),
+ "the localized timestamp contains the milliseconds"
+ );
+}
diff --git a/devtools/client/webconsole/test/xpcshell/xpcshell.toml b/devtools/client/webconsole/test/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..19807e738c
--- /dev/null
+++ b/devtools/client/webconsole/test/xpcshell/xpcshell.toml
@@ -0,0 +1,6 @@
+[DEFAULT]
+tags = "devtools"
+firefox-appdir = "browser"
+skip-if = ["os == 'android'"]
+
+["test_webconsole_l10n.js"]
diff --git a/devtools/client/webconsole/test/yarn.lock b/devtools/client/webconsole/test/yarn.lock
new file mode 100644
index 0000000000..dba0ab7b3c
--- /dev/null
+++ b/devtools/client/webconsole/test/yarn.lock
@@ -0,0 +1,2602 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@types/node@*":
+ version "11.13.2"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-11.13.2.tgz#dc85dde46aa8740bb4aed54b8104250f8f849503"
+
+abab@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e"
+
+acorn-globals@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-3.1.0.tgz#fd8270f71fbb4996b004fa880ee5d46573a731bf"
+ dependencies:
+ acorn "^4.0.4"
+
+acorn@^4.0.4:
+ version "4.0.13"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
+
+airbnb-prop-types@^2.12.0:
+ version "2.13.1"
+ resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.13.1.tgz#abcae3f683c0c918aae5e0a1df6232e39e0abfa5"
+ dependencies:
+ array.prototype.find "^2.0.4"
+ function.prototype.name "^1.1.0"
+ has "^1.0.3"
+ is-regex "^1.0.4"
+ object-is "^1.0.1"
+ object.assign "^4.1.0"
+ object.entries "^1.1.0"
+ prop-types "^15.7.2"
+ prop-types-exact "^1.2.0"
+ react-is "^16.8.6"
+
+ajv@^6.5.5:
+ version "6.10.0"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1"
+ dependencies:
+ fast-deep-equal "^2.0.1"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.4.1"
+ uri-js "^4.2.2"
+
+ansi-escapes@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e"
+
+ansi-regex@^2.0.0, ansi-regex@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+
+ansi-styles@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
+
+ansi-styles@^3.0.0:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+ dependencies:
+ color-convert "^1.9.0"
+
+anymatch@^1.3.0:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a"
+ dependencies:
+ micromatch "^2.1.5"
+ normalize-path "^2.0.0"
+
+append-transform@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-0.4.0.tgz#d76ebf8ca94d276e247a36bad44a4b74ab611991"
+ dependencies:
+ default-require-extensions "^1.0.0"
+
+argparse@^1.0.7:
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
+ dependencies:
+ sprintf-js "~1.0.2"
+
+arr-diff@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf"
+ dependencies:
+ arr-flatten "^1.0.1"
+
+arr-flatten@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
+
+array-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
+
+array-filter@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83"
+
+array-unique@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53"
+
+array.prototype.find@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.0.4.tgz#556a5c5362c08648323ddaeb9de9d14bc1864c90"
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.7.0"
+
+array.prototype.flat@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.1.tgz#812db8f02cad24d3fab65dd67eabe3b8903494a4"
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.10.0"
+ function-bind "^1.1.1"
+
+arrify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
+
+asn1@~0.2.3:
+ version "0.2.4"
+ resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
+ dependencies:
+ safer-buffer "~2.1.0"
+
+assert-plus@1.0.0, assert-plus@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+
+async@^2.1.4:
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/async/-/async-2.6.2.tgz#18330ea7e6e313887f5d2f2a904bac6fe4dd5381"
+ dependencies:
+ lodash "^4.17.11"
+
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+
+aws-sign2@~0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
+
+aws4@^1.8.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
+
+babel-code-frame@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
+ dependencies:
+ chalk "^1.1.3"
+ esutils "^2.0.2"
+ js-tokens "^3.0.2"
+
+babel-core@^6.0.0, babel-core@^6.26.0:
+ version "6.26.3"
+ resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207"
+ dependencies:
+ babel-code-frame "^6.26.0"
+ babel-generator "^6.26.0"
+ babel-helpers "^6.24.1"
+ babel-messages "^6.23.0"
+ babel-register "^6.26.0"
+ babel-runtime "^6.26.0"
+ babel-template "^6.26.0"
+ babel-traverse "^6.26.0"
+ babel-types "^6.26.0"
+ babylon "^6.18.0"
+ convert-source-map "^1.5.1"
+ debug "^2.6.9"
+ json5 "^0.5.1"
+ lodash "^4.17.4"
+ minimatch "^3.0.4"
+ path-is-absolute "^1.0.1"
+ private "^0.1.8"
+ slash "^1.0.0"
+ source-map "^0.5.7"
+
+babel-generator@^6.18.0, babel-generator@^6.26.0:
+ version "6.26.1"
+ resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90"
+ dependencies:
+ babel-messages "^6.23.0"
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ detect-indent "^4.0.0"
+ jsesc "^1.3.0"
+ lodash "^4.17.4"
+ source-map "^0.5.7"
+ trim-right "^1.0.1"
+
+babel-helpers@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-jest@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-20.0.3.tgz#e4a03b13dc10389e140fc645d09ffc4ced301671"
+ dependencies:
+ babel-core "^6.0.0"
+ babel-plugin-istanbul "^4.0.0"
+ babel-preset-jest "^20.0.3"
+
+babel-messages@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-istanbul@^4.0.0:
+ version "4.1.6"
+ resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz#36c59b2192efce81c5b378321b74175add1c9a45"
+ dependencies:
+ babel-plugin-syntax-object-rest-spread "^6.13.0"
+ find-up "^2.1.0"
+ istanbul-lib-instrument "^1.10.1"
+ test-exclude "^4.2.1"
+
+babel-plugin-jest-hoist@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-20.0.3.tgz#afedc853bd3f8dc3548ea671fbe69d03cc2c1767"
+
+babel-plugin-syntax-object-rest-spread@^6.13.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
+
+babel-preset-jest@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-20.0.3.tgz#cbacaadecb5d689ca1e1de1360ebfc66862c178a"
+ dependencies:
+ babel-plugin-jest-hoist "^20.0.3"
+
+babel-register@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071"
+ dependencies:
+ babel-core "^6.26.0"
+ babel-runtime "^6.26.0"
+ core-js "^2.5.0"
+ home-or-tmp "^2.0.0"
+ lodash "^4.17.4"
+ mkdirp "^0.5.1"
+ source-map-support "^0.4.15"
+
+babel-runtime@^6.22.0, babel-runtime@^6.26.0, babel-runtime@^6.6.1:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
+ dependencies:
+ core-js "^2.4.0"
+ regenerator-runtime "^0.11.0"
+
+babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02"
+ dependencies:
+ babel-runtime "^6.26.0"
+ babel-traverse "^6.26.0"
+ babel-types "^6.26.0"
+ babylon "^6.18.0"
+ lodash "^4.17.4"
+
+babel-traverse@^6.18.0, babel-traverse@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee"
+ dependencies:
+ babel-code-frame "^6.26.0"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ babylon "^6.18.0"
+ debug "^2.6.8"
+ globals "^9.18.0"
+ invariant "^2.2.2"
+ lodash "^4.17.4"
+
+babel-types@^6.18.0, babel-types@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
+ dependencies:
+ babel-runtime "^6.26.0"
+ esutils "^2.0.2"
+ lodash "^4.17.4"
+ to-fast-properties "^1.0.3"
+
+babylon@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
+
+balanced-match@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+
+bcrypt-pbkdf@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
+ dependencies:
+ tweetnacl "^0.14.3"
+
+boolbase@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+
+brace-expansion@^1.1.7:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+braces@^1.8.2:
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7"
+ dependencies:
+ expand-range "^1.8.1"
+ preserve "^0.2.0"
+ repeat-element "^1.1.2"
+
+browser-resolve@^1.11.2:
+ version "1.11.3"
+ resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6"
+ dependencies:
+ resolve "1.1.7"
+
+browser-stdout@1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
+
+bser@1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/bser/-/bser-1.0.2.tgz#381116970b2a6deea5646dd15dd7278444b56169"
+ dependencies:
+ node-int64 "^0.4.0"
+
+bser@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719"
+ dependencies:
+ node-int64 "^0.4.0"
+
+builtin-modules@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
+
+callsites@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50"
+
+camelcase@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a"
+
+caseless@~0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+
+chalk@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
+ dependencies:
+ ansi-styles "^2.2.1"
+ escape-string-regexp "^1.0.2"
+ has-ansi "^2.0.0"
+ strip-ansi "^3.0.0"
+ supports-color "^2.0.0"
+
+cheerio@^1.0.0-rc.2:
+ version "1.0.0-rc.3"
+ resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6"
+ dependencies:
+ css-select "~1.2.0"
+ dom-serializer "~0.1.1"
+ entities "~1.1.1"
+ htmlparser2 "^3.9.1"
+ lodash "^4.15.0"
+ parse5 "^3.0.1"
+
+ci-info@^1.5.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497"
+
+cliui@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wrap-ansi "^2.0.0"
+
+code-point-at@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
+
+color-convert@^1.9.0:
+ version "1.9.3"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+ dependencies:
+ color-name "1.1.3"
+
+color-name@1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+
+colors@^1.1.2:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.3.tgz#39e005d546afe01e01f9c4ca8fa50f686a01205d"
+
+combined-stream@^1.0.6, combined-stream@~1.0.6:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828"
+ dependencies:
+ delayed-stream "~1.0.0"
+
+commander@2.15.1:
+ version "2.15.1"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
+
+commander@^2.19.0:
+ version "2.20.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
+
+commander@~2.19.0:
+ version "2.19.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+
+content-type-parser@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.2.tgz#caabe80623e63638b2502fd4c7f12ff4ce2352e7"
+
+convert-source-map@^1.4.0, convert-source-map@^1.5.1:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20"
+ dependencies:
+ safe-buffer "~5.1.1"
+
+core-js@^0.8.3:
+ version "0.8.4"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-0.8.4.tgz#c22665f1e0d1b9c3c5e1b08dabd1f108695e4fcf"
+
+core-js@^2.4.0, core-js@^2.5.0:
+ version "2.6.5"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.5.tgz#44bc8d249e7fb2ff5d00e0341a7ffb94fbf67895"
+
+core-util-is@1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+
+css-select@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
+ dependencies:
+ boolbase "~1.0.0"
+ css-what "2.1"
+ domutils "1.5.1"
+ nth-check "~1.0.1"
+
+css-what@2.1:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
+
+cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0":
+ version "0.3.6"
+ resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.6.tgz#f85206cee04efa841f3c5982a74ba96ab20d65ad"
+
+"cssstyle@>= 0.2.37 < 0.3.0":
+ version "0.2.37"
+ resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-0.2.37.tgz#541097234cb2513c83ceed3acddc27ff27987d54"
+ dependencies:
+ cssom "0.3.x"
+
+dashdash@^1.12.0:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
+ dependencies:
+ assert-plus "^1.0.0"
+
+debug@3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
+ dependencies:
+ ms "2.0.0"
+
+debug@^2.6.8, debug@^2.6.9:
+ version "2.6.9"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+ dependencies:
+ ms "2.0.0"
+
+debug@^3.1.0:
+ version "3.2.6"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
+ dependencies:
+ ms "^2.1.1"
+
+decamelize@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+
+deep-is@~0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
+
+default-require-extensions@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8"
+ dependencies:
+ strip-bom "^2.0.0"
+
+define-properties@^1.1.2, define-properties@^1.1.3, define-properties@~1.1.2:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
+ dependencies:
+ object-keys "^1.0.12"
+
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+
+detect-indent@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208"
+ dependencies:
+ repeating "^2.0.0"
+
+devtools-modules@0.0.37:
+ version "0.0.37"
+ resolved "https://registry.yarnpkg.com/devtools-modules/-/devtools-modules-0.0.37.tgz#29b0041e444fe8b08aae3833b5433ab004d012b3"
+ dependencies:
+ jest "^20.0.4"
+ punycode "^2.1.0"
+
+diff@3.5.0, diff@^3.2.0:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
+
+discontinuous-range@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a"
+
+dom-serializer@0, dom-serializer@~0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0"
+ dependencies:
+ domelementtype "^1.3.0"
+ entities "^1.1.1"
+
+dom-walk@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018"
+
+domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
+
+domhandler@^2.3.0:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
+ dependencies:
+ domelementtype "1"
+
+domutils@1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
+ dependencies:
+ dom-serializer "0"
+ domelementtype "1"
+
+domutils@^1.5.1:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
+ dependencies:
+ dom-serializer "0"
+ domelementtype "1"
+
+ecc-jsbn@~0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
+ dependencies:
+ jsbn "~0.1.0"
+ safer-buffer "^2.1.0"
+
+entities@^1.1.1, entities@~1.1.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
+
+enzyme-adapter-react-16@^1.1.1:
+ version "1.12.1"
+ resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.12.1.tgz#6a2d74c80559d35ac0a91ca162fa45f4186290cf"
+ dependencies:
+ enzyme-adapter-utils "^1.11.0"
+ object.assign "^4.1.0"
+ object.values "^1.1.0"
+ prop-types "^15.7.2"
+ react-is "^16.8.6"
+ react-test-renderer "^16.0.0-0"
+ semver "^5.6.0"
+
+enzyme-adapter-utils@^1.11.0:
+ version "1.11.0"
+ resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.11.0.tgz#6ffff782b1b57dd46c72a845a91fc4103956a117"
+ dependencies:
+ airbnb-prop-types "^2.12.0"
+ function.prototype.name "^1.1.0"
+ object.assign "^4.1.0"
+ object.fromentries "^2.0.0"
+ prop-types "^15.7.2"
+ semver "^5.6.0"
+
+enzyme@^3.3.0:
+ version "3.9.0"
+ resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.9.0.tgz#2b491f06ca966eb56b6510068c7894a7e0be3909"
+ dependencies:
+ array.prototype.flat "^1.2.1"
+ cheerio "^1.0.0-rc.2"
+ function.prototype.name "^1.1.0"
+ has "^1.0.3"
+ html-element-map "^1.0.0"
+ is-boolean-object "^1.0.0"
+ is-callable "^1.1.4"
+ is-number-object "^1.0.3"
+ is-regex "^1.0.4"
+ is-string "^1.0.4"
+ is-subset "^0.1.1"
+ lodash.escape "^4.0.1"
+ lodash.isequal "^4.5.0"
+ object-inspect "^1.6.0"
+ object-is "^1.0.1"
+ object.assign "^4.1.0"
+ object.entries "^1.0.4"
+ object.values "^1.0.4"
+ raf "^3.4.0"
+ rst-selector-parser "^2.2.3"
+ string.prototype.trim "^1.1.2"
+
+errno@~0.1.7:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
+ dependencies:
+ prr "~1.0.1"
+
+error-ex@^1.2.0:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
+ dependencies:
+ is-arrayish "^0.2.1"
+
+es-abstract@^1.10.0, es-abstract@^1.11.0, es-abstract@^1.12.0, es-abstract@^1.5.0, es-abstract@^1.7.0:
+ version "1.13.0"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9"
+ dependencies:
+ es-to-primitive "^1.2.0"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+ is-callable "^1.1.4"
+ is-regex "^1.0.4"
+ object-keys "^1.0.12"
+
+es-to-primitive@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377"
+ dependencies:
+ is-callable "^1.1.4"
+ is-date-object "^1.0.1"
+ is-symbol "^1.0.2"
+
+escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+
+escodegen@^1.6.1:
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.1.tgz#c485ff8d6b4cdb89e27f4a856e91f118401ca510"
+ dependencies:
+ esprima "^3.1.3"
+ estraverse "^4.2.0"
+ esutils "^2.0.2"
+ optionator "^0.8.1"
+ optionalDependencies:
+ source-map "~0.6.1"
+
+esprima@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
+
+esprima@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
+
+estraverse@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
+
+esutils@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
+
+exec-sh@^0.2.0:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.2.tgz#2a5e7ffcbd7d0ba2755bdecb16e5a427dfbdec36"
+ dependencies:
+ merge "^1.2.0"
+
+expand-brackets@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b"
+ dependencies:
+ is-posix-bracket "^0.1.0"
+
+expand-range@^1.8.1:
+ version "1.8.2"
+ resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337"
+ dependencies:
+ fill-range "^2.1.0"
+
+expect@^1.16.0:
+ version "1.20.2"
+ resolved "https://registry.yarnpkg.com/expect/-/expect-1.20.2.tgz#d458fe4c56004036bae3232416a3f6361f04f965"
+ dependencies:
+ define-properties "~1.1.2"
+ has "^1.0.1"
+ is-equal "^1.5.1"
+ is-regex "^1.0.3"
+ object-inspect "^1.1.0"
+ object-keys "^1.0.9"
+ tmatch "^2.0.1"
+
+extend@~3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
+
+extglob@^0.3.1:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1"
+ dependencies:
+ is-extglob "^1.0.0"
+
+extsprintf@1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
+
+extsprintf@^1.2.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
+
+fast-deep-equal@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
+
+fast-json-stable-stringify@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
+
+fast-levenshtein@~2.0.4:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+
+fb-watchman@^1.8.0:
+ version "1.9.2"
+ resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-1.9.2.tgz#a24cf47827f82d38fb59a69ad70b76e3b6ae7383"
+ dependencies:
+ bser "1.0.2"
+
+fb-watchman@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58"
+ dependencies:
+ bser "^2.0.0"
+
+filename-regex@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26"
+
+fileset@^2.0.2:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/fileset/-/fileset-2.0.3.tgz#8e7548a96d3cc2327ee5e674168723a333bba2a0"
+ dependencies:
+ glob "^7.0.3"
+ minimatch "^3.0.3"
+
+fill-range@^2.1.0:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.4.tgz#eb1e773abb056dcd8df2bfdf6af59b8b3a936565"
+ dependencies:
+ is-number "^2.1.0"
+ isobject "^2.0.0"
+ randomatic "^3.0.0"
+ repeat-element "^1.1.2"
+ repeat-string "^1.5.2"
+
+find-up@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
+ dependencies:
+ path-exists "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+find-up@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
+ dependencies:
+ locate-path "^2.0.0"
+
+for-in@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+
+for-own@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce"
+ dependencies:
+ for-in "^1.0.1"
+
+forever-agent@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
+
+form-data@~2.3.2:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.6"
+ mime-types "^2.1.12"
+
+formatio@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.1.1.tgz#5ed3ccd636551097383465d996199100e86161e9"
+ dependencies:
+ samsam "~1.1"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+
+function-bind@^1.0.2, function-bind@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+
+function.prototype.name@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.0.tgz#8bd763cc0af860a859cc5d49384d74b932cd2327"
+ dependencies:
+ define-properties "^1.1.2"
+ function-bind "^1.1.1"
+ is-callable "^1.1.3"
+
+get-caller-file@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
+
+getpass@^0.1.1:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
+ dependencies:
+ assert-plus "^1.0.0"
+
+glob-base@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
+ dependencies:
+ glob-parent "^2.0.0"
+ is-glob "^2.0.0"
+
+glob-parent@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28"
+ dependencies:
+ is-glob "^2.0.0"
+
+glob@7.1.2:
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.4"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+glob@^7.0.3, glob@^7.1.1, glob@^7.1.3:
+ version "7.1.3"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.4"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+global@^4.3.2:
+ version "4.3.2"
+ resolved "https://registry.yarnpkg.com/global/-/global-4.3.2.tgz#e76989268a6c74c38908b1305b10fc0e394e9d0f"
+ dependencies:
+ min-document "^2.19.0"
+ process "~0.5.1"
+
+globals@^9.18.0:
+ version "9.18.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
+
+graceful-fs@^4.1.11, graceful-fs@^4.1.2:
+ version "4.1.15"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
+
+growl@1.10.5:
+ version "1.10.5"
+ resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"
+
+growly@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
+
+handlebars@^4.0.3:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.1.tgz#6e4e41c18ebe7719ae4d38e5aca3d32fa3dd23d3"
+ dependencies:
+ neo-async "^2.6.0"
+ optimist "^0.6.1"
+ source-map "^0.6.1"
+ optionalDependencies:
+ uglify-js "^3.1.4"
+
+har-schema@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
+
+har-validator@~5.1.0:
+ version "5.1.3"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
+ dependencies:
+ ajv "^6.5.5"
+ har-schema "^2.0.0"
+
+has-ansi@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
+ dependencies:
+ ansi-regex "^2.0.0"
+
+has-flag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa"
+
+has-flag@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+
+has-symbols@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44"
+
+has@^1.0.1, has@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+ dependencies:
+ function-bind "^1.1.1"
+
+he@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
+
+home-or-tmp@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.1"
+
+hosted-git-info@^2.1.4:
+ version "2.7.1"
+ resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047"
+
+html-element-map@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/html-element-map/-/html-element-map-1.0.1.tgz#3c4fcb4874ebddfe4283b51c8994e7713782b592"
+ dependencies:
+ array-filter "^1.0.0"
+
+html-encoding-sniffer@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8"
+ dependencies:
+ whatwg-encoding "^1.0.1"
+
+htmlparser2@^3.9.1:
+ version "3.10.1"
+ resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
+ dependencies:
+ domelementtype "^1.3.1"
+ domhandler "^2.3.0"
+ domutils "^1.5.1"
+ entities "^1.1.1"
+ inherits "^2.0.1"
+ readable-stream "^3.1.1"
+
+http-signature@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
+ dependencies:
+ assert-plus "^1.0.0"
+ jsprim "^1.2.2"
+ sshpk "^1.7.0"
+
+iconv-lite@0.4.24:
+ version "0.4.24"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3"
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+
+invariant@^2.2.2:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
+ dependencies:
+ loose-envify "^1.0.0"
+
+invert-kv@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
+
+is-arrayish@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+
+is-arrow-function@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/is-arrow-function/-/is-arrow-function-2.0.3.tgz#29be2c2d8d9450852b8bbafb635ba7b8d8e87ec2"
+ dependencies:
+ is-callable "^1.0.4"
+
+is-boolean-object@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.0.tgz#98f8b28030684219a95f375cfbd88ce3405dff93"
+
+is-buffer@^1.1.5:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
+
+is-builtin-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe"
+ dependencies:
+ builtin-modules "^1.0.0"
+
+is-callable@^1.0.4, is-callable@^1.1.3, is-callable@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75"
+
+is-ci@^1.0.10:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c"
+ dependencies:
+ ci-info "^1.5.0"
+
+is-date-object@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16"
+
+is-dotfile@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
+
+is-equal-shallow@^0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534"
+ dependencies:
+ is-primitive "^2.0.0"
+
+is-equal@^1.5.1:
+ version "1.5.5"
+ resolved "https://registry.yarnpkg.com/is-equal/-/is-equal-1.5.5.tgz#5e85f1957e052883247feb386965a3bba15fbb3d"
+ dependencies:
+ has "^1.0.1"
+ is-arrow-function "^2.0.3"
+ is-boolean-object "^1.0.0"
+ is-callable "^1.1.3"
+ is-date-object "^1.0.1"
+ is-generator-function "^1.0.6"
+ is-number-object "^1.0.3"
+ is-regex "^1.0.3"
+ is-string "^1.0.4"
+ is-symbol "^1.0.1"
+ object.entries "^1.0.4"
+
+is-extendable@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
+
+is-extglob@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0"
+
+is-finite@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa"
+ dependencies:
+ number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
+ dependencies:
+ number-is-nan "^1.0.0"
+
+is-generator-function@^1.0.6:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522"
+
+is-glob@^2.0.0, is-glob@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863"
+ dependencies:
+ is-extglob "^1.0.0"
+
+is-number-object@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.3.tgz#f265ab89a9f445034ef6aff15a8f00b00f551799"
+
+is-number@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f"
+ dependencies:
+ kind-of "^3.0.2"
+
+is-number@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff"
+
+is-posix-bracket@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4"
+
+is-primitive@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575"
+
+is-regex@^1.0.3, is-regex@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491"
+ dependencies:
+ has "^1.0.1"
+
+is-string@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.4.tgz#cc3a9b69857d621e963725a24caeec873b826e64"
+
+is-subset@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6"
+
+is-symbol@^1.0.1, is-symbol@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38"
+ dependencies:
+ has-symbols "^1.0.0"
+
+is-typedarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+
+is-utf8@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
+
+is-wsl@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
+
+isarray@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+
+isobject@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
+ dependencies:
+ isarray "1.0.0"
+
+isstream@~0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+
+istanbul-api@^1.1.1:
+ version "1.3.7"
+ resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.3.7.tgz#a86c770d2b03e11e3f778cd7aedd82d2722092aa"
+ dependencies:
+ async "^2.1.4"
+ fileset "^2.0.2"
+ istanbul-lib-coverage "^1.2.1"
+ istanbul-lib-hook "^1.2.2"
+ istanbul-lib-instrument "^1.10.2"
+ istanbul-lib-report "^1.1.5"
+ istanbul-lib-source-maps "^1.2.6"
+ istanbul-reports "^1.5.1"
+ js-yaml "^3.7.0"
+ mkdirp "^0.5.1"
+ once "^1.4.0"
+
+istanbul-lib-coverage@^1.0.1, istanbul-lib-coverage@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz#ccf7edcd0a0bb9b8f729feeb0930470f9af664f0"
+
+istanbul-lib-hook@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.2.2.tgz#bc6bf07f12a641fbf1c85391d0daa8f0aea6bf86"
+ dependencies:
+ append-transform "^0.4.0"
+
+istanbul-lib-instrument@^1.10.1, istanbul-lib-instrument@^1.10.2, istanbul-lib-instrument@^1.4.2:
+ version "1.10.2"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz#1f55ed10ac3c47f2bdddd5307935126754d0a9ca"
+ dependencies:
+ babel-generator "^6.18.0"
+ babel-template "^6.16.0"
+ babel-traverse "^6.18.0"
+ babel-types "^6.18.0"
+ babylon "^6.18.0"
+ istanbul-lib-coverage "^1.2.1"
+ semver "^5.3.0"
+
+istanbul-lib-report@^1.1.5:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.5.tgz#f2a657fc6282f96170aaf281eb30a458f7f4170c"
+ dependencies:
+ istanbul-lib-coverage "^1.2.1"
+ mkdirp "^0.5.1"
+ path-parse "^1.0.5"
+ supports-color "^3.1.2"
+
+istanbul-lib-source-maps@^1.1.0, istanbul-lib-source-maps@^1.2.6:
+ version "1.2.6"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.6.tgz#37b9ff661580f8fca11232752ee42e08c6675d8f"
+ dependencies:
+ debug "^3.1.0"
+ istanbul-lib-coverage "^1.2.1"
+ mkdirp "^0.5.1"
+ rimraf "^2.6.1"
+ source-map "^0.5.3"
+
+istanbul-reports@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.5.1.tgz#97e4dbf3b515e8c484caea15d6524eebd3ff4e1a"
+ dependencies:
+ handlebars "^4.0.3"
+
+jest-changed-files@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-20.0.3.tgz#9394d5cc65c438406149bef1bf4d52b68e03e3f8"
+
+jest-cli@^20.0.4:
+ version "20.0.4"
+ resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-20.0.4.tgz#e532b19d88ae5bc6c417e8b0593a6fe954b1dc93"
+ dependencies:
+ ansi-escapes "^1.4.0"
+ callsites "^2.0.0"
+ chalk "^1.1.3"
+ graceful-fs "^4.1.11"
+ is-ci "^1.0.10"
+ istanbul-api "^1.1.1"
+ istanbul-lib-coverage "^1.0.1"
+ istanbul-lib-instrument "^1.4.2"
+ istanbul-lib-source-maps "^1.1.0"
+ jest-changed-files "^20.0.3"
+ jest-config "^20.0.4"
+ jest-docblock "^20.0.3"
+ jest-environment-jsdom "^20.0.3"
+ jest-haste-map "^20.0.4"
+ jest-jasmine2 "^20.0.4"
+ jest-message-util "^20.0.3"
+ jest-regex-util "^20.0.3"
+ jest-resolve-dependencies "^20.0.3"
+ jest-runtime "^20.0.4"
+ jest-snapshot "^20.0.3"
+ jest-util "^20.0.3"
+ micromatch "^2.3.11"
+ node-notifier "^5.0.2"
+ pify "^2.3.0"
+ slash "^1.0.0"
+ string-length "^1.0.1"
+ throat "^3.0.0"
+ which "^1.2.12"
+ worker-farm "^1.3.1"
+ yargs "^7.0.2"
+
+jest-config@^20.0.4:
+ version "20.0.4"
+ resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-20.0.4.tgz#e37930ab2217c913605eff13e7bd763ec48faeea"
+ dependencies:
+ chalk "^1.1.3"
+ glob "^7.1.1"
+ jest-environment-jsdom "^20.0.3"
+ jest-environment-node "^20.0.3"
+ jest-jasmine2 "^20.0.4"
+ jest-matcher-utils "^20.0.3"
+ jest-regex-util "^20.0.3"
+ jest-resolve "^20.0.4"
+ jest-validate "^20.0.3"
+ pretty-format "^20.0.3"
+
+jest-diff@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-20.0.3.tgz#81f288fd9e675f0fb23c75f1c2b19445fe586617"
+ dependencies:
+ chalk "^1.1.3"
+ diff "^3.2.0"
+ jest-matcher-utils "^20.0.3"
+ pretty-format "^20.0.3"
+
+jest-docblock@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-20.0.3.tgz#17bea984342cc33d83c50fbe1545ea0efaa44712"
+
+jest-environment-jsdom@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-20.0.3.tgz#048a8ac12ee225f7190417713834bb999787de99"
+ dependencies:
+ jest-mock "^20.0.3"
+ jest-util "^20.0.3"
+ jsdom "^9.12.0"
+
+jest-environment-node@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-20.0.3.tgz#d488bc4612af2c246e986e8ae7671a099163d403"
+ dependencies:
+ jest-mock "^20.0.3"
+ jest-util "^20.0.3"
+
+jest-haste-map@^20.0.4:
+ version "20.0.5"
+ resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-20.0.5.tgz#abad74efb1a005974a7b6517e11010709cab9112"
+ dependencies:
+ fb-watchman "^2.0.0"
+ graceful-fs "^4.1.11"
+ jest-docblock "^20.0.3"
+ micromatch "^2.3.11"
+ sane "~1.6.0"
+ worker-farm "^1.3.1"
+
+jest-jasmine2@^20.0.4:
+ version "20.0.4"
+ resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-20.0.4.tgz#fcc5b1411780d911d042902ef1859e852e60d5e1"
+ dependencies:
+ chalk "^1.1.3"
+ graceful-fs "^4.1.11"
+ jest-diff "^20.0.3"
+ jest-matcher-utils "^20.0.3"
+ jest-matchers "^20.0.3"
+ jest-message-util "^20.0.3"
+ jest-snapshot "^20.0.3"
+ once "^1.4.0"
+ p-map "^1.1.1"
+
+jest-matcher-utils@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-20.0.3.tgz#b3a6b8e37ca577803b0832a98b164f44b7815612"
+ dependencies:
+ chalk "^1.1.3"
+ pretty-format "^20.0.3"
+
+jest-matchers@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/jest-matchers/-/jest-matchers-20.0.3.tgz#ca69db1c32db5a6f707fa5e0401abb55700dfd60"
+ dependencies:
+ jest-diff "^20.0.3"
+ jest-matcher-utils "^20.0.3"
+ jest-message-util "^20.0.3"
+ jest-regex-util "^20.0.3"
+
+jest-message-util@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-20.0.3.tgz#6aec2844306fcb0e6e74d5796c1006d96fdd831c"
+ dependencies:
+ chalk "^1.1.3"
+ micromatch "^2.3.11"
+ slash "^1.0.0"
+
+jest-mock@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-20.0.3.tgz#8bc070e90414aa155c11a8d64c869a0d5c71da59"
+
+jest-regex-util@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-20.0.3.tgz#85bbab5d133e44625b19faf8c6aa5122d085d762"
+
+jest-resolve-dependencies@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-20.0.3.tgz#6e14a7b717af0f2cb3667c549de40af017b1723a"
+ dependencies:
+ jest-regex-util "^20.0.3"
+
+jest-resolve@^20.0.4:
+ version "20.0.4"
+ resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-20.0.4.tgz#9448b3e8b6bafc15479444c6499045b7ffe597a5"
+ dependencies:
+ browser-resolve "^1.11.2"
+ is-builtin-module "^1.0.0"
+ resolve "^1.3.2"
+
+jest-runtime@^20.0.4:
+ version "20.0.4"
+ resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-20.0.4.tgz#a2c802219c4203f754df1404e490186169d124d8"
+ dependencies:
+ babel-core "^6.0.0"
+ babel-jest "^20.0.3"
+ babel-plugin-istanbul "^4.0.0"
+ chalk "^1.1.3"
+ convert-source-map "^1.4.0"
+ graceful-fs "^4.1.11"
+ jest-config "^20.0.4"
+ jest-haste-map "^20.0.4"
+ jest-regex-util "^20.0.3"
+ jest-resolve "^20.0.4"
+ jest-util "^20.0.3"
+ json-stable-stringify "^1.0.1"
+ micromatch "^2.3.11"
+ strip-bom "3.0.0"
+ yargs "^7.0.2"
+
+jest-snapshot@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-20.0.3.tgz#5b847e1adb1a4d90852a7f9f125086e187c76566"
+ dependencies:
+ chalk "^1.1.3"
+ jest-diff "^20.0.3"
+ jest-matcher-utils "^20.0.3"
+ jest-util "^20.0.3"
+ natural-compare "^1.4.0"
+ pretty-format "^20.0.3"
+
+jest-util@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-20.0.3.tgz#0c07f7d80d82f4e5a67c6f8b9c3fe7f65cfd32ad"
+ dependencies:
+ chalk "^1.1.3"
+ graceful-fs "^4.1.11"
+ jest-message-util "^20.0.3"
+ jest-mock "^20.0.3"
+ jest-validate "^20.0.3"
+ leven "^2.1.0"
+ mkdirp "^0.5.1"
+
+jest-validate@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-20.0.3.tgz#d0cfd1de4f579f298484925c280f8f1d94ec3cab"
+ dependencies:
+ chalk "^1.1.3"
+ jest-matcher-utils "^20.0.3"
+ leven "^2.1.0"
+ pretty-format "^20.0.3"
+
+jest@^20.0.4:
+ version "20.0.4"
+ resolved "https://registry.yarnpkg.com/jest/-/jest-20.0.4.tgz#3dd260c2989d6dad678b1e9cc4d91944f6d602ac"
+ dependencies:
+ jest-cli "^20.0.4"
+
+"js-tokens@^3.0.0 || ^4.0.0":
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+
+js-tokens@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
+
+js-yaml@^3.7.0:
+ version "3.13.1"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^4.0.0"
+
+jsbn@~0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+
+jsdom-global@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/jsdom-global/-/jsdom-global-2.1.1.tgz#47d46fe77f6167baf5d34431d3bb59fc41b0915a"
+
+jsdom@^9.12.0, jsdom@^9.4.1:
+ version "9.12.0"
+ resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-9.12.0.tgz#e8c546fffcb06c00d4833ca84410fed7f8a097d4"
+ dependencies:
+ abab "^1.0.3"
+ acorn "^4.0.4"
+ acorn-globals "^3.1.0"
+ array-equal "^1.0.0"
+ content-type-parser "^1.0.1"
+ cssom ">= 0.3.2 < 0.4.0"
+ cssstyle ">= 0.2.37 < 0.3.0"
+ escodegen "^1.6.1"
+ html-encoding-sniffer "^1.0.1"
+ nwmatcher ">= 1.3.9 < 2.0.0"
+ parse5 "^1.5.1"
+ request "^2.79.0"
+ sax "^1.2.1"
+ symbol-tree "^3.2.1"
+ tough-cookie "^2.3.2"
+ webidl-conversions "^4.0.0"
+ whatwg-encoding "^1.0.1"
+ whatwg-url "^4.3.0"
+ xml-name-validator "^2.0.1"
+
+jsesc@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
+
+json-schema-traverse@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+
+json-schema@0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
+
+json-stable-stringify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
+ dependencies:
+ jsonify "~0.0.0"
+
+json-stringify-safe@~5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+
+json5@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
+
+jsonify@~0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
+
+jsprim@^1.2.2:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
+ dependencies:
+ assert-plus "1.0.0"
+ extsprintf "1.3.0"
+ json-schema "0.2.3"
+ verror "1.10.0"
+
+kind-of@^3.0.2:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^6.0.0:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
+
+lcid@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
+ dependencies:
+ invert-kv "^1.0.0"
+
+leven@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580"
+
+levn@~0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
+ dependencies:
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+
+load-json-file@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^2.2.0"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+ strip-bom "^2.0.0"
+
+locate-path@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
+ dependencies:
+ p-locate "^2.0.0"
+ path-exists "^3.0.0"
+
+lodash.escape@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98"
+
+lodash.flattendeep@^4.4.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
+
+lodash.isequal@^4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
+
+lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.4:
+ version "4.17.11"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
+
+lolex@1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.3.2.tgz#7c3da62ffcb30f0f5a80a2566ca24e45d8a01f31"
+
+loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
+ dependencies:
+ js-tokens "^3.0.0 || ^4.0.0"
+
+makeerror@1.0.x:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"
+ dependencies:
+ tmpl "1.0.x"
+
+math-random@^1.0.1:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c"
+
+merge@^1.2.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145"
+
+micromatch@^2.1.5, micromatch@^2.3.11:
+ version "2.3.11"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
+ dependencies:
+ arr-diff "^2.0.0"
+ array-unique "^0.2.1"
+ braces "^1.8.2"
+ expand-brackets "^0.1.4"
+ extglob "^0.3.1"
+ filename-regex "^2.0.0"
+ is-extglob "^1.0.0"
+ is-glob "^2.0.1"
+ kind-of "^3.0.2"
+ normalize-path "^2.0.1"
+ object.omit "^2.0.0"
+ parse-glob "^3.0.4"
+ regex-cache "^0.4.2"
+
+mime-db@~1.38.0:
+ version "1.38.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.38.0.tgz#1a2aab16da9eb167b49c6e4df2d9c68d63d8e2ad"
+
+mime-types@^2.1.12, mime-types@~2.1.19:
+ version "2.1.22"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.22.tgz#fe6b355a190926ab7698c9a0556a11199b2199bd"
+ dependencies:
+ mime-db "~1.38.0"
+
+min-document@^2.19.0:
+ version "2.19.0"
+ resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685"
+ dependencies:
+ dom-walk "^0.1.0"
+
+minimatch@3.0.4, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimist@0.0.8:
+ version "0.0.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
+
+minimist@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
+
+minimist@~0.0.1:
+ version "0.0.10"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
+
+mkdirp@0.5.1, mkdirp@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
+ dependencies:
+ minimist "0.0.8"
+
+mocha@^5.0.1:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6"
+ dependencies:
+ browser-stdout "1.3.1"
+ commander "2.15.1"
+ debug "3.1.0"
+ diff "3.5.0"
+ escape-string-regexp "1.0.5"
+ glob "7.1.2"
+ growl "1.10.5"
+ he "1.1.1"
+ minimatch "3.0.4"
+ mkdirp "0.5.1"
+ supports-color "5.4.0"
+
+mock-local-storage@^1.0.5:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/mock-local-storage/-/mock-local-storage-1.1.8.tgz#82a3f7c666bf955d64b286cb71207478f1a29bf7"
+ dependencies:
+ core-js "^0.8.3"
+ global "^4.3.2"
+
+moo@^0.4.3:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/moo/-/moo-0.4.3.tgz#3f847a26f31cf625a956a87f2b10fbc013bfd10e"
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+
+ms@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
+
+natural-compare@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+
+nearley@^2.7.10:
+ version "2.16.0"
+ resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.16.0.tgz#77c297d041941d268290ec84b739d0ee297e83a7"
+ dependencies:
+ commander "^2.19.0"
+ moo "^0.4.3"
+ railroad-diagrams "^1.0.0"
+ randexp "0.4.6"
+ semver "^5.4.1"
+
+neo-async@^2.6.0:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.0.tgz#b9d15e4d71c6762908654b5183ed38b753340835"
+
+node-int64@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
+
+node-notifier@^5.0.2:
+ version "5.4.0"
+ resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.4.0.tgz#7b455fdce9f7de0c63538297354f3db468426e6a"
+ dependencies:
+ growly "^1.3.0"
+ is-wsl "^1.1.0"
+ semver "^5.5.0"
+ shellwords "^0.1.1"
+ which "^1.3.0"
+
+normalize-package-data@^2.3.2:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
+ dependencies:
+ hosted-git-info "^2.1.4"
+ resolve "^1.10.0"
+ semver "2 || 3 || 4 || 5"
+ validate-npm-package-license "^3.0.1"
+
+normalize-path@^2.0.0, normalize-path@^2.0.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
+ dependencies:
+ remove-trailing-separator "^1.0.1"
+
+nth-check@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
+ dependencies:
+ boolbase "~1.0.0"
+
+number-is-nan@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
+
+"nwmatcher@>= 1.3.9 < 2.0.0":
+ version "1.4.4"
+ resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.4.tgz#2285631f34a95f0d0395cd900c96ed39b58f346e"
+
+oauth-sign@~0.9.0:
+ version "0.9.0"
+ resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
+
+object-assign@^4.1.0, object-assign@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+
+object-inspect@^1.1.0, object-inspect@^1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b"
+
+object-is@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6"
+
+object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.0.9:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
+
+object.assign@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
+ dependencies:
+ define-properties "^1.1.2"
+ function-bind "^1.1.1"
+ has-symbols "^1.0.0"
+ object-keys "^1.0.11"
+
+object.entries@^1.0.4, object.entries@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.0.tgz#2024fc6d6ba246aee38bdb0ffd5cfbcf371b7519"
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.12.0"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+
+object.fromentries@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.0.tgz#49a543d92151f8277b3ac9600f1e930b189d30ab"
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.11.0"
+ function-bind "^1.1.1"
+ has "^1.0.1"
+
+object.omit@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa"
+ dependencies:
+ for-own "^0.1.4"
+ is-extendable "^0.1.1"
+
+object.values@^1.0.4, object.values@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.0.tgz#bf6810ef5da3e5325790eaaa2be213ea84624da9"
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.12.0"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+
+once@^1.3.0, once@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ dependencies:
+ wrappy "1"
+
+optimist@^0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
+ dependencies:
+ minimist "~0.0.1"
+ wordwrap "~0.0.2"
+
+optionator@^0.8.1:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
+ dependencies:
+ deep-is "~0.1.3"
+ fast-levenshtein "~2.0.4"
+ levn "~0.3.0"
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+ wordwrap "~1.0.0"
+
+os-homedir@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
+
+os-locale@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9"
+ dependencies:
+ lcid "^1.0.0"
+
+os-tmpdir@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
+
+p-limit@^1.1.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
+ dependencies:
+ p-try "^1.0.0"
+
+p-locate@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
+ dependencies:
+ p-limit "^1.1.0"
+
+p-map@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b"
+
+p-try@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
+
+parse-glob@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c"
+ dependencies:
+ glob-base "^0.3.0"
+ is-dotfile "^1.0.0"
+ is-extglob "^1.0.0"
+ is-glob "^2.0.0"
+
+parse-json@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
+ dependencies:
+ error-ex "^1.2.0"
+
+parse5@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94"
+
+parse5@^3.0.1:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c"
+ dependencies:
+ "@types/node" "*"
+
+path-exists@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b"
+ dependencies:
+ pinkie-promise "^2.0.0"
+
+path-exists@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+
+path-is-absolute@^1.0.0, path-is-absolute@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+
+path-parse@^1.0.5, path-parse@^1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
+
+path-type@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
+ dependencies:
+ graceful-fs "^4.1.2"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+performance-now@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+
+pify@^2.0.0, pify@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+
+pinkie-promise@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
+ dependencies:
+ pinkie "^2.0.0"
+
+pinkie@^2.0.0:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
+
+prelude-ls@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
+
+preserve@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
+
+pretty-format@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-20.0.3.tgz#020e350a560a1fe1a98dc3beb6ccffb386de8b14"
+ dependencies:
+ ansi-regex "^2.1.1"
+ ansi-styles "^3.0.0"
+
+private@^0.1.8:
+ version "0.1.8"
+ resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
+
+process@~0.5.1:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf"
+
+prop-types-exact@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/prop-types-exact/-/prop-types-exact-1.2.0.tgz#825d6be46094663848237e3925a98c6e944e9869"
+ dependencies:
+ has "^1.0.3"
+ object.assign "^4.1.0"
+ reflect.ownkeys "^0.2.0"
+
+prop-types@^15.6.2, prop-types@^15.7.2:
+ version "15.7.2"
+ resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
+ dependencies:
+ loose-envify "^1.4.0"
+ object-assign "^4.1.1"
+ react-is "^16.8.1"
+
+prr@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
+
+psl@^1.1.24, psl@^1.1.28:
+ version "1.1.31"
+ resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184"
+
+punycode@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+
+punycode@^2.1.0, punycode@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+
+qs@~6.5.2:
+ version "6.5.2"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
+
+raf@^3.4.0:
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
+ dependencies:
+ performance-now "^2.1.0"
+
+railroad-diagrams@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e"
+
+randexp@0.4.6:
+ version "0.4.6"
+ resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3"
+ dependencies:
+ discontinuous-range "1.0.0"
+ ret "~0.1.10"
+
+randomatic@^3.0.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed"
+ dependencies:
+ is-number "^4.0.0"
+ kind-of "^6.0.0"
+ math-random "^1.0.1"
+
+react-is@^16.8.1, react-is@^16.8.6:
+ version "16.8.6"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16"
+
+react-test-renderer@^16.0.0-0:
+ version "16.8.6"
+ resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.8.6.tgz#188d8029b8c39c786f998aa3efd3ffe7642d5ba1"
+ dependencies:
+ object-assign "^4.1.1"
+ prop-types "^15.6.2"
+ react-is "^16.8.6"
+ scheduler "^0.13.6"
+
+read-pkg-up@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
+ dependencies:
+ find-up "^1.0.0"
+ read-pkg "^1.0.0"
+
+read-pkg@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
+ dependencies:
+ load-json-file "^1.0.0"
+ normalize-package-data "^2.3.2"
+ path-type "^1.0.0"
+
+readable-stream@^3.1.1:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.3.0.tgz#cb8011aad002eb717bf040291feba8569c986fb9"
+ dependencies:
+ inherits "^2.0.3"
+ string_decoder "^1.1.1"
+ util-deprecate "^1.0.1"
+
+reflect.ownkeys@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"
+
+regenerator-runtime@^0.11.0:
+ version "0.11.1"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
+
+regex-cache@^0.4.2:
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd"
+ dependencies:
+ is-equal-shallow "^0.1.3"
+
+remove-trailing-separator@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
+
+repeat-element@^1.1.2:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
+
+repeat-string@^1.5.2:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+
+repeating@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda"
+ dependencies:
+ is-finite "^1.0.0"
+
+request@^2.79.0:
+ version "2.88.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
+ dependencies:
+ aws-sign2 "~0.7.0"
+ aws4 "^1.8.0"
+ caseless "~0.12.0"
+ combined-stream "~1.0.6"
+ extend "~3.0.2"
+ forever-agent "~0.6.1"
+ form-data "~2.3.2"
+ har-validator "~5.1.0"
+ http-signature "~1.2.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.19"
+ oauth-sign "~0.9.0"
+ performance-now "^2.1.0"
+ qs "~6.5.2"
+ safe-buffer "^5.1.2"
+ tough-cookie "~2.4.3"
+ tunnel-agent "^0.6.0"
+ uuid "^3.3.2"
+
+require-directory@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+
+require-hacker@^2.1.4:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/require-hacker/-/require-hacker-2.1.4.tgz#1683da866119495e0ffcda8ebed9bbcf556849f2"
+ dependencies:
+ babel-runtime "^6.6.1"
+ colors "^1.1.2"
+
+require-main-filename@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
+
+resolve@1.1.7:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
+
+resolve@^1.10.0, resolve@^1.3.2:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba"
+ dependencies:
+ path-parse "^1.0.6"
+
+ret@~0.1.10:
+ version "0.1.15"
+ resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
+
+rimraf@^2.6.1:
+ version "2.6.3"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
+ dependencies:
+ glob "^7.1.3"
+
+rst-selector-parser@^2.2.3:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91"
+ dependencies:
+ lodash.flattendeep "^4.4.0"
+ nearley "^2.7.10"
+
+safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+
+"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+
+samsam@1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567"
+
+samsam@~1.1:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.3.tgz#9f5087419b4d091f232571e7fa52e90b0f552621"
+
+sane@~1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/sane/-/sane-1.6.0.tgz#9610c452307a135d29c1fdfe2547034180c46775"
+ dependencies:
+ anymatch "^1.3.0"
+ exec-sh "^0.2.0"
+ fb-watchman "^1.8.0"
+ minimatch "^3.0.2"
+ minimist "^1.1.1"
+ walker "~1.0.5"
+ watch "~0.10.0"
+
+sax@^1.2.1:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
+
+scheduler@^0.13.6:
+ version "0.13.6"
+ resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.6.tgz#466a4ec332467b31a91b9bf74e5347072e4cd889"
+ dependencies:
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+
+"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0:
+ version "5.7.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b"
+
+set-blocking@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+
+shellwords@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
+
+sinon@^1.17.5:
+ version "1.17.7"
+ resolved "https://registry.yarnpkg.com/sinon/-/sinon-1.17.7.tgz#4542a4f49ba0c45c05eb2e9dd9d203e2b8efe0bf"
+ dependencies:
+ formatio "1.1.1"
+ lolex "1.3.2"
+ samsam "1.1.2"
+ util ">=0.10.3 <1"
+
+slash@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
+
+source-map-support@^0.4.15:
+ version "0.4.18"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f"
+ dependencies:
+ source-map "^0.5.6"
+
+source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7:
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
+
+source-map@^0.6.1, source-map@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+
+spdx-correct@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4"
+ dependencies:
+ spdx-expression-parse "^3.0.0"
+ spdx-license-ids "^3.0.0"
+
+spdx-exceptions@^2.1.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977"
+
+spdx-expression-parse@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0"
+ dependencies:
+ spdx-exceptions "^2.1.0"
+ spdx-license-ids "^3.0.0"
+
+spdx-license-ids@^3.0.0:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz#75ecd1a88de8c184ef015eafb51b5b48bfd11bb1"
+
+sprintf-js@~1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+
+sshpk@^1.7.0:
+ version "1.16.1"
+ resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
+ dependencies:
+ asn1 "~0.2.3"
+ assert-plus "^1.0.0"
+ bcrypt-pbkdf "^1.0.0"
+ dashdash "^1.12.0"
+ ecc-jsbn "~0.1.1"
+ getpass "^0.1.1"
+ jsbn "~0.1.0"
+ safer-buffer "^2.0.2"
+ tweetnacl "~0.14.0"
+
+string-length@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/string-length/-/string-length-1.0.1.tgz#56970fb1c38558e9e70b728bf3de269ac45adfac"
+ dependencies:
+ strip-ansi "^3.0.0"
+
+string-width@^1.0.1, string-width@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
+ dependencies:
+ code-point-at "^1.0.0"
+ is-fullwidth-code-point "^1.0.0"
+ strip-ansi "^3.0.0"
+
+string.prototype.trim@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz#d04de2c89e137f4d7d206f086b5ed2fae6be8cea"
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.5.0"
+ function-bind "^1.0.2"
+
+string_decoder@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d"
+ dependencies:
+ safe-buffer "~5.1.0"
+
+strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+ dependencies:
+ ansi-regex "^2.0.0"
+
+strip-bom@3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+
+strip-bom@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
+ dependencies:
+ is-utf8 "^0.2.0"
+
+supports-color@5.4.0:
+ version "5.4.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54"
+ dependencies:
+ has-flag "^3.0.0"
+
+supports-color@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
+
+supports-color@^3.1.2:
+ version "3.2.3"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6"
+ dependencies:
+ has-flag "^1.0.0"
+
+symbol-tree@^3.2.1:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6"
+
+test-exclude@^4.2.1:
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.2.3.tgz#a9a5e64474e4398339245a0a769ad7c2f4a97c20"
+ dependencies:
+ arrify "^1.0.1"
+ micromatch "^2.3.11"
+ object-assign "^4.1.0"
+ read-pkg-up "^1.0.1"
+ require-main-filename "^1.0.1"
+
+throat@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/throat/-/throat-3.2.0.tgz#50cb0670edbc40237b9e347d7e1f88e4620af836"
+
+tmatch@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/tmatch/-/tmatch-2.0.1.tgz#0c56246f33f30da1b8d3d72895abaf16660f38cf"
+
+tmpl@1.0.x:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
+
+to-fast-properties@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
+
+tough-cookie@^2.3.2:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
+ dependencies:
+ psl "^1.1.28"
+ punycode "^2.1.1"
+
+tough-cookie@~2.4.3:
+ version "2.4.3"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
+ dependencies:
+ psl "^1.1.24"
+ punycode "^1.4.1"
+
+tr46@~0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
+
+trim-right@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
+
+tunnel-agent@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+ dependencies:
+ safe-buffer "^5.0.1"
+
+tweetnacl@^0.14.3, tweetnacl@~0.14.0:
+ version "0.14.5"
+ resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
+
+type-check@~0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
+ dependencies:
+ prelude-ls "~1.1.2"
+
+uglify-js@^3.1.4:
+ version "3.5.3"
+ resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.5.3.tgz#d490bb5347f23025f0c1bc0dee901d98e4d6b063"
+ dependencies:
+ commander "~2.19.0"
+ source-map "~0.6.1"
+
+uri-js@^4.2.2:
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
+ dependencies:
+ punycode "^2.1.0"
+
+util-deprecate@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+
+"util@>=0.10.3 <1":
+ version "0.11.1"
+ resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61"
+ dependencies:
+ inherits "2.0.3"
+
+uuid@^3.3.2:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
+
+validate-npm-package-license@^3.0.1:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
+ dependencies:
+ spdx-correct "^3.0.0"
+ spdx-expression-parse "^3.0.0"
+
+verror@1.10.0:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
+ dependencies:
+ assert-plus "^1.0.0"
+ core-util-is "1.0.2"
+ extsprintf "^1.2.0"
+
+walker@~1.0.5:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"
+ dependencies:
+ makeerror "1.0.x"
+
+watch@~0.10.0:
+ version "0.10.0"
+ resolved "https://registry.yarnpkg.com/watch/-/watch-0.10.0.tgz#77798b2da0f9910d595f1ace5b0c2258521f21dc"
+
+webidl-conversions@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
+
+webidl-conversions@^4.0.0:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
+
+whatwg-encoding@^1.0.1:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
+ dependencies:
+ iconv-lite "0.4.24"
+
+whatwg-url@^4.3.0:
+ version "4.8.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-4.8.0.tgz#d2981aa9148c1e00a41c5a6131166ab4683bbcc0"
+ dependencies:
+ tr46 "~0.0.3"
+ webidl-conversions "^3.0.0"
+
+which-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
+
+which@^1.2.12, which@^1.3.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+ dependencies:
+ isexe "^2.0.0"
+
+wordwrap@~0.0.2:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
+
+wordwrap@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
+
+worker-farm@^1.3.1:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.6.0.tgz#aecc405976fab5a95526180846f0dba288f3a4a0"
+ dependencies:
+ errno "~0.1.7"
+
+wrap-ansi@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+
+xml-name-validator@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635"
+
+y18n@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
+
+yargs-parser@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a"
+ dependencies:
+ camelcase "^3.0.0"
+
+yargs@^7.0.2:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8"
+ dependencies:
+ camelcase "^3.0.0"
+ cliui "^3.2.0"
+ decamelize "^1.1.1"
+ get-caller-file "^1.0.1"
+ os-locale "^1.4.0"
+ read-pkg-up "^1.0.1"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^1.0.2"
+ which-module "^1.0.0"
+ y18n "^3.2.1"
+ yargs-parser "^5.0.0"
diff --git a/devtools/client/webconsole/types.js b/devtools/client/webconsole/types.js
new file mode 100644
index 0000000000..e91e3e88bf
--- /dev/null
+++ b/devtools/client/webconsole/types.js
@@ -0,0 +1,90 @@
+/* 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 {
+ MESSAGE_SOURCE,
+ MESSAGE_TYPE,
+ MESSAGE_LEVEL,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+exports.ConsoleCommand = function (props) {
+ return Object.assign(
+ {
+ id: null,
+ allowRepeating: false,
+ messageText: null,
+ source: MESSAGE_SOURCE.JAVASCRIPT,
+ type: MESSAGE_TYPE.COMMAND,
+ level: MESSAGE_LEVEL.LOG,
+ groupId: null,
+ indent: 0,
+ private: false,
+ timeStamp: null,
+ },
+ props
+ );
+};
+
+exports.ConsoleMessage = function (props) {
+ return Object.assign(
+ {
+ id: null,
+ innerWindowID: null,
+ targetFront: null,
+ allowRepeating: true,
+ source: null,
+ timeStamp: null,
+ type: null,
+ helperType: null,
+ level: null,
+ category: null,
+ messageText: null,
+ parameters: null,
+ repeatId: null,
+ stacktrace: null,
+ frame: null,
+ groupId: null,
+ errorMessageName: null,
+ exceptionDocURL: null,
+ cssSelectors: "",
+ userProvidedStyles: null,
+ notes: null,
+ indent: 0,
+ prefix: "",
+ private: false,
+ chromeContext: false,
+ hasException: false,
+ isPromiseRejection: false,
+ },
+ props
+ );
+};
+
+exports.NetworkEventMessage = function (props) {
+ return Object.assign(
+ {
+ id: null,
+ actor: null,
+ targetFront: null,
+ level: MESSAGE_LEVEL.LOG,
+ isXHR: false,
+ request: null,
+ response: null,
+ source: MESSAGE_SOURCE.NETWORK,
+ type: MESSAGE_TYPE.LOG,
+ groupId: null,
+ timeStamp: null,
+ totalTime: null,
+ indent: 0,
+ updates: null,
+ securityState: null,
+ securityInfo: null,
+ requestHeadersFromUploadStream: null,
+ private: false,
+ blockedReason: null,
+ },
+ props
+ );
+};
diff --git a/devtools/client/webconsole/utils.js b/devtools/client/webconsole/utils.js
new file mode 100644
index 0000000000..74b4ccfab1
--- /dev/null
+++ b/devtools/client/webconsole/utils.js
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Number of terminal entries for the self-xss prevention to go away
+const CONSOLE_ENTRY_THRESHOLD = 5;
+
+var WebConsoleUtils = {
+ CONSOLE_ENTRY_THRESHOLD,
+
+ /**
+ * Wrap a string in an nsISupportsString object.
+ *
+ * @param string string
+ * @return nsISupportsString
+ */
+ supportsString(string) {
+ const str = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ str.data = string;
+ return str;
+ },
+
+ /**
+ * Value of devtools.selfxss.count preference
+ *
+ * @type number
+ * @private
+ */
+ _usageCount: 0,
+ get usageCount() {
+ if (WebConsoleUtils._usageCount < CONSOLE_ENTRY_THRESHOLD) {
+ WebConsoleUtils._usageCount = Services.prefs.getIntPref(
+ "devtools.selfxss.count"
+ );
+ if (Services.prefs.getBoolPref("devtools.chrome.enabled")) {
+ WebConsoleUtils.usageCount = CONSOLE_ENTRY_THRESHOLD;
+ }
+ }
+ return WebConsoleUtils._usageCount;
+ },
+ set usageCount(newUC) {
+ if (newUC <= CONSOLE_ENTRY_THRESHOLD) {
+ WebConsoleUtils._usageCount = newUC;
+ Services.prefs.setIntPref("devtools.selfxss.count", newUC);
+ }
+ },
+};
+
+exports.Utils = WebConsoleUtils;
diff --git a/devtools/client/webconsole/utils/clipboard.js b/devtools/client/webconsole/utils/clipboard.js
new file mode 100644
index 0000000000..72b064f323
--- /dev/null
+++ b/devtools/client/webconsole/utils/clipboard.js
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Get the text of the element parameter, as provided by the Selection API. We need to
+ * rely on the Selection API as it mimics exactly what the user would have if they do a
+ * selection using the mouse and copy it. `HTMLElement.textContent` and
+ * `HTMLElement.innerText` follow a different codepath than user selection + copy.
+ * They both have issues when dealing with whitespaces, and therefore can't be used to
+ * have a reliable result.
+ *
+ * As the Selection API is exposed through the Window object, if we don't have a window
+ * we fallback to `HTMLElement.textContent`.
+ *
+ * @param {HTMLElement} el: The element we want the text of.
+ * @returns {String|null} The text of the element, or null if el is falsy.
+ */
+function getElementText(el) {
+ if (!el) {
+ return null;
+ }
+ // If we can, we use the Selection API to match what the user would get if they
+ // manually select and copy the message.
+ const doc = el.ownerDocument;
+ const win = doc && doc.defaultView;
+
+ if (!win) {
+ return el.textContent;
+ }
+
+ // We store the current selected range and unselect everything.
+ const selection = win.getSelection();
+ const currentSelectedRange =
+ !selection.isCollapsed && selection.getRangeAt(0);
+ selection.removeAllRanges();
+
+ // Then creates a range from `el`, and get the text content.
+ const range = doc.createRange();
+ range.selectNode(el);
+ selection.addRange(range);
+ const text = selection.toString();
+
+ // Finally we revert the selection to what it was.
+ selection.removeRange(range);
+ if (currentSelectedRange) {
+ selection.addRange(currentSelectedRange);
+ }
+
+ return text;
+}
+
+module.exports = {
+ getElementText,
+};
diff --git a/devtools/client/webconsole/utils/context-menu.js b/devtools/client/webconsole/utils/context-menu.js
new file mode 100644
index 0000000000..7996194d56
--- /dev/null
+++ b/devtools/client/webconsole/utils/context-menu.js
@@ -0,0 +1,368 @@
+/* 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 Menu = require("resource://devtools/client/framework/menu.js");
+const MenuItem = require("resource://devtools/client/framework/menu-item.js");
+
+const {
+ MESSAGE_SOURCE,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+const clipboardHelper = require("resource://devtools/shared/platform/clipboard.js");
+const {
+ l10n,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+const actions = require("resource://devtools/client/webconsole/actions/index.js");
+
+loader.lazyRequireGetter(
+ this,
+ "saveAs",
+ "resource://devtools/shared/DevToolsUtils.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "openContentLink",
+ "resource://devtools/client/shared/link.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "getElementText",
+ "resource://devtools/client/webconsole/utils/clipboard.js",
+ true
+);
+
+/**
+ * Create a Menu instance for the webconsole.
+ *
+ * @param {Event} context menu event
+ * {Object} message (optional) message object containing metadata such as:
+ * - {String} source
+ * - {String} request
+ * @param {Object} options
+ * - {Actions} bound actions
+ * - {WebConsoleWrapper} wrapper instance used for accessing properties like the store
+ * and window.
+ */
+function createContextMenu(event, message, webConsoleWrapper) {
+ const { target } = event;
+ const { parentNode, toolbox, hud } = webConsoleWrapper;
+ const store = webConsoleWrapper.getStore();
+ const { dispatch } = store;
+
+ const messageEl = target.closest(".message");
+ const clipboardText = getElementText(messageEl);
+
+ const linkEl = target.closest("a[href]");
+ const url = linkEl && linkEl.href;
+
+ const messageVariable = target.closest(".objectBox");
+ // Ensure that console.group and console.groupCollapsed commands are not captured
+ const variableText =
+ messageVariable &&
+ !messageEl.classList.contains("startGroup") &&
+ !messageEl.classList.contains("startGroupCollapsed")
+ ? messageVariable.textContent
+ : null;
+
+ // Retrieve closes actor id from the DOM.
+ const actorEl =
+ target.closest("[data-link-actor-id]") ||
+ target.querySelector("[data-link-actor-id]");
+ const actor = actorEl ? actorEl.dataset.linkActorId : null;
+
+ const rootObjectInspector = target.closest(".object-inspector");
+ const rootActor = rootObjectInspector
+ ? rootObjectInspector.querySelector("[data-link-actor-id]")
+ : null;
+ // We can have object which are not displayed inside an ObjectInspector (e.g. Errors),
+ // so let's default to `actor`.
+ const rootActorId = rootActor ? rootActor.dataset.linkActorId : actor;
+
+ const elementNode =
+ target.closest(".objectBox-node") || target.closest(".objectBox-textNode");
+ const isConnectedElement =
+ elementNode && elementNode.querySelector(".open-inspector") !== null;
+
+ const win = parentNode.ownerDocument.defaultView;
+ const selection = win.getSelection();
+
+ const { source, request, messageId } = message || {};
+
+ const menu = new Menu({ id: "webconsole-menu" });
+
+ // Copy URL for a network request.
+ menu.append(
+ new MenuItem({
+ id: "console-menu-copy-url",
+ label: l10n.getStr("webconsole.menu.copyURL.label"),
+ accesskey: l10n.getStr("webconsole.menu.copyURL.accesskey"),
+ visible: source === MESSAGE_SOURCE.NETWORK,
+ click: () => {
+ if (!request) {
+ return;
+ }
+ clipboardHelper.copyString(request.url);
+ },
+ })
+ );
+
+ if (toolbox && request) {
+ // Open Network message in the Network panel.
+ menu.append(
+ new MenuItem({
+ id: "console-menu-open-in-network-panel",
+ label: l10n.getStr("webconsole.menu.openInNetworkPanel.label"),
+ accesskey: l10n.getStr("webconsole.menu.openInNetworkPanel.accesskey"),
+ visible: source === MESSAGE_SOURCE.NETWORK,
+ click: () => dispatch(actions.openNetworkPanel(message.messageId)),
+ })
+ );
+ // Resend Network message.
+ menu.append(
+ new MenuItem({
+ id: "console-menu-resend-network-request",
+ label: l10n.getStr("webconsole.menu.resendNetworkRequest.label"),
+ accesskey: l10n.getStr(
+ "webconsole.menu.resendNetworkRequest.accesskey"
+ ),
+ visible: source === MESSAGE_SOURCE.NETWORK,
+ click: () => dispatch(actions.resendNetworkRequest(messageId)),
+ })
+ );
+ }
+
+ // Open URL in a new tab for a network request.
+ menu.append(
+ new MenuItem({
+ id: "console-menu-open-url",
+ label: l10n.getStr("webconsole.menu.openURL.label"),
+ accesskey: l10n.getStr("webconsole.menu.openURL.accesskey"),
+ visible: source === MESSAGE_SOURCE.NETWORK,
+ click: () => {
+ if (!request) {
+ return;
+ }
+ openContentLink(request.url);
+ },
+ })
+ );
+
+ // Open DOM node in the Inspector panel.
+ const contentDomReferenceEl = target.closest(
+ "[data-link-content-dom-reference]"
+ );
+ if (isConnectedElement && contentDomReferenceEl) {
+ const contentDomReference = contentDomReferenceEl.getAttribute(
+ "data-link-content-dom-reference"
+ );
+
+ menu.append(
+ new MenuItem({
+ id: "console-menu-open-node",
+ label: l10n.getStr("webconsole.menu.openNodeInInspector.label"),
+ accesskey: l10n.getStr("webconsole.menu.openNodeInInspector.accesskey"),
+ disabled: false,
+ click: () =>
+ dispatch(
+ actions.openNodeInInspector(JSON.parse(contentDomReference))
+ ),
+ })
+ );
+ }
+
+ // Store as global variable.
+ menu.append(
+ new MenuItem({
+ id: "console-menu-store",
+ label: l10n.getStr("webconsole.menu.storeAsGlobalVar.label"),
+ accesskey: l10n.getStr("webconsole.menu.storeAsGlobalVar.accesskey"),
+ disabled: !actor,
+ click: () => dispatch(actions.storeAsGlobal(actor)),
+ })
+ );
+
+ // Copy message or grip.
+ menu.append(
+ new MenuItem({
+ id: "console-menu-copy",
+ label: l10n.getStr("webconsole.menu.copyMessage.label"),
+ accesskey: l10n.getStr("webconsole.menu.copyMessage.accesskey"),
+ // Disabled if there is no selection and no message element available to copy.
+ disabled: selection.isCollapsed && !clipboardText,
+ click: () => {
+ if (selection.isCollapsed) {
+ // If the selection is empty/collapsed, copy the text content of the
+ // message for which the context menu was opened.
+ clipboardHelper.copyString(clipboardText);
+ } else {
+ clipboardHelper.copyString(selection.toString());
+ }
+ },
+ })
+ );
+
+ // Copy message object.
+ menu.append(
+ new MenuItem({
+ id: "console-menu-copy-object",
+ label: l10n.getStr("webconsole.menu.copyObject.label"),
+ accesskey: l10n.getStr("webconsole.menu.copyObject.accesskey"),
+ // Disabled if there is no actor and no variable text associated.
+ disabled: !actor && !variableText,
+ click: () => dispatch(actions.copyMessageObject(actor, variableText)),
+ })
+ );
+
+ // Export to clipboard
+ menu.append(
+ new MenuItem({
+ id: "console-menu-export-clipboard",
+ label: l10n.getStr("webconsole.menu.copyAllMessages.label"),
+ accesskey: l10n.getStr("webconsole.menu.copyAllMessages.accesskey"),
+ disabled: false,
+ async click() {
+ const outputText = await getUnvirtualizedConsoleOutputText(
+ webConsoleWrapper
+ );
+ clipboardHelper.copyString(outputText);
+ },
+ })
+ );
+
+ // Export to file
+ menu.append(
+ new MenuItem({
+ id: "console-menu-export-file",
+ label: l10n.getStr("webconsole.menu.saveAllMessagesFile.label"),
+ accesskey: l10n.getStr("webconsole.menu.saveAllMessagesFile.accesskey"),
+ disabled: false,
+ // Note: not async, but returns a promise for the actual save.
+ click: async () => {
+ const date = new Date();
+ const suggestedName =
+ `console-export-${date.getFullYear()}-` +
+ `${date.getMonth() + 1}-${date.getDate()}_${date.getHours()}-` +
+ `${date.getMinutes()}-${date.getSeconds()}.txt`;
+ const outputText = await getUnvirtualizedConsoleOutputText(
+ webConsoleWrapper
+ );
+ const data = new TextEncoder().encode(outputText);
+ saveAs(window, data, suggestedName);
+ },
+ })
+ );
+
+ // Open object in sidebar.
+ const shouldOpenSidebar = store.getState().prefs.sidebarToggle;
+ if (shouldOpenSidebar) {
+ menu.append(
+ new MenuItem({
+ id: "console-menu-open-sidebar",
+ label: l10n.getStr("webconsole.menu.openInSidebar.label1"),
+ accesskey: l10n.getStr("webconsole.menu.openInSidebar.accesskey"),
+ disabled: !rootActorId,
+ click: () => dispatch(actions.openSidebar(messageId, rootActorId)),
+ })
+ );
+ }
+
+ if (url) {
+ menu.append(
+ new MenuItem({
+ id: "console-menu-open-url",
+ label: l10n.getStr("webconsole.menu.openURL.label"),
+ accesskey: l10n.getStr("webconsole.menu.openURL.accesskey"),
+ click: () =>
+ openContentLink(url, {
+ inBackground: true,
+ relatedToCurrent: true,
+ }),
+ })
+ );
+ menu.append(
+ new MenuItem({
+ id: "console-menu-copy-url",
+ label: l10n.getStr("webconsole.menu.copyURL.label"),
+ accesskey: l10n.getStr("webconsole.menu.copyURL.accesskey"),
+ click: () => clipboardHelper.copyString(url),
+ })
+ );
+ }
+
+ // Emit the "menu-open" event for testing.
+ const { screenX, screenY } = event;
+ menu.once("open", () => webConsoleWrapper.emitForTests("menu-open"));
+ menu.popup(screenX, screenY, hud.chromeWindow.document);
+
+ return menu;
+}
+
+exports.createContextMenu = createContextMenu;
+
+/**
+ * Returns the whole text content of the console output.
+ * We're creating a new ConsoleOutput using the current store, turning off virtualization
+ * so we can have access to all the messages.
+ *
+ * @param {WebConsoleWrapper} webConsoleWrapper
+ * @returns Promise<String>
+ */
+async function getUnvirtualizedConsoleOutputText(webConsoleWrapper) {
+ return new Promise(resolve => {
+ const ReactDOM = require("resource://devtools/client/shared/vendor/react-dom.js");
+ const {
+ createElement,
+ createFactory,
+ } = require("resource://devtools/client/shared/vendor/react.js");
+ const ConsoleOutput = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/ConsoleOutput.js")
+ );
+ const {
+ Provider,
+ createProvider,
+ } = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+ const { parentNode, toolbox } = webConsoleWrapper;
+ const doc = parentNode.ownerDocument;
+
+ // Create an element that won't impact the layout of the console
+ const singleUseElement = doc.createElement("section");
+ singleUseElement.classList.add("clipboard-only");
+ doc.body.append(singleUseElement);
+
+ const consoleOutput = ConsoleOutput({
+ serviceContainer: {
+ ...webConsoleWrapper.getServiceContainer(),
+ preventStacktraceInitialRenderDelay: true,
+ },
+ disableVirtualization: true,
+ });
+
+ ReactDOM.render(
+ createElement(
+ Provider,
+ {
+ store: webConsoleWrapper.getStore(),
+ },
+ toolbox
+ ? createElement(
+ createProvider(toolbox.commands.targetCommand.storeId),
+ { store: toolbox.commands.targetCommand.store },
+ consoleOutput
+ )
+ : consoleOutput
+ ),
+ singleUseElement,
+ () => {
+ resolve(getElementText(singleUseElement));
+ singleUseElement.remove();
+ ReactDOM.unmountComponentAtNode(singleUseElement);
+ }
+ );
+ });
+}
diff --git a/devtools/client/webconsole/utils/id-generator.js b/devtools/client/webconsole/utils/id-generator.js
new file mode 100644
index 0000000000..17144dd73c
--- /dev/null
+++ b/devtools/client/webconsole/utils/id-generator.js
@@ -0,0 +1,15 @@
+/* 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";
+
+exports.IdGenerator = class IdGenerator {
+ constructor() {
+ this.messageId = 1;
+ }
+
+ getNextId(packet) {
+ return packet && packet.actor ? packet.actor : "" + this.messageId++;
+ }
+};
diff --git a/devtools/client/webconsole/utils/l10n.js b/devtools/client/webconsole/utils/l10n.js
new file mode 100644
index 0000000000..5c647f84e6
--- /dev/null
+++ b/devtools/client/webconsole/utils/l10n.js
@@ -0,0 +1,70 @@
+/* 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 { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+const helper = new LocalizationHelper(
+ "devtools/client/locales/webconsole.properties"
+);
+
+const l10n = {
+ /**
+ * Generates a formatted timestamp string for displaying in console messages.
+ *
+ * @param integer [milliseconds]
+ * Optional, allows you to specify the timestamp in milliseconds since
+ * the UNIX epoch.
+ * @return string
+ * The timestamp formatted for display.
+ */
+ timestampString(milliseconds) {
+ const d = new Date(milliseconds ? milliseconds : null);
+ const hours = d.getHours();
+ const minutes = d.getMinutes();
+ const seconds = d.getSeconds();
+ milliseconds = d.getMilliseconds();
+ const parameters = [hours, minutes, seconds, milliseconds];
+ return l10n.getFormatStr("timestampFormat", parameters);
+ },
+
+ /**
+ * Retrieve a localized string.
+ *
+ * @param string name
+ * The string name you want from the Web Console string bundle.
+ * @return string
+ * The localized string.
+ */
+ getStr(name) {
+ try {
+ return helper.getStr(name);
+ } catch (ex) {
+ console.error("Failed to get string: " + name);
+ throw ex;
+ }
+ },
+
+ /**
+ * Retrieve a localized string formatted with values coming from the given
+ * array.
+ *
+ * @param string name
+ * The string name you want from the Web Console string bundle.
+ * @param array array
+ * The array of values you want in the formatted string.
+ * @return string
+ * The formatted local string.
+ */
+ getFormatStr(name, array) {
+ try {
+ return helper.getFormatStr(name, ...array);
+ } catch (ex) {
+ console.error("Failed to format string: " + name);
+ throw ex;
+ }
+ },
+};
+
+module.exports = l10n;
diff --git a/devtools/client/webconsole/utils/messages.js b/devtools/client/webconsole/utils/messages.js
new file mode 100644
index 0000000000..135da20536
--- /dev/null
+++ b/devtools/client/webconsole/utils/messages.js
@@ -0,0 +1,1028 @@
+/* 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 l10n = require("resource://devtools/client/webconsole/utils/l10n.js");
+const ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");
+const {
+ isSupportedByConsoleTable,
+} = require("resource://devtools/shared/webconsole/messages.js");
+
+loader.lazyRequireGetter(
+ this,
+ "getAdHocFrontOrPrimitiveGrip",
+ "resource://devtools/client/fronts/object.js",
+ true
+);
+
+// URL Regex, common idioms:
+//
+// Lead-in (URL):
+// ( Capture because we need to know if there was a lead-in
+// character so we can include it as part of the text
+// preceding the match. We lack look-behind matching.
+// ^| The URL can start at the beginning of the string.
+// [\s(,;'"`“] Or whitespace or some punctuation that does not imply
+// a context which would preclude a URL.
+// )
+//
+// We do not need a trailing look-ahead because our regex's will terminate
+// because they run out of characters they can eat.
+
+// What we do not attempt to have the regexp do:
+// - Avoid trailing '.' and ')' characters. We let our greedy match absorb
+// these, but have a separate regex for extra characters to leave off at the
+// end.
+//
+// The Regex (apart from lead-in/lead-out):
+// ( Begin capture of the URL
+// (?: (potential detect beginnings)
+// https?:\/\/| Start with "http" or "https"
+// www\d{0,3}[.][a-z0-9.\-]{2,249}|
+// Start with "www", up to 3 numbers, then "." then
+// something that looks domain-namey. We differ from the
+// next case in that we do not constrain the top-level
+// domain as tightly and do not require a trailing path
+// indicator of "/". This is IDN root compatible.
+// [a-z0-9.\-]{2,250}[.][a-z]{2,4}\/
+// Detect a non-www domain, but requiring a trailing "/"
+// to indicate a path. This only detects IDN domains
+// with a non-IDN root. This is reasonable in cases where
+// there is no explicit http/https start us out, but
+// unreasonable where there is. Our real fix is the bug
+// to port the Thunderbird/gecko linkification logic.
+//
+// Domain names can be up to 253 characters long, and are
+// limited to a-zA-Z0-9 and '-'. The roots don't have
+// hyphens unless they are IDN roots. Root zones can be
+// found here: http://www.iana.org/domains/root/db
+// )
+// [-\w.!~*'();,/?:@&=+$#%]*
+// path onwards. We allow the set of characters that
+// encodeURI does not escape plus the result of escaping
+// (so also '%')
+// )
+// eslint-disable-next-line max-len
+const urlRegex =
+ /(^|[\s(,;'"`“])((?:https?:\/\/|www\d{0,3}[.][a-z0-9.\-]{2,249}|[a-z0-9.\-]{2,250}[.][a-z]{2,4}\/)[-\w.!~*'();,/?:@&=+$#%]*)/im;
+
+// Set of terminators that are likely to have been part of the context rather
+// than part of the URL and so should be uneaten. This is '(', ',', ';', plus
+// quotes and question end-ing punctuation and the potential permutations with
+// parentheses (english-specific).
+const uneatLastUrlCharsRegex = /(?:[),;.!?`'"]|[.!?]\)|\)[.!?])$/;
+
+const {
+ MESSAGE_SOURCE,
+ MESSAGE_TYPE,
+ MESSAGE_LEVEL,
+} = require("resource://devtools/client/webconsole/constants.js");
+const {
+ ConsoleMessage,
+ NetworkEventMessage,
+} = require("resource://devtools/client/webconsole/types.js");
+
+function prepareMessage(resource, idGenerator, persistLogs) {
+ if (!resource.source) {
+ resource = transformResource(resource, persistLogs);
+ }
+
+ resource.id = idGenerator.getNextId(resource);
+ return resource;
+}
+
+/**
+ * Transforms a resource given its type.
+ *
+ * @param {Object} resource: This can be either a simple RDP packet or an object emitted
+ * by the Resource API.
+ * @param {Boolean} persistLogs: Value of the "Persist logs" setting
+ */
+function transformResource(resource, persistLogs) {
+ switch (resource.resourceType || resource.type) {
+ case ResourceCommand.TYPES.CONSOLE_MESSAGE: {
+ return transformConsoleAPICallResource(resource, persistLogs);
+ }
+
+ case ResourceCommand.TYPES.PLATFORM_MESSAGE: {
+ return transformPlatformMessageResource(resource);
+ }
+
+ case ResourceCommand.TYPES.ERROR_MESSAGE: {
+ return transformPageErrorResource(resource);
+ }
+
+ case ResourceCommand.TYPES.CSS_MESSAGE: {
+ return transformCSSMessageResource(resource);
+ }
+
+ case ResourceCommand.TYPES.NETWORK_EVENT: {
+ return transformNetworkEventResource(resource);
+ }
+
+ case ResourceCommand.TYPES.JSTRACER_TRACE: {
+ return transformTraceResource(resource);
+ }
+
+ case ResourceCommand.TYPES.JSTRACER_STATE: {
+ return transformTracerStateResource(resource);
+ }
+
+ case "will-navigate": {
+ return transformNavigationMessagePacket(resource);
+ }
+
+ case "evaluationResult":
+ default: {
+ return transformEvaluationResultPacket(resource);
+ }
+ }
+}
+
+// eslint-disable-next-line complexity
+function transformConsoleAPICallResource(consoleMessageResource, persistLogs) {
+ const { message, targetFront } = consoleMessageResource;
+
+ let parameters = message.arguments;
+ let type = message.level;
+ let level = getLevelFromType(type);
+ let messageText = null;
+ const { timer } = message;
+
+ // Special per-type conversion.
+ switch (type) {
+ case "clear":
+ // We show a message to users when calls console.clear() is called.
+ parameters = [
+ l10n.getStr(persistLogs ? "preventedConsoleClear" : "consoleCleared"),
+ ];
+ break;
+ case "count":
+ case "countReset":
+ // Chrome RDP doesn't have a special type for count.
+ type = MESSAGE_TYPE.LOG;
+ const { counter } = message;
+
+ if (!counter) {
+ // We don't show anything if we don't have counter data.
+ type = MESSAGE_TYPE.NULL_MESSAGE;
+ } else if (counter.error) {
+ messageText = l10n.getFormatStr(counter.error, [counter.label]);
+ level = MESSAGE_LEVEL.WARN;
+ parameters = null;
+ } else {
+ const label = counter.label
+ ? counter.label
+ : l10n.getStr("noCounterLabel");
+ messageText = `${label}: ${counter.count}`;
+ parameters = null;
+ }
+ break;
+ case "timeStamp":
+ type = MESSAGE_TYPE.NULL_MESSAGE;
+ break;
+ case "time":
+ parameters = null;
+ if (timer && timer.error) {
+ messageText = l10n.getFormatStr(timer.error, [timer.name]);
+ level = MESSAGE_LEVEL.WARN;
+ } else {
+ // We don't show anything for console.time calls to match Chrome's behaviour.
+ type = MESSAGE_TYPE.NULL_MESSAGE;
+ }
+ break;
+ case "timeLog":
+ case "timeEnd":
+ if (timer && timer.error) {
+ parameters = null;
+ messageText = l10n.getFormatStr(timer.error, [timer.name]);
+ level = MESSAGE_LEVEL.WARN;
+ } else if (timer) {
+ // We show the duration to users when calls console.timeLog/timeEnd is called,
+ // if corresponding console.time() was called before.
+ const duration = Math.round(timer.duration * 100) / 100;
+ if (type === "timeEnd") {
+ messageText = l10n.getFormatStr("console.timeEnd", [
+ timer.name,
+ duration,
+ ]);
+ parameters = null;
+ } else if (type === "timeLog") {
+ const [, ...rest] = parameters;
+ parameters = [
+ l10n.getFormatStr("timeLog", [timer.name, duration]),
+ ...rest,
+ ];
+ }
+ } else {
+ // If the `timer` property does not exists, we don't output anything.
+ type = MESSAGE_TYPE.NULL_MESSAGE;
+ }
+ break;
+ case "table":
+ if (!isSupportedByConsoleTable(parameters)) {
+ // If the class of the first parameter is not supported,
+ // we handle the call as a simple console.log
+ type = "log";
+ }
+ break;
+ case "group":
+ type = MESSAGE_TYPE.START_GROUP;
+ if (parameters.length === 0) {
+ parameters = [l10n.getStr("noGroupLabel")];
+ }
+ break;
+ case "groupCollapsed":
+ type = MESSAGE_TYPE.START_GROUP_COLLAPSED;
+ if (parameters.length === 0) {
+ parameters = [l10n.getStr("noGroupLabel")];
+ }
+ break;
+ case "groupEnd":
+ type = MESSAGE_TYPE.END_GROUP;
+ parameters = null;
+ break;
+ case "dirxml":
+ // Handle console.dirxml calls as simple console.log
+ type = "log";
+ break;
+ }
+
+ const frame = message.filename
+ ? {
+ source: message.filename,
+ sourceId: message.sourceId,
+ line: message.lineNumber,
+ column: message.columnNumber,
+ }
+ : null;
+
+ if (frame && (type === "logPointError" || type === "logPoint")) {
+ frame.options = { logPoint: true };
+ }
+
+ return new ConsoleMessage({
+ targetFront,
+ source: MESSAGE_SOURCE.CONSOLE_API,
+ type,
+ level,
+ parameters,
+ messageText,
+ stacktrace: message.stacktrace ? message.stacktrace : null,
+ frame,
+ timeStamp: message.timeStamp,
+ userProvidedStyles: message.styles,
+ prefix: message.prefix,
+ private: message.private,
+ chromeContext: message.chromeContext,
+ });
+}
+
+function transformNavigationMessagePacket(packet) {
+ const { url } = packet;
+ return new ConsoleMessage({
+ source: MESSAGE_SOURCE.CONSOLE_FRONTEND,
+ type: MESSAGE_TYPE.NAVIGATION_MARKER,
+ level: MESSAGE_LEVEL.LOG,
+ messageText: l10n.getFormatStr("webconsole.navigated", [url]),
+ timeStamp: packet.timeStamp,
+ allowRepeating: false,
+ });
+}
+
+function transformPlatformMessageResource(platformMessageResource) {
+ const { message, timeStamp, targetFront } = platformMessageResource;
+ return new ConsoleMessage({
+ targetFront,
+ source: MESSAGE_SOURCE.CONSOLE_API,
+ type: MESSAGE_TYPE.LOG,
+ level: MESSAGE_LEVEL.LOG,
+ messageText: message,
+ timeStamp,
+ chromeContext: true,
+ });
+}
+
+function transformPageErrorResource(pageErrorResource, override = {}) {
+ const { pageError, targetFront } = pageErrorResource;
+ let level = MESSAGE_LEVEL.ERROR;
+ if (pageError.warning) {
+ level = MESSAGE_LEVEL.WARN;
+ } else if (pageError.info) {
+ level = MESSAGE_LEVEL.INFO;
+ }
+
+ const frame = pageError.sourceName
+ ? {
+ source: pageError.sourceName,
+ sourceId: pageError.sourceId,
+ line: pageError.lineNumber,
+ column: pageError.columnNumber,
+ }
+ : null;
+
+ return new ConsoleMessage(
+ Object.assign(
+ {
+ targetFront,
+ innerWindowID: pageError.innerWindowID,
+ source: MESSAGE_SOURCE.JAVASCRIPT,
+ type: MESSAGE_TYPE.LOG,
+ level,
+ category: pageError.category,
+ messageText: pageError.errorMessage,
+ stacktrace: pageError.stacktrace ? pageError.stacktrace : null,
+ frame,
+ errorMessageName: pageError.errorMessageName,
+ exceptionDocURL: pageError.exceptionDocURL,
+ hasException: pageError.hasException,
+ parameters: pageError.hasException ? [pageError.exception] : null,
+ timeStamp: pageError.timeStamp,
+ notes: pageError.notes,
+ private: pageError.private,
+ chromeContext: pageError.chromeContext,
+ isPromiseRejection: pageError.isPromiseRejection,
+ },
+ override
+ )
+ );
+}
+
+function transformCSSMessageResource(cssMessageResource) {
+ return transformPageErrorResource(cssMessageResource, {
+ cssSelectors: cssMessageResource.cssSelectors,
+ source: MESSAGE_SOURCE.CSS,
+ });
+}
+
+function transformNetworkEventResource(networkEventResource) {
+ return new NetworkEventMessage(networkEventResource);
+}
+
+function transformTraceResource(traceResource) {
+ const { targetFront, prefix, timeStamp } = traceResource;
+ if (traceResource.eventName) {
+ const { eventName } = traceResource;
+
+ return new ConsoleMessage({
+ targetFront,
+ source: MESSAGE_SOURCE.JSTRACER,
+ depth: 0,
+ eventName,
+ timeStamp,
+ prefix,
+ allowRepeating: false,
+ });
+ }
+ const {
+ depth,
+ implementation,
+ displayName,
+ filename,
+ lineNumber,
+ columnNumber,
+ args,
+ sourceId,
+
+ returnedValue,
+ relatedTraceId,
+ why,
+ } = traceResource;
+
+ const frame = {
+ source: filename,
+ sourceId,
+ line: lineNumber,
+ column: columnNumber,
+ };
+
+ return new ConsoleMessage({
+ targetFront,
+ source: MESSAGE_SOURCE.JSTRACER,
+ frame,
+ depth,
+ implementation,
+ displayName,
+ parameters: args
+ ? args.map(p => (p ? getAdHocFrontOrPrimitiveGrip(p, targetFront) : p))
+ : null,
+ returnedValue: why
+ ? getAdHocFrontOrPrimitiveGrip(returnedValue, targetFront)
+ : null,
+ relatedTraceId,
+ why,
+ messageText: null,
+ timeStamp,
+ prefix,
+ // Allow the identical frames to be coallesced into a unique message
+ // with a repeatition counter so that we keep the output short in case of loops.
+ allowRepeating: true,
+ });
+}
+
+function transformTracerStateResource(stateResource) {
+ const { targetFront, enabled, logMethod, timeStamp, reason } = stateResource;
+ let message;
+ if (enabled) {
+ if (logMethod == "stdout") {
+ message = l10n.getStr("webconsole.message.commands.startTracingToStdout");
+ } else if (logMethod == "console") {
+ message = l10n.getStr(
+ "webconsole.message.commands.startTracingToWebConsole"
+ );
+ } else if (logMethod == "profiler") {
+ message = l10n.getStr(
+ "webconsole.message.commands.startTracingToProfiler"
+ );
+ } else {
+ throw new Error(`Unsupported tracer log method ${logMethod}`);
+ }
+ } else if (reason) {
+ message = l10n.getFormatStr(
+ "webconsole.message.commands.stopTracingWithReason",
+ [reason]
+ );
+ } else {
+ message = l10n.getStr("webconsole.message.commands.stopTracing");
+ }
+ return new ConsoleMessage({
+ targetFront,
+ source: MESSAGE_SOURCE.CONSOLE_API,
+ type: MESSAGE_TYPE.JSTRACER,
+ level: MESSAGE_LEVEL.LOG,
+ messageText: message,
+ timeStamp,
+ });
+}
+
+function transformEvaluationResultPacket(packet) {
+ let {
+ exceptionMessage,
+ errorMessageName,
+ exceptionDocURL,
+ exception,
+ exceptionStack,
+ hasException,
+ frame,
+ result,
+ helperResult,
+ timestamp: timeStamp,
+ notes,
+ } = packet;
+
+ let parameter;
+
+ if (hasException) {
+ // If we have an exception, we prefix it, and we reset the exception message, as we're
+ // not going to use it.
+ parameter = exception;
+ exceptionMessage = null;
+ } else if (helperResult?.object) {
+ parameter = helperResult.object;
+ } else if (helperResult?.type === "error") {
+ try {
+ exceptionMessage = l10n.getFormatStr(
+ helperResult.message,
+ helperResult.messageArgs || []
+ );
+ } catch (ex) {
+ exceptionMessage = helperResult.message;
+ }
+ } else {
+ parameter = result;
+ }
+
+ const level =
+ typeof exceptionMessage !== "undefined" && packet.exceptionMessage !== null
+ ? MESSAGE_LEVEL.ERROR
+ : MESSAGE_LEVEL.LOG;
+
+ return new ConsoleMessage({
+ source: MESSAGE_SOURCE.JAVASCRIPT,
+ type: MESSAGE_TYPE.RESULT,
+ helperType: helperResult ? helperResult.type : null,
+ level,
+ messageText: exceptionMessage,
+ hasException,
+ parameters: [parameter],
+ errorMessageName,
+ exceptionDocURL,
+ stacktrace: exceptionStack,
+ frame,
+ timeStamp,
+ notes,
+ private: packet.private,
+ allowRepeating: false,
+ });
+}
+
+/**
+ * Return if passed messages are similar and can thus be "repeated".
+ * ⚠ This function is on a hot path, called for (almost) every message being sent by
+ * the server. This should be kept as fast as possible.
+ *
+ * @param {Message} message1
+ * @param {Message} message2
+ * @returns {Boolean}
+ */
+// eslint-disable-next-line complexity
+function areMessagesSimilar(message1, message2) {
+ if (!message1 || !message2) {
+ return false;
+ }
+
+ if (!areMessagesParametersSimilar(message1, message2)) {
+ return false;
+ }
+
+ if (!areMessagesStacktracesSimilar(message1, message2)) {
+ return false;
+ }
+
+ if (
+ !message1.allowRepeating ||
+ !message2.allowRepeating ||
+ message1.type !== message2.type ||
+ message1.level !== message2.level ||
+ message1.source !== message2.source ||
+ message1.category !== message2.category ||
+ message1.frame?.source !== message2.frame?.source ||
+ message1.frame?.line !== message2.frame?.line ||
+ message1.frame?.column !== message2.frame?.column ||
+ message1.messageText !== message2.messageText ||
+ message1.private !== message2.private ||
+ message1.errorMessageName !== message2.errorMessageName ||
+ message1.hasException !== message2.hasException ||
+ message1.isPromiseRejection !== message2.isPromiseRejection ||
+ message1.userProvidedStyles?.length !==
+ message2.userProvidedStyles?.length ||
+ `${message1.userProvidedStyles}` !== `${message2.userProvidedStyles}`
+ ) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Return if passed messages parameters are similar
+ * ⚠ This function is on a hot path, called for (almost) every message being sent by
+ * the server. This should be kept as fast as possible.
+ *
+ * @param {Message} message1
+ * @param {Message} message2
+ * @returns {Boolean}
+ */
+function areMessagesParametersSimilar(message1, message2) {
+ const message1ParamsLength = message1.parameters?.length;
+ if (message1ParamsLength !== message2.parameters?.length) {
+ return false;
+ }
+
+ if (!message1ParamsLength) {
+ return true;
+ }
+
+ for (let i = 0; i < message1ParamsLength; i++) {
+ const message1Parameter = message1.parameters[i];
+ const message2Parameter = message2.parameters[i];
+ // exceptions have a grip, but we want to consider 2 messages similar as long as
+ // they refer to the same error.
+ if (
+ message1.hasException &&
+ message2.hasException &&
+ message1Parameter._grip?.class == message2Parameter._grip?.class &&
+ message1Parameter._grip?.preview?.message ==
+ message2Parameter._grip?.preview?.message &&
+ message1Parameter._grip?.preview?.stack ==
+ message2Parameter._grip?.preview?.stack
+ ) {
+ continue;
+ }
+
+ // For object references (grips), that are not exceptions, we don't want to consider
+ // messages to be the same as we only have a preview of what they look like, and not
+ // some kind of property that would give us the state of a given instance at a given
+ // time.
+ if (message1Parameter._grip || message2Parameter._grip) {
+ return false;
+ }
+
+ if (message1Parameter.type !== message2Parameter.type) {
+ return false;
+ }
+
+ if (message1Parameter.type) {
+ if (message1Parameter.text !== message2Parameter.text) {
+ return false;
+ }
+ } else if (message1Parameter !== message2Parameter) {
+ return false;
+ }
+ }
+ return true;
+}
+
+/**
+ * Return if passed messages stacktraces are similar
+ *
+ * @param {Message} message1
+ * @param {Message} message2
+ * @returns {Boolean}
+ */
+function areMessagesStacktracesSimilar(message1, message2) {
+ const message1StackLength = message1.stacktrace?.length;
+ if (message1StackLength !== message2.stacktrace?.length) {
+ return false;
+ }
+
+ if (!message1StackLength) {
+ return true;
+ }
+
+ for (let i = 0; i < message1StackLength; i++) {
+ const message1Frame = message1.stacktrace[i];
+ const message2Frame = message2.stacktrace[i];
+
+ if (message1Frame.filename !== message2Frame.filename) {
+ return false;
+ }
+
+ if (message1Frame.columnNumber !== message2Frame.columnNumber) {
+ return false;
+ }
+
+ if (message1Frame.lineNumber !== message2Frame.lineNumber) {
+ return false;
+ }
+ }
+ return true;
+}
+
+/**
+ * Maps a Firefox RDP type to its corresponding level.
+ */
+function getLevelFromType(type) {
+ const levels = {
+ LEVEL_ERROR: "error",
+ LEVEL_WARNING: "warn",
+ LEVEL_INFO: "info",
+ LEVEL_LOG: "log",
+ LEVEL_DEBUG: "debug",
+ };
+
+ // A mapping from the console API log event levels to the Web Console levels.
+ const levelMap = {
+ error: levels.LEVEL_ERROR,
+ exception: levels.LEVEL_ERROR,
+ assert: levels.LEVEL_ERROR,
+ logPointError: levels.LEVEL_ERROR,
+ warn: levels.LEVEL_WARNING,
+ info: levels.LEVEL_INFO,
+ log: levels.LEVEL_LOG,
+ clear: levels.LEVEL_LOG,
+ trace: levels.LEVEL_LOG,
+ table: levels.LEVEL_LOG,
+ debug: levels.LEVEL_DEBUG,
+ dir: levels.LEVEL_LOG,
+ dirxml: levels.LEVEL_LOG,
+ group: levels.LEVEL_LOG,
+ groupCollapsed: levels.LEVEL_LOG,
+ groupEnd: levels.LEVEL_LOG,
+ time: levels.LEVEL_LOG,
+ timeEnd: levels.LEVEL_LOG,
+ count: levels.LEVEL_LOG,
+ };
+
+ return levelMap[type] || MESSAGE_TYPE.LOG;
+}
+
+function isGroupType(type) {
+ return [
+ MESSAGE_TYPE.START_GROUP,
+ MESSAGE_TYPE.START_GROUP_COLLAPSED,
+ ].includes(type);
+}
+
+function isPacketPrivate(packet) {
+ return (
+ packet.private === true ||
+ (packet.message && packet.message.private === true) ||
+ (packet.pageError && packet.pageError.private === true) ||
+ (packet.networkEvent && packet.networkEvent.private === true)
+ );
+}
+
+function createWarningGroupMessage(id, type, firstMessage) {
+ return new ConsoleMessage({
+ id,
+ allowRepeating: false,
+ level: MESSAGE_LEVEL.WARN,
+ source: MESSAGE_SOURCE.CONSOLE_FRONTEND,
+ type,
+ messageText: getWarningGroupLabel(firstMessage),
+ timeStamp: firstMessage.timeStamp,
+ innerWindowID: firstMessage.innerWindowID,
+ });
+}
+
+function createSimpleTableMessage(columns, items, timeStamp) {
+ return new ConsoleMessage({
+ allowRepeating: false,
+ level: MESSAGE_LEVEL.LOG,
+ source: MESSAGE_SOURCE.CONSOLE_FRONTEND,
+ type: MESSAGE_TYPE.SIMPLE_TABLE,
+ columns,
+ items,
+ timeStamp,
+ });
+}
+
+/**
+ * Given the a regular warning message, compute the label of the warning group the message
+ * could be in.
+ * For example, if the message text is:
+ * The resource at “http://evil.com” was blocked because content blocking is enabled
+ *
+ * it may be turned into
+ *
+ * The resource at “<URL>” was blocked because content blocking is enabled
+ *
+ * @param {ConsoleMessage} firstMessage
+ * @returns {String} The computed label
+ */
+function getWarningGroupLabel(firstMessage) {
+ if (
+ isContentBlockingMessage(firstMessage) ||
+ isStorageIsolationMessage(firstMessage) ||
+ isTrackingProtectionMessage(firstMessage)
+ ) {
+ return replaceURL(firstMessage.messageText, "<URL>");
+ }
+
+ if (isCookieSameSiteMessage(firstMessage)) {
+ if (Services.prefs.getBoolPref("network.cookie.sameSite.laxByDefault")) {
+ return l10n.getStr("webconsole.group.cookieSameSiteLaxByDefaultEnabled2");
+ }
+ return l10n.getStr("webconsole.group.cookieSameSiteLaxByDefaultDisabled2");
+ }
+
+ if (isCSPMessage(firstMessage)) {
+ return l10n.getStr("webconsole.group.csp");
+ }
+
+ return "";
+}
+
+/**
+ * Replace any URL in the provided text by the provided replacement text, or an empty
+ * string.
+ *
+ * @param {String} text
+ * @param {String} replacementText
+ * @returns {String}
+ */
+function replaceURL(text, replacementText = "") {
+ let result = "";
+ let currentIndex = 0;
+ let contentStart;
+ while (true) {
+ const url = urlRegex.exec(text);
+ // Pick the regexp with the earlier content; index will always be zero.
+ if (!url) {
+ break;
+ }
+ contentStart = url.index + url[1].length;
+ if (contentStart > 0) {
+ const nonUrlText = text.substring(0, contentStart);
+ result += nonUrlText;
+ }
+
+ // There are some final characters for a URL that are much more likely
+ // to have been part of the enclosing text rather than the end of the
+ // URL.
+ let useUrl = url[2];
+ const uneat = uneatLastUrlCharsRegex.exec(useUrl);
+ if (uneat) {
+ useUrl = useUrl.substring(0, uneat.index);
+ }
+
+ if (useUrl) {
+ result += replacementText;
+ }
+
+ currentIndex = currentIndex + contentStart;
+
+ currentIndex = currentIndex + useUrl.length;
+ text = text.substring(url.index + url[1].length + useUrl.length);
+ }
+
+ return result + text;
+}
+
+/**
+ * Get the warningGroup type in which the message could be in.
+ * @param {ConsoleMessage} message
+ * @returns {String|null} null if the message can't be part of a warningGroup.
+ */
+function getWarningGroupType(message) {
+ // We got report that this can be called with `undefined` (See Bug 1801462 and Bug 1810109).
+ // Until we manage to reproduce and find why this happens, guard on message so at least
+ // we don't crash the console.
+ if (!message) {
+ return null;
+ }
+
+ if (
+ message.level !== MESSAGE_LEVEL.WARN &&
+ // CookieSameSite messages are not warnings but infos
+ message.level !== MESSAGE_LEVEL.INFO
+ ) {
+ return null;
+ }
+
+ if (isContentBlockingMessage(message)) {
+ return MESSAGE_TYPE.CONTENT_BLOCKING_GROUP;
+ }
+
+ if (isStorageIsolationMessage(message)) {
+ return MESSAGE_TYPE.STORAGE_ISOLATION_GROUP;
+ }
+
+ if (isTrackingProtectionMessage(message)) {
+ return MESSAGE_TYPE.TRACKING_PROTECTION_GROUP;
+ }
+
+ if (isCookieSameSiteMessage(message)) {
+ return MESSAGE_TYPE.COOKIE_SAMESITE_GROUP;
+ }
+
+ if (isCSPMessage(message)) {
+ return MESSAGE_TYPE.CSP_GROUP;
+ }
+
+ return null;
+}
+
+/**
+ * Returns a computed id given a message
+ *
+ * @param {ConsoleMessage} type: the message type, from MESSAGE_TYPE.
+ * @param {Integer} innerWindowID: the message innerWindowID.
+ * @returns {String}
+ */
+function getParentWarningGroupMessageId(message) {
+ const warningGroupType = getWarningGroupType(message);
+ if (!warningGroupType) {
+ return null;
+ }
+
+ return `${warningGroupType}-${message.innerWindowID}`;
+}
+
+/**
+ * Returns true if the message is a warningGroup message (i.e. the "Header").
+ * @param {ConsoleMessage} message
+ * @returns {Boolean}
+ */
+function isWarningGroup(message) {
+ return (
+ message.type === MESSAGE_TYPE.CONTENT_BLOCKING_GROUP ||
+ message.type === MESSAGE_TYPE.STORAGE_ISOLATION_GROUP ||
+ message.type === MESSAGE_TYPE.TRACKING_PROTECTION_GROUP ||
+ message.type === MESSAGE_TYPE.COOKIE_SAMESITE_GROUP ||
+ message.type === MESSAGE_TYPE.CORS_GROUP ||
+ message.type === MESSAGE_TYPE.CSP_GROUP
+ );
+}
+
+/**
+ * Returns true if the message is a content blocking message.
+ * @param {ConsoleMessage} message
+ * @returns {Boolean}
+ */
+function isContentBlockingMessage(message) {
+ const { category } = message;
+ return (
+ category == "cookieBlockedPermission" ||
+ category == "cookieBlockedTracker" ||
+ category == "cookieBlockedAll" ||
+ category == "cookieBlockedForeign"
+ );
+}
+
+/**
+ * Returns true if the message is a storage isolation message.
+ * @param {ConsoleMessage} message
+ * @returns {Boolean}
+ */
+function isStorageIsolationMessage(message) {
+ const { category } = message;
+ return category == "cookiePartitionedForeign";
+}
+
+/**
+ * Returns true if the message is a tracking protection message.
+ * @param {ConsoleMessage} message
+ * @returns {Boolean}
+ */
+function isTrackingProtectionMessage(message) {
+ const { category } = message;
+ return category == "Tracking Protection";
+}
+
+/**
+ * Returns true if the message is a cookie message.
+ * @param {ConsoleMessage} message
+ * @returns {Boolean}
+ */
+function isCookieSameSiteMessage(message) {
+ const { category } = message;
+ return category == "cookieSameSite";
+}
+
+/**
+ * Returns true if the message is a Content Security Policy (CSP) message.
+ * @param {ConsoleMessage} message
+ * @returns {Boolean}
+ */
+function isCSPMessage(message) {
+ const { category } = message;
+ return typeof category == "string" && category.startsWith("CSP_");
+}
+
+function getDescriptorValue(descriptor) {
+ if (!descriptor) {
+ return descriptor;
+ }
+
+ if (Object.prototype.hasOwnProperty.call(descriptor, "safeGetterValues")) {
+ return descriptor.safeGetterValues;
+ }
+
+ if (Object.prototype.hasOwnProperty.call(descriptor, "getterValue")) {
+ return descriptor.getterValue;
+ }
+
+ if (Object.prototype.hasOwnProperty.call(descriptor, "value")) {
+ return descriptor.value;
+ }
+ return descriptor;
+}
+
+function getNaturalOrder(messageA, messageB) {
+ const aFirst = -1;
+ const bFirst = 1;
+
+ // It can happen that messages are emitted in the same microsecond, making their
+ // timestamp similar. In such case, we rely on which message came first through
+ // the console API service, checking their id, except for expression result, which we'll
+ // always insert after because console API messages emitted from the expression need to
+ // be rendered before.
+ if (messageA.timeStamp === messageB.timeStamp) {
+ if (messageA.type === "result") {
+ return bFirst;
+ }
+
+ if (messageB.type === "result") {
+ return aFirst;
+ }
+
+ if (
+ !Number.isNaN(parseInt(messageA.id, 10)) &&
+ !Number.isNaN(parseInt(messageB.id, 10))
+ ) {
+ return parseInt(messageA.id, 10) < parseInt(messageB.id, 10)
+ ? aFirst
+ : bFirst;
+ }
+ }
+ return messageA.timeStamp < messageB.timeStamp ? aFirst : bFirst;
+}
+
+function isMessageNetworkError(message) {
+ return (
+ message.source === MESSAGE_SOURCE.NETWORK &&
+ message?.status &&
+ message?.status.toString().match(/^[4,5]\d\d$/)
+ );
+}
+
+module.exports = {
+ areMessagesSimilar,
+ createWarningGroupMessage,
+ createSimpleTableMessage,
+ getDescriptorValue,
+ getNaturalOrder,
+ getParentWarningGroupMessageId,
+ getWarningGroupType,
+ isContentBlockingMessage,
+ isGroupType,
+ isMessageNetworkError,
+ isPacketPrivate,
+ isWarningGroup,
+ l10n,
+ prepareMessage,
+};
diff --git a/devtools/client/webconsole/utils/moz.build b/devtools/client/webconsole/utils/moz.build
new file mode 100644
index 0000000000..8367b8c195
--- /dev/null
+++ b/devtools/client/webconsole/utils/moz.build
@@ -0,0 +1,14 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ "clipboard.js",
+ "context-menu.js",
+ "id-generator.js",
+ "l10n.js",
+ "messages.js",
+ "object-inspector.js",
+ "prefs.js",
+)
diff --git a/devtools/client/webconsole/utils/object-inspector.js b/devtools/client/webconsole/utils/object-inspector.js
new file mode 100644
index 0000000000..8001828b51
--- /dev/null
+++ b/devtools/client/webconsole/utils/object-inspector.js
@@ -0,0 +1,158 @@
+/* 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 {
+ createFactory,
+ createElement,
+} = require("resource://devtools/client/shared/vendor/react.js");
+
+loader.lazyGetter(this, "REPS", function () {
+ return require("resource://devtools/client/shared/components/reps/index.js")
+ .REPS;
+});
+loader.lazyGetter(this, "MODE", function () {
+ return require("resource://devtools/client/shared/components/reps/index.js")
+ .MODE;
+});
+loader.lazyGetter(this, "ObjectInspector", function () {
+ const reps = require("resource://devtools/client/shared/components/reps/index.js");
+ return createFactory(reps.objectInspector.ObjectInspector);
+});
+
+loader.lazyRequireGetter(
+ this,
+ "SmartTrace",
+ "resource://devtools/client/shared/components/SmartTrace.js"
+);
+
+loader.lazyRequireGetter(
+ this,
+ "LongStringFront",
+ "resource://devtools/client/fronts/string.js",
+ true
+);
+
+loader.lazyRequireGetter(
+ this,
+ "ObjectFront",
+ "resource://devtools/client/fronts/object.js",
+ true
+);
+
+/**
+ * Create and return an ObjectInspector for the given front.
+ *
+ * @param {Object} grip
+ * The object grip to create an ObjectInspector for.
+ * @param {Object} serviceContainer
+ * Object containing various utility functions
+ * @param {Object} override
+ * Object containing props that should override the default props passed to
+ * ObjectInspector.
+ * @returns {ObjectInspector}
+ * An ObjectInspector for the given grip.
+ */
+function getObjectInspector(
+ frontOrPrimitiveGrip,
+ serviceContainer,
+ override = {}
+) {
+ let onDOMNodeMouseOver;
+ let onDOMNodeMouseOut;
+ let onInspectIconClick;
+
+ if (serviceContainer) {
+ onDOMNodeMouseOver = serviceContainer.highlightDomElement
+ ? object => serviceContainer.highlightDomElement(object)
+ : null;
+ onDOMNodeMouseOut = serviceContainer.unHighlightDomElement
+ ? object => serviceContainer.unHighlightDomElement(object)
+ : null;
+ onInspectIconClick = serviceContainer.openNodeInInspector
+ ? (object, e) => {
+ // Stop the event propagation so we don't trigger ObjectInspector expand/collapse.
+ e.stopPropagation();
+ serviceContainer.openNodeInInspector(object);
+ }
+ : null;
+ }
+
+ const roots = createRoots(frontOrPrimitiveGrip, override.pathPrefix);
+
+ const objectInspectorProps = {
+ autoExpandDepth: 0,
+ mode: MODE.LONG,
+ standalone: true,
+ roots,
+ onViewSourceInDebugger: serviceContainer.onViewSourceInDebugger,
+ recordTelemetryEvent: serviceContainer.recordTelemetryEvent,
+ openLink: serviceContainer.openLink,
+ sourceMapURLService: serviceContainer.sourceMapURLService,
+ customFormat: override.customFormat !== false,
+ setExpanded: override.setExpanded,
+ initiallyExpanded: override.initiallyExpanded,
+ queueActorsForCleanup: override.queueActorsForCleanup,
+ cachedNodes: override.cachedNodes,
+ urlCropLimit: 120,
+ renderStacktrace: stacktrace => {
+ const attrs = {
+ key: "stacktrace",
+ stacktrace,
+ onViewSourceInDebugger: serviceContainer
+ ? serviceContainer.onViewSourceInDebugger ||
+ serviceContainer.onViewSource
+ : null,
+ onViewSource: serviceContainer.onViewSource,
+ onReady: override.maybeScrollToBottom,
+ sourceMapURLService: serviceContainer
+ ? serviceContainer.sourceMapURLService
+ : null,
+ };
+
+ if (serviceContainer?.preventStacktraceInitialRenderDelay) {
+ attrs.initialRenderDelay = 0;
+ }
+ return createElement(SmartTrace, attrs);
+ },
+ onDOMNodeMouseOver,
+ onDOMNodeMouseOut,
+ onInspectIconClick,
+ defaultRep: REPS.Grip,
+ createElement: serviceContainer?.createElement,
+ mayUseCustomFormatter: true,
+ ...override,
+ };
+
+ if (override.autoFocusRoot) {
+ Object.assign(objectInspectorProps, {
+ focusedItem: objectInspectorProps.roots[0],
+ });
+ }
+
+ return ObjectInspector(objectInspectorProps);
+}
+
+function createRoots(frontOrPrimitiveGrip, pathPrefix = "") {
+ const isFront =
+ frontOrPrimitiveGrip instanceof ObjectFront ||
+ frontOrPrimitiveGrip instanceof LongStringFront;
+ const grip = isFront ? frontOrPrimitiveGrip.getGrip() : frontOrPrimitiveGrip;
+
+ return [
+ {
+ path: `${pathPrefix}${
+ frontOrPrimitiveGrip
+ ? frontOrPrimitiveGrip.actorID || frontOrPrimitiveGrip.actor
+ : null
+ }`,
+ contents: { value: grip, front: isFront ? frontOrPrimitiveGrip : null },
+ },
+ ];
+}
+
+module.exports = {
+ getObjectInspector,
+};
diff --git a/devtools/client/webconsole/utils/prefs.js b/devtools/client/webconsole/utils/prefs.js
new file mode 100644
index 0000000000..9235c82069
--- /dev/null
+++ b/devtools/client/webconsole/utils/prefs.js
@@ -0,0 +1,46 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function getPreferenceName(hud, suffix) {
+ if (!suffix) {
+ console.error("Suffix shouldn't be falsy", { suffix });
+ return null;
+ }
+
+ if (!hud) {
+ console.error("hud shouldn't be falsy", { hud });
+ return null;
+ }
+
+ if (suffix.startsWith("devtools.")) {
+ // We don't have a suffix but a full pref name. Let's return it.
+ return suffix;
+ }
+
+ const component = hud.isBrowserConsole ? "browserconsole" : "webconsole";
+ return `devtools.${component}.${suffix}`;
+}
+
+function getPrefsService(hud) {
+ const getPrefName = pref => getPreferenceName(hud, pref);
+
+ return {
+ getBoolPref: (pref, deflt) =>
+ Services.prefs.getBoolPref(getPrefName(pref), deflt),
+ getIntPref: (pref, deflt) =>
+ Services.prefs.getIntPref(getPrefName(pref), deflt),
+ setBoolPref: (pref, value) =>
+ Services.prefs.setBoolPref(getPrefName(pref), value),
+ setIntPref: (pref, value) =>
+ Services.prefs.setIntPref(getPrefName(pref), value),
+ clearUserPref: pref => Services.prefs.clearUserPref(getPrefName(pref)),
+ getPrefName,
+ };
+}
+
+module.exports = {
+ getPrefsService,
+};
diff --git a/devtools/client/webconsole/webconsole-ui.js b/devtools/client/webconsole/webconsole-ui.js
new file mode 100644
index 0000000000..7dfa44a402
--- /dev/null
+++ b/devtools/client/webconsole/webconsole-ui.js
@@ -0,0 +1,746 @@
+/* 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 EventEmitter = require("resource://devtools/shared/event-emitter.js");
+const KeyShortcuts = require("resource://devtools/client/shared/key-shortcuts.js");
+const {
+ l10n,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+
+const { BrowserLoader } = ChromeUtils.import(
+ "resource://devtools/shared/loader/browser-loader.js"
+);
+const {
+ getAdHocFrontOrPrimitiveGrip,
+} = require("resource://devtools/client/fronts/object.js");
+
+const { PREFS } = require("resource://devtools/client/webconsole/constants.js");
+
+const FirefoxDataProvider = require("resource://devtools/client/netmonitor/src/connector/firefox-data-provider.js");
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ AppConstants: "resource://gre/modules/AppConstants.sys.mjs",
+});
+
+loader.lazyRequireGetter(
+ this,
+ "START_IGNORE_ACTION",
+ "resource://devtools/client/shared/redux/middleware/ignore.js",
+ true
+);
+const ZoomKeys = require("resource://devtools/client/shared/zoom-keys.js");
+
+const PREF_SIDEBAR_ENABLED = "devtools.webconsole.sidebarToggle";
+const PREF_BROWSERTOOLBOX_SCOPE = "devtools.browsertoolbox.scope";
+
+/**
+ * A WebConsoleUI instance is an interactive console initialized *per target*
+ * that displays console log data as well as provides an interactive terminal to
+ * manipulate the target's document content.
+ *
+ * The WebConsoleUI is responsible for the actual Web Console UI
+ * implementation.
+ */
+class WebConsoleUI {
+ /*
+ * @param {WebConsole} hud: The WebConsole owner object.
+ */
+ constructor(hud) {
+ this.hud = hud;
+ this.hudId = this.hud.hudId;
+ this.isBrowserConsole = this.hud.isBrowserConsole;
+
+ this.isBrowserToolboxConsole =
+ this.hud.commands.descriptorFront.isBrowserProcessDescriptor &&
+ !this.isBrowserConsole;
+
+ this.window = this.hud.iframeWindow;
+
+ this._onPanelSelected = this._onPanelSelected.bind(this);
+ this._onChangeSplitConsoleState =
+ this._onChangeSplitConsoleState.bind(this);
+ this._onTargetAvailable = this._onTargetAvailable.bind(this);
+ this._onTargetDestroyed = this._onTargetDestroyed.bind(this);
+ this._onResourceAvailable = this._onResourceAvailable.bind(this);
+ this._onNetworkResourceUpdated = this._onNetworkResourceUpdated.bind(this);
+ this._onScopePrefChanged = this._onScopePrefChanged.bind(this);
+ this._onShowConsoleEvaluation = this._onShowConsoleEvaluation.bind(this);
+
+ if (this.isBrowserConsole) {
+ Services.prefs.addObserver(
+ PREF_BROWSERTOOLBOX_SCOPE,
+ this._onScopePrefChanged
+ );
+ }
+
+ EventEmitter.decorate(this);
+ }
+
+ /**
+ * Initialize the WebConsoleUI instance.
+ * @return object
+ * A promise object that resolves once the frame is ready to use.
+ */
+ init() {
+ if (this._initializer) {
+ return this._initializer;
+ }
+
+ this._initializer = (async () => {
+ this._initUI();
+
+ if (this.isBrowserConsole) {
+ // Bug 1605763:
+ // TargetCommand.startListening will start fetching additional targets
+ // and may overload the Browser Console with loads of targets and resources.
+ // We can call it from here, as `_attachTargets` is called after the UI is initialized.
+ // Bug 1642599:
+ // TargetCommand.startListening has to be called before:
+ // - `_attachTargets`, in order to set TargetCommand.watcherFront which is used by ResourceWatcher.watchResources.
+ // - `ConsoleCommands`, in order to set TargetCommand.targetFront which is wrapped by hud.currentTarget
+ await this.hud.commands.targetCommand.startListening();
+ if (this._destroyed) {
+ return;
+ }
+ }
+
+ await this.wrapper.init();
+ if (this._destroyed) {
+ return;
+ }
+
+ // Bug 1605763: It's important to call _attachTargets once the UI is initialized, as
+ // it may overload the Browser Console with many updates.
+ // It is also important to do it only after the wrapper is initialized,
+ // otherwise its `store` will be null while we already call a few dispatch methods
+ // from onResourceAvailable
+ await this._attachTargets();
+ if (this._destroyed) {
+ return;
+ }
+
+ // `_attachTargets` will process resources and throttle some actions
+ // Wait for these actions to be dispatched before reporting that the
+ // console is initialized. Otherwise `showToolbox` will resolve before
+ // all already existing console messages are displayed.
+ await this.wrapper.waitAsyncDispatches();
+ this._initNotifications();
+ })();
+
+ return this._initializer;
+ }
+
+ destroy() {
+ if (this._destroyed) {
+ return;
+ }
+
+ this._destroyed = true;
+
+ this.React = this.ReactDOM = this.FrameView = null;
+
+ if (this.wrapper) {
+ this.wrapper.getStore()?.dispatch(START_IGNORE_ACTION);
+ this.wrapper.destroy();
+ }
+
+ if (this.jsterm) {
+ this.jsterm.destroy();
+ this.jsterm = null;
+ }
+
+ const { toolbox } = this.hud;
+ if (toolbox) {
+ toolbox.off("webconsole-selected", this._onPanelSelected);
+ toolbox.off("split-console", this._onChangeSplitConsoleState);
+ toolbox.off("select", this._onChangeSplitConsoleState);
+ toolbox.off(
+ "show-original-variable-mapping-warnings",
+ this._onShowConsoleEvaluation
+ );
+ }
+
+ if (this.isBrowserConsole) {
+ Services.prefs.removeObserver(
+ PREF_BROWSERTOOLBOX_SCOPE,
+ this._onScopePrefChanged
+ );
+ }
+
+ // Stop listening for targets
+ this.hud.commands.targetCommand.unwatchTargets({
+ types: this.hud.commands.targetCommand.ALL_TYPES,
+ onAvailable: this._onTargetAvailable,
+ onDestroyed: this._onTargetDestroyed,
+ });
+
+ const resourceCommand = this.hud.resourceCommand;
+ resourceCommand.unwatchResources(
+ [
+ resourceCommand.TYPES.CONSOLE_MESSAGE,
+ resourceCommand.TYPES.ERROR_MESSAGE,
+ resourceCommand.TYPES.PLATFORM_MESSAGE,
+ resourceCommand.TYPES.DOCUMENT_EVENT,
+ resourceCommand.TYPES.LAST_PRIVATE_CONTEXT_EXIT,
+ resourceCommand.TYPES.JSTRACER_TRACE,
+ resourceCommand.TYPES.JSTRACER_STATE,
+ ],
+ { onAvailable: this._onResourceAvailable }
+ );
+ resourceCommand.unwatchResources([resourceCommand.TYPES.CSS_MESSAGE], {
+ onAvailable: this._onResourceAvailable,
+ });
+
+ this.stopWatchingNetworkResources();
+
+ if (this.networkDataProvider) {
+ this.networkDataProvider.destroy();
+ this.networkDataProvider = null;
+ }
+
+ // Nullify `hud` last as it nullify also target which is used on destroy
+ this.window = this.hud = this.wrapper = null;
+ }
+
+ /**
+ * Clear the Web Console output.
+ *
+ * This method emits the "messages-cleared" notification.
+ *
+ * @param boolean clearStorage
+ * True if you want to clear the console messages storage associated to
+ * this Web Console.
+ * @param object event
+ * If the event exists, calls preventDefault on it.
+ */
+ async clearOutput(clearStorage, event) {
+ if (event) {
+ event.preventDefault();
+ }
+ if (this.wrapper) {
+ this.wrapper.dispatchMessagesClear();
+ }
+
+ if (clearStorage) {
+ await this.clearMessagesCache();
+ }
+ this.emitForTests("messages-cleared");
+ }
+
+ async clearMessagesCache() {
+ if (this._destroyed) {
+ return;
+ }
+
+ // This can be called during console destruction and getAllFronts would reject in such case.
+ try {
+ const consoleFronts = await this.hud.commands.targetCommand.getAllFronts(
+ this.hud.commands.targetCommand.ALL_TYPES,
+ "console"
+ );
+ const promises = [];
+ for (const consoleFront of consoleFronts) {
+ promises.push(consoleFront.clearMessagesCacheAsync());
+ }
+ await Promise.all(promises);
+ this.emitForTests("messages-cache-cleared");
+ } catch (e) {
+ console.warn("Exception in clearMessagesCache", e);
+ }
+ }
+
+ /**
+ * Remove all of the private messages from the Web Console output.
+ *
+ * This method emits the "private-messages-cleared" notification.
+ */
+ clearPrivateMessages() {
+ if (this._destroyed) {
+ return;
+ }
+
+ this.wrapper.dispatchPrivateMessagesClear();
+ this.emitForTests("private-messages-cleared");
+ }
+
+ inspectObjectActor(objectActor) {
+ const { targetFront } = this.hud.commands.targetCommand;
+ this.wrapper.dispatchMessageAdd(
+ {
+ helperResult: {
+ type: "inspectObject",
+ object:
+ objectActor && objectActor.getGrip
+ ? objectActor
+ : getAdHocFrontOrPrimitiveGrip(objectActor, targetFront),
+ },
+ },
+ true
+ );
+ return this.wrapper;
+ }
+
+ disableAllNetworkMessages() {
+ if (this._destroyed) {
+ return;
+ }
+ this.wrapper.dispatchNetworkMessagesDisable();
+ }
+
+ getPanelWindow() {
+ return this.window;
+ }
+
+ logWarningAboutReplacedAPI() {
+ return this.hud.currentTarget.logWarningInPage(
+ l10n.getStr("ConsoleAPIDisabled"),
+ "ConsoleAPIDisabled"
+ );
+ }
+
+ /**
+ * Connect to the server using the remote debugging protocol.
+ *
+ * @private
+ * @return object
+ * A promise object that is resolved/reject based on the proxies connections.
+ */
+ async _attachTargets() {
+ const { commands, resourceCommand } = this.hud;
+ this.networkDataProvider = new FirefoxDataProvider({
+ commands,
+ actions: {
+ updateRequest: (id, data) =>
+ this.wrapper.batchedRequestUpdates({ id, data }),
+ },
+ owner: this,
+ });
+
+ // Listen for all target types, including:
+ // - frames, in order to get the parent process target
+ // which is considered as a frame rather than a process.
+ // - workers, for similar reason. When we open a toolbox
+ // for just a worker, the top level target is a worker target.
+ // - processes, as we want to spawn additional proxies for them.
+ await commands.targetCommand.watchTargets({
+ types: this.hud.commands.targetCommand.ALL_TYPES,
+ onAvailable: this._onTargetAvailable,
+ onDestroyed: this._onTargetDestroyed,
+ });
+
+ await resourceCommand.watchResources(
+ [
+ resourceCommand.TYPES.CONSOLE_MESSAGE,
+ resourceCommand.TYPES.ERROR_MESSAGE,
+ resourceCommand.TYPES.PLATFORM_MESSAGE,
+ resourceCommand.TYPES.DOCUMENT_EVENT,
+ resourceCommand.TYPES.LAST_PRIVATE_CONTEXT_EXIT,
+ resourceCommand.TYPES.JSTRACER_TRACE,
+ resourceCommand.TYPES.JSTRACER_STATE,
+ ],
+ { onAvailable: this._onResourceAvailable }
+ );
+
+ if (this.isBrowserConsole || this.isBrowserToolboxConsole) {
+ const shouldEnableNetworkMonitoring = Services.prefs.getBoolPref(
+ PREFS.UI.ENABLE_NETWORK_MONITORING
+ );
+ if (shouldEnableNetworkMonitoring) {
+ await this.startWatchingNetworkResources();
+ } else {
+ await this.stopWatchingNetworkResources();
+ }
+ } else {
+ // We should always watch for network resources in the webconsole
+ await this.startWatchingNetworkResources();
+ }
+ }
+
+ async startWatchingNetworkResources() {
+ const { commands, resourceCommand } = this.hud;
+ await resourceCommand.watchResources(
+ [
+ resourceCommand.TYPES.NETWORK_EVENT,
+ resourceCommand.TYPES.NETWORK_EVENT_STACKTRACE,
+ ],
+ {
+ onAvailable: this._onResourceAvailable,
+ onUpdated: this._onNetworkResourceUpdated,
+ }
+ );
+
+ // When opening a worker toolbox from about:debugging,
+ // we do not instantiate any Watcher actor yet and would throw here.
+ // But even once we do, we wouldn't support network inspection anyway.
+ if (commands.targetCommand.hasTargetWatcherSupport()) {
+ const networkFront = await commands.watcherFront.getNetworkParentActor();
+ // There is no way to view response bodies from the Browser Console, so do
+ // not waste the memory.
+ const saveBodies =
+ !this.isBrowserConsole &&
+ Services.prefs.getBoolPref(
+ "devtools.netmonitor.saveRequestAndResponseBodies"
+ );
+ await networkFront.setSaveRequestAndResponseBodies(saveBodies);
+ }
+ }
+
+ async stopWatchingNetworkResources() {
+ if (this._destroyed) {
+ return;
+ }
+
+ await this.hud.resourceCommand.unwatchResources(
+ [
+ this.hud.resourceCommand.TYPES.NETWORK_EVENT,
+ this.hud.resourceCommand.TYPES.NETWORK_EVENT_STACKTRACE,
+ ],
+ {
+ onAvailable: this._onResourceAvailable,
+ onUpdated: this._onNetworkResourceUpdated,
+ }
+ );
+ }
+
+ handleDocumentEvent(resource) {
+ // Only consider top level document, and ignore remote iframes top document
+ if (!resource.targetFront.isTopLevel) {
+ return;
+ }
+
+ if (resource.name == "will-navigate") {
+ this.handleWillNavigate({
+ timeStamp: resource.time,
+ url: resource.newURI,
+ });
+ } else if (resource.name == "dom-complete") {
+ this.handleNavigated({
+ hasNativeConsoleAPI: resource.hasNativeConsoleAPI,
+ });
+ }
+ // For now, ignore all other DOCUMENT_EVENT's.
+ }
+
+ /**
+ * Handler for when the page is done loading.
+ *
+ * @param Boolean hasNativeConsoleAPI
+ * True if the `console` object is the native one and hasn't been overloaded by a custom
+ * object by the page itself.
+ */
+ async handleNavigated({ hasNativeConsoleAPI }) {
+ // Updates instant evaluation on page navigation
+ this.wrapper.dispatchUpdateInstantEvaluationResultForCurrentExpression();
+
+ // Wait for completion of any async dispatch before notifying that the console
+ // is fully updated after a page reload
+ await this.wrapper.waitAsyncDispatches();
+
+ if (!hasNativeConsoleAPI) {
+ this.logWarningAboutReplacedAPI();
+ }
+
+ this.emit("reloaded");
+ }
+
+ handleWillNavigate({ timeStamp, url }) {
+ this.wrapper.dispatchTabWillNavigate({ timeStamp, url });
+ }
+
+ async watchCssMessages() {
+ const { resourceCommand } = this.hud;
+ await resourceCommand.watchResources([resourceCommand.TYPES.CSS_MESSAGE], {
+ onAvailable: this._onResourceAvailable,
+ });
+ }
+
+ _onResourceAvailable(resources) {
+ if (this._destroyed) {
+ return;
+ }
+
+ const messages = [];
+ for (const resource of resources) {
+ const { TYPES } = this.hud.resourceCommand;
+ if (resource.resourceType === TYPES.DOCUMENT_EVENT) {
+ this.handleDocumentEvent(resource);
+ continue;
+ }
+ if (resource.resourceType == TYPES.LAST_PRIVATE_CONTEXT_EXIT) {
+ // Private messages only need to be removed from the output in Browser Console/Browser Toolbox
+ // (but in theory this resource should only be send from parent process watchers)
+ if (this.isBrowserConsole || this.isBrowserToolboxConsole) {
+ this.clearPrivateMessages();
+ }
+ continue;
+ }
+ // Ignore messages forwarded from content processes if we're in fission browser toolbox.
+ if (
+ !this.wrapper ||
+ ((resource.resourceType === TYPES.ERROR_MESSAGE ||
+ resource.resourceType === TYPES.CSS_MESSAGE) &&
+ resource.pageError?.isForwardedFromContentProcess &&
+ (this.isBrowserToolboxConsole || this.isBrowserConsole))
+ ) {
+ continue;
+ }
+
+ // Don't show messages emitted from a private window before the Browser Console was
+ // opened to avoid leaking data from past usage of the browser (e.g. content message
+ // from now closed private tabs)
+ if (
+ (this.isBrowserToolboxConsole || this.isBrowserConsole) &&
+ resource.isAlreadyExistingResource &&
+ (resource.pageError?.private || resource.message?.private)
+ ) {
+ continue;
+ }
+
+ if (resource.resourceType === TYPES.NETWORK_EVENT_STACKTRACE) {
+ this.networkDataProvider?.onStackTraceAvailable(resource);
+ continue;
+ }
+
+ if (resource.resourceType === TYPES.NETWORK_EVENT) {
+ this.networkDataProvider?.onNetworkResourceAvailable(resource);
+ }
+ messages.push(resource);
+ }
+ this.wrapper.dispatchMessagesAdd(messages);
+ }
+
+ _onNetworkResourceUpdated(updates) {
+ if (this._destroyed) {
+ return;
+ }
+
+ const messageUpdates = [];
+ for (const { resource } of updates) {
+ if (
+ resource.resourceType == this.hud.resourceCommand.TYPES.NETWORK_EVENT
+ ) {
+ this.networkDataProvider?.onNetworkResourceUpdated(resource);
+ messageUpdates.push(resource);
+ }
+ }
+ this.wrapper.dispatchMessagesUpdate(messageUpdates);
+ }
+
+ /**
+ * Called any time a new target is available.
+ * i.e. it was already existing or has just been created.
+ *
+ * @private
+ * @param Front targetFront
+ * The Front of the target that is available.
+ * This Front inherits from TargetMixin and is typically
+ * composed of a WindowGlobalTargetFront or ContentProcessTargetFront.
+ */
+ async _onTargetAvailable({ targetFront }) {
+ // onTargetAvailable is a mandatory argument for watchTargets,
+ // we still define it solely for being able to use onTargetDestroyed.
+ }
+
+ _onTargetDestroyed({ targetFront, isModeSwitching }) {
+ // Don't try to do anything if the WebConsole is being destroyed
+ if (this._destroyed) {
+ return;
+ }
+
+ // We only want to remove messages from a target destroyed when we're switching mode
+ // in the Browser Console/Browser Toolbox Console.
+ // For regular cases, we want to keep the message history (the output will still be
+ // cleared when the top level target navigates, if "Persist Logs" isn't true, via handleWillNavigate)
+ if (isModeSwitching) {
+ this.wrapper.dispatchTargetMessagesRemove(targetFront);
+ }
+ }
+
+ _initUI() {
+ this.document = this.window.document;
+ this.rootElement = this.document.documentElement;
+
+ this.outputNode = this.document.getElementById("app-wrapper");
+
+ const { toolbox } = this.hud;
+
+ // Initialize module loader and load all the WebConsoleWrapper. The entire code-base
+ // doesn't need any extra privileges and runs entirely in content scope.
+ const WebConsoleWrapper = BrowserLoader({
+ baseURI: "resource://devtools/client/webconsole/",
+ window: this.window,
+ }).require("resource://devtools/client/webconsole/webconsole-wrapper.js");
+
+ this.wrapper = new WebConsoleWrapper(
+ this.outputNode,
+ this,
+ toolbox,
+ this.document
+ );
+
+ this._initShortcuts();
+ this._initOutputSyntaxHighlighting();
+
+ if (toolbox) {
+ toolbox.on("webconsole-selected", this._onPanelSelected);
+ toolbox.on("split-console", this._onChangeSplitConsoleState);
+ toolbox.on("select", this._onChangeSplitConsoleState);
+ }
+ }
+
+ _initOutputSyntaxHighlighting() {
+ // Given a DOM node, we syntax highlight identically to how the input field
+ // looks. See https://codemirror.net/demo/runmode.html;
+ const syntaxHighlightNode = node => {
+ const editor = this.jsterm && this.jsterm.editor;
+ if (node && editor) {
+ node.classList.add("cm-s-mozilla");
+ editor.CodeMirror.runMode(
+ node.textContent,
+ "application/javascript",
+ node
+ );
+ }
+ };
+
+ // Use a Custom Element to handle syntax highlighting to avoid
+ // dealing with refs or innerHTML from React.
+ const win = this.window;
+ win.customElements.define(
+ "syntax-highlighted",
+ class extends win.HTMLElement {
+ connectedCallback() {
+ if (!this.connected) {
+ this.connected = true;
+ syntaxHighlightNode(this);
+
+ // Highlight Again when the innerText changes
+ // We remove the listener before running codemirror mode and add
+ // it again to capture text changes
+ this.observer = new win.MutationObserver((mutations, observer) => {
+ observer.disconnect();
+ syntaxHighlightNode(this);
+ observer.observe(this, { childList: true });
+ });
+
+ this.observer.observe(this, { childList: true });
+ }
+ }
+ }
+ );
+ }
+
+ _initNotifications() {
+ if (this.hud.toolbox) {
+ this.wrapper.toggleOriginalVariableMappingEvaluationNotification(
+ !!this.hud.toolbox
+ .getPanel("jsdebugger")
+ ?.shouldShowOriginalVariableMappingWarnings()
+ );
+ this.hud.toolbox.on(
+ "show-original-variable-mapping-warnings",
+ this._onShowConsoleEvaluation
+ );
+ }
+ }
+
+ _initShortcuts() {
+ const shortcuts = new KeyShortcuts({
+ window: this.window,
+ });
+
+ let clearShortcut;
+ if (lazy.AppConstants.platform === "macosx") {
+ const alternativaClearShortcut = l10n.getStr(
+ "webconsole.clear.alternativeKeyOSX"
+ );
+ shortcuts.on(alternativaClearShortcut, event =>
+ this.clearOutput(true, event)
+ );
+ clearShortcut = l10n.getStr("webconsole.clear.keyOSX");
+ } else {
+ clearShortcut = l10n.getStr("webconsole.clear.key");
+ }
+
+ shortcuts.on(clearShortcut, event => this.clearOutput(true, event));
+
+ if (this.isBrowserConsole) {
+ // Make sure keyboard shortcuts work immediately after opening
+ // the Browser Console (Bug 1461366).
+ this.window.focus();
+ shortcuts.on(
+ l10n.getStr("webconsole.close.key"),
+ this.window.close.bind(this.window)
+ );
+
+ ZoomKeys.register(this.window, shortcuts);
+
+ /* This is the same as DevelopmentHelpers.quickRestart, but it runs in all
+ * builds (even official). This allows a user to do a restart + session restore
+ * with Ctrl+Shift+J (open Browser Console) and then Ctrl+Alt+R (restart).
+ */
+ shortcuts.on("CmdOrCtrl+Alt+R", () => {
+ this.hud.commands.targetCommand.reloadTopLevelTarget();
+ });
+ } else if (Services.prefs.getBoolPref(PREF_SIDEBAR_ENABLED)) {
+ shortcuts.on("Esc", event => {
+ this.wrapper.dispatchSidebarClose();
+ if (this.jsterm) {
+ this.jsterm.focus();
+ }
+ });
+ }
+ }
+
+ /**
+ * Sets the focus to JavaScript input field when the web console tab is
+ * selected or when there is a split console present.
+ * @private
+ */
+ _onPanelSelected() {
+ // We can only focus when we have the jsterm reference. This is fine because if the
+ // jsterm is not mounted yet, it will be focused in JSTerm's componentDidMount.
+ if (this.jsterm) {
+ this.jsterm.focus();
+ }
+ }
+
+ _onChangeSplitConsoleState() {
+ this.wrapper.dispatchSplitConsoleCloseButtonToggle();
+ }
+
+ _onScopePrefChanged() {
+ if (this.isBrowserConsole) {
+ this.hud.updateWindowTitle();
+ }
+ }
+
+ _onShowConsoleEvaluation(isOriginalVariableMappingEnabled) {
+ this.wrapper.toggleOriginalVariableMappingEvaluationNotification(
+ isOriginalVariableMappingEnabled
+ );
+ }
+
+ getInputCursor() {
+ return this.jsterm && this.jsterm.getSelectionStart();
+ }
+
+ getJsTermTooltipAnchor() {
+ return this.outputNode.querySelector(".CodeMirror-cursor");
+ }
+
+ attachRef(id, node) {
+ this[id] = node;
+ }
+
+ getSelectedNodeActorID() {
+ const inspectorSelection = this.hud.getInspectorSelection();
+ return inspectorSelection?.nodeFront?.actorID;
+ }
+}
+
+exports.WebConsoleUI = WebConsoleUI;
diff --git a/devtools/client/webconsole/webconsole-wrapper.js b/devtools/client/webconsole/webconsole-wrapper.js
new file mode 100644
index 0000000000..acfc43c3cd
--- /dev/null
+++ b/devtools/client/webconsole/webconsole-wrapper.js
@@ -0,0 +1,486 @@
+/* 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 {
+ createElement,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const ReactDOM = require("resource://devtools/client/shared/vendor/react-dom.js");
+const {
+ Provider,
+ createProvider,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const actions = require("resource://devtools/client/webconsole/actions/index.js");
+const {
+ configureStore,
+} = require("resource://devtools/client/webconsole/store.js");
+
+const {
+ isPacketPrivate,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+const {
+ getMutableMessagesById,
+ getMessage,
+ getAllNetworkMessagesUpdateById,
+} = require("resource://devtools/client/webconsole/selectors/messages.js");
+
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+const App = createFactory(
+ require("resource://devtools/client/webconsole/components/App.js")
+);
+
+loader.lazyGetter(this, "AppErrorBoundary", () =>
+ createFactory(
+ require("resource://devtools/client/shared/components/AppErrorBoundary.js")
+ )
+);
+
+const {
+ setupServiceContainer,
+} = require("resource://devtools/client/webconsole/service-container.js");
+
+loader.lazyRequireGetter(
+ this,
+ "Constants",
+ "resource://devtools/client/webconsole/constants.js"
+);
+
+// Localized strings for (devtools/client/locales/en-US/startup.properties)
+loader.lazyGetter(this, "L10N", function () {
+ const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+ return new LocalizationHelper("devtools/client/locales/startup.properties");
+});
+
+// Only Browser Console needs Fluent bundles at the moment
+loader.lazyRequireGetter(
+ this,
+ "FluentL10n",
+ "resource://devtools/client/shared/fluent-l10n/fluent-l10n.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "LocalizationProvider",
+ "resource://devtools/client/shared/vendor/fluent-react.js",
+ true
+);
+
+let store = null;
+
+class WebConsoleWrapper {
+ /**
+ *
+ * @param {HTMLElement} parentNode
+ * @param {WebConsoleUI} webConsoleUI
+ * @param {Toolbox} toolbox
+ * @param {Document} document
+ *
+ */
+ constructor(parentNode, webConsoleUI, toolbox, document) {
+ EventEmitter.decorate(this);
+
+ this.parentNode = parentNode;
+ this.webConsoleUI = webConsoleUI;
+ this.toolbox = toolbox;
+ this.hud = this.webConsoleUI.hud;
+ this.document = document;
+
+ this.init = this.init.bind(this);
+
+ this.queuedMessageAdds = [];
+ this.queuedMessageUpdates = [];
+ this.queuedRequestUpdates = [];
+ this.throttledDispatchPromise = null;
+
+ this.telemetry = this.hud.telemetry;
+ }
+
+ #serviceContainer;
+
+ async init() {
+ const { webConsoleUI } = this;
+
+ let fluentBundles;
+ if (webConsoleUI.isBrowserConsole) {
+ const fluentL10n = new FluentL10n();
+ await fluentL10n.init(["devtools/client/toolbox.ftl"]);
+ fluentBundles = fluentL10n.getBundles();
+ }
+
+ return new Promise(resolve => {
+ store = configureStore(this.webConsoleUI, {
+ // We may not have access to the toolbox (e.g. in the browser console).
+ telemetry: this.telemetry,
+ thunkArgs: {
+ webConsoleUI,
+ hud: this.hud,
+ toolbox: this.toolbox,
+ commands: this.hud.commands,
+ },
+ });
+
+ const app = AppErrorBoundary(
+ {
+ componentName: "Console",
+ panel: L10N.getStr("ToolboxTabWebconsole.label"),
+ },
+ App({
+ serviceContainer: this.getServiceContainer(),
+ webConsoleUI,
+ onFirstMeaningfulPaint: resolve,
+ closeSplitConsole: this.closeSplitConsole.bind(this),
+ inputEnabled:
+ !webConsoleUI.isBrowserConsole ||
+ Services.prefs.getBoolPref("devtools.chrome.enabled"),
+ })
+ );
+
+ // Render the root Application component.
+ if (this.parentNode) {
+ const maybeLocalizedElement = fluentBundles
+ ? createElement(LocalizationProvider, { bundles: fluentBundles }, app)
+ : app;
+
+ this.body = ReactDOM.render(
+ createElement(
+ Provider,
+ { store },
+ createElement(
+ createProvider(this.hud.commands.targetCommand.storeId),
+ { store: this.hud.commands.targetCommand.store },
+ maybeLocalizedElement
+ )
+ ),
+ this.parentNode
+ );
+ } else {
+ // If there's no parentNode, we are in a test. So we can resolve immediately.
+ resolve();
+ }
+ });
+ }
+
+ destroy() {
+ // This component can be instantiated from jest test, in which case we don't have
+ // a parentNode reference.
+ if (this.parentNode) {
+ ReactDOM.unmountComponentAtNode(this.parentNode);
+ }
+ }
+
+ dispatchMessageAdd(packet) {
+ this.batchedMessagesAdd([packet]);
+ }
+
+ dispatchMessagesAdd(messages) {
+ this.batchedMessagesAdd(messages);
+ }
+
+ dispatchNetworkMessagesDisable() {
+ const networkMessageIds = Object.keys(
+ getAllNetworkMessagesUpdateById(store.getState())
+ );
+ store.dispatch(actions.messagesDisable(networkMessageIds));
+ }
+
+ dispatchMessagesClear() {
+ // We might still have pending message additions and updates when the clear action is
+ // triggered, so we need to flush them to make sure we don't have unexpected behavior
+ // in the ConsoleOutput. *But* we want to keep any pending navigation request,
+ // as we want to keep displaying them even if we received a clear request.
+ function filter(l) {
+ return l.filter(update => update.isNavigationRequest);
+ }
+ this.queuedMessageAdds = filter(this.queuedMessageAdds);
+ this.queuedMessageUpdates = filter(this.queuedMessageUpdates);
+ this.queuedRequestUpdates = this.queuedRequestUpdates.filter(
+ update => update.data.isNavigationRequest
+ );
+
+ store?.dispatch(actions.messagesClear());
+ this.webConsoleUI.emitForTests("messages-cleared");
+ }
+
+ dispatchPrivateMessagesClear() {
+ // We might still have pending private message additions when the private messages
+ // clear action is triggered. We need to remove any private-window-issued packets from
+ // the queue so they won't appear in the output.
+
+ // For (network) message updates, we need to check both messages queue and the state
+ // since we can receive updates even if the message isn't rendered yet.
+ const messages = [...getMutableMessagesById(store.getState()).values()];
+ this.queuedMessageUpdates = this.queuedMessageUpdates.filter(
+ ({ actor }) => {
+ const queuedNetworkMessage = this.queuedMessageAdds.find(
+ p => p.actor === actor
+ );
+ if (queuedNetworkMessage && isPacketPrivate(queuedNetworkMessage)) {
+ return false;
+ }
+
+ const requestMessage = messages.find(
+ message => actor === message.actor
+ );
+ if (requestMessage && requestMessage.private === true) {
+ return false;
+ }
+
+ return true;
+ }
+ );
+
+ // For (network) requests updates, we can check only the state, since there must be a
+ // user interaction to get an update (i.e. the network message is displayed and thus
+ // in the state).
+ this.queuedRequestUpdates = this.queuedRequestUpdates.filter(({ id }) => {
+ const requestMessage = getMessage(store.getState(), id);
+ if (requestMessage && requestMessage.private === true) {
+ return false;
+ }
+
+ return true;
+ });
+
+ // Finally we clear the messages queue. This needs to be done here since we use it to
+ // clean the other queues.
+ this.queuedMessageAdds = this.queuedMessageAdds.filter(
+ p => !isPacketPrivate(p)
+ );
+
+ store.dispatch(actions.privateMessagesClear());
+ }
+
+ dispatchTargetMessagesRemove(targetFront) {
+ // We might still have pending packets in the queues from the target that we need to remove
+ // to prevent messages appearing in the output.
+
+ for (let i = this.queuedMessageUpdates.length - 1; i >= 0; i--) {
+ const packet = this.queuedMessageUpdates[i];
+ if (packet.targetFront == targetFront) {
+ this.queuedMessageUpdates.splice(i, 1);
+ }
+ }
+
+ for (let i = this.queuedRequestUpdates.length - 1; i >= 0; i--) {
+ const packet = this.queuedRequestUpdates[i];
+ if (packet.data.targetFront == targetFront) {
+ this.queuedRequestUpdates.splice(i, 1);
+ }
+ }
+
+ for (let i = this.queuedMessageAdds.length - 1; i >= 0; i--) {
+ const packet = this.queuedMessageAdds[i];
+ // Keep in sync with the check done in the reducer for the TARGET_MESSAGES_REMOVE action.
+ if (
+ packet.targetFront == targetFront &&
+ packet.type !== Constants.MESSAGE_TYPE.COMMAND &&
+ packet.type !== Constants.MESSAGE_TYPE.RESULT
+ ) {
+ this.queuedMessageAdds.splice(i, 1);
+ }
+ }
+
+ store.dispatch(actions.targetMessagesRemove(targetFront));
+ }
+
+ dispatchMessagesUpdate(messages) {
+ this.batchedMessagesUpdates(messages);
+ }
+
+ dispatchSidebarClose() {
+ store.dispatch(actions.sidebarClose());
+ }
+
+ dispatchSplitConsoleCloseButtonToggle() {
+ store.dispatch(
+ actions.splitConsoleCloseButtonToggle(
+ this.toolbox && this.toolbox.currentToolId !== "webconsole"
+ )
+ );
+ }
+
+ dispatchTabWillNavigate(packet) {
+ const { ui } = store.getState();
+
+ // For the browser console, we receive tab navigation
+ // when the original top level window we attached to is closed,
+ // but we don't want to reset console history and just switch to
+ // the next available window.
+ if (ui.persistLogs || this.webConsoleUI.isBrowserConsole) {
+ // Add a type in order for this event packet to be identified by
+ // utils/messages.js's `transformPacket`
+ packet.type = "will-navigate";
+ this.dispatchMessageAdd(packet);
+ } else {
+ this.dispatchMessagesClear();
+ store.dispatch({
+ type: Constants.WILL_NAVIGATE,
+ });
+ }
+ }
+
+ batchedMessagesUpdates(messages) {
+ if (messages.length) {
+ this.queuedMessageUpdates.push(...messages);
+ this.setTimeoutIfNeeded();
+ }
+ }
+
+ batchedRequestUpdates(message) {
+ this.queuedRequestUpdates.push(message);
+ return this.setTimeoutIfNeeded();
+ }
+
+ batchedMessagesAdd(messages) {
+ if (messages.length) {
+ this.queuedMessageAdds.push(...messages);
+ this.setTimeoutIfNeeded();
+ }
+ }
+
+ dispatchClearHistory() {
+ store.dispatch(actions.clearHistory());
+ }
+
+ /**
+ *
+ * @param {String} expression: The expression to evaluate
+ */
+ dispatchEvaluateExpression(expression) {
+ store.dispatch(actions.evaluateExpression(expression));
+ }
+
+ dispatchUpdateInstantEvaluationResultForCurrentExpression() {
+ store.dispatch(actions.updateInstantEvaluationResultForCurrentExpression());
+ }
+
+ /**
+ * Returns a Promise that resolves once any async dispatch is finally dispatched.
+ */
+ waitAsyncDispatches() {
+ if (!this.throttledDispatchPromise) {
+ return Promise.resolve();
+ }
+ // When closing the console during initialization,
+ // setTimeoutIfNeeded may never resolve its promise
+ // as window.setTimeout will be disabled on document destruction.
+ const onUnload = new Promise(r =>
+ window.addEventListener("unload", r, { once: true })
+ );
+ return Promise.race([this.throttledDispatchPromise, onUnload]);
+ }
+
+ setTimeoutIfNeeded() {
+ if (this.throttledDispatchPromise) {
+ return this.throttledDispatchPromise;
+ }
+ this.throttledDispatchPromise = new Promise(done => {
+ setTimeout(async () => {
+ this.throttledDispatchPromise = null;
+
+ if (!store) {
+ // The store is not initialized yet, we can call setTimeoutIfNeeded so the
+ // messages will be handled in the next timeout when the store is ready.
+ this.setTimeoutIfNeeded();
+ done();
+ return;
+ }
+
+ const { ui } = store.getState();
+ store.dispatch(
+ actions.messagesAdd(this.queuedMessageAdds, null, ui.persistLogs)
+ );
+
+ const { length } = this.queuedMessageAdds;
+
+ // This telemetry event is only useful when we have a toolbox so only
+ // send it when we have one.
+ if (this.toolbox) {
+ this.telemetry.addEventProperty(
+ this.toolbox,
+ "enter",
+ "webconsole",
+ null,
+ "message_count",
+ length
+ );
+ }
+
+ this.queuedMessageAdds = [];
+
+ if (this.queuedMessageUpdates.length) {
+ await store.dispatch(
+ actions.networkMessageUpdates(this.queuedMessageUpdates, null)
+ );
+ this.webConsoleUI.emitForTests("network-messages-updated");
+ this.queuedMessageUpdates = [];
+ }
+ if (this.queuedRequestUpdates.length) {
+ await store.dispatch(
+ actions.networkUpdateRequests(this.queuedRequestUpdates)
+ );
+ const updateCount = this.queuedRequestUpdates.length;
+ this.queuedRequestUpdates = [];
+
+ // Fire an event indicating that all data fetched from
+ // the backend has been received. This is based on
+ // 'FirefoxDataProvider.isQueuePayloadReady', see more
+ // comments in that method.
+ // (netmonitor/src/connector/firefox-data-provider).
+ // This event might be utilized in tests to find the right
+ // time when to finish.
+
+ this.webConsoleUI.emitForTests(
+ "network-request-payload-ready",
+ updateCount
+ );
+ }
+ done();
+ }, 50);
+ });
+ return this.throttledDispatchPromise;
+ }
+
+ getStore() {
+ return store;
+ }
+
+ getServiceContainer() {
+ if (!this.#serviceContainer) {
+ this.#serviceContainer = setupServiceContainer({
+ webConsoleUI: this.webConsoleUI,
+ toolbox: this.toolbox,
+ hud: this.hud,
+ webConsoleWrapper: this,
+ });
+ }
+ return this.#serviceContainer;
+ }
+
+ subscribeToStore(callback) {
+ store.subscribe(() => callback(store.getState()));
+ }
+
+ createElement(nodename) {
+ return this.document.createElement(nodename);
+ }
+
+ // Called by pushing close button.
+ closeSplitConsole() {
+ this.toolbox.closeSplitConsole();
+ }
+
+ toggleOriginalVariableMappingEvaluationNotification(show) {
+ store.dispatch(
+ actions.showEvaluationNotification(
+ show ? Constants.ORIGINAL_VARIABLE_MAPPING : ""
+ )
+ );
+ }
+}
+
+// Exports from this module
+module.exports = WebConsoleWrapper;
diff --git a/devtools/client/webconsole/webconsole.js b/devtools/client/webconsole/webconsole.js
new file mode 100644
index 0000000000..7280bf7810
--- /dev/null
+++ b/devtools/client/webconsole/webconsole.js
@@ -0,0 +1,469 @@
+/* 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";
+
+loader.lazyRequireGetter(
+ this,
+ "Utils",
+ "resource://devtools/client/webconsole/utils.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "WebConsoleUI",
+ "resource://devtools/client/webconsole/webconsole-ui.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "gDevTools",
+ "resource://devtools/client/framework/devtools.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "openDocLink",
+ "resource://devtools/client/shared/link.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "DevToolsUtils",
+ "resource://devtools/shared/DevToolsUtils.js"
+);
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+const Telemetry = require("resource://devtools/client/shared/telemetry.js");
+
+var gHudId = 0;
+const isMacOS = Services.appinfo.OS === "Darwin";
+
+/**
+ * A WebConsole instance is an interactive console initialized *per target*
+ * that displays console log data as well as provides an interactive terminal to
+ * manipulate the target's document content.
+ *
+ * This object only wraps the iframe that holds the Web Console UI. This is
+ * meant to be an integration point between the Firefox UI and the Web Console
+ * UI and features.
+ */
+class WebConsole {
+ /*
+ * @constructor
+ * @param object toolbox
+ * The toolbox where the web console is displayed.
+ * @param object commands
+ * The commands object with all interfaces defined from devtools/shared/commands/
+ * @param nsIDOMWindow iframeWindow
+ * The window where the web console UI is already loaded.
+ * @param nsIDOMWindow chromeWindow
+ * The window of the web console owner.
+ * @param bool isBrowserConsole
+ */
+ constructor(
+ toolbox,
+ commands,
+ iframeWindow,
+ chromeWindow,
+ isBrowserConsole = false
+ ) {
+ this.toolbox = toolbox;
+ this.commands = commands;
+ this.iframeWindow = iframeWindow;
+ this.chromeWindow = chromeWindow;
+ this.hudId = "hud_" + ++gHudId;
+ this.browserWindow = DevToolsUtils.getTopWindow(this.chromeWindow);
+ this.isBrowserConsole = isBrowserConsole;
+
+ // On the browser console, where we don't have a toolbox, we instantiate a dedicated Telemetry instance.
+ this.telemetry = toolbox?.telemetry || new Telemetry();
+
+ const element = this.browserWindow.document.documentElement;
+ if (element.getAttribute("windowtype") != gDevTools.chromeWindowType) {
+ this.browserWindow = Services.wm.getMostRecentWindow(
+ gDevTools.chromeWindowType
+ );
+ }
+ this.ui = new WebConsoleUI(this);
+ this._destroyer = null;
+
+ EventEmitter.decorate(this);
+ }
+
+ recordEvent(event, extra = {}) {
+ this.telemetry.recordEvent(event, "webconsole", null, extra);
+ }
+
+ get currentTarget() {
+ return this.commands.targetCommand.targetFront;
+ }
+
+ get resourceCommand() {
+ return this.commands.resourceCommand;
+ }
+
+ /**
+ * Getter for the window that can provide various utilities that the web
+ * console makes use of, like opening links, managing popups, etc. In
+ * most cases, this will be |this.browserWindow|, but in some uses (such as
+ * the Browser Toolbox), there is no browser window, so an alternative window
+ * hosts the utilities there.
+ * @type nsIDOMWindow
+ */
+ get chromeUtilsWindow() {
+ if (this.browserWindow) {
+ return this.browserWindow;
+ }
+ return DevToolsUtils.getTopWindow(this.chromeWindow);
+ }
+
+ get gViewSourceUtils() {
+ return this.chromeUtilsWindow.gViewSourceUtils;
+ }
+
+ getFrontByID(id) {
+ return this.commands.client.getFrontByID(id);
+ }
+
+ /**
+ * Initialize the Web Console instance.
+ *
+ * @param {Boolean} emitCreatedEvent: Defaults to true. If false is passed,
+ * We won't be sending the 'web-console-created' event.
+ *
+ * @return object
+ * A promise for the initialization.
+ */
+ async init(emitCreatedEvent = true) {
+ await this.ui.init();
+
+ // This event needs to be fired later in the case of the BrowserConsole
+ if (emitCreatedEvent) {
+ const id = Utils.supportsString(this.hudId);
+ Services.obs.notifyObservers(id, "web-console-created");
+ }
+ }
+
+ /**
+ * The JSTerm object that manages the console's input.
+ * @see webconsole.js::JSTerm
+ * @type object
+ */
+ get jsterm() {
+ return this.ui ? this.ui.jsterm : null;
+ }
+
+ /**
+ * Get the value from the input field.
+ * @returns {String|null} returns null if there's no input.
+ */
+ getInputValue() {
+ if (!this.jsterm) {
+ return null;
+ }
+
+ return this.jsterm._getValue();
+ }
+
+ inputHasSelection() {
+ const { editor } = this.jsterm || {};
+ return editor && !!editor.getSelection();
+ }
+
+ getInputSelection() {
+ if (!this.jsterm || !this.jsterm.editor) {
+ return null;
+ }
+ return this.jsterm.editor.getSelection();
+ }
+
+ /**
+ * Sets the value of the input field (command line)
+ *
+ * @param {String} newValue: The new value to set.
+ */
+ setInputValue(newValue) {
+ if (!this.jsterm) {
+ return;
+ }
+
+ this.jsterm._setValue(newValue);
+ }
+
+ focusInput() {
+ return this.jsterm && this.jsterm.focus();
+ }
+
+ /**
+ * Open a link in a new tab.
+ *
+ * @param string link
+ * The URL you want to open in a new tab.
+ */
+ openLink(link, e = {}) {
+ openDocLink(link, {
+ relatedToCurrent: true,
+ inBackground: isMacOS ? e.metaKey : e.ctrlKey,
+ });
+ if (e && typeof e.stopPropagation === "function") {
+ e.stopPropagation();
+ }
+ }
+
+ /**
+ * Open a link in Firefox's view source.
+ *
+ * @param string sourceURL
+ * The URL of the file.
+ * @param integer sourceLine
+ * The line number which should be highlighted.
+ */
+ viewSource(sourceURL, sourceLine) {
+ this.gViewSourceUtils.viewSource({
+ URL: sourceURL,
+ lineNumber: sourceLine || -1,
+ });
+ }
+
+ /**
+ * Tries to open a JavaScript file related to the web page for the web console
+ * instance in the Script Debugger. If the file is not found, it is opened in
+ * source view instead.
+ *
+ * Manually handle the case where toolbox does not exist (Browser Console).
+ *
+ * @param string sourceURL
+ * The URL of the file.
+ * @param integer sourceLine
+ * The line number which you want to place the caret.
+ * @param integer sourceColumn
+ * The column number which you want to place the caret.
+ */
+ async viewSourceInDebugger(sourceURL, sourceLine, sourceColumn) {
+ const { toolbox } = this;
+ if (!toolbox) {
+ this.viewSource(sourceURL, sourceLine, sourceColumn);
+ return;
+ }
+
+ await toolbox.viewSourceInDebugger(sourceURL, sourceLine, sourceColumn);
+ this.ui.emitForTests("source-in-debugger-opened");
+ }
+
+ /**
+ * Retrieve information about the JavaScript debugger's currently selected stackframe.
+ * is used to allow the Web Console to evaluate code in the selected stackframe.
+ *
+ * @return {String}
+ * The Frame Actor ID.
+ * If the debugger is not open or if it's not paused, then |null| is
+ * returned.
+ */
+ getSelectedFrameActorID() {
+ const { toolbox } = this;
+ if (!toolbox) {
+ return null;
+ }
+ const panel = toolbox.getPanel("jsdebugger");
+
+ if (!panel) {
+ return null;
+ }
+
+ return panel.getSelectedFrameActorID();
+ }
+
+ /**
+ * Given an expression, returns an object containing a new expression, mapped by the
+ * parser worker to provide additional feature for the user (top-level await,
+ * original languages mapping, …).
+ *
+ * @param {String} expression: The input to maybe map.
+ * @returns {Object|null}
+ * Returns null if the input can't be mapped.
+ * If it can, returns an object containing the following:
+ * - {String} expression: The mapped expression
+ * - {Object} mapped: An object containing the different mapping that could
+ * be done and if they were applied on the input.
+ * At the moment, contains `await`, `bindings` and
+ * `originalExpression`.
+ */
+ getMappedExpression(expression) {
+ const { toolbox } = this;
+
+ // We need to check if the debugger is open, since it may perform a variable name
+ // substitution for sourcemapped script (i.e. evaluated `myVar.trim()` might need to
+ // be transformed into `a.trim()`).
+ const panel = toolbox && toolbox.getPanel("jsdebugger");
+ if (panel) {
+ return panel.getMappedExpression(expression);
+ }
+
+ if (expression.includes("await ")) {
+ const shouldMapBindings = false;
+ const shouldMapAwait = true;
+ const res = this.parserWorker.mapExpression(
+ expression,
+ null,
+ null,
+ shouldMapBindings,
+ shouldMapAwait
+ );
+ return res;
+ }
+
+ return null;
+ }
+
+ getMappedVariables() {
+ const { toolbox } = this;
+ return toolbox?.getPanel("jsdebugger")?.getMappedVariables();
+ }
+
+ get parserWorker() {
+ // If we have a toolbox, we could reuse the parser already instantiated for the debugger.
+ // Note that we won't have a toolbox when running the Browser Console...
+ if (this.toolbox) {
+ return this.toolbox.parserWorker;
+ }
+
+ if (this._parserWorker) {
+ return this._parserWorker;
+ }
+
+ const {
+ ParserDispatcher,
+ } = require("resource://devtools/client/debugger/src/workers/parser/index.js");
+
+ this._parserWorker = new ParserDispatcher();
+ return this._parserWorker;
+ }
+
+ /**
+ * Retrieves the current selection from the Inspector, if such a selection
+ * exists. This is used to pass the ID of the selected actor to the Web
+ * Console server for the $0 helper.
+ *
+ * @return object|null
+ * A Selection referring to the currently selected node in the
+ * Inspector.
+ * If the inspector was never opened, or no node was ever selected,
+ * then |null| is returned.
+ */
+ getInspectorSelection() {
+ const { toolbox } = this;
+ if (!toolbox) {
+ return null;
+ }
+ const panel = toolbox.getPanel("inspector");
+ if (!panel || !panel.selection) {
+ return null;
+ }
+ return panel.selection;
+ }
+
+ async onViewSourceInDebugger({ id, url, line, column }) {
+ if (this.toolbox) {
+ await this.toolbox.viewSourceInDebugger(url, line, column, id);
+
+ this.recordEvent("jump_to_source");
+ this.emitForTests("source-in-debugger-opened");
+ }
+ }
+
+ async onViewSourceInStyleEditor({ url, line, column }) {
+ if (!this.toolbox) {
+ return;
+ }
+ await this.toolbox.viewSourceInStyleEditorByURL(url, line, column);
+ this.recordEvent("jump_to_source");
+ }
+
+ async openNetworkPanel(requestId) {
+ if (!this.toolbox) {
+ return;
+ }
+ const netmonitor = await this.toolbox.selectTool("netmonitor");
+ await netmonitor.panelWin.Netmonitor.inspectRequest(requestId);
+ }
+
+ getHighlighter() {
+ if (!this.toolbox) {
+ return null;
+ }
+
+ if (this._highlighter) {
+ return this._highlighter;
+ }
+
+ this._highlighter = this.toolbox.getHighlighter();
+ return this._highlighter;
+ }
+
+ async resendNetworkRequest(requestId) {
+ if (!this.toolbox) {
+ return;
+ }
+
+ const api = await this.toolbox.getNetMonitorAPI();
+ await api.resendRequest(requestId);
+ }
+
+ async openNodeInInspector(grip) {
+ if (!this.toolbox) {
+ return;
+ }
+
+ const onSelectInspector = this.toolbox.selectTool(
+ "inspector",
+ "inspect_dom"
+ );
+
+ const onNodeFront = this.toolbox.target
+ .getFront("inspector")
+ .then(inspectorFront => inspectorFront.getNodeFrontFromNodeGrip(grip));
+
+ const [nodeFront, inspectorPanel] = await Promise.all([
+ onNodeFront,
+ onSelectInspector,
+ ]);
+
+ const onInspectorUpdated = inspectorPanel.once("inspector-updated");
+ const onNodeFrontSet = this.toolbox.selection.setNodeFront(nodeFront, {
+ reason: "console",
+ });
+
+ await Promise.all([onNodeFrontSet, onInspectorUpdated]);
+ }
+
+ /**
+ * Destroy the object. Call this method to avoid memory leaks when the Web
+ * Console is closed.
+ *
+ * @return object
+ * A promise object that is resolved once the Web Console is closed.
+ */
+ destroy() {
+ if (!this.hudId) {
+ return;
+ }
+
+ if (this.ui) {
+ this.ui.destroy();
+ }
+
+ if (this._parserWorker) {
+ this._parserWorker.stop();
+ this._parserWorker = null;
+ }
+
+ const id = Utils.supportsString(this.hudId);
+ Services.obs.notifyObservers(id, "web-console-destroyed");
+ this.hudId = null;
+
+ this.emit("destroyed");
+ }
+}
+
+module.exports = WebConsole;