summaryrefslogtreecommitdiffstats
path: root/devtools/client/webconsole
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /devtools/client/webconsole
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'devtools/client/webconsole')
-rw-r--r--devtools/client/webconsole/.babelrc3
-rw-r--r--devtools/client/webconsole/console-commands.js103
-rw-r--r--devtools/client/webconsole/console-output.js3638
-rw-r--r--devtools/client/webconsole/hudservice.js718
-rw-r--r--devtools/client/webconsole/jsterm.js1766
-rw-r--r--devtools/client/webconsole/moz.build22
-rw-r--r--devtools/client/webconsole/net/.eslintrc.js20
-rw-r--r--devtools/client/webconsole/net/components/cookies-tab.js75
-rw-r--r--devtools/client/webconsole/net/components/headers-tab.js79
-rw-r--r--devtools/client/webconsole/net/components/moz.build25
-rw-r--r--devtools/client/webconsole/net/components/net-info-body.css112
-rw-r--r--devtools/client/webconsole/net/components/net-info-body.js179
-rw-r--r--devtools/client/webconsole/net/components/net-info-group-list.js47
-rw-r--r--devtools/client/webconsole/net/components/net-info-group.css80
-rw-r--r--devtools/client/webconsole/net/components/net-info-group.js80
-rw-r--r--devtools/client/webconsole/net/components/net-info-params.css23
-rw-r--r--devtools/client/webconsole/net/components/net-info-params.js58
-rw-r--r--devtools/client/webconsole/net/components/params-tab.js41
-rw-r--r--devtools/client/webconsole/net/components/post-tab.js279
-rw-r--r--devtools/client/webconsole/net/components/response-tab.css21
-rw-r--r--devtools/client/webconsole/net/components/response-tab.js277
-rw-r--r--devtools/client/webconsole/net/components/size-limit.css15
-rw-r--r--devtools/client/webconsole/net/components/size-limit.js62
-rw-r--r--devtools/client/webconsole/net/components/spinner.js26
-rw-r--r--devtools/client/webconsole/net/components/stacktrace-tab.js29
-rw-r--r--devtools/client/webconsole/net/data-provider.js66
-rw-r--r--devtools/client/webconsole/net/main.js98
-rw-r--r--devtools/client/webconsole/net/moz.build19
-rw-r--r--devtools/client/webconsole/net/net-request.css35
-rw-r--r--devtools/client/webconsole/net/net-request.js323
-rw-r--r--devtools/client/webconsole/net/test/mochitest/.eslintrc.js6
-rw-r--r--devtools/client/webconsole/net/test/mochitest/browser.ini22
-rw-r--r--devtools/client/webconsole/net/test/mochitest/browser_net_basic.js33
-rw-r--r--devtools/client/webconsole/net/test/mochitest/browser_net_cookies.js54
-rw-r--r--devtools/client/webconsole/net/test/mochitest/browser_net_headers.js40
-rw-r--r--devtools/client/webconsole/net/test/mochitest/browser_net_params.js69
-rw-r--r--devtools/client/webconsole/net/test/mochitest/browser_net_post.js88
-rw-r--r--devtools/client/webconsole/net/test/mochitest/browser_net_response.js86
-rw-r--r--devtools/client/webconsole/net/test/mochitest/head.js209
-rw-r--r--devtools/client/webconsole/net/test/mochitest/page_basic.html14
-rw-r--r--devtools/client/webconsole/net/test/mochitest/test-cookies.json1
-rw-r--r--devtools/client/webconsole/net/test/mochitest/test-cookies.json^headers^2
-rw-r--r--devtools/client/webconsole/net/test/mochitest/test.json1
-rw-r--r--devtools/client/webconsole/net/test/mochitest/test.json^headers^1
-rw-r--r--devtools/client/webconsole/net/test/mochitest/test.txt1
-rw-r--r--devtools/client/webconsole/net/test/mochitest/test.xml1
-rw-r--r--devtools/client/webconsole/net/test/mochitest/test.xml^headers^1
-rw-r--r--devtools/client/webconsole/net/test/unit/.eslintrc.js6
-rw-r--r--devtools/client/webconsole/net/test/unit/test_json-utils.js45
-rw-r--r--devtools/client/webconsole/net/test/unit/test_net-utils.js77
-rw-r--r--devtools/client/webconsole/net/test/unit/xpcshell.ini9
-rw-r--r--devtools/client/webconsole/net/utils/events.js21
-rw-r--r--devtools/client/webconsole/net/utils/json.js234
-rw-r--r--devtools/client/webconsole/net/utils/moz.build11
-rw-r--r--devtools/client/webconsole/net/utils/net.js134
-rw-r--r--devtools/client/webconsole/new-console-output/actions/enhancers.js20
-rw-r--r--devtools/client/webconsole/new-console-output/actions/filters.js55
-rw-r--r--devtools/client/webconsole/new-console-output/actions/index.js18
-rw-r--r--devtools/client/webconsole/new-console-output/actions/messages.js100
-rw-r--r--devtools/client/webconsole/new-console-output/actions/moz.build12
-rw-r--r--devtools/client/webconsole/new-console-output/actions/ui.js27
-rw-r--r--devtools/client/webconsole/new-console-output/components/collapse-button.js50
-rw-r--r--devtools/client/webconsole/new-console-output/components/console-output.js125
-rw-r--r--devtools/client/webconsole/new-console-output/components/console-table.js202
-rw-r--r--devtools/client/webconsole/new-console-output/components/filter-bar.js170
-rw-r--r--devtools/client/webconsole/new-console-output/components/filter-button.js46
-rw-r--r--devtools/client/webconsole/new-console-output/components/grip-message-body.js102
-rw-r--r--devtools/client/webconsole/new-console-output/components/message-container.js92
-rw-r--r--devtools/client/webconsole/new-console-output/components/message-icon.js32
-rw-r--r--devtools/client/webconsole/new-console-output/components/message-indent.js37
-rw-r--r--devtools/client/webconsole/new-console-output/components/message-repeat.js36
-rw-r--r--devtools/client/webconsole/new-console-output/components/message-types/console-api-call.js132
-rw-r--r--devtools/client/webconsole/new-console-output/components/message-types/console-command.js57
-rw-r--r--devtools/client/webconsole/new-console-output/components/message-types/default-renderer.js22
-rw-r--r--devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js64
-rw-r--r--devtools/client/webconsole/new-console-output/components/message-types/moz.build13
-rw-r--r--devtools/client/webconsole/new-console-output/components/message-types/network-event-message.js63
-rw-r--r--devtools/client/webconsole/new-console-output/components/message-types/page-error.js69
-rw-r--r--devtools/client/webconsole/new-console-output/components/message.js176
-rw-r--r--devtools/client/webconsole/new-console-output/components/moz.build23
-rw-r--r--devtools/client/webconsole/new-console-output/components/variables-view-link.js34
-rw-r--r--devtools/client/webconsole/new-console-output/constants.js81
-rw-r--r--devtools/client/webconsole/new-console-output/main.js23
-rw-r--r--devtools/client/webconsole/new-console-output/moz.build21
-rw-r--r--devtools/client/webconsole/new-console-output/new-console-output-wrapper.js134
-rw-r--r--devtools/client/webconsole/new-console-output/reducers/filters.js39
-rw-r--r--devtools/client/webconsole/new-console-output/reducers/index.js18
-rw-r--r--devtools/client/webconsole/new-console-output/reducers/messages.js135
-rw-r--r--devtools/client/webconsole/new-console-output/reducers/moz.build12
-rw-r--r--devtools/client/webconsole/new-console-output/reducers/prefs.js18
-rw-r--r--devtools/client/webconsole/new-console-output/reducers/ui.js39
-rw-r--r--devtools/client/webconsole/new-console-output/selectors/filters.js12
-rw-r--r--devtools/client/webconsole/new-console-output/selectors/messages.js168
-rw-r--r--devtools/client/webconsole/new-console-output/selectors/moz.build11
-rw-r--r--devtools/client/webconsole/new-console-output/selectors/prefs.js12
-rw-r--r--devtools/client/webconsole/new-console-output/selectors/ui.js20
-rw-r--r--devtools/client/webconsole/new-console-output/store.js74
-rw-r--r--devtools/client/webconsole/new-console-output/test/.eslintrc.js5
-rw-r--r--devtools/client/webconsole/new-console-output/test/chrome/chrome.ini7
-rw-r--r--devtools/client/webconsole/new-console-output/test/chrome/head.js16
-rw-r--r--devtools/client/webconsole/new-console-output/test/chrome/test_render_perf.html90
-rw-r--r--devtools/client/webconsole/new-console-output/test/components/console-api-call.test.js230
-rw-r--r--devtools/client/webconsole/new-console-output/test/components/evaluation-result.test.js84
-rw-r--r--devtools/client/webconsole/new-console-output/test/components/filter-bar.test.js96
-rw-r--r--devtools/client/webconsole/new-console-output/test/components/filter-button.test.js34
-rw-r--r--devtools/client/webconsole/new-console-output/test/components/message-container.test.js54
-rw-r--r--devtools/client/webconsole/new-console-output/test/components/message-icon.test.js23
-rw-r--r--devtools/client/webconsole/new-console-output/test/components/message-repeat.test.js25
-rw-r--r--devtools/client/webconsole/new-console-output/test/components/network-event-message.test.js74
-rw-r--r--devtools/client/webconsole/new-console-output/test/components/page-error.test.js126
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/L10n.js27
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/LocalizationHelper.js10
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/ObjectClient.js9
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/PluralForm.js18
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/Services.js27
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/WebConsoleUtils.js14
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/moz.build9
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer.js17
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser.ini18
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_console_api.js56
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_evaluation_result.js32
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_network_event.js47
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_page_error.js48
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/head.js192
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/moz.build8
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js148
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html11
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-network-event.html11
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js0
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stubs/consoleApi.js1482
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stubs/evaluationResult.js182
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stubs/index.js29
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stubs/moz.build11
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stubs/networkEvent.js189
-rw-r--r--devtools/client/webconsole/new-console-output/test/fixtures/stubs/pageError.js102
-rw-r--r--devtools/client/webconsole/new-console-output/test/helpers.js67
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/browser.ini21
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_batching.js51
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_group.js91
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_table.js173
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_filters.js72
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_init.js35
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_input_focus.js57
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_keyboard_accessibility.js71
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_observer_notifications.js47
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_vview_close_on_esc_key.js46
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/head.js137
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/test-batching.html28
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/test-console-filters.html17
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/test-console-group.html28
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/test-console-table.html19
-rw-r--r--devtools/client/webconsole/new-console-output/test/mochitest/test-console.html18
-rw-r--r--devtools/client/webconsole/new-console-output/test/moz.build17
-rw-r--r--devtools/client/webconsole/new-console-output/test/requireHelper.js38
-rw-r--r--devtools/client/webconsole/new-console-output/test/store/filters.test.js215
-rw-r--r--devtools/client/webconsole/new-console-output/test/store/messages.test.js353
-rw-r--r--devtools/client/webconsole/new-console-output/test/utils/getRepeatId.test.js41
-rw-r--r--devtools/client/webconsole/new-console-output/types.js53
-rw-r--r--devtools/client/webconsole/new-console-output/utils/id-generator.js22
-rw-r--r--devtools/client/webconsole/new-console-output/utils/messages.js283
-rw-r--r--devtools/client/webconsole/new-console-output/utils/moz.build10
-rw-r--r--devtools/client/webconsole/new-console-output/utils/variables-view.js20
-rw-r--r--devtools/client/webconsole/package.json20
-rw-r--r--devtools/client/webconsole/panel.js118
-rw-r--r--devtools/client/webconsole/test/.eslintrc.js6
-rw-r--r--devtools/client/webconsole/test/browser.ini396
-rw-r--r--devtools/client/webconsole/test/browser_bug1045902_console_csp_ignore_reflected_xss_message.js52
-rw-r--r--devtools/client/webconsole/test/browser_bug664688_sandbox_update_after_navigation.js92
-rw-r--r--devtools/client/webconsole/test/browser_bug_638949_copy_link_location.js107
-rw-r--r--devtools/client/webconsole/test/browser_bug_862916_console_dir_and_filter_off.js31
-rw-r--r--devtools/client/webconsole/test/browser_bug_865288_repeat_different_objects.js63
-rw-r--r--devtools/client/webconsole/test/browser_bug_865871_variables_view_close_on_esc_key.js75
-rw-r--r--devtools/client/webconsole/test/browser_bug_869003_inspect_cross_domain_object.js77
-rw-r--r--devtools/client/webconsole/test/browser_bug_871156_ctrlw_close_tab.js78
-rw-r--r--devtools/client/webconsole/test/browser_cached_messages.js59
-rw-r--r--devtools/client/webconsole/test/browser_console.js160
-rw-r--r--devtools/client/webconsole/test/browser_console_addonsdk_loader_exception.js92
-rw-r--r--devtools/client/webconsole/test/browser_console_clear_method.js41
-rw-r--r--devtools/client/webconsole/test/browser_console_clear_on_reload.js86
-rw-r--r--devtools/client/webconsole/test/browser_console_click_focus.js59
-rw-r--r--devtools/client/webconsole/test/browser_console_consolejsm_output.js285
-rw-r--r--devtools/client/webconsole/test/browser_console_copy_command.js76
-rw-r--r--devtools/client/webconsole/test/browser_console_copy_entire_message_context_menu.js97
-rw-r--r--devtools/client/webconsole/test/browser_console_dead_objects.js88
-rw-r--r--devtools/client/webconsole/test/browser_console_error_source_click.js79
-rw-r--r--devtools/client/webconsole/test/browser_console_filters.js60
-rw-r--r--devtools/client/webconsole/test/browser_console_hide_jsterm_when_devtools_chrome_enabled_false.js114
-rw-r--r--devtools/client/webconsole/test/browser_console_history_persist.js119
-rw-r--r--devtools/client/webconsole/test/browser_console_iframe_messages.js114
-rw-r--r--devtools/client/webconsole/test/browser_console_keyboard_accessibility.js89
-rw-r--r--devtools/client/webconsole/test/browser_console_log_inspectable_object.js52
-rw-r--r--devtools/client/webconsole/test/browser_console_native_getters.js101
-rw-r--r--devtools/client/webconsole/test/browser_console_navigation_marker.js81
-rw-r--r--devtools/client/webconsole/test/browser_console_netlogging.js38
-rw-r--r--devtools/client/webconsole/test/browser_console_nsiconsolemessage.js85
-rw-r--r--devtools/client/webconsole/test/browser_console_open_or_focus.js46
-rw-r--r--devtools/client/webconsole/test/browser_console_optimized_out_vars.js91
-rw-r--r--devtools/client/webconsole/test/browser_console_private_browsing.js192
-rw-r--r--devtools/client/webconsole/test/browser_console_server_logging.js74
-rw-r--r--devtools/client/webconsole/test/browser_console_variables_view.js204
-rw-r--r--devtools/client/webconsole/test/browser_console_variables_view_dom_nodes.js59
-rw-r--r--devtools/client/webconsole/test/browser_console_variables_view_dont_sort_non_sortable_classes_properties.js135
-rw-r--r--devtools/client/webconsole/test/browser_console_variables_view_filter.js80
-rw-r--r--devtools/client/webconsole/test/browser_console_variables_view_highlighter.js97
-rw-r--r--devtools/client/webconsole/test/browser_console_variables_view_special_names.js38
-rw-r--r--devtools/client/webconsole/test/browser_console_variables_view_while_debugging.js109
-rw-r--r--devtools/client/webconsole/test/browser_console_variables_view_while_debugging_and_inspecting.js112
-rw-r--r--devtools/client/webconsole/test/browser_eval_in_debugger_stackframe.js157
-rw-r--r--devtools/client/webconsole/test/browser_eval_in_debugger_stackframe2.js71
-rw-r--r--devtools/client/webconsole/test/browser_jsterm_inspect.js47
-rw-r--r--devtools/client/webconsole/test/browser_longstring_hang.js57
-rw-r--r--devtools/client/webconsole/test/browser_netmonitor_shows_reqs_in_webconsole.js74
-rw-r--r--devtools/client/webconsole/test/browser_output_breaks_after_console_dir_uninspectable.js47
-rw-r--r--devtools/client/webconsole/test/browser_output_longstring_expand.js85
-rw-r--r--devtools/client/webconsole/test/browser_repeated_messages_accuracy.js178
-rw-r--r--devtools/client/webconsole/test/browser_result_format_as_string.js40
-rw-r--r--devtools/client/webconsole/test/browser_warn_user_about_replaced_api.js86
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_allow_mixedcontent_securityerrors.js69
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_assert.js56
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_autocomplete-properties-with-non-alphanumeric-names.js47
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_autocomplete_accessibility.js60
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_autocomplete_and_selfxss.js130
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_autocomplete_crossdomain_iframe.js64
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_autocomplete_in_debugger_stackframe.js245
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_autocomplete_popup_close_on_tab_switch.js27
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_block_mixedcontent_securityerrors.js110
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_1006027_message_timestamps_incorrect.js45
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_1010953_cspro.js55
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_1050691_click_function_to_source.js60
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_1247459_violation.js40
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_578437_page_reload.js41
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_579412_input_focus.js20
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_580001_closing_after_completion.js47
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_580030_errors_after_page_reload.js50
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_580454_timestamp_l10n.js26
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_582201_duplicate_errors.js49
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_583816_No_input_and_Tab_key_pressed.js35
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_585237_line_limit.js89
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_585956_console_trace.js70
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js367
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_585991_autocomplete_popup.js123
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_586388_select_all.js84
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_587617_output_copy.js106
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_588342_document_focus.js36
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_588730_text_node_insertion.js53
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_588967_input_expansion.js44
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_589162_css_filter.js39
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_592442_closing_brackets.js29
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_593003_iframe_wrong_hud.js68
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_594497_history_arrow_keys.js155
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_595223_file_uri.js64
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js100
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_595934_message_categories.js211
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js97
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_597136_external_script_errors.js33
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_597136_network_requests_from_chrome.js52
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_597460_filter_scroll.js80
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_597756_reopen_closed_tab.js70
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_599725_response_headers.js67
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_600183_charset.js59
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_601177_log_levels.js76
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_601352_scroll.js84
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_601667_filter_buttons.js267
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_603750_websocket.js37
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_611795.js67
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_613013_console_api_iframe.js26
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_613280_jsterm_copy.js64
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_613642_maintain_scroll.js119
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_613642_prune_scroll.js82
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_614793_jsterm_scroll.js54
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_618078_network_exceptions.js36
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_621644_jsterm_dollar.js47
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_622303_persistent_filters.js149
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_623749_ctrl_a_select_all_winnt.js32
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js120
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js47
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js84
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_632817.js217
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_642108_pruneTest.js81
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_644419_log_limits.js235
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_646025_console_file_location.js57
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js102
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js109
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_658368_time_methods.js67
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_659907_console_dir.js36
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_660806_history_nav.js54
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_664131_console_group.js79
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_686937_autocomplete_JSTerm_helpers.js75
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_704295.js41
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_734061_No_input_change_and_Tab_key_pressed.js35
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_737873_mixedcontent.js63
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_752559_ineffective_iframe_sandbox_warning.js83
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_about_blank_web_console_warning.js32
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_web_console_warning.js62
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_764572_output_open_url.js142
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_766001_JS_Console_in_Debugger.js88
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_770099_violation.js35
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js140
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_804845_ctrl_key_nav.js227
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_817834_add_edited_input_to_history.js57
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_837351_securityerrors.js42
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_bug_922212_console_dirxml.js48
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_cached_autocomplete.js114
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_cd_iframe.js115
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_certificate_messages.js81
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_chrome.js38
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_clear_method.js131
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_clickable_urls.js103
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_closure_inspection.js100
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_column_numbers.js46
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_completion.js106
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_console_api_stackframe.js85
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_console_custom_styles.js81
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_console_extras.js43
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_console_logging_api.js102
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_console_logging_workers_api.js39
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_console_trace_async.js75
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_console_trace_duplicates.js50
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_context_menu_open_in_var_view.js51
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_context_menu_store_as_global.js66
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_count.js77
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_dont_navigate_on_doubleclick.js56
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_exception_stackframe.js104
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_execution_scope.js37
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_expandable_timestamps.js57
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_filter_buttons_contextmenu.js95
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_for_of.js32
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_history.js62
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_hpkp_invalid-headers.js126
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_hsts_invalid-headers.js92
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_input_field_focus_on_panel_select.js34
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_inspect-parsed-documents.js35
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_js_input_expansion.js55
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_jsterm.js195
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_live_filtering_of_message_types.js56
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_live_filtering_on_search_strings.js96
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_log_file_filter.js83
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_message_node_id.js28
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_multiline_input.js70
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_netlogging.js139
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_netlogging_basic.js44
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_netlogging_panel.js30
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_netlogging_reset_filter.js95
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_notifications.js77
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_open-links-without-callback.js54
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_01.js122
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_02.js183
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_03.js168
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_04.js129
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_05.js177
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_06.js283
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_copy_newlines.js72
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_dom_elements_01.js122
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_dom_elements_02.js66
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_dom_elements_03.js70
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_dom_elements_04.js113
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_dom_elements_05.js47
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_events.js54
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_order.js47
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_regexp.js35
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_output_table.js199
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_promise.js35
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_property_provider.js46
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_reflow.js33
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_scratchpad_panel_link.js76
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_script_errordoc_urls.js67
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_show_subresource_security_errors.js39
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_shows_reqs_in_netmonitor.js73
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_split.js268
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_split_escape_key.js158
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_split_focus.js66
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_split_persist.js119
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_start_netmon_first.js38
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_strict_mode_errors.js83
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_trackingprotection_errors.js54
-rw-r--r--devtools/client/webconsole/test/browser_webconsole_view_source.js52
-rw-r--r--devtools/client/webconsole/test/head.js1844
-rw-r--r--devtools/client/webconsole/test/test-autocomplete-in-stackframe.html50
-rw-r--r--devtools/client/webconsole/test/test-bug-585956-console-trace.html27
-rw-r--r--devtools/client/webconsole/test/test-bug-593003-iframe-wrong-hud-iframe.html13
-rw-r--r--devtools/client/webconsole/test/test-bug-593003-iframe-wrong-hud.html14
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-canvas-css.html17
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-canvas-css.js10
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-css-loader.css10
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-css-loader.css^headers^1
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-css-loader.html13
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-css-parser.css10
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-css-parser.html14
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-empty-getelementbyid.html16
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-empty-getelementbyid.js8
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-html.html16
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-image.html15
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-image.jpgbin0 -> 2532 bytes
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-imagemap.html17
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-malformedxml-external.html19
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-malformedxml-external.xml8
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-malformedxml.xhtml10
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-svg.xhtml17
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-workers.html18
-rw-r--r--devtools/client/webconsole/test/test-bug-595934-workers.js14
-rw-r--r--devtools/client/webconsole/test/test-bug-597136-external-script-errors.html25
-rw-r--r--devtools/client/webconsole/test/test-bug-597136-external-script-errors.js9
-rw-r--r--devtools/client/webconsole/test/test-bug-597756-reopen-closed-tab.html18
-rw-r--r--devtools/client/webconsole/test/test-bug-599725-response-headers.sjs25
-rw-r--r--devtools/client/webconsole/test/test-bug-600183-charset.html9
-rw-r--r--devtools/client/webconsole/test/test-bug-600183-charset.html^headers^1
-rw-r--r--devtools/client/webconsole/test/test-bug-601177-log-levels.html20
-rw-r--r--devtools/client/webconsole/test/test-bug-601177-log-levels.js8
-rw-r--r--devtools/client/webconsole/test/test-bug-603750-websocket.html14
-rw-r--r--devtools/client/webconsole/test/test-bug-603750-websocket.js20
-rw-r--r--devtools/client/webconsole/test/test-bug-609872-cd-iframe-child.html13
-rw-r--r--devtools/client/webconsole/test/test-bug-609872-cd-iframe-parent.html14
-rw-r--r--devtools/client/webconsole/test/test-bug-613013-console-api-iframe.html21
-rw-r--r--devtools/client/webconsole/test/test-bug-618078-network-exceptions.html24
-rw-r--r--devtools/client/webconsole/test/test-bug-621644-jsterm-dollar.html23
-rw-r--r--devtools/client/webconsole/test/test-bug-630733-response-redirect-headers.sjs16
-rw-r--r--devtools/client/webconsole/test/test-bug-632275-getters.html20
-rw-r--r--devtools/client/webconsole/test/test-bug-632347-iterators-generators.html56
-rw-r--r--devtools/client/webconsole/test/test-bug-644419-log-limits.html21
-rw-r--r--devtools/client/webconsole/test/test-bug-646025-console-file-location.html12
-rw-r--r--devtools/client/webconsole/test/test-bug-658368-time-methods.html24
-rw-r--r--devtools/client/webconsole/test/test-bug-737873-mixedcontent.html15
-rw-r--r--devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-inner.html13
-rw-r--r--devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested1.html14
-rw-r--r--devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested2.html14
-rw-r--r--devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning0.html13
-rw-r--r--devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning1.html13
-rw-r--r--devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning2.html13
-rw-r--r--devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning3.html14
-rw-r--r--devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning4.html14
-rw-r--r--devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning5.html14
-rw-r--r--devtools/client/webconsole/test/test-bug-762593-insecure-passwords-about-blank-web-console-warning.html28
-rw-r--r--devtools/client/webconsole/test/test-bug-762593-insecure-passwords-web-console-warning.html16
-rw-r--r--devtools/client/webconsole/test/test-bug-766001-console-log.js10
-rw-r--r--devtools/client/webconsole/test/test-bug-766001-js-console-links.html14
-rw-r--r--devtools/client/webconsole/test/test-bug-766001-js-errors.js8
-rw-r--r--devtools/client/webconsole/test/test-bug-782653-css-errors-1.css10
-rw-r--r--devtools/client/webconsole/test/test-bug-782653-css-errors-2.css10
-rw-r--r--devtools/client/webconsole/test/test-bug-782653-css-errors.html14
-rw-r--r--devtools/client/webconsole/test/test-bug-837351-security-errors.html15
-rw-r--r--devtools/client/webconsole/test/test-bug-859170-longstring-hang.html23
-rw-r--r--devtools/client/webconsole/test/test-bug-869003-iframe.html20
-rw-r--r--devtools/client/webconsole/test/test-bug-869003-top-window.html14
-rw-r--r--devtools/client/webconsole/test/test-bug-952277-highlight-nodes-in-vview.html15
-rw-r--r--devtools/client/webconsole/test/test-bug-989025-iframe-parent.html13
-rw-r--r--devtools/client/webconsole/test/test-bug_1050691_click_function_to_source.html11
-rw-r--r--devtools/client/webconsole/test/test-bug_1050691_click_function_to_source.js10
-rw-r--r--devtools/client/webconsole/test/test-bug_923281_console_log_filter.html12
-rw-r--r--devtools/client/webconsole/test/test-bug_923281_test1.js7
-rw-r--r--devtools/client/webconsole/test/test-bug_923281_test2.js8
-rw-r--r--devtools/client/webconsole/test/test-bug_939783_console_trace_duplicates.html35
-rw-r--r--devtools/client/webconsole/test/test-certificate-messages.html22
-rw-r--r--devtools/client/webconsole/test/test-closure-optimized-out.html34
-rw-r--r--devtools/client/webconsole/test/test-closures.html26
-rw-r--r--devtools/client/webconsole/test/test-console-api-stackframe.html32
-rw-r--r--devtools/client/webconsole/test/test-console-assert.html23
-rw-r--r--devtools/client/webconsole/test/test-console-clear.html16
-rw-r--r--devtools/client/webconsole/test/test-console-column.html17
-rw-r--r--devtools/client/webconsole/test/test-console-count-external-file.js11
-rw-r--r--devtools/client/webconsole/test/test-console-count.html56
-rw-r--r--devtools/client/webconsole/test/test-console-extras.html18
-rw-r--r--devtools/client/webconsole/test/test-console-output-02.html66
-rw-r--r--devtools/client/webconsole/test/test-console-output-03.html30
-rw-r--r--devtools/client/webconsole/test/test-console-output-04.html77
-rw-r--r--devtools/client/webconsole/test/test-console-output-dom-elements.html91
-rw-r--r--devtools/client/webconsole/test/test-console-output-events.html42
-rw-r--r--devtools/client/webconsole/test/test-console-output-regexp.html23
-rw-r--r--devtools/client/webconsole/test/test-console-replaced-api.html12
-rw-r--r--devtools/client/webconsole/test/test-console-server-logging-array.sjs32
-rw-r--r--devtools/client/webconsole/test/test-console-server-logging.sjs32
-rw-r--r--devtools/client/webconsole/test/test-console-table.html63
-rw-r--r--devtools/client/webconsole/test/test-console-trace-async.html24
-rw-r--r--devtools/client/webconsole/test/test-console-workers.html13
-rw-r--r--devtools/client/webconsole/test/test-console.html34
-rw-r--r--devtools/client/webconsole/test/test-consoleiframes.html13
-rw-r--r--devtools/client/webconsole/test/test-cu-reporterror.js4
-rw-r--r--devtools/client/webconsole/test/test-data.json1
-rw-r--r--devtools/client/webconsole/test/test-data.json^headers^1
-rw-r--r--devtools/client/webconsole/test/test-duplicate-error.html21
-rw-r--r--devtools/client/webconsole/test/test-encoding-ISO-8859-1.html7
-rw-r--r--devtools/client/webconsole/test/test-error.html21
-rw-r--r--devtools/client/webconsole/test/test-eval-in-stackframe.html39
-rw-r--r--devtools/client/webconsole/test/test-exception-stackframe.html43
-rw-r--r--devtools/client/webconsole/test/test-file-location.js12
-rw-r--r--devtools/client/webconsole/test/test-filter.html11
-rw-r--r--devtools/client/webconsole/test/test-for-of.html8
-rw-r--r--devtools/client/webconsole/test/test-iframe-762593-insecure-form-action.html15
-rw-r--r--devtools/client/webconsole/test/test-iframe-762593-insecure-frame.html15
-rw-r--r--devtools/client/webconsole/test/test-iframe1.html10
-rw-r--r--devtools/client/webconsole/test/test-iframe2.html11
-rw-r--r--devtools/client/webconsole/test/test-iframe3.html11
-rw-r--r--devtools/client/webconsole/test/test-image.pngbin0 -> 580 bytes
-rw-r--r--devtools/client/webconsole/test/test-mixedcontent-securityerrors.html21
-rw-r--r--devtools/client/webconsole/test/test-mutation.html16
-rw-r--r--devtools/client/webconsole/test/test-network-request.html40
-rw-r--r--devtools/client/webconsole/test/test-network.html11
-rw-r--r--devtools/client/webconsole/test/test-observe-http-ajax.html17
-rw-r--r--devtools/client/webconsole/test/test-own-console.html24
-rw-r--r--devtools/client/webconsole/test/test-property-provider.html14
-rw-r--r--devtools/client/webconsole/test/test-repeated-messages.html53
-rw-r--r--devtools/client/webconsole/test/test-result-format-as-string.html25
-rw-r--r--devtools/client/webconsole/test/test-trackingprotection-securityerrors.html12
-rw-r--r--devtools/client/webconsole/test/test-webconsole-error-observer.html25
-rw-r--r--devtools/client/webconsole/test/test_bug1045902_console_csp_ignore_reflected_xss_message.html10
-rw-r--r--devtools/client/webconsole/test/test_bug1045902_console_csp_ignore_reflected_xss_message.html^headers^1
-rw-r--r--devtools/client/webconsole/test/test_bug1092055_shouldwarn.html15
-rw-r--r--devtools/client/webconsole/test/test_bug1092055_shouldwarn.js2
-rw-r--r--devtools/client/webconsole/test/test_bug1092055_shouldwarn.js^headers^1
-rw-r--r--devtools/client/webconsole/test/test_bug_1010953_cspro.html20
-rw-r--r--devtools/client/webconsole/test/test_bug_1010953_cspro.html^headers^2
-rw-r--r--devtools/client/webconsole/test/test_bug_1247459_violation.html15
-rw-r--r--devtools/client/webconsole/test/test_bug_770099_violation.html13
-rw-r--r--devtools/client/webconsole/test/test_bug_770099_violation.html^headers^1
-rw-r--r--devtools/client/webconsole/test/test_hpkp-invalid-headers.sjs53
-rw-r--r--devtools/client/webconsole/test/test_hsts-invalid-headers.sjs39
-rw-r--r--devtools/client/webconsole/test/testscript.js2
-rw-r--r--devtools/client/webconsole/utils.js395
-rw-r--r--devtools/client/webconsole/webconsole.js3658
-rw-r--r--devtools/client/webconsole/webconsole.xul214
519 files changed, 44898 insertions, 0 deletions
diff --git a/devtools/client/webconsole/.babelrc b/devtools/client/webconsole/.babelrc
new file mode 100644
index 000000000..af0f0c3d3
--- /dev/null
+++ b/devtools/client/webconsole/.babelrc
@@ -0,0 +1,3 @@
+{
+ "presets": ["es2015"]
+} \ No newline at end of file
diff --git a/devtools/client/webconsole/console-commands.js b/devtools/client/webconsole/console-commands.js
new file mode 100644
index 000000000..0bc9e8edb
--- /dev/null
+++ b/devtools/client/webconsole/console-commands.js
@@ -0,0 +1,103 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft= javascript ts=2 et sw=2 tw=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";
+
+const l10n = require("gcli/l10n");
+loader.lazyRequireGetter(this, "gDevTools",
+ "devtools/client/framework/devtools", true);
+
+exports.items = [
+ {
+ item: "command",
+ runAt: "client",
+ name: "splitconsole",
+ hidden: true,
+ buttonId: "command-button-splitconsole",
+ buttonClass: "command-button command-button-invertable",
+ tooltipText: l10n.lookup("splitconsoleTooltip"),
+ isRemoteSafe: true,
+ state: {
+ isChecked: function (target) {
+ let toolbox = gDevTools.getToolbox(target);
+ return !!(toolbox && toolbox.splitConsole);
+ },
+ onChange: function (target, changeHandler) {
+ // Register handlers for when a change event should be fired
+ // (which resets the checked state of the button).
+ let toolbox = gDevTools.getToolbox(target);
+ let callback = changeHandler.bind(null, "changed", { target: target });
+
+ if (!toolbox) {
+ return;
+ }
+
+ toolbox.on("split-console", callback);
+ toolbox.once("destroyed", () => {
+ toolbox.off("split-console", callback);
+ });
+ }
+ },
+ exec: function (args, context) {
+ let target = context.environment.target;
+ let toolbox = gDevTools.getToolbox(target);
+
+ if (!toolbox) {
+ return gDevTools.showToolbox(target, "inspector").then((newToolbox) => {
+ newToolbox.toggleSplitConsole();
+ });
+ }
+ return toolbox.toggleSplitConsole();
+ }
+ },
+ {
+ name: "console",
+ description: l10n.lookup("consoleDesc"),
+ manual: l10n.lookup("consoleManual")
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "console clear",
+ description: l10n.lookup("consoleclearDesc"),
+ exec: function (args, context) {
+ let toolbox = gDevTools.getToolbox(context.environment.target);
+ if (toolbox == null) {
+ return null;
+ }
+
+ let panel = toolbox.getPanel("webconsole");
+ if (panel == null) {
+ return null;
+ }
+
+ let onceMessagesCleared = panel.hud.jsterm.once("messages-cleared");
+ panel.hud.jsterm.clearOutput();
+ return onceMessagesCleared;
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "console close",
+ description: l10n.lookup("consolecloseDesc"),
+ exec: function (args, context) {
+ // Don't return a value to GCLI
+ return gDevTools.closeToolbox(context.environment.target).then(() => {});
+ }
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "console open",
+ description: l10n.lookup("consoleopenDesc"),
+ exec: function (args, context) {
+ const target = context.environment.target;
+ // Don't return a value to GCLI
+ return gDevTools.showToolbox(target, "webconsole").then(() => {});
+ }
+ }
+];
diff --git a/devtools/client/webconsole/console-output.js b/devtools/client/webconsole/console-output.js
new file mode 100644
index 000000000..52d848494
--- /dev/null
+++ b/devtools/client/webconsole/console-output.js
@@ -0,0 +1,3638 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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";
+
+const {Ci, Cu} = require("chrome");
+
+loader.lazyImporter(this, "VariablesView", "resource://devtools/client/shared/widgets/VariablesView.jsm");
+loader.lazyImporter(this, "escapeHTML", "resource://devtools/client/shared/widgets/VariablesView.jsm");
+
+loader.lazyRequireGetter(this, "promise");
+loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
+loader.lazyRequireGetter(this, "TableWidget", "devtools/client/shared/widgets/TableWidget", true);
+loader.lazyRequireGetter(this, "ObjectClient", "devtools/shared/client/main", true);
+
+const { extend } = require("sdk/core/heritage");
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const STRINGS_URI = "devtools/client/locales/webconsole.properties";
+
+const WebConsoleUtils = require("devtools/client/webconsole/utils").Utils;
+const { getSourceNames } = require("devtools/client/shared/source-utils");
+const {Task} = require("devtools/shared/task");
+const l10n = new WebConsoleUtils.L10n(STRINGS_URI);
+const nodeConstants = require("devtools/shared/dom-node-constants");
+const {PluralForm} = require("devtools/shared/plural-form");
+
+const MAX_STRING_GRIP_LENGTH = 36;
+const {ELLIPSIS} = require("devtools/shared/l10n");
+
+const validProtocols = /^(http|https|ftp|data|javascript|resource|chrome):/i;
+
+// Constants for compatibility with the Web Console output implementation before
+// bug 778766.
+// TODO: remove these once bug 778766 is fixed.
+const COMPAT = {
+ // The various categories of messages.
+ CATEGORIES: {
+ NETWORK: 0,
+ CSS: 1,
+ JS: 2,
+ WEBDEV: 3,
+ INPUT: 4,
+ OUTPUT: 5,
+ SECURITY: 6,
+ SERVER: 7,
+ },
+
+ // The possible message severities.
+ SEVERITIES: {
+ ERROR: 0,
+ WARNING: 1,
+ INFO: 2,
+ LOG: 3,
+ },
+
+ // The preference keys to use for each category/severity combination, indexed
+ // first by category (rows) and then by severity (columns).
+ //
+ // Most of these rather idiosyncratic names are historical and predate the
+ // division of message type into "category" and "severity".
+ /* eslint-disable no-multi-spaces */
+ /* eslint-disable max-len */
+ /* eslint-disable no-inline-comments */
+ PREFERENCE_KEYS: [
+ // Error Warning Info Log
+ [ "network", "netwarn", null, "networkinfo", ], // Network
+ [ "csserror", "cssparser", null, null, ], // CSS
+ [ "exception", "jswarn", null, "jslog", ], // JS
+ [ "error", "warn", "info", "log", ], // Web Developer
+ [ null, null, null, null, ], // Input
+ [ null, null, null, null, ], // Output
+ [ "secerror", "secwarn", null, null, ], // Security
+ [ "servererror", "serverwarn", "serverinfo", "serverlog", ], // Server Logging
+ ],
+ /* eslint-enable no-inline-comments */
+ /* eslint-enable max-len */
+ /* eslint-enable no-multi-spaces */
+
+ // The fragment of a CSS class name that identifies each category.
+ CATEGORY_CLASS_FRAGMENTS: [ "network", "cssparser", "exception", "console",
+ "input", "output", "security", "server" ],
+
+ // The fragment of a CSS class name that identifies each severity.
+ SEVERITY_CLASS_FRAGMENTS: [ "error", "warn", "info", "log" ],
+
+ // The indent of a console group in pixels.
+ GROUP_INDENT: 12,
+};
+
+// A map from the console API call levels to the Web Console severities.
+const CONSOLE_API_LEVELS_TO_SEVERITIES = {
+ error: "error",
+ exception: "error",
+ assert: "error",
+ warn: "warning",
+ info: "info",
+ log: "log",
+ clear: "log",
+ trace: "log",
+ table: "log",
+ debug: "log",
+ dir: "log",
+ dirxml: "log",
+ group: "log",
+ groupCollapsed: "log",
+ groupEnd: "log",
+ time: "log",
+ timeEnd: "log",
+ count: "log"
+};
+
+// Array of known message source URLs we need to hide from output.
+const IGNORED_SOURCE_URLS = ["debugger eval code"];
+
+// The maximum length of strings to be displayed by the Web Console.
+const MAX_LONG_STRING_LENGTH = 200000;
+
+// Regular expression that matches the allowed CSS property names when using
+// the `window.console` API.
+const RE_ALLOWED_STYLES = /^(?:-moz-)?(?:background|border|box|clear|color|cursor|display|float|font|line|margin|padding|text|transition|outline|white-space|word|writing|(?:min-|max-)?width|(?:min-|max-)?height)/;
+
+// Regular expressions to search and replace with 'notallowed' in the styles
+// given to the `window.console` API methods.
+const RE_CLEANUP_STYLES = [
+ // url(), -moz-element()
+ /\b(?:url|(?:-moz-)?element)[\s('"]+/gi,
+
+ // various URL protocols
+ /['"(]*(?:chrome|resource|about|app|data|https?|ftp|file):+\/*/gi,
+];
+
+// Maximum number of rows to display in console.table().
+const TABLE_ROW_MAX_ITEMS = 1000;
+
+// Maximum number of columns to display in console.table().
+const TABLE_COLUMN_MAX_ITEMS = 10;
+
+/**
+ * The ConsoleOutput object is used to manage output of messages in the Web
+ * Console.
+ *
+ * @constructor
+ * @param object owner
+ * The console output owner. This usually the WebConsoleFrame instance.
+ * Any other object can be used, as long as it has the following
+ * properties and methods:
+ * - window
+ * - document
+ * - outputMessage(category, methodOrNode[, methodArguments])
+ * TODO: this is needed temporarily, until bug 778766 is fixed.
+ */
+function ConsoleOutput(owner)
+{
+ this.owner = owner;
+ this._onFlushOutputMessage = this._onFlushOutputMessage.bind(this);
+}
+
+ConsoleOutput.prototype = {
+ _dummyElement: null,
+
+ /**
+ * The output container.
+ * @type DOMElement
+ */
+ get element() {
+ return this.owner.outputNode;
+ },
+
+ /**
+ * The document that holds the output.
+ * @type DOMDocument
+ */
+ get document() {
+ return this.owner ? this.owner.document : null;
+ },
+
+ /**
+ * The DOM window that holds the output.
+ * @type Window
+ */
+ get window() {
+ return this.owner.window;
+ },
+
+ /**
+ * Getter for the debugger WebConsoleClient.
+ * @type object
+ */
+ get webConsoleClient() {
+ return this.owner.webConsoleClient;
+ },
+
+ /**
+ * Getter for the current toolbox debuggee target.
+ * @type Target
+ */
+ get toolboxTarget() {
+ return this.owner.owner.target;
+ },
+
+ /**
+ * Release an actor.
+ *
+ * @private
+ * @param string actorId
+ * The actor ID you want to release.
+ */
+ _releaseObject: function (actorId)
+ {
+ this.owner._releaseObject(actorId);
+ },
+
+ /**
+ * Add a message to output.
+ *
+ * @param object ...args
+ * Any number of Message objects.
+ * @return this
+ */
+ addMessage: function (...args)
+ {
+ for (let msg of args) {
+ msg.init(this);
+ this.owner.outputMessage(msg._categoryCompat, this._onFlushOutputMessage,
+ [msg]);
+ }
+ return this;
+ },
+
+ /**
+ * Message renderer used for compatibility with the current Web Console output
+ * implementation. This method is invoked for every message object that is
+ * flushed to output. The message object is initialized and rendered, then it
+ * is displayed.
+ *
+ * TODO: remove this method once bug 778766 is fixed.
+ *
+ * @private
+ * @param object message
+ * The message object to render.
+ * @return DOMElement
+ * The message DOM element that can be added to the console output.
+ */
+ _onFlushOutputMessage: function (message)
+ {
+ return message.render().element;
+ },
+
+ /**
+ * Get an array of selected messages. This list is based on the text selection
+ * start and end points.
+ *
+ * @param number [limit]
+ * Optional limit of selected messages you want. If no value is given,
+ * all of the selected messages are returned.
+ * @return array
+ * Array of DOM elements for each message that is currently selected.
+ */
+ getSelectedMessages: function (limit)
+ {
+ let selection = this.window.getSelection();
+ if (selection.isCollapsed) {
+ return [];
+ }
+
+ if (selection.containsNode(this.element, true)) {
+ return Array.slice(this.element.children);
+ }
+
+ let anchor = this.getMessageForElement(selection.anchorNode);
+ let focus = this.getMessageForElement(selection.focusNode);
+ if (!anchor || !focus) {
+ return [];
+ }
+
+ let start, end;
+ if (anchor.timestamp > focus.timestamp) {
+ start = focus;
+ end = anchor;
+ } else {
+ start = anchor;
+ end = focus;
+ }
+
+ let result = [];
+ let current = start;
+ while (current) {
+ result.push(current);
+ if (current == end || (limit && result.length == limit)) {
+ break;
+ }
+ current = current.nextSibling;
+ }
+ return result;
+ },
+
+ /**
+ * Find the DOM element of a message for any given descendant.
+ *
+ * @param DOMElement elem
+ * The element to start the search from.
+ * @return DOMElement|null
+ * The DOM element of the message, if any.
+ */
+ getMessageForElement: function (elem)
+ {
+ while (elem && elem.parentNode) {
+ if (elem.classList && elem.classList.contains("message")) {
+ return elem;
+ }
+ elem = elem.parentNode;
+ }
+ return null;
+ },
+
+ /**
+ * Select all messages.
+ */
+ selectAllMessages: function ()
+ {
+ let selection = this.window.getSelection();
+ selection.removeAllRanges();
+ let range = this.document.createRange();
+ range.selectNodeContents(this.element);
+ selection.addRange(range);
+ },
+
+ /**
+ * Add a message to the selection.
+ *
+ * @param DOMElement elem
+ * The message element to select.
+ */
+ selectMessage: function (elem)
+ {
+ let selection = this.window.getSelection();
+ selection.removeAllRanges();
+ let range = this.document.createRange();
+ range.selectNodeContents(elem);
+ selection.addRange(range);
+ },
+
+ /**
+ * Open an URL in a new tab.
+ * @see WebConsole.openLink() in hudservice.js
+ */
+ openLink: function ()
+ {
+ this.owner.owner.openLink.apply(this.owner.owner, arguments);
+ },
+
+ openLocationInDebugger: function ({url, line}) {
+ return this.owner.owner.viewSourceInDebugger(url, line);
+ },
+
+ /**
+ * Open the variables view to inspect an object actor.
+ * @see JSTerm.openVariablesView() in webconsole.js
+ */
+ openVariablesView: function ()
+ {
+ this.owner.jsterm.openVariablesView.apply(this.owner.jsterm, arguments);
+ },
+
+ /**
+ * Destroy this ConsoleOutput instance.
+ */
+ destroy: function ()
+ {
+ this._dummyElement = null;
+ this.owner = null;
+ },
+}; // ConsoleOutput.prototype
+
+/**
+ * Message objects container.
+ * @type object
+ */
+var Messages = {};
+
+/**
+ * The BaseMessage object is used for all types of messages. Every kind of
+ * message should use this object as its base.
+ *
+ * @constructor
+ */
+Messages.BaseMessage = function ()
+{
+ this.widgets = new Set();
+ this._onClickAnchor = this._onClickAnchor.bind(this);
+ this._repeatID = { uid: gSequenceId() };
+ this.textContent = "";
+};
+
+Messages.BaseMessage.prototype = {
+ /**
+ * Reference to the ConsoleOutput owner.
+ *
+ * @type object|null
+ * This is |null| if the message is not yet initialized.
+ */
+ output: null,
+
+ /**
+ * Reference to the parent message object, if this message is in a group or if
+ * it is otherwise owned by another message.
+ *
+ * @type object|null
+ */
+ parent: null,
+
+ /**
+ * Message DOM element.
+ *
+ * @type DOMElement|null
+ * This is |null| if the message is not yet rendered.
+ */
+ element: null,
+
+ /**
+ * Tells if this message is visible or not.
+ * @type boolean
+ */
+ get visible() {
+ return this.element && this.element.parentNode;
+ },
+
+ /**
+ * The owner DOM document.
+ * @type DOMElement
+ */
+ get document() {
+ return this.output.document;
+ },
+
+ /**
+ * Holds the text-only representation of the message.
+ * @type string
+ */
+ textContent: null,
+
+ /**
+ * Set of widgets included in this message.
+ * @type Set
+ */
+ widgets: null,
+
+ // Properties that allow compatibility with the current Web Console output
+ // implementation.
+ _categoryCompat: null,
+ _severityCompat: null,
+ _categoryNameCompat: null,
+ _severityNameCompat: null,
+ _filterKeyCompat: null,
+
+ /**
+ * Object that is JSON-ified and used as a non-unique ID for tracking
+ * duplicate messages.
+ * @private
+ * @type object
+ */
+ _repeatID: null,
+
+ /**
+ * Initialize the message.
+ *
+ * @param object output
+ * The ConsoleOutput owner.
+ * @param object [parent=null]
+ * Optional: a different message object that owns this instance.
+ * @return this
+ */
+ init: function (output, parent = null)
+ {
+ this.output = output;
+ this.parent = parent;
+ return this;
+ },
+
+ /**
+ * Non-unique ID for this message object used for tracking duplicate messages.
+ * Different message kinds can identify themselves based their own criteria.
+ *
+ * @return string
+ */
+ getRepeatID: function ()
+ {
+ return JSON.stringify(this._repeatID);
+ },
+
+ /**
+ * Render the message. After this method is invoked the |element| property
+ * will point to the DOM element of this message.
+ * @return this
+ */
+ render: function ()
+ {
+ if (!this.element) {
+ this.element = this._renderCompat();
+ }
+ return this;
+ },
+
+ /**
+ * Prepare the message container for the Web Console, such that it is
+ * compatible with the current implementation.
+ * TODO: remove this once bug 778766 is fixed.
+ *
+ * @private
+ * @return Element
+ * The DOM element that wraps the message.
+ */
+ _renderCompat: function ()
+ {
+ let doc = this.output.document;
+ let container = doc.createElementNS(XHTML_NS, "div");
+ container.id = "console-msg-" + gSequenceId();
+ container.className = "message";
+ if (this.category == "input") {
+ // Assistive technology tools shouldn't echo input to the user,
+ // as the user knows what they've just typed.
+ container.setAttribute("aria-live", "off");
+ }
+ container.category = this._categoryCompat;
+ container.severity = this._severityCompat;
+ container.setAttribute("category", this._categoryNameCompat);
+ container.setAttribute("severity", this._severityNameCompat);
+ container.setAttribute("filter", this._filterKeyCompat);
+ container.clipboardText = this.textContent;
+ container.timestamp = this.timestamp;
+ container._messageObject = this;
+
+ return container;
+ },
+
+ /**
+ * Add a click callback to a given DOM element.
+ *
+ * @private
+ * @param Element element
+ * The DOM element to which you want to add a click event handler.
+ * @param function [callback=this._onClickAnchor]
+ * Optional click event handler. The default event handler is
+ * |this._onClickAnchor|.
+ */
+ _addLinkCallback: function (element, callback = this._onClickAnchor)
+ {
+ // This is going into the WebConsoleFrame object instance that owns
+ // the ConsoleOutput object. The WebConsoleFrame owner is the WebConsole
+ // object instance from hudservice.js.
+ // TODO: move _addMessageLinkCallback() into ConsoleOutput once bug 778766
+ // is fixed.
+ this.output.owner._addMessageLinkCallback(element, callback);
+ },
+
+ /**
+ * The default |click| event handler for links in the output. This function
+ * opens the anchor's link in a new tab.
+ *
+ * @private
+ * @param Event event
+ * The DOM event that invoked this function.
+ */
+ _onClickAnchor: function (event)
+ {
+ this.output.openLink(event.target.href);
+ },
+
+ destroy: function ()
+ {
+ // Destroy all widgets that have registered themselves in this.widgets
+ for (let widget of this.widgets) {
+ widget.destroy();
+ }
+ this.widgets.clear();
+ }
+};
+
+/**
+ * The NavigationMarker is used to show a page load event.
+ *
+ * @constructor
+ * @extends Messages.BaseMessage
+ * @param object response
+ * The response received from the back end.
+ * @param number timestamp
+ * The message date and time, milliseconds elapsed since 1 January 1970
+ * 00:00:00 UTC.
+ */
+Messages.NavigationMarker = function (response, timestamp) {
+ Messages.BaseMessage.call(this);
+
+ // Store the response packet received from the server. It might
+ // be useful for extensions customizing the console output.
+ this.response = response;
+ this._url = response.url;
+ this.textContent = "------ " + this._url;
+ this.timestamp = timestamp;
+};
+
+Messages.NavigationMarker.prototype = extend(Messages.BaseMessage.prototype, {
+ /**
+ * The address of the loading page.
+ * @private
+ * @type string
+ */
+ _url: null,
+
+ /**
+ * Message timestamp.
+ *
+ * @type number
+ * Milliseconds elapsed since 1 January 1970 00:00:00 UTC.
+ */
+ timestamp: 0,
+
+ _categoryCompat: COMPAT.CATEGORIES.NETWORK,
+ _severityCompat: COMPAT.SEVERITIES.LOG,
+ _categoryNameCompat: "network",
+ _severityNameCompat: "info",
+ _filterKeyCompat: "networkinfo",
+
+ /**
+ * Prepare the DOM element for this message.
+ * @return this
+ */
+ render: function () {
+ if (this.element) {
+ return this;
+ }
+
+ let url = this._url;
+ let pos = url.indexOf("?");
+ if (pos > -1) {
+ url = url.substr(0, pos);
+ }
+
+ let doc = this.output.document;
+ let urlnode = doc.createElementNS(XHTML_NS, "a");
+ urlnode.className = "url";
+ urlnode.textContent = url;
+ urlnode.title = this._url;
+ urlnode.href = this._url;
+ urlnode.draggable = false;
+ this._addLinkCallback(urlnode);
+
+ let render = Messages.BaseMessage.prototype.render.bind(this);
+ render().element.appendChild(urlnode);
+ this.element.classList.add("navigation-marker");
+ this.element.url = this._url;
+ this.element.appendChild(doc.createTextNode("\n"));
+
+ return this;
+ },
+});
+
+/**
+ * The Simple message is used to show any basic message in the Web Console.
+ *
+ * @constructor
+ * @extends Messages.BaseMessage
+ * @param string|Node|function message
+ * The message to display.
+ * @param object [options]
+ * Options for this message:
+ * - category: (string) category that this message belongs to. Defaults
+ * to no category.
+ * - severity: (string) severity of the message. Defaults to no severity.
+ * - timestamp: (number) date and time when the message was recorded.
+ * Defaults to |Date.now()|.
+ * - link: (string) if provided, the message will be wrapped in an anchor
+ * pointing to the given URL here.
+ * - linkCallback: (function) if provided, the message will be wrapped in
+ * an anchor. The |linkCallback| function will be added as click event
+ * handler.
+ * - location: object that tells the message source: url, line, column
+ * and lineText.
+ * - stack: array that tells the message source stack.
+ * - className: (string) additional element class names for styling
+ * purposes.
+ * - private: (boolean) mark this as a private message.
+ * - filterDuplicates: (boolean) true if you do want this message to be
+ * filtered as a potential duplicate message, false otherwise.
+ */
+Messages.Simple = function (message, options = {}) {
+ Messages.BaseMessage.call(this);
+
+ this.category = options.category;
+ this.severity = options.severity;
+ this.location = options.location;
+ this.stack = options.stack;
+ this.timestamp = options.timestamp || Date.now();
+ this.prefix = options.prefix;
+ this.private = !!options.private;
+
+ this._message = message;
+ this._className = options.className;
+ this._link = options.link;
+ this._linkCallback = options.linkCallback;
+ this._filterDuplicates = options.filterDuplicates;
+
+ this._onClickCollapsible = this._onClickCollapsible.bind(this);
+};
+
+Messages.Simple.prototype = extend(Messages.BaseMessage.prototype, {
+ /**
+ * Message category.
+ * @type string
+ */
+ category: null,
+
+ /**
+ * Message severity.
+ * @type string
+ */
+ severity: null,
+
+ /**
+ * Message source location. Properties: url, line, column, lineText.
+ * @type object
+ */
+ location: null,
+
+ /**
+ * Holds the stackframes received from the server.
+ *
+ * @private
+ * @type array
+ */
+ stack: null,
+
+ /**
+ * Message prefix
+ * @type string|null
+ */
+ prefix: null,
+
+ /**
+ * Tells if this message comes from a private browsing context.
+ * @type boolean
+ */
+ private: false,
+
+ /**
+ * Custom class name for the DOM element of the message.
+ * @private
+ * @type string
+ */
+ _className: null,
+
+ /**
+ * Message link - if this message is clicked then this URL opens in a new tab.
+ * @private
+ * @type string
+ */
+ _link: null,
+
+ /**
+ * Message click event handler.
+ * @private
+ * @type function
+ */
+ _linkCallback: null,
+
+ /**
+ * Tells if this message should be checked if it is a duplicate of another
+ * message or not.
+ */
+ _filterDuplicates: false,
+
+ /**
+ * The raw message displayed by this Message object. This can be a function,
+ * DOM node or a string.
+ *
+ * @private
+ * @type mixed
+ */
+ _message: null,
+
+ /**
+ * The message's "attachment" element to be displayed under the message.
+ * Used for things like stack traces or tables in console.table().
+ *
+ * @private
+ * @type DOMElement|null
+ */
+ _attachment: null,
+
+ _objectActors: null,
+ _groupDepthCompat: 0,
+
+ /**
+ * Message timestamp.
+ *
+ * @type number
+ * Milliseconds elapsed since 1 January 1970 00:00:00 UTC.
+ */
+ timestamp: 0,
+
+ get _categoryCompat() {
+ return this.category ?
+ COMPAT.CATEGORIES[this.category.toUpperCase()] : null;
+ },
+ get _severityCompat() {
+ return this.severity ?
+ COMPAT.SEVERITIES[this.severity.toUpperCase()] : null;
+ },
+ get _categoryNameCompat() {
+ return this.category ?
+ COMPAT.CATEGORY_CLASS_FRAGMENTS[this._categoryCompat] : null;
+ },
+ get _severityNameCompat() {
+ return this.severity ?
+ COMPAT.SEVERITY_CLASS_FRAGMENTS[this._severityCompat] : null;
+ },
+
+ get _filterKeyCompat() {
+ return this._categoryCompat !== null && this._severityCompat !== null ?
+ COMPAT.PREFERENCE_KEYS[this._categoryCompat][this._severityCompat] :
+ null;
+ },
+
+ init: function ()
+ {
+ Messages.BaseMessage.prototype.init.apply(this, arguments);
+ this._groupDepthCompat = this.output.owner.groupDepth;
+ this._initRepeatID();
+ return this;
+ },
+
+ /**
+ * Tells if the message can be expanded/collapsed.
+ * @type boolean
+ */
+ collapsible: false,
+
+ /**
+ * Getter that tells if this message is collapsed - no details are shown.
+ * @type boolean
+ */
+ get collapsed() {
+ return this.collapsible && this.element && !this.element.hasAttribute("open");
+ },
+
+ _initRepeatID: function ()
+ {
+ if (!this._filterDuplicates) {
+ return;
+ }
+
+ // Add the properties we care about for identifying duplicate messages.
+ let rid = this._repeatID;
+ delete rid.uid;
+
+ rid.category = this.category;
+ rid.severity = this.severity;
+ rid.prefix = this.prefix;
+ rid.private = this.private;
+ rid.location = this.location;
+ rid.link = this._link;
+ rid.linkCallback = this._linkCallback + "";
+ rid.className = this._className;
+ rid.groupDepth = this._groupDepthCompat;
+ rid.textContent = "";
+ },
+
+ getRepeatID: function ()
+ {
+ // No point in returning a string that includes other properties when there
+ // is a unique ID.
+ if (this._repeatID.uid) {
+ return JSON.stringify({ uid: this._repeatID.uid });
+ }
+
+ return JSON.stringify(this._repeatID);
+ },
+
+ render: function ()
+ {
+ if (this.element) {
+ return this;
+ }
+
+ let timestamp = new Widgets.MessageTimestamp(this, this.timestamp).render();
+
+ let icon = this.document.createElementNS(XHTML_NS, "span");
+ icon.className = "icon";
+ icon.title = l10n.getStr("severity." + this._severityNameCompat);
+ if (this.stack) {
+ icon.addEventListener("click", this._onClickCollapsible);
+ }
+
+ let prefixNode;
+ if (this.prefix) {
+ prefixNode = this.document.createElementNS(XHTML_NS, "span");
+ prefixNode.className = "prefix devtools-monospace";
+ prefixNode.textContent = this.prefix + ":";
+ }
+
+ // Apply the current group by indenting appropriately.
+ // TODO: remove this once bug 778766 is fixed.
+ let indent = this._groupDepthCompat * COMPAT.GROUP_INDENT;
+ let indentNode = this.document.createElementNS(XHTML_NS, "span");
+ indentNode.className = "indent";
+ indentNode.style.width = indent + "px";
+
+ let body = this._renderBody();
+
+ Messages.BaseMessage.prototype.render.call(this);
+ if (this._className) {
+ this.element.className += " " + this._className;
+ }
+
+ this.element.appendChild(timestamp.element);
+ this.element.appendChild(indentNode);
+ this.element.appendChild(icon);
+ if (prefixNode) {
+ this.element.appendChild(prefixNode);
+ }
+
+ if (this.stack) {
+ let twisty = this.document.createElementNS(XHTML_NS, "a");
+ twisty.className = "theme-twisty";
+ twisty.href = "#";
+ twisty.title = l10n.getStr("messageToggleDetails");
+ twisty.addEventListener("click", this._onClickCollapsible);
+ this.element.appendChild(twisty);
+ this.collapsible = true;
+ this.element.setAttribute("collapsible", true);
+ }
+
+ this.element.appendChild(body);
+
+ this.element.clipboardText = this.element.textContent;
+
+ if (this.private) {
+ this.element.setAttribute("private", true);
+ }
+
+ // TODO: handle object releasing in a more elegant way once all console
+ // messages use the new API - bug 778766.
+ this.element._objectActors = this._objectActors;
+ this._objectActors = null;
+
+ return this;
+ },
+
+ /**
+ * Render the message body DOM element.
+ * @private
+ * @return Element
+ */
+ _renderBody: function ()
+ {
+ let bodyWrapper = this.document.createElementNS(XHTML_NS, "span");
+ bodyWrapper.className = "message-body-wrapper";
+
+ let bodyFlex = this.document.createElementNS(XHTML_NS, "span");
+ bodyFlex.className = "message-flex-body";
+ bodyWrapper.appendChild(bodyFlex);
+
+ let body = this.document.createElementNS(XHTML_NS, "span");
+ body.className = "message-body devtools-monospace";
+ bodyFlex.appendChild(body);
+
+ let anchor, container = body;
+ if (this._link || this._linkCallback) {
+ container = anchor = this.document.createElementNS(XHTML_NS, "a");
+ anchor.href = this._link || "#";
+ anchor.draggable = false;
+ this._addLinkCallback(anchor, this._linkCallback);
+ body.appendChild(anchor);
+ }
+
+ if (typeof this._message == "function") {
+ container.appendChild(this._message(this));
+ } else if (this._message instanceof Ci.nsIDOMNode) {
+ container.appendChild(this._message);
+ } else {
+ container.textContent = this._message;
+ }
+
+ // do this before repeatNode is rendered - it has no effect afterwards
+ this._repeatID.textContent += "|" + container.textContent;
+
+ let repeatNode = this._renderRepeatNode();
+ let location = this._renderLocation();
+
+ if (repeatNode) {
+ bodyFlex.appendChild(this.document.createTextNode(" "));
+ bodyFlex.appendChild(repeatNode);
+ }
+ if (location) {
+ bodyFlex.appendChild(this.document.createTextNode(" "));
+ bodyFlex.appendChild(location);
+ }
+
+ bodyFlex.appendChild(this.document.createTextNode("\n"));
+
+ if (this.stack) {
+ this._attachment = new Widgets.Stacktrace(this, this.stack).render().element;
+ }
+
+ if (this._attachment) {
+ bodyWrapper.appendChild(this._attachment);
+ }
+
+ return bodyWrapper;
+ },
+
+ /**
+ * Render the repeat bubble DOM element part of the message.
+ * @private
+ * @return Element
+ */
+ _renderRepeatNode: function ()
+ {
+ if (!this._filterDuplicates) {
+ return null;
+ }
+
+ let repeatNode = this.document.createElementNS(XHTML_NS, "span");
+ repeatNode.setAttribute("value", "1");
+ repeatNode.className = "message-repeats";
+ repeatNode.textContent = 1;
+ repeatNode._uid = this.getRepeatID();
+ return repeatNode;
+ },
+
+ /**
+ * Render the message source location DOM element.
+ * @private
+ * @return Element
+ */
+ _renderLocation: function ()
+ {
+ if (!this.location) {
+ return null;
+ }
+
+ let {url, line, column} = this.location;
+ if (IGNORED_SOURCE_URLS.indexOf(url) != -1) {
+ return null;
+ }
+
+ // The ConsoleOutput owner is a WebConsoleFrame instance from webconsole.js.
+ // TODO: move createLocationNode() into this file when bug 778766 is fixed.
+ return this.output.owner.createLocationNode({url, line, column });
+ },
+
+ /**
+ * The click event handler for the message expander arrow element. This method
+ * toggles the display of message details.
+ *
+ * @private
+ * @param nsIDOMEvent ev
+ * The DOM event object.
+ * @see this.toggleDetails()
+ */
+ _onClickCollapsible: function (ev)
+ {
+ ev.preventDefault();
+ this.toggleDetails();
+ },
+
+ /**
+ * Expand/collapse message details.
+ */
+ toggleDetails: function ()
+ {
+ let twisty = this.element.querySelector(".theme-twisty");
+ if (this.element.hasAttribute("open")) {
+ this.element.removeAttribute("open");
+ twisty.removeAttribute("open");
+ } else {
+ this.element.setAttribute("open", true);
+ twisty.setAttribute("open", true);
+ }
+ },
+}); // Messages.Simple.prototype
+
+
+/**
+ * The Extended message.
+ *
+ * @constructor
+ * @extends Messages.Simple
+ * @param array messagePieces
+ * The message to display given as an array of elements. Each array
+ * element can be a DOM node, function, ObjectActor, LongString or
+ * a string.
+ * @param object [options]
+ * Options for rendering this message:
+ * - quoteStrings: boolean that tells if you want strings to be wrapped
+ * in quotes or not.
+ */
+Messages.Extended = function (messagePieces, options = {})
+{
+ Messages.Simple.call(this, null, options);
+
+ this._messagePieces = messagePieces;
+
+ if ("quoteStrings" in options) {
+ this._quoteStrings = options.quoteStrings;
+ }
+
+ this._repeatID.quoteStrings = this._quoteStrings;
+ this._repeatID.messagePieces = JSON.stringify(messagePieces);
+ this._repeatID.actors = new Set(); // using a set to avoid duplicates
+};
+
+Messages.Extended.prototype = extend(Messages.Simple.prototype, {
+ /**
+ * The message pieces displayed by this message instance.
+ * @private
+ * @type array
+ */
+ _messagePieces: null,
+
+ /**
+ * Boolean that tells if the strings displayed in this message are wrapped.
+ * @private
+ * @type boolean
+ */
+ _quoteStrings: true,
+
+ getRepeatID: function ()
+ {
+ if (this._repeatID.uid) {
+ return JSON.stringify({ uid: this._repeatID.uid });
+ }
+
+ // Sets are not stringified correctly. Temporarily switching to an array.
+ let actors = this._repeatID.actors;
+ this._repeatID.actors = [...actors];
+ let result = JSON.stringify(this._repeatID);
+ this._repeatID.actors = actors;
+ return result;
+ },
+
+ render: function ()
+ {
+ let result = this.document.createDocumentFragment();
+
+ for (let i = 0; i < this._messagePieces.length; i++) {
+ let separator = i > 0 ? this._renderBodyPieceSeparator() : null;
+ if (separator) {
+ result.appendChild(separator);
+ }
+
+ let piece = this._messagePieces[i];
+ result.appendChild(this._renderBodyPiece(piece));
+ }
+
+ this._message = result;
+ this._messagePieces = null;
+ return Messages.Simple.prototype.render.call(this);
+ },
+
+ /**
+ * Render the separator between the pieces of the message.
+ *
+ * @private
+ * @return Element
+ */
+ _renderBodyPieceSeparator: function () { return null; },
+
+ /**
+ * Render one piece/element of the message array.
+ *
+ * @private
+ * @param mixed piece
+ * Message element to display - this can be a LongString, ObjectActor,
+ * DOM node or a function to invoke.
+ * @return Element
+ */
+ _renderBodyPiece: function (piece, options = {})
+ {
+ if (piece instanceof Ci.nsIDOMNode) {
+ return piece;
+ }
+ if (typeof piece == "function") {
+ return piece(this);
+ }
+
+ return this._renderValueGrip(piece, options);
+ },
+
+ /**
+ * Render a grip that represents a value received from the server. This method
+ * picks the appropriate widget to render the value with.
+ *
+ * @private
+ * @param object grip
+ * The value grip received from the server.
+ * @param object options
+ * Options for displaying the value. Available options:
+ * - noStringQuotes - boolean that tells the renderer to not use quotes
+ * around strings.
+ * - concise - boolean that tells the renderer to compactly display the
+ * grip. This is typically set to true when the object needs to be
+ * displayed in an array preview, or as a property value in object
+ * previews, etc.
+ * - shorten - boolean that tells the renderer to display a truncated
+ * grip.
+ * @return DOMElement
+ * The DOM element that displays the given grip.
+ */
+ _renderValueGrip: function (grip, options = {})
+ {
+ let isPrimitive = VariablesView.isPrimitive({ value: grip });
+ let isActorGrip = WebConsoleUtils.isActorGrip(grip);
+ let noStringQuotes = !this._quoteStrings;
+ if ("noStringQuotes" in options) {
+ noStringQuotes = options.noStringQuotes;
+ }
+
+ if (isActorGrip) {
+ this._repeatID.actors.add(grip.actor);
+
+ if (!isPrimitive) {
+ return this._renderObjectActor(grip, options);
+ }
+ if (grip.type == "longString") {
+ let widget = new Widgets.LongString(this, grip, options).render();
+ return widget.element;
+ }
+ }
+
+ let unshortenedGrip = grip;
+ if (options.shorten) {
+ grip = this.shortenValueGrip(grip);
+ }
+
+ let result = this.document.createElementNS(XHTML_NS, "span");
+ if (isPrimitive) {
+ if (Widgets.URLString.prototype.containsURL.call(Widgets.URLString.prototype, grip)) {
+ let widget = new Widgets.URLString(this, grip, unshortenedGrip).render();
+ return widget.element;
+ }
+
+ let className = this.getClassNameForValueGrip(grip);
+ if (className) {
+ result.className = className;
+ }
+
+ result.textContent = VariablesView.getString(grip, {
+ noStringQuotes: noStringQuotes,
+ concise: options.concise,
+ });
+ } else {
+ result.textContent = grip;
+ }
+
+ return result;
+ },
+
+ /**
+ * Shorten grips of the type string, leaves other grips unmodified.
+ *
+ * @param object grip
+ * Value grip from the server.
+ * @return object
+ * Possible values of object:
+ * - A shortened string, if original grip was of string type.
+ * - The unmodified input grip, if it wasn't of string type.
+ */
+ shortenValueGrip: function (grip)
+ {
+ let shortVal = grip;
+ if (typeof (grip) == "string") {
+ shortVal = grip.replace(/(\r\n|\n|\r)/gm, " ");
+ if (shortVal.length > MAX_STRING_GRIP_LENGTH) {
+ shortVal = shortVal.substring(0, MAX_STRING_GRIP_LENGTH - 1) + ELLIPSIS;
+ }
+ }
+
+ return shortVal;
+ },
+
+ /**
+ * Get a CodeMirror-compatible class name for a given value grip.
+ *
+ * @param object grip
+ * Value grip from the server.
+ * @return string
+ * The class name for the grip.
+ */
+ getClassNameForValueGrip: function (grip)
+ {
+ let map = {
+ "number": "cm-number",
+ "longstring": "console-string",
+ "string": "console-string",
+ "regexp": "cm-string-2",
+ "boolean": "cm-atom",
+ "-infinity": "cm-atom",
+ "infinity": "cm-atom",
+ "null": "cm-atom",
+ "undefined": "cm-comment",
+ "symbol": "cm-atom"
+ };
+
+ let className = map[typeof grip];
+ if (!className && grip && grip.type) {
+ className = map[grip.type.toLowerCase()];
+ }
+ if (!className && grip && grip.class) {
+ className = map[grip.class.toLowerCase()];
+ }
+
+ return className;
+ },
+
+ /**
+ * Display an object actor with the appropriate renderer.
+ *
+ * @private
+ * @param object objectActor
+ * The ObjectActor to display.
+ * @param object options
+ * Options to use for displaying the ObjectActor.
+ * @see this._renderValueGrip for the available options.
+ * @return DOMElement
+ * The DOM element that displays the object actor.
+ */
+ _renderObjectActor: function (objectActor, options = {})
+ {
+ let widget = Widgets.ObjectRenderers.byClass[objectActor.class];
+
+ let { preview } = objectActor;
+ if ((!widget || (widget.canRender && !widget.canRender(objectActor)))
+ && preview
+ && preview.kind) {
+ widget = Widgets.ObjectRenderers.byKind[preview.kind];
+ }
+
+ if (!widget || (widget.canRender && !widget.canRender(objectActor))) {
+ widget = Widgets.JSObject;
+ }
+
+ let instance = new widget(this, objectActor, options).render();
+ return instance.element;
+ },
+}); // Messages.Extended.prototype
+
+
+
+/**
+ * The JavaScriptEvalOutput message.
+ *
+ * @constructor
+ * @extends Messages.Extended
+ * @param object evalResponse
+ * The evaluation response packet received from the server.
+ * @param string [errorMessage]
+ * Optional error message to display.
+ * @param string [errorDocLink]
+ * Optional error doc URL to link to.
+ */
+Messages.JavaScriptEvalOutput = function (evalResponse, errorMessage, errorDocLink)
+{
+ let severity = "log", msg, quoteStrings = true;
+
+ // Store also the response packet from the back end. It might
+ // be useful to extensions customizing the console output.
+ this.response = evalResponse;
+
+ if (typeof (errorMessage) !== "undefined") {
+ severity = "error";
+ msg = errorMessage;
+ quoteStrings = false;
+ } else {
+ msg = evalResponse.result;
+ }
+
+ let options = {
+ className: "cm-s-mozilla",
+ timestamp: evalResponse.timestamp,
+ category: "output",
+ severity: severity,
+ quoteStrings: quoteStrings,
+ };
+
+ let messages = [msg];
+ if (errorDocLink) {
+ messages.push(errorDocLink);
+ }
+
+ Messages.Extended.call(this, messages, options);
+};
+
+Messages.JavaScriptEvalOutput.prototype = Messages.Extended.prototype;
+
+/**
+ * The ConsoleGeneric message is used for console API calls.
+ *
+ * @constructor
+ * @extends Messages.Extended
+ * @param object packet
+ * The Console API call packet received from the server.
+ */
+Messages.ConsoleGeneric = function (packet)
+{
+ let options = {
+ className: "cm-s-mozilla",
+ timestamp: packet.timeStamp,
+ category: packet.category || "webdev",
+ severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level],
+ prefix: packet.prefix,
+ private: packet.private,
+ filterDuplicates: true,
+ location: {
+ url: packet.filename,
+ line: packet.lineNumber,
+ column: packet.columnNumber
+ },
+ };
+
+ switch (packet.level) {
+ case "count": {
+ let counter = packet.counter, label = counter.label;
+ if (!label) {
+ label = l10n.getStr("noCounterLabel");
+ }
+ Messages.Extended.call(this, [label + ": " + counter.count], options);
+ break;
+ }
+ default:
+ Messages.Extended.call(this, packet.arguments, options);
+ break;
+ }
+
+ this._repeatID.consoleApiLevel = packet.level;
+ this._repeatID.styles = packet.styles;
+ this.stack = this._repeatID.stacktrace = packet.stacktrace;
+ this._styles = packet.styles || [];
+};
+
+Messages.ConsoleGeneric.prototype = extend(Messages.Extended.prototype, {
+ _styles: null,
+
+ _renderBodyPieceSeparator: function ()
+ {
+ return this.document.createTextNode(" ");
+ },
+
+ render: function ()
+ {
+ let result = this.document.createDocumentFragment();
+ this._renderBodyPieces(result);
+
+ this._message = result;
+ this._stacktrace = null;
+
+ Messages.Simple.prototype.render.call(this);
+
+ return this;
+ },
+
+ _renderBodyPieces: function (container)
+ {
+ let lastStyle = null;
+ let stylePieces = this._styles.length > 0 ? this._styles.length : 1;
+
+ for (let i = 0; i < this._messagePieces.length; i++) {
+ // Pieces with an associated style definition come from "%c" formatting.
+ // For body pieces beyond that, add a separator before each one.
+ if (i >= stylePieces) {
+ container.appendChild(this._renderBodyPieceSeparator());
+ }
+
+ let piece = this._messagePieces[i];
+ let style = this._styles[i];
+
+ // No long string support.
+ lastStyle = (style && typeof style == "string") ?
+ this.cleanupStyle(style) : null;
+
+ container.appendChild(this._renderBodyPiece(piece, lastStyle));
+ }
+
+ this._messagePieces = null;
+ this._styles = null;
+ },
+
+ _renderBodyPiece: function (piece, style)
+ {
+ // Skip quotes for top-level strings.
+ let options = { noStringQuotes: true };
+ let elem = Messages.Extended.prototype._renderBodyPiece.call(this, piece, options);
+ let result = elem;
+
+ if (style) {
+ if (elem.nodeType == nodeConstants.ELEMENT_NODE) {
+ elem.style = style;
+ } else {
+ let span = this.document.createElementNS(XHTML_NS, "span");
+ span.style = style;
+ span.appendChild(elem);
+ result = span;
+ }
+ }
+
+ return result;
+ },
+
+ /**
+ * Given a style attribute value, return a cleaned up version of the string
+ * such that:
+ *
+ * - no external URL is allowed to load. See RE_CLEANUP_STYLES.
+ * - only some of the properties are allowed, based on a whitelist. See
+ * RE_ALLOWED_STYLES.
+ *
+ * @param string style
+ * The style string to cleanup.
+ * @return string
+ * The style value after cleanup.
+ */
+ cleanupStyle: function (style)
+ {
+ for (let r of RE_CLEANUP_STYLES) {
+ style = style.replace(r, "notallowed");
+ }
+
+ let dummy = this.output._dummyElement;
+ if (!dummy) {
+ dummy = this.output._dummyElement =
+ this.document.createElementNS(XHTML_NS, "div");
+ }
+ dummy.style = style;
+
+ let toRemove = [];
+ for (let i = 0; i < dummy.style.length; i++) {
+ let prop = dummy.style[i];
+ if (!RE_ALLOWED_STYLES.test(prop)) {
+ toRemove.push(prop);
+ }
+ }
+
+ for (let prop of toRemove) {
+ dummy.style.removeProperty(prop);
+ }
+
+ style = dummy.style.cssText;
+
+ dummy.style = "";
+
+ return style;
+ },
+}); // Messages.ConsoleGeneric.prototype
+
+/**
+ * The ConsoleTrace message is used for console.trace() calls.
+ *
+ * @constructor
+ * @extends Messages.Simple
+ * @param object packet
+ * The Console API call packet received from the server.
+ */
+Messages.ConsoleTrace = function (packet)
+{
+ let options = {
+ className: "cm-s-mozilla",
+ timestamp: packet.timeStamp,
+ category: packet.category || "webdev",
+ severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level],
+ private: packet.private,
+ filterDuplicates: true,
+ location: {
+ url: packet.filename,
+ line: packet.lineNumber,
+ },
+ };
+
+ Messages.Simple.call(this, null, options);
+
+ this._repeatID.consoleApiLevel = packet.level;
+ this._stacktrace = this._repeatID.stacktrace = packet.stacktrace;
+ this._arguments = packet.arguments;
+};
+
+Messages.ConsoleTrace.prototype = extend(Messages.Simple.prototype, {
+ /**
+ * Holds the stackframes received from the server.
+ *
+ * @private
+ * @type array
+ */
+ _stacktrace: null,
+
+ /**
+ * Holds the arguments the content script passed to the console.trace()
+ * method. This array is cleared when the message is initialized, and
+ * associated actors are released.
+ *
+ * @private
+ * @type array
+ */
+ _arguments: null,
+
+ init: function ()
+ {
+ let result = Messages.Simple.prototype.init.apply(this, arguments);
+
+ // We ignore console.trace() arguments. Release object actors.
+ if (Array.isArray(this._arguments)) {
+ for (let arg of this._arguments) {
+ if (WebConsoleUtils.isActorGrip(arg)) {
+ this.output._releaseObject(arg.actor);
+ }
+ }
+ }
+ this._arguments = null;
+
+ return result;
+ },
+
+ render: function () {
+ this._message = this._renderMessage();
+ this._attachment = this._renderStack();
+
+ Messages.Simple.prototype.render.apply(this, arguments);
+ this.element.setAttribute("open", true);
+ return this;
+ },
+
+ /**
+ * Render the console messageNode
+ */
+ _renderMessage: function () {
+ let cmvar = this.document.createElementNS(XHTML_NS, "span");
+ cmvar.className = "cm-variable";
+ cmvar.textContent = "console";
+
+ let cmprop = this.document.createElementNS(XHTML_NS, "span");
+ cmprop.className = "cm-property";
+ cmprop.textContent = "trace";
+
+ let frag = this.document.createDocumentFragment();
+ frag.appendChild(cmvar);
+ frag.appendChild(this.document.createTextNode("."));
+ frag.appendChild(cmprop);
+ frag.appendChild(this.document.createTextNode("():"));
+
+ return frag;
+ },
+
+ /**
+ * Render the stack frames.
+ *
+ * @private
+ * @return DOMElement
+ */
+ _renderStack: function () {
+ return new Widgets.Stacktrace(this, this._stacktrace).render().element;
+ },
+}); // Messages.ConsoleTrace.prototype
+
+/**
+ * The ConsoleTable message is used for console.table() calls.
+ *
+ * @constructor
+ * @extends Messages.Extended
+ * @param object packet
+ * The Console API call packet received from the server.
+ */
+Messages.ConsoleTable = function (packet)
+{
+ let options = {
+ className: "cm-s-mozilla",
+ timestamp: packet.timeStamp,
+ category: packet.category || "webdev",
+ severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level],
+ private: packet.private,
+ filterDuplicates: false,
+ location: {
+ url: packet.filename,
+ line: packet.lineNumber,
+ },
+ };
+
+ this._populateTableData = this._populateTableData.bind(this);
+ this._renderMessage = this._renderMessage.bind(this);
+ Messages.Extended.call(this, [this._renderMessage], options);
+
+ this._repeatID.consoleApiLevel = packet.level;
+ this._arguments = packet.arguments;
+};
+
+Messages.ConsoleTable.prototype = extend(Messages.Extended.prototype, {
+ /**
+ * Holds the arguments the content script passed to the console.table()
+ * method.
+ *
+ * @private
+ * @type array
+ */
+ _arguments: null,
+
+ /**
+ * Array of objects that holds the data to log in the table.
+ *
+ * @private
+ * @type array
+ */
+ _data: null,
+
+ /**
+ * Key value pair of the id and display name for the columns in the table.
+ * Refer to the TableWidget API.
+ *
+ * @private
+ * @type object
+ */
+ _columns: null,
+
+ /**
+ * A promise that resolves when the table data is ready or null if invalid
+ * arguments are provided.
+ *
+ * @private
+ * @type promise|null
+ */
+ _populatePromise: null,
+
+ init: function ()
+ {
+ let result = Messages.Extended.prototype.init.apply(this, arguments);
+ this._data = [];
+ this._columns = {};
+
+ this._populatePromise = this._populateTableData();
+
+ return result;
+ },
+
+ /**
+ * Sets the key value pair of the id and display name for the columns in the
+ * table.
+ *
+ * @private
+ * @param array|string columns
+ * Either a string or array containing the names for the columns in
+ * the output table.
+ */
+ _setColumns: function (columns)
+ {
+ if (columns.class == "Array") {
+ let items = columns.preview.items;
+
+ for (let item of items) {
+ if (typeof item == "string") {
+ this._columns[item] = item;
+ }
+ }
+ } else if (typeof columns == "string" && columns) {
+ this._columns[columns] = columns;
+ }
+ },
+
+ /**
+ * Retrieves the table data and columns from the arguments received from the
+ * server.
+ *
+ * @return Promise|null
+ * Returns a promise that resolves when the table data is ready or
+ * null if the arguments are invalid.
+ */
+ _populateTableData: function ()
+ {
+ let deferred = promise.defer();
+
+ if (this._arguments.length <= 0) {
+ return;
+ }
+
+ let data = this._arguments[0];
+ if (data.class != "Array" && data.class != "Object" &&
+ data.class != "Map" && data.class != "Set" &&
+ data.class != "WeakMap" && data.class != "WeakSet") {
+ return;
+ }
+
+ let hasColumnsArg = false;
+ if (this._arguments.length > 1) {
+ if (data.class == "Object" || data.class == "Array") {
+ this._columns["_index"] = l10n.getStr("table.index");
+ } else {
+ this._columns["_index"] = l10n.getStr("table.iterationIndex");
+ }
+
+ this._setColumns(this._arguments[1]);
+ hasColumnsArg = true;
+ }
+
+ if (data.class == "Object" || data.class == "Array") {
+ // Get the object properties, and parse the key and value properties into
+ // the table data and columns.
+ this.client = new ObjectClient(this.output.owner.jsterm.hud.proxy.client,
+ data);
+ this.client.getPrototypeAndProperties(aResponse => {
+ let {ownProperties} = aResponse;
+ let rowCount = 0;
+ let columnCount = 0;
+
+ for (let index of Object.keys(ownProperties || {})) {
+ // Avoid outputting the length property if the data argument provided
+ // is an array
+ if (data.class == "Array" && index == "length") {
+ continue;
+ }
+
+ if (!hasColumnsArg) {
+ this._columns["_index"] = l10n.getStr("table.index");
+ }
+
+ if (data.class == "Array") {
+ if (index == parseInt(index)) {
+ index = parseInt(index);
+ }
+ }
+
+ let property = ownProperties[index].value;
+ let item = { _index: index };
+
+ if (property.class == "Object" || property.class == "Array") {
+ let {preview} = property;
+ let entries = property.class == "Object" ?
+ preview.ownProperties : preview.items;
+
+ for (let key of Object.keys(entries)) {
+ let value = property.class == "Object" ?
+ preview.ownProperties[key].value : preview.items[key];
+
+ item[key] = this._renderValueGrip(value, { concise: true });
+
+ if (!hasColumnsArg && !(key in this._columns) &&
+ (++columnCount <= TABLE_COLUMN_MAX_ITEMS)) {
+ this._columns[key] = key;
+ }
+ }
+ } else {
+ // Display the value for any non-object data input.
+ item["_value"] = this._renderValueGrip(property, { concise: true });
+
+ if (!hasColumnsArg && !("_value" in this._columns)) {
+ this._columns["_value"] = l10n.getStr("table.value");
+ }
+ }
+
+ this._data.push(item);
+
+ if (++rowCount == TABLE_ROW_MAX_ITEMS) {
+ break;
+ }
+ }
+
+ deferred.resolve();
+ });
+ } else if (data.class == "Map" || data.class == "WeakMap") {
+ let entries = data.preview.entries;
+
+ if (!hasColumnsArg) {
+ this._columns["_index"] = l10n.getStr("table.iterationIndex");
+ this._columns["_key"] = l10n.getStr("table.key");
+ this._columns["_value"] = l10n.getStr("table.value");
+ }
+
+ let rowCount = 0;
+ for (let [key, value] of entries) {
+ let item = {
+ _index: rowCount,
+ _key: this._renderValueGrip(key, { concise: true }),
+ _value: this._renderValueGrip(value, { concise: true })
+ };
+
+ this._data.push(item);
+
+ if (++rowCount == TABLE_ROW_MAX_ITEMS) {
+ break;
+ }
+ }
+
+ deferred.resolve();
+ } else if (data.class == "Set" || data.class == "WeakSet") {
+ let entries = data.preview.items;
+
+ if (!hasColumnsArg) {
+ this._columns["_index"] = l10n.getStr("table.iterationIndex");
+ this._columns["_value"] = l10n.getStr("table.value");
+ }
+
+ let rowCount = 0;
+ for (let entry of entries) {
+ let item = {
+ _index : rowCount,
+ _value: this._renderValueGrip(entry, { concise: true })
+ };
+
+ this._data.push(item);
+
+ if (++rowCount == TABLE_ROW_MAX_ITEMS) {
+ break;
+ }
+ }
+
+ deferred.resolve();
+ }
+
+ return deferred.promise;
+ },
+
+ render: function ()
+ {
+ this._attachment = this._renderTable();
+ Messages.Extended.prototype.render.apply(this, arguments);
+ this.element.setAttribute("open", true);
+ return this;
+ },
+
+ _renderMessage: function () {
+ let cmvar = this.document.createElementNS(XHTML_NS, "span");
+ cmvar.className = "cm-variable";
+ cmvar.textContent = "console";
+
+ let cmprop = this.document.createElementNS(XHTML_NS, "span");
+ cmprop.className = "cm-property";
+ cmprop.textContent = "table";
+
+ let frag = this.document.createDocumentFragment();
+ frag.appendChild(cmvar);
+ frag.appendChild(this.document.createTextNode("."));
+ frag.appendChild(cmprop);
+ frag.appendChild(this.document.createTextNode("():"));
+
+ return frag;
+ },
+
+ /**
+ * Render the table.
+ *
+ * @private
+ * @return DOMElement
+ */
+ _renderTable: function () {
+ let result = this.document.createElementNS(XHTML_NS, "div");
+
+ if (this._populatePromise) {
+ this._populatePromise.then(() => {
+ if (this._data.length > 0) {
+ let widget = new Widgets.Table(this, this._data, this._columns).render();
+ result.appendChild(widget.element);
+ }
+
+ result.scrollIntoView();
+ this.output.owner.emit("messages-table-rendered");
+
+ // Release object actors
+ if (Array.isArray(this._arguments)) {
+ for (let arg of this._arguments) {
+ if (WebConsoleUtils.isActorGrip(arg)) {
+ this.output._releaseObject(arg.actor);
+ }
+ }
+ }
+ this._arguments = null;
+ });
+ }
+
+ return result;
+ },
+}); // Messages.ConsoleTable.prototype
+
+var Widgets = {};
+
+/**
+ * The base widget class.
+ *
+ * @constructor
+ * @param object message
+ * The owning message.
+ */
+Widgets.BaseWidget = function (message)
+{
+ this.message = message;
+};
+
+Widgets.BaseWidget.prototype = {
+ /**
+ * The owning message object.
+ * @type object
+ */
+ message: null,
+
+ /**
+ * The DOM element of the rendered widget.
+ * @type Element
+ */
+ element: null,
+
+ /**
+ * Getter for the DOM document that holds the output.
+ * @type Document
+ */
+ get document() {
+ return this.message.document;
+ },
+
+ /**
+ * The ConsoleOutput instance that owns this widget instance.
+ */
+ get output() {
+ return this.message.output;
+ },
+
+ /**
+ * Render the widget DOM element.
+ * @return this
+ */
+ render: function () { },
+
+ /**
+ * Destroy this widget instance.
+ */
+ destroy: function () { },
+
+ /**
+ * Helper for creating DOM elements for widgets.
+ *
+ * Usage:
+ * this.el("tag#id.class.names"); // create element "tag" with ID "id" and
+ * two class names, .class and .names.
+ *
+ * this.el("span", { attr1: "value1", ... }) // second argument can be an
+ * object that holds element attributes and values for the new DOM element.
+ *
+ * this.el("p", { attr1: "value1", ... }, "text content"); // the third
+ * argument can include the default .textContent of the new DOM element.
+ *
+ * this.el("p", "text content"); // if the second argument is not an object,
+ * it will be used as .textContent for the new DOM element.
+ *
+ * @param string tagNameIdAndClasses
+ * Tag name for the new element, optionally followed by an ID and/or
+ * class names. Examples: "span", "div#fooId", "div.class.names",
+ * "p#id.class".
+ * @param string|object [attributesOrTextContent]
+ * If this argument is an object it will be used to set the attributes
+ * of the new DOM element. Otherwise, the value becomes the
+ * .textContent of the new DOM element.
+ * @param string [textContent]
+ * If this argument is provided the value is used as the textContent of
+ * the new DOM element.
+ * @return DOMElement
+ * The new DOM element.
+ */
+ el: function (tagNameIdAndClasses)
+ {
+ let attrs, text;
+ if (typeof arguments[1] == "object") {
+ attrs = arguments[1];
+ text = arguments[2];
+ } else {
+ text = arguments[1];
+ }
+
+ let tagName = tagNameIdAndClasses.split(/#|\./)[0];
+
+ let elem = this.document.createElementNS(XHTML_NS, tagName);
+ for (let name of Object.keys(attrs || {})) {
+ elem.setAttribute(name, attrs[name]);
+ }
+ if (text !== undefined && text !== null) {
+ elem.textContent = text;
+ }
+
+ let idAndClasses = tagNameIdAndClasses.match(/([#.][^#.]+)/g);
+ for (let idOrClass of (idAndClasses || [])) {
+ if (idOrClass.charAt(0) == "#") {
+ elem.id = idOrClass.substr(1);
+ } else {
+ elem.classList.add(idOrClass.substr(1));
+ }
+ }
+
+ return elem;
+ },
+};
+
+/**
+ * The timestamp widget.
+ *
+ * @constructor
+ * @param object message
+ * The owning message.
+ * @param number timestamp
+ * The UNIX timestamp to display.
+ */
+Widgets.MessageTimestamp = function (message, timestamp)
+{
+ Widgets.BaseWidget.call(this, message);
+ this.timestamp = timestamp;
+};
+
+Widgets.MessageTimestamp.prototype = extend(Widgets.BaseWidget.prototype, {
+ /**
+ * The UNIX timestamp.
+ * @type number
+ */
+ timestamp: 0,
+
+ render: function ()
+ {
+ if (this.element) {
+ return this;
+ }
+
+ this.element = this.document.createElementNS(XHTML_NS, "span");
+ this.element.className = "timestamp devtools-monospace";
+ this.element.textContent = l10n.timestampString(this.timestamp) + " ";
+
+ return this;
+ },
+}); // Widgets.MessageTimestamp.prototype
+
+
+/**
+ * The URLString widget, for rendering strings where at least one token is a
+ * URL.
+ *
+ * @constructor
+ * @param object message
+ * The owning message.
+ * @param string str
+ * The string, which contains at least one valid URL.
+ * @param string unshortenedStr
+ * The unshortened form of the string, if it was shortened.
+ */
+Widgets.URLString = function (message, str, unshortenedStr)
+{
+ Widgets.BaseWidget.call(this, message);
+ this.str = str;
+ this.unshortenedStr = unshortenedStr;
+};
+
+Widgets.URLString.prototype = extend(Widgets.BaseWidget.prototype, {
+ /**
+ * The string to format, which contains at least one valid URL.
+ * @type string
+ */
+ str: "",
+
+ render: function ()
+ {
+ if (this.element) {
+ return this;
+ }
+
+ // The rendered URLString will be a <span> containing a number of text
+ // <spans> for non-URL tokens and <a>'s for URL tokens.
+ this.element = this.el("span", {
+ class: "console-string"
+ });
+ this.element.appendChild(this._renderText("\""));
+
+ // As we walk through the tokens of the source string, we make sure to preserve
+ // the original whitespace that separated the tokens.
+ let tokens = this.str.split(/\s+/);
+ let textStart = 0;
+ let tokenStart;
+ for (let i = 0; i < tokens.length; i++) {
+ let token = tokens[i];
+ let unshortenedToken;
+ tokenStart = this.str.indexOf(token, textStart);
+ if (this._isURL(token)) {
+ // The last URL in the string might be shortened. If so, get the
+ // real URL so the rendered link can point to it.
+ if (i === tokens.length - 1 && this.unshortenedStr) {
+ unshortenedToken = this.unshortenedStr.slice(tokenStart).split(/\s+/, 1)[0];
+ }
+ this.element.appendChild(this._renderText(this.str.slice(textStart, tokenStart)));
+ textStart = tokenStart + token.length;
+ this.element.appendChild(this._renderURL(token, unshortenedToken));
+ }
+ }
+
+ // Clean up any non-URL text at the end of the source string.
+ this.element.appendChild(this._renderText(this.str.slice(textStart, this.str.length)));
+ this.element.appendChild(this._renderText("\""));
+
+ return this;
+ },
+
+ /**
+ * Determines whether a grip is a string containing a URL.
+ *
+ * @param string grip
+ * The grip, which may contain a URL.
+ * @return boolean
+ * Whether the grip is a string containing a URL.
+ */
+ containsURL: function (grip)
+ {
+ if (typeof grip != "string") {
+ return false;
+ }
+
+ let tokens = grip.split(/\s+/);
+ return tokens.some(this._isURL);
+ },
+
+ /**
+ * Determines whether a string token is a valid URL.
+ *
+ * @param string token
+ * The token.
+ * @return boolean
+ * Whenther the token is a URL.
+ */
+ _isURL: function (token) {
+ try {
+ if (!validProtocols.test(token)) {
+ return false;
+ }
+ new URL(token);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ },
+
+ /**
+ * Renders a string as a URL.
+ *
+ * @param string url
+ * The string to be rendered as a url.
+ * @param string fullUrl
+ * The unshortened form of the URL, if it was shortened.
+ * @return DOMElement
+ * An element containing the rendered string.
+ */
+ _renderURL: function (url, fullUrl)
+ {
+ let unshortened = fullUrl || url;
+ let result = this.el("a", {
+ class: "url",
+ title: unshortened,
+ href: unshortened,
+ draggable: false
+ }, url);
+ this.message._addLinkCallback(result);
+ return result;
+ },
+
+ _renderText: function (text) {
+ return this.el("span", text);
+ },
+}); // Widgets.URLString.prototype
+
+/**
+ * Widget used for displaying ObjectActors that have no specialised renderers.
+ *
+ * @constructor
+ * @param object message
+ * The owning message.
+ * @param object objectActor
+ * The ObjectActor to display.
+ * @param object [options]
+ * Options for displaying the given ObjectActor. See
+ * Messages.Extended.prototype._renderValueGrip for the available
+ * options.
+ */
+Widgets.JSObject = function (message, objectActor, options = {})
+{
+ Widgets.BaseWidget.call(this, message);
+ this.objectActor = objectActor;
+ this.options = options;
+ this._onClick = this._onClick.bind(this);
+};
+
+Widgets.JSObject.prototype = extend(Widgets.BaseWidget.prototype, {
+ /**
+ * The ObjectActor displayed by the widget.
+ * @type object
+ */
+ objectActor: null,
+
+ render: function ()
+ {
+ if (!this.element) {
+ this._render();
+ }
+
+ return this;
+ },
+
+ _render: function ()
+ {
+ let str = VariablesView.getString(this.objectActor, this.options);
+ let className = this.message.getClassNameForValueGrip(this.objectActor);
+ if (!className && this.objectActor.class == "Object") {
+ className = "cm-variable";
+ }
+
+ this.element = this._anchor(str, { className: className });
+ },
+
+ /**
+ * Render a concise representation of an object.
+ */
+ _renderConciseObject: function ()
+ {
+ this.element = this._anchor(this.objectActor.class,
+ { className: "cm-variable" });
+ },
+
+ /**
+ * Render the `<class> { ` prefix of an object.
+ */
+ _renderObjectPrefix: function ()
+ {
+ let { kind } = this.objectActor.preview;
+ this.element = this.el("span.kind-" + kind);
+ this._anchor(this.objectActor.class, { className: "cm-variable" });
+ this._text(" { ");
+ },
+
+ /**
+ * Render the ` }` suffix of an object.
+ */
+ _renderObjectSuffix: function ()
+ {
+ this._text(" }");
+ },
+
+ /**
+ * Render an object property.
+ *
+ * @param String key
+ * The property name.
+ * @param Object value
+ * The property value, as an RDP grip.
+ * @param nsIDOMNode container
+ * The container node to render to.
+ * @param Boolean needsComma
+ * True if there was another property before this one and we need to
+ * separate them with a comma.
+ * @param Boolean valueIsText
+ * Add the value as is, don't treat it as a grip and pass it to
+ * `_renderValueGrip`.
+ */
+ _renderObjectProperty: function (key, value, container, needsComma, valueIsText = false)
+ {
+ if (needsComma) {
+ this._text(", ");
+ }
+
+ container.appendChild(this.el("span.cm-property", key));
+ this._text(": ");
+
+ if (valueIsText) {
+ this._text(value);
+ } else {
+ let valueElem = this.message._renderValueGrip(value, { concise: true, shorten: true });
+ container.appendChild(valueElem);
+ }
+ },
+
+ /**
+ * Render this object's properties.
+ *
+ * @param nsIDOMNode container
+ * The container node to render to.
+ * @param Boolean needsComma
+ * True if there was another property before this one and we need to
+ * separate them with a comma.
+ */
+ _renderObjectProperties: function (container, needsComma)
+ {
+ let { preview } = this.objectActor;
+ let { ownProperties, safeGetterValues } = preview;
+
+ let shown = 0;
+
+ let getValue = desc => {
+ if (desc.get) {
+ return "Getter";
+ } else if (desc.set) {
+ return "Setter";
+ } else {
+ return desc.value;
+ }
+ };
+
+ for (let key of Object.keys(ownProperties || {})) {
+ this._renderObjectProperty(key, getValue(ownProperties[key]), container,
+ shown > 0 || needsComma,
+ ownProperties[key].get || ownProperties[key].set);
+ shown++;
+ }
+
+ let ownPropertiesShown = shown;
+
+ for (let key of Object.keys(safeGetterValues || {})) {
+ this._renderObjectProperty(key, safeGetterValues[key].getterValue,
+ container, shown > 0 || needsComma);
+ shown++;
+ }
+
+ if (typeof preview.ownPropertiesLength == "number" &&
+ ownPropertiesShown < preview.ownPropertiesLength) {
+ this._text(", ");
+
+ let n = preview.ownPropertiesLength - ownPropertiesShown;
+ let str = VariablesView.stringifiers._getNMoreString(n);
+ this._anchor(str);
+ }
+ },
+
+ /**
+ * Render an anchor with a given text content and link.
+ *
+ * @private
+ * @param string text
+ * Text to show in the anchor.
+ * @param object [options]
+ * Available options:
+ * - onClick (function): "click" event handler.By default a click on
+ * the anchor opens the variables view for the current object actor
+ * (this.objectActor).
+ * - href (string): if given the string is used as a link, and clicks
+ * on the anchor open the link in a new tab.
+ * - appendTo (DOMElement): append the element to the given DOM
+ * element. If not provided, the anchor is appended to |this.element|
+ * if it is available. If |appendTo| is provided and if it is a falsy
+ * value, the anchor is not appended to any element.
+ * @return DOMElement
+ * The DOM element of the new anchor.
+ */
+ _anchor: function (text, options = {})
+ {
+ if (!options.onClick) {
+ // If the anchor has an URL, open it in a new tab. If not, show the
+ // current object actor.
+ options.onClick = options.href ? this._onClickAnchor : this._onClick;
+ }
+
+ options.onContextMenu = options.onContextMenu || this._onContextMenu;
+
+ let anchor = this.el("a", {
+ class: options.className,
+ draggable: false,
+ href: options.href || "#",
+ }, text);
+
+ this.message._addLinkCallback(anchor, options.onClick);
+
+ anchor.addEventListener("contextmenu", options.onContextMenu.bind(this));
+
+ if (options.appendTo) {
+ options.appendTo.appendChild(anchor);
+ } else if (!("appendTo" in options) && this.element) {
+ this.element.appendChild(anchor);
+ }
+
+ return anchor;
+ },
+
+ openObjectInVariablesView: function ()
+ {
+ this.output.openVariablesView({
+ label: VariablesView.getString(this.objectActor, { concise: true }),
+ objectActor: this.objectActor,
+ autofocus: true,
+ });
+ },
+
+ storeObjectInWindow: function ()
+ {
+ let evalString = `{ let i = 0;
+ while (this.hasOwnProperty("temp" + i) && i < 1000) {
+ i++;
+ }
+ this["temp" + i] = _self;
+ "temp" + i;
+ }`;
+ let options = {
+ selectedObjectActor: this.objectActor.actor,
+ };
+
+ this.output.owner.jsterm.requestEvaluation(evalString, options).then((res) => {
+ this.output.owner.jsterm.focus();
+ this.output.owner.jsterm.setInputValue(res.result);
+ });
+ },
+
+ /**
+ * The click event handler for objects shown inline.
+ * @private
+ */
+ _onClick: function ()
+ {
+ this.openObjectInVariablesView();
+ },
+
+ _onContextMenu: function (ev) {
+ // TODO offer a nice API for the context menu.
+ // Probably worth to take a look at Firebug's way
+ // https://github.com/firebug/firebug/blob/master/extension/content/firebug/chrome/menu.js
+ let doc = ev.target.ownerDocument;
+ let cmPopup = doc.getElementById("output-contextmenu");
+
+ let openInVarViewCmd = doc.getElementById("menu_openInVarView");
+ let openVarView = this.openObjectInVariablesView.bind(this);
+ openInVarViewCmd.addEventListener("command", openVarView);
+ openInVarViewCmd.removeAttribute("disabled");
+ cmPopup.addEventListener("popuphiding", function onPopupHiding() {
+ cmPopup.removeEventListener("popuphiding", onPopupHiding);
+ openInVarViewCmd.removeEventListener("command", openVarView);
+ openInVarViewCmd.setAttribute("disabled", "true");
+ });
+
+ // 'Store as global variable' command isn't supported on pre-44 servers,
+ // so remove it from the menu in that case.
+ let storeInGlobalCmd = doc.getElementById("menu_storeAsGlobal");
+ if (!this.output.webConsoleClient.traits.selectedObjectActor) {
+ storeInGlobalCmd.remove();
+ } else if (storeInGlobalCmd) {
+ let storeObjectInWindow = this.storeObjectInWindow.bind(this);
+ storeInGlobalCmd.addEventListener("command", storeObjectInWindow);
+ storeInGlobalCmd.removeAttribute("disabled");
+ cmPopup.addEventListener("popuphiding", function onPopupHiding() {
+ cmPopup.removeEventListener("popuphiding", onPopupHiding);
+ storeInGlobalCmd.removeEventListener("command", storeObjectInWindow);
+ storeInGlobalCmd.setAttribute("disabled", "true");
+ });
+ }
+ },
+
+ /**
+ * Add a string to the message.
+ *
+ * @private
+ * @param string str
+ * String to add.
+ * @param DOMElement [target = this.element]
+ * Optional DOM element to append the string to. The default is
+ * this.element.
+ */
+ _text: function (str, target = this.element)
+ {
+ target.appendChild(this.document.createTextNode(str));
+ },
+}); // Widgets.JSObject.prototype
+
+Widgets.ObjectRenderers = {};
+Widgets.ObjectRenderers.byKind = {};
+Widgets.ObjectRenderers.byClass = {};
+
+/**
+ * Add an object renderer.
+ *
+ * @param object obj
+ * An object that represents the renderer. Properties:
+ * - byClass (string, optional): this renderer will be used for the given
+ * object class.
+ * - byKind (string, optional): this renderer will be used for the given
+ * object kind.
+ * One of byClass or byKind must be provided.
+ * - extends (object, optional): the renderer object extends the given
+ * object. Default: Widgets.JSObject.
+ * - canRender (function, optional): this method is invoked when
+ * a candidate object needs to be displayed. The method is invoked as
+ * a static method, as such, none of the properties of the renderer
+ * object will be available. You get one argument: the object actor grip
+ * received from the server. If the method returns true, then this
+ * renderer is used for displaying the object, otherwise not.
+ * - initialize (function, optional): the constructor of the renderer
+ * widget. This function is invoked with the following arguments: the
+ * owner message object instance, the object actor grip to display, and
+ * an options object. See Messages.Extended.prototype._renderValueGrip()
+ * for details about the options object.
+ * - render (function, required): the method that displays the given
+ * object actor.
+ */
+Widgets.ObjectRenderers.add = function (obj)
+{
+ let extendObj = obj.extends || Widgets.JSObject;
+
+ let constructor = function () {
+ if (obj.initialize) {
+ obj.initialize.apply(this, arguments);
+ } else {
+ extendObj.apply(this, arguments);
+ }
+ };
+
+ let proto = WebConsoleUtils.cloneObject(obj, false, function (key) {
+ if (key == "initialize" || key == "canRender" ||
+ (key == "render" && extendObj === Widgets.JSObject)) {
+ return false;
+ }
+ return true;
+ });
+
+ if (extendObj === Widgets.JSObject) {
+ proto._render = obj.render;
+ }
+
+ constructor.canRender = obj.canRender;
+ constructor.prototype = extend(extendObj.prototype, proto);
+
+ if (obj.byClass) {
+ Widgets.ObjectRenderers.byClass[obj.byClass] = constructor;
+ } else if (obj.byKind) {
+ Widgets.ObjectRenderers.byKind[obj.byKind] = constructor;
+ } else {
+ throw new Error("You are adding an object renderer without any byClass or " +
+ "byKind property.");
+ }
+};
+
+
+/**
+ * The widget used for displaying Date objects.
+ */
+Widgets.ObjectRenderers.add({
+ byClass: "Date",
+
+ render: function ()
+ {
+ let {preview} = this.objectActor;
+ this.element = this.el("span.class-" + this.objectActor.class);
+
+ let anchorText = this.objectActor.class;
+ let anchorClass = "cm-variable";
+ if (preview && "timestamp" in preview && typeof preview.timestamp != "number") {
+ anchorText = new Date(preview.timestamp).toString(); // invalid date
+ anchorClass = "";
+ }
+
+ this._anchor(anchorText, { className: anchorClass });
+
+ if (!preview || !("timestamp" in preview) || typeof preview.timestamp != "number") {
+ return;
+ }
+
+ this._text(" ");
+
+ let elem = this.el("span.cm-string-2", new Date(preview.timestamp).toISOString());
+ this.element.appendChild(elem);
+ },
+});
+
+/**
+ * The widget used for displaying Function objects.
+ */
+Widgets.ObjectRenderers.add({
+ byClass: "Function",
+
+ render: function ()
+ {
+ let grip = this.objectActor;
+ this.element = this.el("span.class-" + this.objectActor.class);
+
+ // TODO: Bug 948484 - support arrow functions and ES6 generators
+ let name = grip.userDisplayName || grip.displayName || grip.name || "";
+ name = VariablesView.getString(name, { noStringQuotes: true });
+
+ let str = this.options.concise ? name || "function " : "function " + name;
+
+ if (this.options.concise) {
+ this._anchor(name || "function", {
+ className: name ? "cm-variable" : "cm-keyword",
+ });
+ if (!name) {
+ this._text(" ");
+ }
+ } else if (name) {
+ this.element.appendChild(this.el("span.cm-keyword", "function"));
+ this._text(" ");
+ this._anchor(name, { className: "cm-variable" });
+ } else {
+ this._anchor("function", { className: "cm-keyword" });
+ this._text(" ");
+ }
+
+ this._text("(");
+
+ // TODO: Bug 948489 - Support functions with destructured parameters and
+ // rest parameters
+ let params = grip.parameterNames || [];
+ let shown = 0;
+ for (let param of params) {
+ if (shown > 0) {
+ this._text(", ");
+ }
+ this.element.appendChild(this.el("span.cm-def", param));
+ shown++;
+ }
+
+ this._text(")");
+ },
+
+ _onClick: function () {
+ let location = this.objectActor.location;
+ if (location && IGNORED_SOURCE_URLS.indexOf(location.url) === -1) {
+ this.output.openLocationInDebugger(location);
+ }
+ else {
+ this.openObjectInVariablesView();
+ }
+ }
+}); // Widgets.ObjectRenderers.byClass.Function
+
+/**
+ * The widget used for displaying ArrayLike objects.
+ */
+Widgets.ObjectRenderers.add({
+ byKind: "ArrayLike",
+
+ render: function ()
+ {
+ let {preview} = this.objectActor;
+ let {items} = preview;
+ this.element = this.el("span.kind-" + preview.kind);
+
+ this._anchor(this.objectActor.class, { className: "cm-variable" });
+
+ if (!items || this.options.concise) {
+ this._text("[");
+ this.element.appendChild(this.el("span.cm-number", preview.length));
+ this._text("]");
+ return this;
+ }
+
+ this._text(" [ ");
+
+ let isFirst = true;
+ let emptySlots = 0;
+ // A helper that renders a comma between items if isFirst == false.
+ let renderSeparator = () => !isFirst && this._text(", ");
+
+ for (let item of items) {
+ if (item === null) {
+ emptySlots++;
+ }
+ else {
+ renderSeparator();
+ isFirst = false;
+
+ if (emptySlots) {
+ this._renderEmptySlots(emptySlots);
+ emptySlots = 0;
+ }
+
+ let elem = this.message._renderValueGrip(item, { concise: true, shorten: true });
+ this.element.appendChild(elem);
+ }
+ }
+
+ if (emptySlots) {
+ renderSeparator();
+ this._renderEmptySlots(emptySlots, false);
+ }
+
+ let shown = items.length;
+ if (shown < preview.length) {
+ this._text(", ");
+
+ let n = preview.length - shown;
+ let str = VariablesView.stringifiers._getNMoreString(n);
+ this._anchor(str);
+ }
+
+ this._text(" ]");
+ },
+
+ _renderEmptySlots: function (aNumSlots, aAppendComma = true) {
+ let slotLabel = l10n.getStr("emptySlotLabel");
+ let slotText = PluralForm.get(aNumSlots, slotLabel);
+ this._text("<" + slotText.replace("#1", aNumSlots) + ">");
+ if (aAppendComma) {
+ this._text(", ");
+ }
+ },
+
+}); // Widgets.ObjectRenderers.byKind.ArrayLike
+
+/**
+ * The widget used for displaying MapLike objects.
+ */
+Widgets.ObjectRenderers.add({
+ byKind: "MapLike",
+
+ render: function ()
+ {
+ let {preview} = this.objectActor;
+ let {entries} = preview;
+
+ let container = this.element = this.el("span.kind-" + preview.kind);
+ this._anchor(this.objectActor.class, { className: "cm-variable" });
+
+ if (!entries || this.options.concise) {
+ if (typeof preview.size == "number") {
+ this._text("[");
+ container.appendChild(this.el("span.cm-number", preview.size));
+ this._text("]");
+ }
+ return;
+ }
+
+ this._text(" { ");
+
+ let shown = 0;
+ for (let [key, value] of entries) {
+ if (shown > 0) {
+ this._text(", ");
+ }
+
+ let keyElem = this.message._renderValueGrip(key, {
+ concise: true,
+ noStringQuotes: true,
+ });
+
+ // Strings are property names.
+ if (keyElem.classList && keyElem.classList.contains("console-string")) {
+ keyElem.classList.remove("console-string");
+ keyElem.classList.add("cm-property");
+ }
+
+ container.appendChild(keyElem);
+
+ this._text(": ");
+
+ let valueElem = this.message._renderValueGrip(value, { concise: true });
+ container.appendChild(valueElem);
+
+ shown++;
+ }
+
+ if (typeof preview.size == "number" && shown < preview.size) {
+ this._text(", ");
+
+ let n = preview.size - shown;
+ let str = VariablesView.stringifiers._getNMoreString(n);
+ this._anchor(str);
+ }
+
+ this._text(" }");
+ },
+}); // Widgets.ObjectRenderers.byKind.MapLike
+
+/**
+ * The widget used for displaying objects with a URL.
+ */
+Widgets.ObjectRenderers.add({
+ byKind: "ObjectWithURL",
+
+ render: function ()
+ {
+ this.element = this._renderElement(this.objectActor,
+ this.objectActor.preview.url);
+ },
+
+ _renderElement: function (objectActor, url)
+ {
+ let container = this.el("span.kind-" + objectActor.preview.kind);
+
+ this._anchor(objectActor.class, {
+ className: "cm-variable",
+ appendTo: container,
+ });
+
+ if (!VariablesView.isFalsy({ value: url })) {
+ this._text(" \u2192 ", container);
+ let shortUrl = getSourceNames(url)[this.options.concise ? "short" : "long"];
+ this._anchor(shortUrl, { href: url, appendTo: container });
+ }
+
+ return container;
+ },
+}); // Widgets.ObjectRenderers.byKind.ObjectWithURL
+
+/**
+ * The widget used for displaying objects with a string next to them.
+ */
+Widgets.ObjectRenderers.add({
+ byKind: "ObjectWithText",
+
+ render: function ()
+ {
+ let {preview} = this.objectActor;
+ this.element = this.el("span.kind-" + preview.kind);
+
+ this._anchor(this.objectActor.class, { className: "cm-variable" });
+
+ if (!this.options.concise) {
+ this._text(" ");
+ this.element.appendChild(this.el("span.theme-fg-color6",
+ VariablesView.getString(preview.text)));
+ }
+ },
+});
+
+/**
+ * The widget used for displaying DOM event previews.
+ */
+Widgets.ObjectRenderers.add({
+ byKind: "DOMEvent",
+
+ render: function ()
+ {
+ let {preview} = this.objectActor;
+
+ let container = this.element = this.el("span.kind-" + preview.kind);
+
+ this._anchor(preview.type || this.objectActor.class,
+ { className: "cm-variable" });
+
+ if (this.options.concise) {
+ return;
+ }
+
+ if (preview.eventKind == "key" && preview.modifiers &&
+ preview.modifiers.length) {
+ this._text(" ");
+
+ let mods = 0;
+ for (let mod of preview.modifiers) {
+ if (mods > 0) {
+ this._text("-");
+ }
+ container.appendChild(this.el("span.cm-keyword", mod));
+ mods++;
+ }
+ }
+
+ this._text(" { ");
+
+ let shown = 0;
+ if (preview.target) {
+ container.appendChild(this.el("span.cm-property", "target"));
+ this._text(": ");
+ let target = this.message._renderValueGrip(preview.target, { concise: true });
+ container.appendChild(target);
+ shown++;
+ }
+
+ for (let key of Object.keys(preview.properties || {})) {
+ if (shown > 0) {
+ this._text(", ");
+ }
+
+ container.appendChild(this.el("span.cm-property", key));
+ this._text(": ");
+
+ let value = preview.properties[key];
+ let valueElem = this.message._renderValueGrip(value, { concise: true });
+ container.appendChild(valueElem);
+
+ shown++;
+ }
+
+ this._text(" }");
+ },
+}); // Widgets.ObjectRenderers.byKind.DOMEvent
+
+/**
+ * The widget used for displaying DOM node previews.
+ */
+Widgets.ObjectRenderers.add({
+ byKind: "DOMNode",
+
+ canRender: function (objectActor) {
+ let {preview} = objectActor;
+ if (!preview) {
+ return false;
+ }
+
+ switch (preview.nodeType) {
+ case nodeConstants.DOCUMENT_NODE:
+ case nodeConstants.ATTRIBUTE_NODE:
+ case nodeConstants.TEXT_NODE:
+ case nodeConstants.COMMENT_NODE:
+ case nodeConstants.DOCUMENT_FRAGMENT_NODE:
+ case nodeConstants.ELEMENT_NODE:
+ return true;
+ default:
+ return false;
+ }
+ },
+
+ render: function ()
+ {
+ switch (this.objectActor.preview.nodeType) {
+ case nodeConstants.DOCUMENT_NODE:
+ this._renderDocumentNode();
+ break;
+ case nodeConstants.ATTRIBUTE_NODE: {
+ let {preview} = this.objectActor;
+ this.element = this.el("span.attributeNode.kind-" + preview.kind);
+ let attr = this._renderAttributeNode(preview.nodeName, preview.value, true);
+ this.element.appendChild(attr);
+ break;
+ }
+ case nodeConstants.TEXT_NODE:
+ this._renderTextNode();
+ break;
+ case nodeConstants.COMMENT_NODE:
+ this._renderCommentNode();
+ break;
+ case nodeConstants.DOCUMENT_FRAGMENT_NODE:
+ this._renderDocumentFragmentNode();
+ break;
+ case nodeConstants.ELEMENT_NODE:
+ this._renderElementNode();
+ break;
+ default:
+ throw new Error("Unsupported nodeType: " + preview.nodeType);
+ }
+ },
+
+ _renderDocumentNode: function ()
+ {
+ let fn =
+ Widgets.ObjectRenderers.byKind.ObjectWithURL.prototype._renderElement;
+ this.element = fn.call(this, this.objectActor,
+ this.objectActor.preview.location);
+ this.element.classList.add("documentNode");
+ },
+
+ _renderAttributeNode: function (nodeName, nodeValue, addLink)
+ {
+ let value = VariablesView.getString(nodeValue, { noStringQuotes: true });
+
+ let fragment = this.document.createDocumentFragment();
+ if (addLink) {
+ this._anchor(nodeName, { className: "cm-attribute", appendTo: fragment });
+ } else {
+ fragment.appendChild(this.el("span.cm-attribute", nodeName));
+ }
+
+ this._text("=\"", fragment);
+ fragment.appendChild(this.el("span.theme-fg-color6", escapeHTML(value)));
+ this._text("\"", fragment);
+
+ return fragment;
+ },
+
+ _renderTextNode: function ()
+ {
+ let {preview} = this.objectActor;
+ this.element = this.el("span.textNode.kind-" + preview.kind);
+
+ this._anchor(preview.nodeName, { className: "cm-variable" });
+ this._text(" ");
+
+ let text = VariablesView.getString(preview.textContent);
+ this.element.appendChild(this.el("span.console-string", text));
+ },
+
+ _renderCommentNode: function ()
+ {
+ let {preview} = this.objectActor;
+ let comment = "<!-- " + VariablesView.getString(preview.textContent, {
+ noStringQuotes: true,
+ }) + " -->";
+
+ this.element = this._anchor(comment, {
+ className: "kind-" + preview.kind + " commentNode cm-comment",
+ });
+ },
+
+ _renderDocumentFragmentNode: function ()
+ {
+ let {preview} = this.objectActor;
+ let {childNodes} = preview;
+ let container = this.element = this.el("span.documentFragmentNode.kind-" +
+ preview.kind);
+
+ this._anchor(this.objectActor.class, { className: "cm-variable" });
+
+ if (!childNodes || this.options.concise) {
+ this._text("[");
+ container.appendChild(this.el("span.cm-number", preview.childNodesLength));
+ this._text("]");
+ return;
+ }
+
+ this._text(" [ ");
+
+ let shown = 0;
+ for (let item of childNodes) {
+ if (shown > 0) {
+ this._text(", ");
+ }
+
+ let elem = this.message._renderValueGrip(item, { concise: true });
+ container.appendChild(elem);
+ shown++;
+ }
+
+ if (shown < preview.childNodesLength) {
+ this._text(", ");
+
+ let n = preview.childNodesLength - shown;
+ let str = VariablesView.stringifiers._getNMoreString(n);
+ this._anchor(str);
+ }
+
+ this._text(" ]");
+ },
+
+ _renderElementNode: function ()
+ {
+ let doc = this.document;
+ let {attributes, nodeName} = this.objectActor.preview;
+
+ this.element = this.el("span." + "kind-" + this.objectActor.preview.kind + ".elementNode");
+
+ this._text("<");
+ let openTag = this.el("span.cm-tag");
+ this.element.appendChild(openTag);
+
+ let tagName = this._anchor(nodeName, {
+ className: "cm-tag",
+ appendTo: openTag
+ });
+
+ if (this.options.concise) {
+ if (attributes.id) {
+ tagName.appendChild(this.el("span.cm-attribute", "#" + attributes.id));
+ }
+ if (attributes.class) {
+ tagName.appendChild(this.el("span.cm-attribute", "." + attributes.class.split(/\s+/g).join(".")));
+ }
+ } else {
+ for (let name of Object.keys(attributes)) {
+ let attr = this._renderAttributeNode(" " + name, attributes[name]);
+ this.element.appendChild(attr);
+ }
+ }
+
+ this._text(">");
+
+ // Register this widget in the owner message so that it gets destroyed when
+ // the message is destroyed.
+ this.message.widgets.add(this);
+
+ this.linkToInspector().then(null, e => console.error(e));
+ },
+
+ /**
+ * If the DOMNode being rendered can be highlit in the page, this function
+ * will attach mouseover/out event listeners to do so, and the inspector icon
+ * to open the node in the inspector.
+ * @return a promise that resolves when the node has been linked to the
+ * inspector, or rejects if it wasn't (either if no toolbox could be found to
+ * access the inspector, or if the node isn't present in the inspector, i.e.
+ * if the node is in a DocumentFragment or not part of the tree, or not of
+ * type nodeConstants.ELEMENT_NODE).
+ */
+ linkToInspector: Task.async(function* ()
+ {
+ if (this._linkedToInspector) {
+ return;
+ }
+
+ // Checking the node type
+ if (this.objectActor.preview.nodeType !== nodeConstants.ELEMENT_NODE) {
+ throw new Error("The object cannot be linked to the inspector as it " +
+ "isn't an element node");
+ }
+
+ // Checking the presence of a toolbox
+ let target = this.message.output.toolboxTarget;
+ this.toolbox = gDevTools.getToolbox(target);
+ if (!this.toolbox) {
+ // In cases like the browser console, there is no toolbox.
+ return;
+ }
+
+ // Checking that the inspector supports the node
+ yield this.toolbox.initInspector();
+ this._nodeFront = yield this.toolbox.walker.getNodeActorFromObjectActor(this.objectActor.actor);
+ if (!this._nodeFront) {
+ throw new Error("The object cannot be linked to the inspector, the " +
+ "corresponding nodeFront could not be found");
+ }
+
+ // At this stage, the message may have been cleared already
+ if (!this.document) {
+ throw new Error("The object cannot be linked to the inspector, the " +
+ "message was got cleared away");
+ }
+
+ // Check it again as this method is async!
+ if (this._linkedToInspector) {
+ return;
+ }
+ this._linkedToInspector = true;
+
+ this.highlightDomNode = this.highlightDomNode.bind(this);
+ this.element.addEventListener("mouseover", this.highlightDomNode, false);
+ this.unhighlightDomNode = this.unhighlightDomNode.bind(this);
+ this.element.addEventListener("mouseout", this.unhighlightDomNode, false);
+
+ this._openInspectorNode = this._anchor("", {
+ className: "open-inspector",
+ onClick: this.openNodeInInspector.bind(this)
+ });
+ this._openInspectorNode.title = l10n.getStr("openNodeInInspector");
+ }),
+
+ /**
+ * Highlight the DOMNode corresponding to the ObjectActor in the page.
+ * @return a promise that resolves when the node has been highlighted, or
+ * rejects if the node cannot be highlighted (detached from the DOM)
+ */
+ highlightDomNode: Task.async(function* ()
+ {
+ yield this.linkToInspector();
+ let isAttached = yield this.toolbox.walker.isInDOMTree(this._nodeFront);
+ if (isAttached) {
+ yield this.toolbox.highlighterUtils.highlightNodeFront(this._nodeFront);
+ } else {
+ throw null;
+ }
+ }),
+
+ /**
+ * Unhighlight a previously highlit node
+ * @see highlightDomNode
+ * @return a promise that resolves when the highlighter has been hidden
+ */
+ unhighlightDomNode: function ()
+ {
+ return this.linkToInspector().then(() => {
+ return this.toolbox.highlighterUtils.unhighlight();
+ }).then(null, e => console.error(e));
+ },
+
+ /**
+ * Open the DOMNode corresponding to the ObjectActor in the inspector panel
+ * @return a promise that resolves when the inspector has been switched to
+ * and the node has been selected, or rejects if the node cannot be selected
+ * (detached from the DOM). Note that in any case, the inspector panel will
+ * be switched to.
+ */
+ openNodeInInspector: Task.async(function* ()
+ {
+ yield this.linkToInspector();
+ yield this.toolbox.selectTool("inspector");
+
+ let isAttached = yield this.toolbox.walker.isInDOMTree(this._nodeFront);
+ if (isAttached) {
+ let onReady = promise.defer();
+ this.toolbox.inspector.once("inspector-updated", onReady.resolve);
+ yield this.toolbox.selection.setNodeFront(this._nodeFront, "console");
+ yield onReady.promise;
+ } else {
+ throw null;
+ }
+ }),
+
+ destroy: function ()
+ {
+ if (this.toolbox && this._nodeFront) {
+ this.element.removeEventListener("mouseover", this.highlightDomNode, false);
+ this.element.removeEventListener("mouseout", this.unhighlightDomNode, false);
+ this._openInspectorNode.removeEventListener("mousedown", this.openNodeInInspector, true);
+
+ if (this._linkedToInspector) {
+ this.unhighlightDomNode().then(() => {
+ this.toolbox = null;
+ this._nodeFront = null;
+ });
+ } else {
+ this.toolbox = null;
+ this._nodeFront = null;
+ }
+ }
+ },
+}); // Widgets.ObjectRenderers.byKind.DOMNode
+
+/**
+ * The widget user for displaying Promise objects.
+ */
+Widgets.ObjectRenderers.add({
+ byClass: "Promise",
+
+ render: function ()
+ {
+ let { ownProperties, safeGetterValues } = this.objectActor.preview || {};
+ if ((!ownProperties && !safeGetterValues) || this.options.concise) {
+ this._renderConciseObject();
+ return;
+ }
+
+ this._renderObjectPrefix();
+ let container = this.element;
+ let addedPromiseInternalProps = false;
+
+ if (this.objectActor.promiseState) {
+ const { state, value, reason } = this.objectActor.promiseState;
+
+ this._renderObjectProperty("<state>", state, container, false);
+ addedPromiseInternalProps = true;
+
+ if (state == "fulfilled") {
+ this._renderObjectProperty("<value>", value, container, true);
+ } else if (state == "rejected") {
+ this._renderObjectProperty("<reason>", reason, container, true);
+ }
+ }
+
+ this._renderObjectProperties(container, addedPromiseInternalProps);
+ this._renderObjectSuffix();
+ }
+}); // Widgets.ObjectRenderers.byClass.Promise
+
+/*
+ * A renderer used for wrapped primitive objects.
+ */
+
+function WrappedPrimitiveRenderer() {
+ let { ownProperties, safeGetterValues } = this.objectActor.preview || {};
+ if ((!ownProperties && !safeGetterValues) || this.options.concise) {
+ this._renderConciseObject();
+ return;
+ }
+
+ this._renderObjectPrefix();
+
+ let elem =
+ this.message._renderValueGrip(this.objectActor.preview.wrappedValue);
+ this.element.appendChild(elem);
+
+ this._renderObjectProperties(this.element, true);
+ this._renderObjectSuffix();
+}
+
+/**
+ * The widget used for displaying Boolean previews.
+ */
+Widgets.ObjectRenderers.add({
+ byClass: "Boolean",
+
+ render: WrappedPrimitiveRenderer,
+});
+
+/**
+ * The widget used for displaying Number previews.
+ */
+Widgets.ObjectRenderers.add({
+ byClass: "Number",
+
+ render: WrappedPrimitiveRenderer,
+});
+
+/**
+ * The widget used for displaying String previews.
+ */
+Widgets.ObjectRenderers.add({
+ byClass: "String",
+
+ render: WrappedPrimitiveRenderer,
+});
+
+/**
+ * The widget used for displaying generic JS object previews.
+ */
+Widgets.ObjectRenderers.add({
+ byKind: "Object",
+
+ render: function ()
+ {
+ let { ownProperties, safeGetterValues } = this.objectActor.preview || {};
+ if ((!ownProperties && !safeGetterValues) || this.options.concise) {
+ this._renderConciseObject();
+ return;
+ }
+
+ this._renderObjectPrefix();
+ this._renderObjectProperties(this.element, false);
+ this._renderObjectSuffix();
+ },
+}); // Widgets.ObjectRenderers.byKind.Object
+
+/**
+ * The long string widget.
+ *
+ * @constructor
+ * @param object message
+ * The owning message.
+ * @param object longStringActor
+ * The LongStringActor to display.
+ * @param object options
+ * Options, such as noStringQuotes
+ */
+Widgets.LongString = function (message, longStringActor, options)
+{
+ Widgets.BaseWidget.call(this, message);
+ this.longStringActor = longStringActor;
+ this.noStringQuotes = (options && "noStringQuotes" in options) ?
+ options.noStringQuotes : !this.message._quoteStrings;
+
+ this._onClick = this._onClick.bind(this);
+ this._onSubstring = this._onSubstring.bind(this);
+};
+
+Widgets.LongString.prototype = extend(Widgets.BaseWidget.prototype, {
+ /**
+ * The LongStringActor displayed by the widget.
+ * @type object
+ */
+ longStringActor: null,
+
+ render: function ()
+ {
+ if (this.element) {
+ return this;
+ }
+
+ let result = this.element = this.document.createElementNS(XHTML_NS, "span");
+ result.className = "longString console-string";
+ this._renderString(this.longStringActor.initial);
+ result.appendChild(this._renderEllipsis());
+
+ return this;
+ },
+
+ /**
+ * Render the long string in the widget element.
+ * @private
+ * @param string str
+ * The string to display.
+ */
+ _renderString: function (str)
+ {
+ this.element.textContent = VariablesView.getString(str, {
+ noStringQuotes: this.noStringQuotes,
+ noEllipsis: true,
+ });
+ },
+
+ /**
+ * Render the anchor ellipsis that allows the user to expand the long string.
+ *
+ * @private
+ * @return Element
+ */
+ _renderEllipsis: function ()
+ {
+ let ellipsis = this.document.createElementNS(XHTML_NS, "a");
+ ellipsis.className = "longStringEllipsis";
+ ellipsis.textContent = l10n.getStr("longStringEllipsis");
+ ellipsis.href = "#";
+ ellipsis.draggable = false;
+ this.message._addLinkCallback(ellipsis, this._onClick);
+
+ return ellipsis;
+ },
+
+ /**
+ * The click event handler for the ellipsis shown after the short string. This
+ * function expands the element to show the full string.
+ * @private
+ */
+ _onClick: function ()
+ {
+ let longString = this.output.webConsoleClient.longString(this.longStringActor);
+ let toIndex = Math.min(longString.length, MAX_LONG_STRING_LENGTH);
+
+ longString.substring(longString.initial.length, toIndex, this._onSubstring);
+ },
+
+ /**
+ * The longString substring response callback.
+ *
+ * @private
+ * @param object response
+ * Response packet.
+ */
+ _onSubstring: function (response)
+ {
+ if (response.error) {
+ console.error("LongString substring failure: " + response.error);
+ return;
+ }
+
+ this.element.lastChild.remove();
+ this.element.classList.remove("longString");
+
+ this._renderString(this.longStringActor.initial + response.substring);
+
+ this.output.owner.emit("new-messages", new Set([{
+ update: true,
+ node: this.message.element,
+ response: response,
+ }]));
+
+ let toIndex = Math.min(this.longStringActor.length, MAX_LONG_STRING_LENGTH);
+ if (toIndex != this.longStringActor.length) {
+ this._logWarningAboutStringTooLong();
+ }
+ },
+
+ /**
+ * Inform user that the string he tries to view is too long.
+ * @private
+ */
+ _logWarningAboutStringTooLong: function ()
+ {
+ let msg = new Messages.Simple(l10n.getStr("longStringTooLong"), {
+ category: "output",
+ severity: "warning",
+ });
+ this.output.addMessage(msg);
+ },
+}); // Widgets.LongString.prototype
+
+
+/**
+ * The stacktrace widget.
+ *
+ * @constructor
+ * @extends Widgets.BaseWidget
+ * @param object message
+ * The owning message.
+ * @param array stacktrace
+ * The stacktrace to display, array of frames as supplied by the server,
+ * over the remote protocol.
+ */
+Widgets.Stacktrace = function (message, stacktrace) {
+ Widgets.BaseWidget.call(this, message);
+ this.stacktrace = stacktrace;
+};
+
+Widgets.Stacktrace.prototype = extend(Widgets.BaseWidget.prototype, {
+ /**
+ * The stackframes received from the server.
+ * @type array
+ */
+ stacktrace: null,
+
+ render() {
+ if (this.element) {
+ return this;
+ }
+
+ let result = this.element = this.document.createElementNS(XHTML_NS, "div");
+ result.className = "stacktrace devtools-monospace";
+
+ if (this.stacktrace) {
+ this.output.owner.ReactDOM.render(this.output.owner.StackTraceView({
+ stacktrace: this.stacktrace,
+ onViewSourceInDebugger: frame => this.output.openLocationInDebugger(frame)
+ }), result);
+ }
+
+ return this;
+ }
+});
+
+/**
+ * The table widget.
+ *
+ * @constructor
+ * @extends Widgets.BaseWidget
+ * @param object message
+ * The owning message.
+ * @param array data
+ * Array of objects that holds the data to log in the table.
+ * @param object columns
+ * Object containing the key value pair of the id and display name for
+ * the columns in the table.
+ */
+Widgets.Table = function (message, data, columns)
+{
+ Widgets.BaseWidget.call(this, message);
+ this.data = data;
+ this.columns = columns;
+};
+
+Widgets.Table.prototype = extend(Widgets.BaseWidget.prototype, {
+ /**
+ * Array of objects that holds the data to output in the table.
+ * @type array
+ */
+ data: null,
+
+ /**
+ * Object containing the key value pair of the id and display name for
+ * the columns in the table.
+ * @type object
+ */
+ columns: null,
+
+ render: function () {
+ if (this.element) {
+ return this;
+ }
+
+ let result = this.element = this.document.createElementNS(XHTML_NS, "div");
+ result.className = "consoletable devtools-monospace";
+
+ this.table = new TableWidget(result, {
+ wrapTextInElements: true,
+ initialColumns: this.columns,
+ uniqueId: "_index",
+ firstColumn: "_index"
+ });
+
+ for (let row of this.data) {
+ this.table.push(row);
+ }
+
+ return this;
+ }
+}); // Widgets.Table.prototype
+
+function gSequenceId()
+{
+ return gSequenceId.n++;
+}
+gSequenceId.n = 0;
+
+exports.ConsoleOutput = ConsoleOutput;
+exports.Messages = Messages;
+exports.Widgets = Widgets;
diff --git a/devtools/client/webconsole/hudservice.js b/devtools/client/webconsole/hudservice.js
new file mode 100644
index 000000000..46b4f2a13
--- /dev/null
+++ b/devtools/client/webconsole/hudservice.js
@@ -0,0 +1,718 @@
+/* This Source Code Form is subject to the terms of 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 {Cc, Ci, Cu} = require("chrome");
+
+var WebConsoleUtils = require("devtools/client/webconsole/utils").Utils;
+var { extend } = require("sdk/core/heritage");
+var {TargetFactory} = require("devtools/client/framework/target");
+var {Tools} = require("devtools/client/definitions");
+const { Task } = require("devtools/shared/task");
+var promise = require("promise");
+var Services = require("Services");
+
+loader.lazyRequireGetter(this, "Telemetry", "devtools/client/shared/telemetry");
+loader.lazyRequireGetter(this, "WebConsoleFrame", "devtools/client/webconsole/webconsole", true);
+loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
+loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
+loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true);
+loader.lazyRequireGetter(this, "showDoorhanger", "devtools/client/shared/doorhanger", true);
+loader.lazyRequireGetter(this, "viewSource", "devtools/client/shared/view-source");
+
+const STRINGS_URI = "devtools/client/locales/webconsole.properties";
+var l10n = new WebConsoleUtils.L10n(STRINGS_URI);
+
+const BROWSER_CONSOLE_WINDOW_FEATURES = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+// The preference prefix for all of the Browser Console filters.
+const BROWSER_CONSOLE_FILTER_PREFS_PREFIX = "devtools.browserconsole.filter.";
+
+var gHudId = 0;
+
+// The HUD service
+
+function HUD_SERVICE()
+{
+ this.consoles = new Map();
+ this.lastFinishedRequest = { callback: null };
+}
+
+HUD_SERVICE.prototype =
+{
+ _browserConsoleID: null,
+ _browserConsoleDefer: null,
+
+ /**
+ * Keeps a reference for each Web Console / Browser Console that is created.
+ * @type Map
+ */
+ consoles: null,
+
+ /**
+ * Assign a function to this property to listen for every request that
+ * completes. Used by unit tests. The callback takes one argument: the HTTP
+ * activity object as received from the remote Web Console.
+ *
+ * @type object
+ * Includes a property named |callback|. Assign the function to the
+ * |callback| property of this object.
+ */
+ lastFinishedRequest: null,
+
+ /**
+ * Get the current context, which is the main application window.
+ *
+ * @returns nsIDOMWindow
+ */
+ currentContext: function HS_currentContext() {
+ return Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+ },
+
+ /**
+ * Open a Web Console for the given target.
+ *
+ * @see devtools/framework/target.js for details about targets.
+ *
+ * @param object aTarget
+ * The target that the web console will connect to.
+ * @param nsIDOMWindow aIframeWindow
+ * The window where the web console UI is already loaded.
+ * @param nsIDOMWindow aChromeWindow
+ * The window of the web console owner.
+ * @return object
+ * A promise object for the opening of the new WebConsole instance.
+ */
+ openWebConsole:
+ function HS_openWebConsole(aTarget, aIframeWindow, aChromeWindow)
+ {
+ let hud = new WebConsole(aTarget, aIframeWindow, aChromeWindow);
+ this.consoles.set(hud.hudId, hud);
+ return hud.init();
+ },
+
+ /**
+ * Open a Browser Console for the given target.
+ *
+ * @see devtools/framework/target.js for details about targets.
+ *
+ * @param object aTarget
+ * The target that the browser console will connect to.
+ * @param nsIDOMWindow aIframeWindow
+ * The window where the browser console UI is already loaded.
+ * @param nsIDOMWindow aChromeWindow
+ * The window of the browser console owner.
+ * @return object
+ * A promise object for the opening of the new BrowserConsole instance.
+ */
+ openBrowserConsole:
+ function HS_openBrowserConsole(aTarget, aIframeWindow, aChromeWindow)
+ {
+ let hud = new BrowserConsole(aTarget, aIframeWindow, aChromeWindow);
+ this._browserConsoleID = hud.hudId;
+ this.consoles.set(hud.hudId, hud);
+ return hud.init();
+ },
+
+ /**
+ * Returns the Web Console object associated to a content window.
+ *
+ * @param nsIDOMWindow aContentWindow
+ * @returns object
+ */
+ getHudByWindow: function HS_getHudByWindow(aContentWindow)
+ {
+ for (let [hudId, hud] of this.consoles) {
+ let target = hud.target;
+ if (target && target.tab && target.window === aContentWindow) {
+ return hud;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Returns the console instance for a given id.
+ *
+ * @param string aId
+ * @returns Object
+ */
+ getHudReferenceById: function HS_getHudReferenceById(aId)
+ {
+ return this.consoles.get(aId);
+ },
+
+ /**
+ * Find if there is a Web Console open for the current tab and return the
+ * instance.
+ * @return object|null
+ * The WebConsole object or null if the active tab has no open Web
+ * Console.
+ */
+ getOpenWebConsole: function HS_getOpenWebConsole()
+ {
+ let tab = this.currentContext().gBrowser.selectedTab;
+ if (!tab || !TargetFactory.isKnownTab(tab)) {
+ return null;
+ }
+ let target = TargetFactory.forTab(tab);
+ let toolbox = gDevTools.getToolbox(target);
+ let panel = toolbox ? toolbox.getPanel("webconsole") : null;
+ return panel ? panel.hud : null;
+ },
+
+ /**
+ * Toggle the Browser Console.
+ */
+ toggleBrowserConsole: function HS_toggleBrowserConsole()
+ {
+ if (this._browserConsoleID) {
+ let hud = this.getHudReferenceById(this._browserConsoleID);
+ return hud.destroy();
+ }
+
+ if (this._browserConsoleDefer) {
+ return this._browserConsoleDefer.promise;
+ }
+
+ this._browserConsoleDefer = promise.defer();
+
+ function connect()
+ {
+ let deferred = promise.defer();
+
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ DebuggerServer.allowChromeProcess = true;
+
+ let client = new DebuggerClient(DebuggerServer.connectPipe());
+ return client.connect()
+ .then(() => client.getProcess())
+ .then(aResponse => {
+ // Set chrome:false in order to attach to the target
+ // (i.e. send an `attach` request to the chrome actor)
+ return { form: aResponse.form, client: client, chrome: false };
+ });
+ }
+
+ let target;
+ function getTarget(aConnection)
+ {
+ return TargetFactory.forRemoteTab(aConnection);
+ }
+
+ function openWindow(aTarget)
+ {
+ target = aTarget;
+
+ let deferred = promise.defer();
+
+ let win = Services.ww.openWindow(null, Tools.webConsole.url, "_blank",
+ BROWSER_CONSOLE_WINDOW_FEATURES, null);
+ win.addEventListener("DOMContentLoaded", function onLoad() {
+ win.removeEventListener("DOMContentLoaded", onLoad);
+
+ // Set the correct Browser Console title.
+ let root = win.document.documentElement;
+ root.setAttribute("title", root.getAttribute("browserConsoleTitle"));
+
+ deferred.resolve(win);
+ });
+
+ return deferred.promise;
+ }
+
+ connect().then(getTarget).then(openWindow).then((aWindow) => {
+ return this.openBrowserConsole(target, aWindow, aWindow)
+ .then((aBrowserConsole) => {
+ this._browserConsoleDefer.resolve(aBrowserConsole);
+ this._browserConsoleDefer = null;
+ });
+ }, console.error.bind(console));
+
+ return this._browserConsoleDefer.promise;
+ },
+
+ /**
+ * Opens or focuses the Browser Console.
+ */
+ openBrowserConsoleOrFocus: function HS_openBrowserConsoleOrFocus()
+ {
+ let hud = this.getBrowserConsole();
+ if (hud) {
+ hud.iframeWindow.focus();
+ return promise.resolve(hud);
+ }
+ else {
+ 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: function HS_getBrowserConsole()
+ {
+ return this.getHudReferenceById(this._browserConsoleID);
+ },
+};
+
+
+/**
+ * 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.
+ *
+ * @constructor
+ * @param object aTarget
+ * The target that the web console will connect to.
+ * @param nsIDOMWindow aIframeWindow
+ * The window where the web console UI is already loaded.
+ * @param nsIDOMWindow aChromeWindow
+ * The window of the web console owner.
+ */
+function WebConsole(aTarget, aIframeWindow, aChromeWindow)
+{
+ this.iframeWindow = aIframeWindow;
+ this.chromeWindow = aChromeWindow;
+ this.hudId = "hud_" + ++gHudId;
+ this.target = aTarget;
+
+ this.browserWindow = this.chromeWindow.top;
+
+ let element = this.browserWindow.document.documentElement;
+ if (element.getAttribute("windowtype") != gDevTools.chromeWindowType) {
+ this.browserWindow = HUDService.currentContext();
+ }
+
+ this.ui = new WebConsoleFrame(this);
+}
+
+WebConsole.prototype = {
+ iframeWindow: null,
+ chromeWindow: null,
+ browserWindow: null,
+ hudId: null,
+ target: null,
+ ui: null,
+ _browserConsole: false,
+ _destroyer: null,
+
+ /**
+ * Getter for a function to to listen for every request that completes. Used
+ * by unit tests. The callback takes one argument: the HTTP activity object as
+ * received from the remote Web Console.
+ *
+ * @type function
+ */
+ get lastFinishedRequestCallback()
+ {
+ return HUDService.lastFinishedRequest.callback;
+ },
+
+ /**
+ * 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 this.chromeWindow.top;
+ },
+
+ /**
+ * Getter for the xul:popupset that holds any popups we open.
+ * @type nsIDOMElement
+ */
+ get mainPopupSet()
+ {
+ return this.chromeUtilsWindow.document.getElementById("mainPopupSet");
+ },
+
+ /**
+ * Getter for the output element that holds messages we display.
+ * @type nsIDOMElement
+ */
+ get outputNode()
+ {
+ return this.ui ? this.ui.outputNode : null;
+ },
+
+ get gViewSourceUtils()
+ {
+ return this.chromeUtilsWindow.gViewSourceUtils;
+ },
+
+ /**
+ * Initialize the Web Console instance.
+ *
+ * @return object
+ * A promise for the initialization.
+ */
+ init: function WC_init()
+ {
+ return this.ui.init().then(() => this);
+ },
+
+ /**
+ * Retrieve the Web Console panel title.
+ *
+ * @return string
+ * The Web Console panel title.
+ */
+ getPanelTitle: function WC_getPanelTitle()
+ {
+ let url = this.ui ? this.ui.contentLocation : "";
+ return l10n.getFormatStr("webConsoleWindowTitleAndURL", [url]);
+ },
+
+ /**
+ * The JSTerm object that manages the console's input.
+ * @see webconsole.js::JSTerm
+ * @type object
+ */
+ get jsterm()
+ {
+ return this.ui ? this.ui.jsterm : null;
+ },
+
+ /**
+ * The clear output button handler.
+ * @private
+ */
+ _onClearButton: function WC__onClearButton()
+ {
+ if (this.target.isLocalTab) {
+ this.browserWindow.DeveloperToolbar.resetErrorsCount(this.target.tab);
+ }
+ },
+
+ /**
+ * Alias for the WebConsoleFrame.setFilterState() method.
+ * @see webconsole.js::WebConsoleFrame.setFilterState()
+ */
+ setFilterState: function WC_setFilterState()
+ {
+ this.ui && this.ui.setFilterState.apply(this.ui, arguments);
+ },
+
+ /**
+ * Open a link in a new tab.
+ *
+ * @param string aLink
+ * The URL you want to open in a new tab.
+ */
+ openLink: function WC_openLink(aLink)
+ {
+ this.chromeUtilsWindow.openUILinkIn(aLink, "tab");
+ },
+
+ /**
+ * Open a link in Firefox's view source.
+ *
+ * @param string aSourceURL
+ * The URL of the file.
+ * @param integer aSourceLine
+ * The line number which should be highlighted.
+ */
+ viewSource: function WC_viewSource(aSourceURL, aSourceLine) {
+ // Attempt to access view source via a browser first, which may display it in
+ // a tab, if enabled.
+ let browserWin = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+ if (browserWin && browserWin.BrowserViewSourceOfDocument) {
+ return browserWin.BrowserViewSourceOfDocument({
+ URL: aSourceURL,
+ lineNumber: aSourceLine
+ });
+ }
+ this.gViewSourceUtils.viewSource(aSourceURL, null, this.iframeWindow.document, aSourceLine || 0);
+ },
+
+ /**
+ * Tries to open a Stylesheet file related to the web page for the web console
+ * instance in the Style Editor. 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 aSourceURL
+ * The URL of the file.
+ * @param integer aSourceLine
+ * The line number which you want to place the caret.
+ */
+ viewSourceInStyleEditor: function WC_viewSourceInStyleEditor(aSourceURL, aSourceLine) {
+ let toolbox = gDevTools.getToolbox(this.target);
+ if (!toolbox) {
+ this.viewSource(aSourceURL, aSourceLine);
+ return;
+ }
+ toolbox.viewSourceInStyleEditor(aSourceURL, aSourceLine);
+ },
+
+ /**
+ * 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 aSourceURL
+ * The URL of the file.
+ * @param integer aSourceLine
+ * The line number which you want to place the caret.
+ */
+ viewSourceInDebugger: function WC_viewSourceInDebugger(aSourceURL, aSourceLine) {
+ let toolbox = gDevTools.getToolbox(this.target);
+ if (!toolbox) {
+ this.viewSource(aSourceURL, aSourceLine);
+ return;
+ }
+ toolbox.viewSourceInDebugger(aSourceURL, aSourceLine).then(() => {
+ this.ui.emit("source-in-debugger-opened");
+ });
+ },
+
+ /**
+ * Tries to open a JavaScript file related to the web page for the web console
+ * instance in the corresponding Scratchpad.
+ *
+ * @param string aSourceURL
+ * The URL of the file which corresponds to a Scratchpad id.
+ */
+ viewSourceInScratchpad: function WC_viewSourceInScratchpad(aSourceURL, aSourceLine) {
+ viewSource.viewSourceInScratchpad(aSourceURL, aSourceLine);
+ },
+
+ /**
+ * Retrieve information about the JavaScript debugger's stackframes list. This
+ * is used to allow the Web Console to evaluate code in the selected
+ * stackframe.
+ *
+ * @return object|null
+ * An object which holds:
+ * - frames: the active ThreadClient.cachedFrames array.
+ * - selected: depth/index of the selected stackframe in the debugger
+ * UI.
+ * If the debugger is not open or if it's not paused, then |null| is
+ * returned.
+ */
+ getDebuggerFrames: function WC_getDebuggerFrames()
+ {
+ let toolbox = gDevTools.getToolbox(this.target);
+ if (!toolbox) {
+ return null;
+ }
+ let panel = toolbox.getPanel("jsdebugger");
+
+ if (!panel) {
+ return null;
+ }
+
+ return panel.getFrames();
+ },
+
+ /**
+ * 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: function WC_getInspectorSelection()
+ {
+ let toolbox = gDevTools.getToolbox(this.target);
+ if (!toolbox) {
+ return null;
+ }
+ let panel = toolbox.getPanel("inspector");
+ if (!panel || !panel.selection) {
+ return null;
+ }
+ return panel.selection;
+ },
+
+ /**
+ * 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: function WC_destroy()
+ {
+ if (this._destroyer) {
+ return this._destroyer.promise;
+ }
+
+ HUDService.consoles.delete(this.hudId);
+
+ this._destroyer = promise.defer();
+
+ // The document may already be removed
+ if (this.chromeUtilsWindow && this.mainPopupSet) {
+ let popupset = this.mainPopupSet;
+ let panels = popupset.querySelectorAll("panel[hudId=" + this.hudId + "]");
+ for (let panel of panels) {
+ panel.hidePopup();
+ }
+ }
+
+ let onDestroy = Task.async(function* () {
+ if (!this._browserConsole) {
+ try {
+ yield this.target.activeTab.focus();
+ }
+ catch (ex) {
+ // Tab focus can fail if the tab or target is closed.
+ }
+ }
+
+ let id = WebConsoleUtils.supportsString(this.hudId);
+ Services.obs.notifyObservers(id, "web-console-destroyed", null);
+ this._destroyer.resolve(null);
+ }.bind(this));
+
+ if (this.ui) {
+ this.ui.destroy().then(onDestroy);
+ }
+ else {
+ onDestroy();
+ }
+
+ return this._destroyer.promise;
+ },
+};
+
+/**
+ * A BrowserConsole 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 Browser Console UI. This is
+ * meant to be an integration point between the Firefox UI and the Browser Console
+ * UI and features.
+ *
+ * @constructor
+ * @param object aTarget
+ * The target that the browser console will connect to.
+ * @param nsIDOMWindow aIframeWindow
+ * The window where the browser console UI is already loaded.
+ * @param nsIDOMWindow aChromeWindow
+ * The window of the browser console owner.
+ */
+function BrowserConsole()
+{
+ WebConsole.apply(this, arguments);
+ this._telemetry = new Telemetry();
+}
+
+BrowserConsole.prototype = extend(WebConsole.prototype, {
+ _browserConsole: true,
+ _bc_init: null,
+ _bc_destroyer: null,
+
+ $init: WebConsole.prototype.init,
+
+ /**
+ * Initialize the Browser Console instance.
+ *
+ * @return object
+ * A promise for the initialization.
+ */
+ init: function BC_init()
+ {
+ if (this._bc_init) {
+ return this._bc_init;
+ }
+
+ this.ui._filterPrefsPrefix = BROWSER_CONSOLE_FILTER_PREFS_PREFIX;
+
+ let window = this.iframeWindow;
+
+ // Make sure that the closing of the Browser Console window destroys this
+ // instance.
+ let onClose = () => {
+ window.removeEventListener("unload", onClose);
+ window.removeEventListener("focus", onFocus);
+ this.destroy();
+ };
+ window.addEventListener("unload", onClose);
+
+ this._telemetry.toolOpened("browserconsole");
+
+ // Create an onFocus handler just to display the dev edition promo.
+ // This is to prevent race conditions in some environments.
+ // Hook to display promotional Developer Edition doorhanger. Only displayed once.
+ let onFocus = () => showDoorhanger({ window, type: "deveditionpromo" });
+ window.addEventListener("focus", onFocus);
+
+ this._bc_init = this.$init();
+ return this._bc_init;
+ },
+
+ $destroy: WebConsole.prototype.destroy,
+
+ /**
+ * Destroy the object.
+ *
+ * @return object
+ * A promise object that is resolved once the Browser Console is closed.
+ */
+ destroy: function BC_destroy()
+ {
+ if (this._bc_destroyer) {
+ return this._bc_destroyer.promise;
+ }
+
+ this._telemetry.toolClosed("browserconsole");
+
+ this._bc_destroyer = promise.defer();
+
+ let chromeWindow = this.chromeWindow;
+ this.$destroy().then(() =>
+ this.target.client.close().then(() => {
+ HUDService._browserConsoleID = null;
+ chromeWindow.close();
+ this._bc_destroyer.resolve(null);
+ }));
+
+ return this._bc_destroyer.promise;
+ },
+});
+
+const HUDService = new HUD_SERVICE();
+
+(() => {
+ let methods = ["openWebConsole", "openBrowserConsole",
+ "toggleBrowserConsole", "getOpenWebConsole",
+ "getBrowserConsole", "getHudByWindow",
+ "openBrowserConsoleOrFocus", "getHudReferenceById"];
+ for (let method of methods) {
+ exports[method] = HUDService[method].bind(HUDService);
+ }
+
+ exports.consoles = HUDService.consoles;
+ exports.lastFinishedRequest = HUDService.lastFinishedRequest;
+})();
diff --git a/devtools/client/webconsole/jsterm.js b/devtools/client/webconsole/jsterm.js
new file mode 100644
index 000000000..8e3259afa
--- /dev/null
+++ b/devtools/client/webconsole/jsterm.js
@@ -0,0 +1,1766 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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";
+
+const {Utils: WebConsoleUtils} =
+ require("devtools/client/webconsole/utils");
+const promise = require("promise");
+const Debugger = require("Debugger");
+const Services = require("Services");
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+
+loader.lazyServiceGetter(this, "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1",
+ "nsIClipboardHelper");
+loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
+loader.lazyRequireGetter(this, "AutocompletePopup", "devtools/client/shared/autocomplete-popup", true);
+loader.lazyRequireGetter(this, "ToolSidebar", "devtools/client/framework/sidebar", true);
+loader.lazyRequireGetter(this, "Messages", "devtools/client/webconsole/console-output", true);
+loader.lazyRequireGetter(this, "asyncStorage", "devtools/shared/async-storage");
+loader.lazyRequireGetter(this, "EnvironmentClient", "devtools/shared/client/main", true);
+loader.lazyRequireGetter(this, "ObjectClient", "devtools/shared/client/main", true);
+loader.lazyImporter(this, "VariablesView", "resource://devtools/client/shared/widgets/VariablesView.jsm");
+loader.lazyImporter(this, "VariablesViewController", "resource://devtools/client/shared/widgets/VariablesViewController.jsm");
+loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
+
+const STRINGS_URI = "devtools/client/locales/webconsole.properties";
+var l10n = new WebConsoleUtils.L10n(STRINGS_URI);
+
+// Constants used for defining the direction of JSTerm input history navigation.
+const HISTORY_BACK = -1;
+const HISTORY_FORWARD = 1;
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers";
+
+const VARIABLES_VIEW_URL = "chrome://devtools/content/shared/widgets/VariablesView.xul";
+
+const PREF_INPUT_HISTORY_COUNT = "devtools.webconsole.inputHistoryCount";
+const PREF_AUTO_MULTILINE = "devtools.webconsole.autoMultiline";
+
+/**
+ * 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, code evaluation and result output.
+ *
+ * @constructor
+ * @param object webConsoleFrame
+ * The WebConsoleFrame object that owns this JSTerm instance.
+ */
+function JSTerm(webConsoleFrame) {
+ this.hud = webConsoleFrame;
+ this.hudId = this.hud.hudId;
+ this.inputHistoryCount = Services.prefs.getIntPref(PREF_INPUT_HISTORY_COUNT);
+
+ this.lastCompletion = { value: null };
+ this._loadHistory();
+
+ this._objectActorsInVariablesViews = new Map();
+
+ this._keyPress = this._keyPress.bind(this);
+ this._inputEventHandler = this._inputEventHandler.bind(this);
+ this._focusEventHandler = this._focusEventHandler.bind(this);
+ this._onKeypressInVariablesView = this._onKeypressInVariablesView.bind(this);
+ this._blurEventHandler = this._blurEventHandler.bind(this);
+
+ EventEmitter.decorate(this);
+}
+
+JSTerm.prototype = {
+ SELECTED_FRAME: -1,
+
+ /**
+ * Load the console history from previous sessions.
+ * @private
+ */
+ _loadHistory: function () {
+ this.history = [];
+ this.historyIndex = this.historyPlaceHolder = 0;
+
+ this.historyLoaded = asyncStorage.getItem("webConsoleHistory")
+ .then(value => {
+ if (Array.isArray(value)) {
+ // Since it was gotten asynchronously, there could be items already in
+ // the history. It's not likely but stick them onto the end anyway.
+ this.history = value.concat(this.history);
+
+ // Holds the number of entries in history. This value is incremented
+ // in this.execute().
+ this.historyIndex = this.history.length;
+
+ // Holds the index of the history entry that the user is currently
+ // viewing. This is reset to this.history.length when this.execute()
+ // is invoked.
+ this.historyPlaceHolder = this.history.length;
+ }
+ }, console.error);
+ },
+
+ /**
+ * 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.
+ * @returns Promise
+ * Resolves once the changes have been persisted.
+ */
+ clearHistory: function () {
+ this.history = [];
+ this.historyIndex = this.historyPlaceHolder = 0;
+ return this.storeHistory();
+ },
+
+ /**
+ * Stores the console history for future console instances.
+ * @returns Promise
+ * Resolves once the changes have been persisted.
+ */
+ storeHistory: function () {
+ return asyncStorage.setItem("webConsoleHistory", this.history);
+ },
+
+ /**
+ * Stores the data for the last completion.
+ * @type object
+ */
+ lastCompletion: null,
+
+ /**
+ * Array that caches the user input suggestions received from the server.
+ * @private
+ * @type array
+ */
+ _autocompleteCache: null,
+
+ /**
+ * The input that caused the last request to the server, whose response is
+ * cached in the _autocompleteCache array.
+ * @private
+ * @type string
+ */
+ _autocompleteQuery: null,
+
+ /**
+ * The frameActorId used in the last autocomplete query. Whenever this changes
+ * the autocomplete cache must be invalidated.
+ * @private
+ * @type string
+ */
+ _lastFrameActorId: null,
+
+ /**
+ * The Web Console sidebar.
+ * @see this._createSidebar()
+ * @see Sidebar.jsm
+ */
+ sidebar: null,
+
+ /**
+ * The Variables View instance shown in the sidebar.
+ * @private
+ * @type object
+ */
+ _variablesView: null,
+
+ /**
+ * Tells if you want the variables view UI updates to be lazy or not. Tests
+ * disable lazy updates.
+ *
+ * @private
+ * @type boolean
+ */
+ _lazyVariablesView: true,
+
+ /**
+ * Holds a map between VariablesView instances and sets of ObjectActor IDs
+ * that have been retrieved from the server. This allows us to release the
+ * objects when needed.
+ *
+ * @private
+ * @type Map
+ */
+ _objectActorsInVariablesViews: null,
+
+ /**
+ * Last input value.
+ * @type string
+ */
+ lastInputValue: "",
+
+ /**
+ * Tells if the input node changed since the last focus.
+ *
+ * @private
+ * @type boolean
+ */
+ _inputChanged: false,
+
+ /**
+ * Tells if the autocomplete popup was navigated since the last open.
+ *
+ * @private
+ * @type boolean
+ */
+ _autocompletePopupNavigated: false,
+
+ /**
+ * History of code that was executed.
+ * @type array
+ */
+ history: null,
+ autocompletePopup: null,
+ inputNode: null,
+ completeNode: null,
+
+ /**
+ * Getter for the element that holds the messages we display.
+ * @type nsIDOMElement
+ */
+ get outputNode() {
+ return this.hud.outputNode;
+ },
+
+ /**
+ * Getter for the debugger WebConsoleClient.
+ * @type object
+ */
+ get webConsoleClient() {
+ return this.hud.webConsoleClient;
+ },
+
+ COMPLETE_FORWARD: 0,
+ COMPLETE_BACKWARD: 1,
+ COMPLETE_HINT_ONLY: 2,
+ COMPLETE_PAGEUP: 3,
+ COMPLETE_PAGEDOWN: 4,
+
+ /**
+ * Initialize the JSTerminal UI.
+ */
+ init: function () {
+ let autocompleteOptions = {
+ onSelect: this.onAutocompleteSelect.bind(this),
+ onClick: this.acceptProposedCompletion.bind(this),
+ listId: "webConsole_autocompletePopupListBox",
+ position: "top",
+ theme: "auto",
+ autoSelect: true
+ };
+
+ let doc = this.hud.document;
+ let toolbox = gDevTools.getToolbox(this.hud.owner.target);
+ let 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);
+
+ let inputContainer = doc.querySelector(".jsterm-input-container");
+ this.completeNode = doc.querySelector(".jsterm-complete-node");
+ this.inputNode = doc.querySelector(".jsterm-input-node");
+
+ if (this.hud.isBrowserConsole &&
+ !Services.prefs.getBoolPref("devtools.chrome.enabled")) {
+ inputContainer.style.display = "none";
+ } else {
+ let okstring = l10n.getStr("selfxss.okstring");
+ let msg = l10n.getFormatStr("selfxss.msg", [okstring]);
+ this._onPaste = WebConsoleUtils.pasteHandlerGen(
+ this.inputNode, doc.getElementById("webconsole-notificationbox"),
+ msg, okstring);
+ this.inputNode.addEventListener("keypress", this._keyPress, false);
+ this.inputNode.addEventListener("paste", this._onPaste);
+ this.inputNode.addEventListener("drop", this._onPaste);
+ this.inputNode.addEventListener("input", this._inputEventHandler, false);
+ this.inputNode.addEventListener("keyup", this._inputEventHandler, false);
+ this.inputNode.addEventListener("focus", this._focusEventHandler, false);
+ }
+
+ this.hud.window.addEventListener("blur", this._blurEventHandler, false);
+ this.lastInputValue && this.setInputValue(this.lastInputValue);
+ },
+
+ focus: function () {
+ if (!this.inputNode.getAttribute("focused")) {
+ this.inputNode.focus();
+ }
+ },
+
+ /**
+ * The JavaScript evaluation response handler.
+ *
+ * @private
+ * @param function [callback]
+ * Optional function to invoke when the evaluation result is added to
+ * the output.
+ * @param object response
+ * The message received from the server.
+ */
+ _executeResultCallback: function (callback, response) {
+ if (!this.hud) {
+ return;
+ }
+ if (response.error) {
+ console.error("Evaluation error " + response.error + ": " +
+ response.message);
+ return;
+ }
+ let errorMessage = response.exceptionMessage;
+ let errorDocURL = response.exceptionDocURL;
+
+ let errorDocLink;
+ if (errorDocURL) {
+ errorMessage += " ";
+ errorDocLink = this.hud.document.createElementNS(XHTML_NS, "a");
+ errorDocLink.className = "learn-more-link webconsole-learn-more-link";
+ errorDocLink.textContent = `[${l10n.getStr("webConsoleMoreInfoLabel")}]`;
+ errorDocLink.title = errorDocURL.split("?")[0];
+ errorDocLink.href = "#";
+ errorDocLink.draggable = false;
+ errorDocLink.addEventListener("click", () => {
+ this.hud.owner.openLink(errorDocURL);
+ });
+ }
+
+ // Wrap thrown strings in Error objects, so `throw "foo"` outputs
+ // "Error: foo"
+ if (typeof response.exception === "string") {
+ errorMessage = new Error(errorMessage).toString();
+ }
+ let result = response.result;
+ let helperResult = response.helperResult;
+ let helperHasRawOutput = !!(helperResult || {}).rawOutput;
+
+ if (helperResult && helperResult.type) {
+ switch (helperResult.type) {
+ case "clearOutput":
+ this.clearOutput();
+ break;
+ case "clearHistory":
+ this.clearHistory();
+ break;
+ case "inspectObject":
+ this.openVariablesView({
+ label:
+ VariablesView.getString(helperResult.object, { concise: true }),
+ objectActor: helperResult.object,
+ });
+ break;
+ case "error":
+ try {
+ errorMessage = l10n.getStr(helperResult.message);
+ } catch (ex) {
+ errorMessage = helperResult.message;
+ }
+ break;
+ case "help":
+ this.hud.owner.openLink(HELP_URL);
+ break;
+ case "copyValueToClipboard":
+ clipboardHelper.copyString(helperResult.value);
+ break;
+ }
+ }
+
+ // Hide undefined results coming from JSTerm helper functions.
+ if (!errorMessage && result && typeof result == "object" &&
+ result.type == "undefined" &&
+ helperResult && !helperHasRawOutput) {
+ callback && callback();
+ return;
+ }
+
+ if (this.hud.NEW_CONSOLE_OUTPUT_ENABLED) {
+ this.hud.newConsoleOutput.dispatchMessageAdd(response, true).then(callback);
+ return;
+ }
+ let msg = new Messages.JavaScriptEvalOutput(response,
+ errorMessage, errorDocLink);
+ this.hud.output.addMessage(msg);
+
+ if (callback) {
+ let oldFlushCallback = this.hud._flushCallback;
+ this.hud._flushCallback = () => {
+ callback(msg.element);
+ if (oldFlushCallback) {
+ oldFlushCallback();
+ this.hud._flushCallback = oldFlushCallback;
+ return true;
+ }
+
+ return false;
+ };
+ }
+
+ msg._objectActors = new Set();
+
+ if (WebConsoleUtils.isActorGrip(response.exception)) {
+ msg._objectActors.add(response.exception.actor);
+ }
+
+ if (WebConsoleUtils.isActorGrip(result)) {
+ msg._objectActors.add(result.actor);
+ }
+ },
+
+ /**
+ * Execute a string. Execution happens asynchronously in the content process.
+ *
+ * @param string [executeString]
+ * The string you want to execute. If this is not provided, the current
+ * user input is used - taken from |this.getInputValue()|.
+ * @param function [callback]
+ * Optional function to invoke when the result is displayed.
+ * This is deprecated - please use the promise return value instead.
+ * @returns Promise
+ * Resolves with the message once the result is displayed.
+ */
+ execute: function (executeString, callback) {
+ let deferred = promise.defer();
+ let resultCallback;
+ if (this.hud.NEW_CONSOLE_OUTPUT_ENABLED) {
+ resultCallback = (msg) => deferred.resolve(msg);
+ } else {
+ resultCallback = (msg) => {
+ deferred.resolve(msg);
+ if (callback) {
+ callback(msg);
+ }
+ };
+ }
+
+ // attempt to execute the content of the inputNode
+ executeString = executeString || this.getInputValue();
+ if (!executeString) {
+ return null;
+ }
+
+ let selectedNodeActor = null;
+ let inspectorSelection = this.hud.owner.getInspectorSelection();
+ if (inspectorSelection && inspectorSelection.nodeFront) {
+ selectedNodeActor = inspectorSelection.nodeFront.actorID;
+ }
+
+ if (this.hud.NEW_CONSOLE_OUTPUT_ENABLED) {
+ const { ConsoleCommand } = require("devtools/client/webconsole/new-console-output/types");
+ let message = new ConsoleCommand({
+ messageText: executeString,
+ });
+ this.hud.proxy.dispatchMessageAdd(message);
+ } else {
+ let message = new Messages.Simple(executeString, {
+ category: "input",
+ severity: "log",
+ });
+ this.hud.output.addMessage(message);
+ }
+ let onResult = this._executeResultCallback.bind(this, resultCallback);
+
+ let options = {
+ frame: this.SELECTED_FRAME,
+ selectedNodeActor: selectedNodeActor,
+ };
+
+ this.requestEvaluation(executeString, options).then(onResult, onResult);
+
+ // Append a new value in the history of executed code, or overwrite the most
+ // recent entry. The most recent entry may contain the last edited input
+ // value that was not evaluated yet.
+ this.history[this.historyIndex++] = executeString;
+ this.historyPlaceHolder = this.history.length;
+
+ if (this.history.length > this.inputHistoryCount) {
+ this.history.splice(0, this.history.length - this.inputHistoryCount);
+ this.historyIndex = this.historyPlaceHolder = this.history.length;
+ }
+ this.storeHistory();
+ WebConsoleUtils.usageCount++;
+ this.setInputValue("");
+ this.clearCompletion();
+ return deferred.promise;
+ },
+
+ /**
+ * Request a JavaScript string evaluation from the server.
+ *
+ * @param string str
+ * String to execute.
+ * @param object [options]
+ * Options for evaluation:
+ * - bindObjectActor: tells the ObjectActor ID for which you want to do
+ * the evaluation. The Debugger.Object of the OA will be bound to
+ * |_self| during evaluation, such that it's usable in the string you
+ * execute.
+ * - frame: tells the stackframe depth to evaluate the string in. If
+ * the jsdebugger is paused, you can pick the stackframe to be used for
+ * evaluation. Use |this.SELECTED_FRAME| to always pick the
+ * user-selected stackframe.
+ * If you do not provide a |frame| the string will be evaluated in the
+ * global content window.
+ * - selectedNodeActor: tells the NodeActor ID of the current selection
+ * in the Inspector, if such a selection exists. This is used by
+ * helper functions that can evaluate on the current selection.
+ * @return object
+ * A promise object that is resolved when the server response is
+ * received.
+ */
+ requestEvaluation: function (str, options = {}) {
+ let deferred = promise.defer();
+
+ function onResult(response) {
+ if (!response.error) {
+ deferred.resolve(response);
+ } else {
+ deferred.reject(response);
+ }
+ }
+
+ let frameActor = null;
+ if ("frame" in options) {
+ frameActor = this.getFrameActor(options.frame);
+ }
+
+ let evalOptions = {
+ bindObjectActor: options.bindObjectActor,
+ frameActor: frameActor,
+ selectedNodeActor: options.selectedNodeActor,
+ selectedObjectActor: options.selectedObjectActor,
+ };
+
+ this.webConsoleClient.evaluateJSAsync(str, onResult, evalOptions);
+ return deferred.promise;
+ },
+
+ /**
+ * Retrieve the FrameActor ID given a frame depth.
+ *
+ * @param number frame
+ * Frame depth.
+ * @return string|null
+ * The FrameActor ID for the given frame depth.
+ */
+ getFrameActor: function (frame) {
+ let state = this.hud.owner.getDebuggerFrames();
+ if (!state) {
+ return null;
+ }
+
+ let grip;
+ if (frame == this.SELECTED_FRAME) {
+ grip = state.frames[state.selected];
+ } else {
+ grip = state.frames[frame];
+ }
+
+ return grip ? grip.actor : null;
+ },
+
+ /**
+ * Opens a new variables view that allows the inspection of the given object.
+ *
+ * @param object options
+ * Options for the variables view:
+ * - objectActor: grip of the ObjectActor you want to show in the
+ * variables view.
+ * - rawObject: the raw object you want to show in the variables view.
+ * - label: label to display in the variables view for inspected
+ * object.
+ * - hideFilterInput: optional boolean, |true| if you want to hide the
+ * variables view filter input.
+ * - targetElement: optional nsIDOMElement to append the variables view
+ * to. An iframe element is used as a container for the view. If this
+ * option is not used, then the variables view opens in the sidebar.
+ * - autofocus: optional boolean, |true| if you want to give focus to
+ * the variables view window after open, |false| otherwise.
+ * @return object
+ * A promise object that is resolved when the variables view has
+ * opened. The new variables view instance is given to the callbacks.
+ */
+ openVariablesView: function (options) {
+ let onContainerReady = (window) => {
+ let container = window.document.querySelector("#variables");
+ let view = this._variablesView;
+ if (!view || options.targetElement) {
+ let viewOptions = {
+ container: container,
+ hideFilterInput: options.hideFilterInput,
+ };
+ view = this._createVariablesView(viewOptions);
+ if (!options.targetElement) {
+ this._variablesView = view;
+ window.addEventListener("keypress", this._onKeypressInVariablesView);
+ }
+ }
+ options.view = view;
+ this._updateVariablesView(options);
+
+ if (!options.targetElement && options.autofocus) {
+ window.focus();
+ }
+
+ this.emit("variablesview-open", view, options);
+ return view;
+ };
+
+ let openPromise;
+ if (options.targetElement) {
+ let deferred = promise.defer();
+ openPromise = deferred.promise;
+ let document = options.targetElement.ownerDocument;
+ let iframe = document.createElementNS(XHTML_NS, "iframe");
+
+ iframe.addEventListener("load", function onIframeLoad() {
+ iframe.removeEventListener("load", onIframeLoad, true);
+ iframe.style.visibility = "visible";
+ deferred.resolve(iframe.contentWindow);
+ }, true);
+
+ iframe.flex = 1;
+ iframe.style.visibility = "hidden";
+ iframe.setAttribute("src", VARIABLES_VIEW_URL);
+ options.targetElement.appendChild(iframe);
+ } else {
+ if (!this.sidebar) {
+ this._createSidebar();
+ }
+ openPromise = this._addVariablesViewSidebarTab();
+ }
+
+ return openPromise.then(onContainerReady);
+ },
+
+ /**
+ * Create the Web Console sidebar.
+ *
+ * @see devtools/framework/sidebar.js
+ * @private
+ */
+ _createSidebar: function () {
+ let tabbox = this.hud.document.querySelector("#webconsole-sidebar");
+ this.sidebar = new ToolSidebar(tabbox, this, "webconsole");
+ this.sidebar.show();
+ this.emit("sidebar-opened");
+ },
+
+ /**
+ * Add the variables view tab to the sidebar.
+ *
+ * @private
+ * @return object
+ * A promise object for the adding of the new tab.
+ */
+ _addVariablesViewSidebarTab: function () {
+ let deferred = promise.defer();
+
+ let onTabReady = () => {
+ let window = this.sidebar.getWindowForTab("variablesview");
+ deferred.resolve(window);
+ };
+
+ let tabPanel = this.sidebar.getTabPanel("variablesview");
+ if (tabPanel) {
+ if (this.sidebar.getCurrentTabID() == "variablesview") {
+ onTabReady();
+ } else {
+ this.sidebar.once("variablesview-selected", onTabReady);
+ this.sidebar.select("variablesview");
+ }
+ } else {
+ this.sidebar.once("variablesview-ready", onTabReady);
+ this.sidebar.addTab("variablesview", VARIABLES_VIEW_URL, {selected: true});
+ }
+
+ return deferred.promise;
+ },
+
+ /**
+ * The keypress event handler for the Variables View sidebar. Currently this
+ * is used for removing the sidebar when Escape is pressed.
+ *
+ * @private
+ * @param nsIDOMEvent event
+ * The keypress DOM event object.
+ */
+ _onKeypressInVariablesView: function (event) {
+ let tag = event.target.nodeName;
+ if (event.keyCode != KeyCodes.DOM_VK_ESCAPE || event.shiftKey ||
+ event.altKey || event.ctrlKey || event.metaKey ||
+ ["input", "textarea", "select", "textbox"].indexOf(tag) > -1) {
+ return;
+ }
+
+ this._sidebarDestroy();
+ this.focus();
+ event.stopPropagation();
+ },
+
+ /**
+ * Create a variables view instance.
+ *
+ * @private
+ * @param object options
+ * Options for the new Variables View instance:
+ * - container: the DOM element where the variables view is inserted.
+ * - hideFilterInput: boolean, if true the variables filter input is
+ * hidden.
+ * @return object
+ * The new Variables View instance.
+ */
+ _createVariablesView: function (options) {
+ let view = new VariablesView(options.container);
+ view.toolbox = gDevTools.getToolbox(this.hud.owner.target);
+ view.searchPlaceholder = l10n.getStr("propertiesFilterPlaceholder");
+ view.emptyText = l10n.getStr("emptyPropertiesList");
+ view.searchEnabled = !options.hideFilterInput;
+ view.lazyEmpty = this._lazyVariablesView;
+
+ VariablesViewController.attach(view, {
+ getEnvironmentClient: grip => {
+ return new EnvironmentClient(this.hud.proxy.client, grip);
+ },
+ getObjectClient: grip => {
+ return new ObjectClient(this.hud.proxy.client, grip);
+ },
+ getLongStringClient: grip => {
+ return this.webConsoleClient.longString(grip);
+ },
+ releaseActor: actor => {
+ this.hud._releaseObject(actor);
+ },
+ simpleValueEvalMacro: simpleValueEvalMacro,
+ overrideValueEvalMacro: overrideValueEvalMacro,
+ getterOrSetterEvalMacro: getterOrSetterEvalMacro,
+ });
+
+ // Relay events from the VariablesView.
+ view.on("fetched", (event, type, variableObject) => {
+ this.emit("variablesview-fetched", variableObject);
+ });
+
+ return view;
+ },
+
+ /**
+ * Update the variables view.
+ *
+ * @private
+ * @param object options
+ * Options for updating the variables view:
+ * - view: the view you want to update.
+ * - objectActor: the grip of the new ObjectActor you want to show in
+ * the view.
+ * - rawObject: the new raw object you want to show.
+ * - label: the new label for the inspected object.
+ */
+ _updateVariablesView: function (options) {
+ let view = options.view;
+ view.empty();
+
+ // We need to avoid pruning the object inspection starting point.
+ // That one is pruned when the console message is removed.
+ view.controller.releaseActors(actor => {
+ return view._consoleLastObjectActor != actor;
+ });
+
+ if (options.objectActor &&
+ (!this.hud.isBrowserConsole ||
+ Services.prefs.getBoolPref("devtools.chrome.enabled"))) {
+ // Make sure eval works in the correct context.
+ view.eval = this._variablesViewEvaluate.bind(this, options);
+ view.switch = this._variablesViewSwitch.bind(this, options);
+ view.delete = this._variablesViewDelete.bind(this, options);
+ } else {
+ view.eval = null;
+ view.switch = null;
+ view.delete = null;
+ }
+
+ let { variable, expanded } = view.controller.setSingleVariable(options);
+ variable.evaluationMacro = simpleValueEvalMacro;
+
+ if (options.objectActor) {
+ view._consoleLastObjectActor = options.objectActor.actor;
+ } else if (options.rawObject) {
+ view._consoleLastObjectActor = null;
+ } else {
+ throw new Error(
+ "Variables View cannot open without giving it an object display.");
+ }
+
+ expanded.then(() => {
+ this.emit("variablesview-updated", view, options);
+ });
+ },
+
+ /**
+ * The evaluation function used by the variables view when editing a property
+ * value.
+ *
+ * @private
+ * @param object options
+ * The options used for |this._updateVariablesView()|.
+ * @param object variableObject
+ * The Variable object instance for the edited property.
+ * @param string value
+ * The value the edited property was changed to.
+ */
+ _variablesViewEvaluate: function (options, variableObject, value) {
+ let updater = this._updateVariablesView.bind(this, options);
+ let onEval = this._silentEvalCallback.bind(this, updater);
+ let string = variableObject.evaluationMacro(variableObject, value);
+
+ let evalOptions = {
+ frame: this.SELECTED_FRAME,
+ bindObjectActor: options.objectActor.actor,
+ };
+
+ this.requestEvaluation(string, evalOptions).then(onEval, onEval);
+ },
+
+ /**
+ * The property deletion function used by the variables view when a property
+ * is deleted.
+ *
+ * @private
+ * @param object options
+ * The options used for |this._updateVariablesView()|.
+ * @param object variableObject
+ * The Variable object instance for the deleted property.
+ */
+ _variablesViewDelete: function (options, variableObject) {
+ let onEval = this._silentEvalCallback.bind(this, null);
+
+ let evalOptions = {
+ frame: this.SELECTED_FRAME,
+ bindObjectActor: options.objectActor.actor,
+ };
+
+ this.requestEvaluation("delete _self" +
+ variableObject.symbolicName, evalOptions).then(onEval, onEval);
+ },
+
+ /**
+ * The property rename function used by the variables view when a property
+ * is renamed.
+ *
+ * @private
+ * @param object options
+ * The options used for |this._updateVariablesView()|.
+ * @param object variableObject
+ * The Variable object instance for the renamed property.
+ * @param string newName
+ * The new name for the property.
+ */
+ _variablesViewSwitch: function (options, variableObject, newName) {
+ let updater = this._updateVariablesView.bind(this, options);
+ let onEval = this._silentEvalCallback.bind(this, updater);
+
+ let evalOptions = {
+ frame: this.SELECTED_FRAME,
+ bindObjectActor: options.objectActor.actor,
+ };
+
+ let newSymbolicName =
+ variableObject.ownerView.symbolicName + '["' + newName + '"]';
+ if (newSymbolicName == variableObject.symbolicName) {
+ return;
+ }
+
+ let code = "_self" + newSymbolicName + " = _self" +
+ variableObject.symbolicName + ";" + "delete _self" +
+ variableObject.symbolicName;
+
+ this.requestEvaluation(code, evalOptions).then(onEval, onEval);
+ },
+
+ /**
+ * A noop callback for JavaScript evaluation. This method releases any
+ * result ObjectActors that come from the server for evaluation requests. This
+ * is used for editing, renaming and deleting properties in the variables
+ * view.
+ *
+ * Exceptions are displayed in the output.
+ *
+ * @private
+ * @param function callback
+ * Function to invoke once the response is received.
+ * @param object response
+ * The response packet received from the server.
+ */
+ _silentEvalCallback: function (callback, response) {
+ if (response.error) {
+ console.error("Web Console evaluation failed. " + response.error + ":" +
+ response.message);
+
+ callback && callback(response);
+ return;
+ }
+
+ if (response.exceptionMessage) {
+ let message = new Messages.Simple(response.exceptionMessage, {
+ category: "output",
+ severity: "error",
+ timestamp: response.timestamp,
+ });
+ this.hud.output.addMessage(message);
+ message._objectActors = new Set();
+ if (WebConsoleUtils.isActorGrip(response.exception)) {
+ message._objectActors.add(response.exception.actor);
+ }
+ }
+
+ let helper = response.helperResult || { type: null };
+ let helperGrip = null;
+ if (helper.type == "inspectObject") {
+ helperGrip = helper.object;
+ }
+
+ let grips = [response.result, helperGrip];
+ for (let grip of grips) {
+ if (WebConsoleUtils.isActorGrip(grip)) {
+ this.hud._releaseObject(grip.actor);
+ }
+ }
+
+ callback && callback(response);
+ },
+
+ /**
+ * 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.
+ */
+ clearOutput: function (clearStorage) {
+ let hud = this.hud;
+ let outputNode = hud.outputNode;
+ let node;
+ while ((node = outputNode.firstChild)) {
+ hud.removeOutputMessage(node);
+ }
+
+ hud.groupDepth = 0;
+ hud._outputQueue.forEach(hud._destroyItem, hud);
+ hud._outputQueue = [];
+ this.webConsoleClient.clearNetworkRequests();
+ hud._repeatNodes = {};
+
+ if (clearStorage) {
+ this.webConsoleClient.clearMessagesCache();
+ }
+
+ this._sidebarDestroy();
+
+ if (hud.NEW_CONSOLE_OUTPUT_ENABLED) {
+ hud.newConsoleOutput.dispatchMessagesClear();
+ }
+
+ this.emit("messages-cleared");
+ },
+
+ /**
+ * Remove all of the private messages from the Web Console output.
+ *
+ * This method emits the "private-messages-cleared" notification.
+ */
+ clearPrivateMessages: function () {
+ let nodes = this.hud.outputNode.querySelectorAll(".message[private]");
+ for (let node of nodes) {
+ this.hud.removeOutputMessage(node);
+ }
+ this.emit("private-messages-cleared");
+ },
+
+ /**
+ * Updates the size of the input field (command line) to fit its contents.
+ *
+ * @returns void
+ */
+ resizeInput: function () {
+ let inputNode = this.inputNode;
+
+ // Reset the height so that scrollHeight will reflect the natural height of
+ // the contents of the input field.
+ inputNode.style.height = "auto";
+
+ // Now resize the input field to fit its contents.
+ let scrollHeight = inputNode.inputField.scrollHeight;
+ if (scrollHeight > 0) {
+ inputNode.style.height = scrollHeight + "px";
+ }
+ },
+
+ /**
+ * Sets the value of the input field (command line), and resizes the field to
+ * fit its contents. This method is preferred over setting "inputNode.value"
+ * directly, because it correctly resizes the field.
+ *
+ * @param string newValue
+ * The new value to set.
+ * @returns void
+ */
+ setInputValue: function (newValue) {
+ this.inputNode.value = newValue;
+ this.lastInputValue = newValue;
+ this.completeNode.value = "";
+ this.resizeInput();
+ this._inputChanged = true;
+ this.emit("set-input-value");
+ },
+
+ /**
+ * Gets the value from the input field
+ * @returns string
+ */
+ getInputValue: function () {
+ return this.inputNode.value || "";
+ },
+
+ /**
+ * The inputNode "input" and "keyup" event handler.
+ * @private
+ */
+ _inputEventHandler: function () {
+ if (this.lastInputValue != this.getInputValue()) {
+ this.resizeInput();
+ this.complete(this.COMPLETE_HINT_ONLY);
+ this.lastInputValue = this.getInputValue();
+ this._inputChanged = true;
+ }
+ },
+
+ /**
+ * The window "blur" event handler.
+ * @private
+ */
+ _blurEventHandler: function () {
+ if (this.autocompletePopup) {
+ this.clearCompletion();
+ }
+ },
+
+ /* eslint-disable complexity */
+ /**
+ * The inputNode "keypress" event handler.
+ *
+ * @private
+ * @param nsIDOMEvent event
+ */
+ _keyPress: function (event) {
+ let inputNode = this.inputNode;
+ let inputValue = this.getInputValue();
+ let inputUpdated = false;
+
+ if (event.ctrlKey) {
+ switch (event.charCode) {
+ case 101:
+ // control-e
+ if (Services.appinfo.OS == "WINNT") {
+ break;
+ }
+ let lineEndPos = inputValue.length;
+ if (this.hasMultilineInput()) {
+ // find index of closest newline >= cursor
+ for (let i = inputNode.selectionEnd; i < lineEndPos; i++) {
+ if (inputValue.charAt(i) == "\r" ||
+ inputValue.charAt(i) == "\n") {
+ lineEndPos = i;
+ break;
+ }
+ }
+ }
+ inputNode.setSelectionRange(lineEndPos, lineEndPos);
+ event.preventDefault();
+ this.clearCompletion();
+ break;
+
+ case 110:
+ // 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.canCaretGoNext() &&
+ this.historyPeruse(HISTORY_FORWARD)) {
+ event.preventDefault();
+ // Ctrl-N is also used to focus the Network category button on
+ // MacOSX. The preventDefault() call doesn't prevent the focus
+ // from moving away from the input.
+ this.focus();
+ }
+ this.clearCompletion();
+ break;
+
+ case 112:
+ // 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.canCaretGoPrevious() &&
+ this.historyPeruse(HISTORY_BACK)) {
+ event.preventDefault();
+ // Ctrl-P may also be used to focus some category button on MacOSX.
+ // The preventDefault() call doesn't prevent the focus from moving
+ // away from the input.
+ this.focus();
+ }
+ this.clearCompletion();
+ break;
+ default:
+ break;
+ }
+ return;
+ } else if (event.keyCode == KeyCodes.DOM_VK_RETURN) {
+ let autoMultiline = Services.prefs.getBoolPref(PREF_AUTO_MULTILINE);
+ if (event.shiftKey ||
+ (!Debugger.isCompilableUnit(inputNode.value) && autoMultiline)) {
+ // shift return or incomplete statement
+ return;
+ }
+ }
+
+ switch (event.keyCode) {
+ case KeyCodes.DOM_VK_ESCAPE:
+ if (this.autocompletePopup.isOpen) {
+ this.clearCompletion();
+ event.preventDefault();
+ event.stopPropagation();
+ } else if (this.sidebar) {
+ this._sidebarDestroy();
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ break;
+
+ case KeyCodes.DOM_VK_RETURN:
+ if (this._autocompletePopupNavigated &&
+ this.autocompletePopup.isOpen &&
+ this.autocompletePopup.selectedIndex > -1) {
+ this.acceptProposedCompletion();
+ } else {
+ this.execute();
+ this._inputChanged = false;
+ }
+ event.preventDefault();
+ break;
+
+ case KeyCodes.DOM_VK_UP:
+ if (this.autocompletePopup.isOpen) {
+ inputUpdated = this.complete(this.COMPLETE_BACKWARD);
+ if (inputUpdated) {
+ this._autocompletePopupNavigated = true;
+ }
+ } else if (this.canCaretGoPrevious()) {
+ inputUpdated = this.historyPeruse(HISTORY_BACK);
+ }
+ if (inputUpdated) {
+ event.preventDefault();
+ }
+ break;
+
+ case KeyCodes.DOM_VK_DOWN:
+ if (this.autocompletePopup.isOpen) {
+ inputUpdated = this.complete(this.COMPLETE_FORWARD);
+ if (inputUpdated) {
+ this._autocompletePopupNavigated = true;
+ }
+ } else if (this.canCaretGoNext()) {
+ inputUpdated = this.historyPeruse(HISTORY_FORWARD);
+ }
+ if (inputUpdated) {
+ event.preventDefault();
+ }
+ break;
+
+ case KeyCodes.DOM_VK_PAGE_UP:
+ if (this.autocompletePopup.isOpen) {
+ inputUpdated = this.complete(this.COMPLETE_PAGEUP);
+ if (inputUpdated) {
+ this._autocompletePopupNavigated = true;
+ }
+ } else {
+ this.hud.outputScroller.scrollTop =
+ Math.max(0,
+ this.hud.outputScroller.scrollTop -
+ this.hud.outputScroller.clientHeight
+ );
+ }
+ event.preventDefault();
+ break;
+
+ case KeyCodes.DOM_VK_PAGE_DOWN:
+ if (this.autocompletePopup.isOpen) {
+ inputUpdated = this.complete(this.COMPLETE_PAGEDOWN);
+ if (inputUpdated) {
+ this._autocompletePopupNavigated = true;
+ }
+ } else {
+ this.hud.outputScroller.scrollTop =
+ Math.min(this.hud.outputScroller.scrollHeight,
+ this.hud.outputScroller.scrollTop +
+ this.hud.outputScroller.clientHeight
+ );
+ }
+ event.preventDefault();
+ break;
+
+ case KeyCodes.DOM_VK_HOME:
+ if (this.autocompletePopup.isOpen) {
+ this.autocompletePopup.selectedIndex = 0;
+ event.preventDefault();
+ } else if (inputValue.length <= 0) {
+ this.hud.outputScroller.scrollTop = 0;
+ event.preventDefault();
+ }
+ break;
+
+ case KeyCodes.DOM_VK_END:
+ if (this.autocompletePopup.isOpen) {
+ this.autocompletePopup.selectedIndex =
+ this.autocompletePopup.itemCount - 1;
+ event.preventDefault();
+ } else if (inputValue.length <= 0) {
+ this.hud.outputScroller.scrollTop =
+ this.hud.outputScroller.scrollHeight;
+ event.preventDefault();
+ }
+ break;
+
+ case KeyCodes.DOM_VK_LEFT:
+ if (this.autocompletePopup.isOpen || this.lastCompletion.value) {
+ this.clearCompletion();
+ }
+ break;
+
+ case KeyCodes.DOM_VK_RIGHT:
+ let cursorAtTheEnd = this.inputNode.selectionStart ==
+ this.inputNode.selectionEnd &&
+ this.inputNode.selectionStart ==
+ inputValue.length;
+ let haveSuggestion = this.autocompletePopup.isOpen ||
+ this.lastCompletion.value;
+ let useCompletion = cursorAtTheEnd || this._autocompletePopupNavigated;
+ if (haveSuggestion && useCompletion &&
+ this.complete(this.COMPLETE_HINT_ONLY) &&
+ this.lastCompletion.value &&
+ this.acceptProposedCompletion()) {
+ event.preventDefault();
+ }
+ if (this.autocompletePopup.isOpen) {
+ this.clearCompletion();
+ }
+ break;
+
+ case KeyCodes.DOM_VK_TAB:
+ // Generate a completion and accept the first proposed value.
+ if (this.complete(this.COMPLETE_HINT_ONLY) &&
+ this.lastCompletion &&
+ this.acceptProposedCompletion()) {
+ event.preventDefault();
+ } else if (this._inputChanged) {
+ this.updateCompleteNode(l10n.getStr("Autocomplete.blank"));
+ event.preventDefault();
+ }
+ break;
+ default:
+ break;
+ }
+ },
+ /* eslint-enable complexity */
+
+ /**
+ * The inputNode "focus" event handler.
+ * @private
+ */
+ _focusEventHandler: function () {
+ this._inputChanged = false;
+ },
+
+ /**
+ * 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: function (direction) {
+ if (!this.history.length) {
+ return false;
+ }
+
+ // Up Arrow key
+ if (direction == HISTORY_BACK) {
+ if (this.historyPlaceHolder <= 0) {
+ return false;
+ }
+ let inputVal = this.history[--this.historyPlaceHolder];
+
+ // Save the current input value as the latest entry in history, only if
+ // the user is already at the last entry.
+ // Note: this code does not store changes to items that are already in
+ // history.
+ if (this.historyPlaceHolder + 1 == this.historyIndex) {
+ this.history[this.historyIndex] = this.getInputValue() || "";
+ }
+
+ this.setInputValue(inputVal);
+ } else if (direction == HISTORY_FORWARD) {
+ // Down Arrow key
+ if (this.historyPlaceHolder >= (this.history.length - 1)) {
+ return false;
+ }
+
+ let inputVal = this.history[++this.historyPlaceHolder];
+ this.setInputValue(inputVal);
+ } else {
+ throw new Error("Invalid argument 0");
+ }
+
+ return true;
+ },
+
+ /**
+ * Test for multiline input.
+ *
+ * @return boolean
+ * True if CR or LF found in node value; else false.
+ */
+ hasMultilineInput: function () {
+ return /[\r\n]/.test(this.getInputValue());
+ },
+
+ /**
+ * 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: function () {
+ let node = this.inputNode;
+ if (node.selectionStart != node.selectionEnd) {
+ return false;
+ }
+
+ let multiline = /[\r\n]/.test(node.value);
+ return node.selectionStart == 0 ? true :
+ node.selectionStart == node.value.length && !multiline;
+ },
+
+ /**
+ * 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: function () {
+ let node = this.inputNode;
+ if (node.selectionStart != node.selectionEnd) {
+ return false;
+ }
+
+ let multiline = /[\r\n]/.test(node.value);
+ return node.selectionStart == node.value.length ? true :
+ node.selectionStart == 0 && !multiline;
+ },
+
+ /**
+ * Completes the current typed text in the inputNode. Completion is performed
+ * only if the selection/cursor is at the end of the string. If no completion
+ * is found, the current inputNode value and cursor/selection stay.
+ *
+ * @param int type possible values are
+ * - this.COMPLETE_FORWARD: If there is more than one possible completion
+ * and the input value stayed the same compared to the last time this
+ * function was called, then the next completion of all possible
+ * completions is used. If the value changed, then the first possible
+ * completion is used and the selection is set from the current
+ * cursor position to the end of the completed text.
+ * If there is only one possible completion, then this completion
+ * value is used and the cursor is put at the end of the completion.
+ * - this.COMPLETE_BACKWARD: Same as this.COMPLETE_FORWARD but if the
+ * value stayed the same as the last time the function was called,
+ * then the previous completion of all possible completions is used.
+ * - this.COMPLETE_PAGEUP: Scroll up one page if available or select the
+ * first item.
+ * - this.COMPLETE_PAGEDOWN: Scroll down one page if available or select
+ * the last item.
+ * - this.COMPLETE_HINT_ONLY: If there is more than one possible
+ * completion and the input value stayed the same compared to the
+ * last time this function was called, then the same completion is
+ * used again. If there is only one possible completion, then
+ * the this.getInputValue() is set to this value and the selection
+ * is set from the current cursor position to the end of the
+ * completed text.
+ * @param function callback
+ * Optional function invoked when the autocomplete properties are
+ * updated.
+ * @returns boolean true if there existed a completion for the current input,
+ * or false otherwise.
+ */
+ complete: function (type, callback) {
+ let inputNode = this.inputNode;
+ let inputValue = this.getInputValue();
+ let frameActor = this.getFrameActor(this.SELECTED_FRAME);
+
+ // If the inputNode has no value, then don't try to complete on it.
+ if (!inputValue) {
+ this.clearCompletion();
+ callback && callback(this);
+ this.emit("autocomplete-updated");
+ return false;
+ }
+
+ // Only complete if the selection is empty.
+ if (inputNode.selectionStart != inputNode.selectionEnd) {
+ this.clearCompletion();
+ callback && callback(this);
+ this.emit("autocomplete-updated");
+ return false;
+ }
+
+ // Update the completion results.
+ if (this.lastCompletion.value != inputValue ||
+ frameActor != this._lastFrameActorId) {
+ this._updateCompletionResult(type, callback);
+ return false;
+ }
+
+ let popup = this.autocompletePopup;
+ let accepted = false;
+
+ if (type != this.COMPLETE_HINT_ONLY && popup.itemCount == 1) {
+ this.acceptProposedCompletion();
+ accepted = true;
+ } else if (type == this.COMPLETE_BACKWARD) {
+ popup.selectPreviousItem();
+ } else if (type == this.COMPLETE_FORWARD) {
+ popup.selectNextItem();
+ } else if (type == this.COMPLETE_PAGEUP) {
+ popup.selectPreviousPageItem();
+ } else if (type == this.COMPLETE_PAGEDOWN) {
+ popup.selectNextPageItem();
+ }
+
+ callback && callback(this);
+ this.emit("autocomplete-updated");
+ return accepted || popup.itemCount > 0;
+ },
+
+ /**
+ * Update the completion result. This operation is performed asynchronously by
+ * fetching updated results from the content process.
+ *
+ * @private
+ * @param int type
+ * Completion type. See this.complete() for details.
+ * @param function [callback]
+ * Optional, function to invoke when completion results are received.
+ */
+ _updateCompletionResult: function (type, callback) {
+ let frameActor = this.getFrameActor(this.SELECTED_FRAME);
+ if (this.lastCompletion.value == this.getInputValue() &&
+ frameActor == this._lastFrameActorId) {
+ return;
+ }
+
+ let requestId = gSequenceId();
+ let cursor = this.inputNode.selectionStart;
+ let input = this.getInputValue().substring(0, cursor);
+ let cache = this._autocompleteCache;
+
+ // If the current input starts with the previous input, then we already
+ // have a list of suggestions and we just need to filter the cached
+ // suggestions. When the current input ends with a non-alphanumeric
+ // character we ask the server again for suggestions.
+
+ // Check if last character is non-alphanumeric
+ if (!/[a-zA-Z0-9]$/.test(input) || frameActor != this._lastFrameActorId) {
+ this._autocompleteQuery = null;
+ this._autocompleteCache = null;
+ }
+
+ if (this._autocompleteQuery && input.startsWith(this._autocompleteQuery)) {
+ let filterBy = input;
+ // Find the last non-alphanumeric other than _ or $ if it exists.
+ let 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);
+ }
+
+ let newList = cache.sort().filter(function (l) {
+ return l.startsWith(filterBy);
+ });
+
+ this.lastCompletion = {
+ requestId: null,
+ completionType: type,
+ value: null,
+ };
+
+ let response = { matches: newList, matchProp: filterBy };
+ this._receiveAutocompleteProperties(null, callback, response);
+ return;
+ }
+
+ this._lastFrameActorId = frameActor;
+
+ this.lastCompletion = {
+ requestId: requestId,
+ completionType: type,
+ value: null,
+ };
+
+ let autocompleteCallback =
+ this._receiveAutocompleteProperties.bind(this, requestId, callback);
+
+ this.webConsoleClient.autocomplete(
+ input, cursor, autocompleteCallback, frameActor);
+ },
+
+ /**
+ * Handler for the autocompletion results. This method takes
+ * the completion result received from the server and updates the UI
+ * accordingly.
+ *
+ * @param number requestId
+ * Request ID.
+ * @param function [callback=null]
+ * Optional, function to invoke when the completion result is received.
+ * @param object message
+ * The JSON message which holds the completion results received from
+ * the content process.
+ */
+ _receiveAutocompleteProperties: function (requestId, callback, message) {
+ let inputNode = this.inputNode;
+ let inputValue = this.getInputValue();
+ if (this.lastCompletion.value == inputValue ||
+ requestId != this.lastCompletion.requestId) {
+ return;
+ }
+ // Cache whatever came from the server if the last char is
+ // alphanumeric or '.'
+ let cursor = inputNode.selectionStart;
+ let inputUntilCursor = inputValue.substring(0, cursor);
+
+ if (requestId != null && /[a-zA-Z0-9.]$/.test(inputUntilCursor)) {
+ this._autocompleteCache = message.matches;
+ this._autocompleteQuery = inputUntilCursor;
+ }
+
+ let matches = message.matches;
+ let lastPart = message.matchProp;
+ if (!matches.length) {
+ this.clearCompletion();
+ callback && callback(this);
+ this.emit("autocomplete-updated");
+ return;
+ }
+
+ let items = matches.reverse().map(function (match) {
+ return { preLabel: lastPart, label: match };
+ });
+
+ let popup = this.autocompletePopup;
+ popup.setItems(items);
+
+ let completionType = this.lastCompletion.completionType;
+ this.lastCompletion = {
+ value: inputValue,
+ matchProp: lastPart,
+ };
+
+ if (items.length > 1 && !popup.isOpen) {
+ let str = this.getInputValue().substr(0, this.inputNode.selectionStart);
+ let offset = str.length - (str.lastIndexOf("\n") + 1) - lastPart.length;
+ let x = offset * this.hud._inputCharWidth;
+ popup.openPopup(inputNode, x + this.hud._chevronWidth);
+ this._autocompletePopupNavigated = false;
+ } else if (items.length < 2 && popup.isOpen) {
+ popup.hidePopup();
+ this._autocompletePopupNavigated = false;
+ }
+
+ if (items.length == 1) {
+ popup.selectedIndex = 0;
+ }
+
+ this.onAutocompleteSelect();
+
+ if (completionType != this.COMPLETE_HINT_ONLY && popup.itemCount == 1) {
+ this.acceptProposedCompletion();
+ } else if (completionType == this.COMPLETE_BACKWARD) {
+ popup.selectPreviousItem();
+ } else if (completionType == this.COMPLETE_FORWARD) {
+ popup.selectNextItem();
+ }
+
+ callback && callback(this);
+ this.emit("autocomplete-updated");
+ },
+
+ onAutocompleteSelect: function () {
+ // Render the suggestion only if the cursor is at the end of the input.
+ if (this.inputNode.selectionStart != this.getInputValue().length) {
+ return;
+ }
+
+ let currentItem = this.autocompletePopup.selectedItem;
+ if (currentItem && this.lastCompletion.value) {
+ let suffix =
+ currentItem.label.substring(this.lastCompletion.matchProp.length);
+ this.updateCompleteNode(suffix);
+ } else {
+ this.updateCompleteNode("");
+ }
+ },
+
+ /**
+ * Clear the current completion information and close the autocomplete popup,
+ * if needed.
+ */
+ clearCompletion: function () {
+ this.autocompletePopup.clearItems();
+ this.lastCompletion = { value: null };
+ this.updateCompleteNode("");
+ if (this.autocompletePopup.isOpen) {
+ // Trigger a blur/focus of the JSTerm input to force screen readers to read the
+ // value again.
+ this.inputNode.blur();
+ this.autocompletePopup.once("popup-closed", () => {
+ this.inputNode.focus();
+ });
+ this.autocompletePopup.hidePopup();
+ this._autocompletePopupNavigated = false;
+ }
+ },
+
+ /**
+ * Accept the proposed input completion.
+ *
+ * @return boolean
+ * True if there was a selected completion item and the input value
+ * was updated, false otherwise.
+ */
+ acceptProposedCompletion: function () {
+ let updated = false;
+
+ let currentItem = this.autocompletePopup.selectedItem;
+ if (currentItem && this.lastCompletion.value) {
+ let suffix =
+ currentItem.label.substring(this.lastCompletion.matchProp.length);
+ let cursor = this.inputNode.selectionStart;
+ let value = this.getInputValue();
+ this.setInputValue(value.substr(0, cursor) +
+ suffix + value.substr(cursor));
+ let newCursor = cursor + suffix.length;
+ this.inputNode.selectionStart = this.inputNode.selectionEnd = newCursor;
+ updated = true;
+ }
+
+ this.clearCompletion();
+
+ return updated;
+ },
+
+ /**
+ * Update the node that displays the currently selected autocomplete proposal.
+ *
+ * @param string suffix
+ * The proposed suffix for the inputNode value.
+ */
+ updateCompleteNode: function (suffix) {
+ // completion prefix = input, with non-control chars replaced by spaces
+ let prefix = suffix ? this.getInputValue().replace(/[\S]/g, " ") : "";
+ this.completeNode.value = prefix + suffix;
+ },
+
+ /**
+ * Destroy the sidebar.
+ * @private
+ */
+ _sidebarDestroy: function () {
+ if (this._variablesView) {
+ this._variablesView.controller.releaseActors();
+ this._variablesView = null;
+ }
+
+ if (this.sidebar) {
+ this.sidebar.hide();
+ this.sidebar.destroy();
+ this.sidebar = null;
+ }
+
+ this.emit("sidebar-closed");
+ },
+
+ /**
+ * Destroy the JSTerm object. Call this method to avoid memory leaks.
+ */
+ destroy: function () {
+ this._sidebarDestroy();
+
+ this.clearCompletion();
+ this.clearOutput();
+
+ this.autocompletePopup.destroy();
+ this.autocompletePopup = null;
+
+ if (this._onPaste) {
+ this.inputNode.removeEventListener("paste", this._onPaste, false);
+ this.inputNode.removeEventListener("drop", this._onPaste, false);
+ this._onPaste = null;
+ }
+
+ this.inputNode.removeEventListener("keypress", this._keyPress, false);
+ this.inputNode.removeEventListener("input", this._inputEventHandler, false);
+ this.inputNode.removeEventListener("keyup", this._inputEventHandler, false);
+ this.inputNode.removeEventListener("focus", this._focusEventHandler, false);
+ this.hud.window.removeEventListener("blur", this._blurEventHandler, false);
+
+ this.hud = null;
+ },
+};
+
+function gSequenceId() {
+ return gSequenceId.n++;
+}
+gSequenceId.n = 0;
+exports.gSequenceId = gSequenceId;
+
+/**
+ * @see VariablesView.simpleValueEvalMacro
+ */
+function simpleValueEvalMacro(item, currentString) {
+ return VariablesView.simpleValueEvalMacro(item, currentString, "_self");
+}
+
+/**
+ * @see VariablesView.overrideValueEvalMacro
+ */
+function overrideValueEvalMacro(item, currentString) {
+ return VariablesView.overrideValueEvalMacro(item, currentString, "_self");
+}
+
+/**
+ * @see VariablesView.getterOrSetterEvalMacro
+ */
+function getterOrSetterEvalMacro(item, currentString) {
+ return VariablesView.getterOrSetterEvalMacro(item, currentString, "_self");
+}
+
+exports.JSTerm = JSTerm;
diff --git a/devtools/client/webconsole/moz.build b/devtools/client/webconsole/moz.build
new file mode 100644
index 000000000..c8324b315
--- /dev/null
+++ b/devtools/client/webconsole/moz.build
@@ -0,0 +1,22 @@
+# -*- 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/.
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+
+DIRS += [
+ 'net',
+ 'new-console-output',
+]
+
+DevToolsModules(
+ 'console-commands.js',
+ 'console-output.js',
+ 'hudservice.js',
+ 'jsterm.js',
+ 'panel.js',
+ 'utils.js',
+ 'webconsole.js',
+)
diff --git a/devtools/client/webconsole/net/.eslintrc.js b/devtools/client/webconsole/net/.eslintrc.js
new file mode 100644
index 000000000..e105ac6e2
--- /dev/null
+++ b/devtools/client/webconsole/net/.eslintrc.js
@@ -0,0 +1,20 @@
+"use strict";
+
+module.exports = {
+ "globals": {
+ "Locale": true,
+ "Document": true,
+ "document": true,
+ "Node": true,
+ "Element": true,
+ "MessageEvent": true,
+ "BrowserLoader": true,
+ "addEventListener": true,
+ "DOMParser": true,
+ "dispatchEvent": true,
+ "setTimeout": true
+ },
+ "rules": {
+ "no-unused-vars": ["error", {"args": "none"}],
+ }
+};
diff --git a/devtools/client/webconsole/net/components/cookies-tab.js b/devtools/client/webconsole/net/components/cookies-tab.js
new file mode 100644
index 000000000..d76414679
--- /dev/null
+++ b/devtools/client/webconsole/net/components/cookies-tab.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 React = require("devtools/client/shared/vendor/react");
+const NetInfoGroupList = React.createFactory(require("./net-info-group-list"));
+const Spinner = React.createFactory(require("./spinner"));
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template represents 'Cookies' tab displayed when the user
+ * expands network log in the Console panel. It's responsible for rendering
+ * sent and received cookies.
+ */
+var CookiesTab = React.createClass({
+ propTypes: {
+ actions: PropTypes.shape({
+ requestData: PropTypes.func.isRequired
+ }),
+ data: PropTypes.object.isRequired,
+ },
+
+ displayName: "CookiesTab",
+
+ componentDidMount() {
+ let { actions, data } = this.props;
+ let requestCookies = data.request.cookies;
+ let responseCookies = data.response.cookies;
+
+ // TODO: use async action objects as soon as Redux is in place
+ if (!requestCookies || !requestCookies.length) {
+ actions.requestData("requestCookies");
+ }
+
+ if (!responseCookies || !responseCookies.length) {
+ actions.requestData("responseCookies");
+ }
+ },
+
+ render() {
+ let { actions, data: file } = this.props;
+ let requestCookies = file.request.cookies;
+ let responseCookies = file.response.cookies;
+
+ // The cookie panel displays two groups of cookies:
+ // 1) Response Cookies
+ // 2) Request Cookies
+ let groups = [{
+ key: "responseCookies",
+ name: Locale.$STR("responseCookies"),
+ params: responseCookies
+ }, {
+ key: "requestCookies",
+ name: Locale.$STR("requestCookies"),
+ params: requestCookies
+ }];
+
+ return (
+ DOM.div({className: "cookiesTabBox"},
+ DOM.div({className: "panelContent"},
+ NetInfoGroupList({
+ groups: groups
+ })
+ )
+ )
+ );
+ }
+});
+
+// Exports from this module
+module.exports = CookiesTab;
diff --git a/devtools/client/webconsole/net/components/headers-tab.js b/devtools/client/webconsole/net/components/headers-tab.js
new file mode 100644
index 000000000..2eca3fd2f
--- /dev/null
+++ b/devtools/client/webconsole/net/components/headers-tab.js
@@ -0,0 +1,79 @@
+/* This Source Code Form is subject to the terms of 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 React = require("devtools/client/shared/vendor/react");
+const NetInfoGroupList = React.createFactory(require("./net-info-group-list"));
+const Spinner = React.createFactory(require("./spinner"));
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template represents 'Headers' tab displayed when the user
+ * expands network log in the Console panel. It's responsible for rendering
+ * request and response HTTP headers.
+ */
+var HeadersTab = React.createClass({
+ propTypes: {
+ actions: PropTypes.shape({
+ requestData: PropTypes.func.isRequired
+ }),
+ data: PropTypes.object.isRequired,
+ },
+
+ displayName: "HeadersTab",
+
+ componentDidMount() {
+ let { actions, data } = this.props;
+ let requestHeaders = data.request.headers;
+ let responseHeaders = data.response.headers;
+
+ // Request headers if they are not available yet.
+ // TODO: use async action objects as soon as Redux is in place
+ if (!requestHeaders) {
+ actions.requestData("requestHeaders");
+ }
+
+ if (!responseHeaders) {
+ actions.requestData("responseHeaders");
+ }
+ },
+
+ render() {
+ let { data } = this.props;
+ let requestHeaders = data.request.headers;
+ let responseHeaders = data.response.headers;
+
+ // TODO: Another groups to implement:
+ // 1) Cached Headers
+ // 2) Headers from upload stream
+ let groups = [{
+ key: "responseHeaders",
+ name: Locale.$STR("responseHeaders"),
+ params: responseHeaders
+ }, {
+ key: "requestHeaders",
+ name: Locale.$STR("requestHeaders"),
+ params: requestHeaders
+ }];
+
+ // If response headers are not available yet, display a spinner
+ if (!responseHeaders || !responseHeaders.length) {
+ groups[0].content = Spinner();
+ }
+
+ return (
+ DOM.div({className: "headersTabBox"},
+ DOM.div({className: "panelContent"},
+ NetInfoGroupList({groups: groups})
+ )
+ )
+ );
+ }
+});
+
+// Exports from this module
+module.exports = HeadersTab;
diff --git a/devtools/client/webconsole/net/components/moz.build b/devtools/client/webconsole/net/components/moz.build
new file mode 100644
index 000000000..0053de780
--- /dev/null
+++ b/devtools/client/webconsole/net/components/moz.build
@@ -0,0 +1,25 @@
+# -*- 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/.
+
+DevToolsModules(
+ 'cookies-tab.js',
+ 'headers-tab.js',
+ 'net-info-body.css',
+ 'net-info-body.js',
+ 'net-info-group-list.js',
+ 'net-info-group.css',
+ 'net-info-group.js',
+ 'net-info-params.css',
+ 'net-info-params.js',
+ 'params-tab.js',
+ 'post-tab.js',
+ 'response-tab.css',
+ 'response-tab.js',
+ 'size-limit.css',
+ 'size-limit.js',
+ 'spinner.js',
+ 'stacktrace-tab.js',
+)
diff --git a/devtools/client/webconsole/net/components/net-info-body.css b/devtools/client/webconsole/net/components/net-info-body.css
new file mode 100644
index 000000000..2d0bac70e
--- /dev/null
+++ b/devtools/client/webconsole/net/components/net-info-body.css
@@ -0,0 +1,112 @@
+/* This Source Code Form is subject to the terms of 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/. */
+
+/******************************************************************************/
+/* Network Info Body */
+
+.netInfoBody {
+ margin: 10px 0 0 0;
+ width: 100%;
+ cursor: default;
+ display: block;
+}
+
+.netInfoBody *:focus {
+ outline: 0 !important;
+}
+
+.netInfoBody .panelContent {
+ word-break: break-all;
+}
+
+/******************************************************************************/
+/* Network Info Body Tabs */
+
+.netInfoBody > .tabs {
+ background-color: transparent;
+ background-image: none;
+ height: 100%;
+}
+
+.netInfoBody > .tabs .tabs-navigation {
+ border-bottom-color: var(--net-border);
+ background-color: transparent;
+ text-decoration: none;
+ padding-top: 3px;
+ padding-left: 7px;
+ padding-bottom: 1px;
+ border-bottom: 1px solid var(--net-border);
+}
+
+.netInfoBody > .tabs .tabs-menu {
+ display: table;
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+/* This is the trick that makes the tab bottom border invisible */
+.netInfoBody > .tabs .tabs-menu-item {
+ position: relative;
+ bottom: -2px;
+ float: left;
+}
+
+.netInfoBody > .tabs .tabs-menu-item a {
+ display: block;
+ border: 1px solid transparent;
+ text-decoration: none;
+ padding: 5px 8px 4px 8px;;
+ font-weight: bold;
+ color: var(--theme-body-color);
+ border-radius: 4px 4px 0 0;
+}
+
+.netInfoBody > .tabs .tab-panel {
+ background-color: var(--theme-body-background);
+ border: 1px solid transparent;
+ border-top: none;
+ padding: 10px;
+ overflow: auto;
+ height: calc(100% - 31px); /* minus the height of the tab bar */
+}
+
+.netInfoBody > .tabs .tab-panel > div,
+.netInfoBody > .tabs .tab-panel > div > div {
+ height: 100%;
+}
+
+.netInfoBody > .tabs .tabs-menu-item.is-active a,
+.netInfoBody > .tabs .tabs-menu-item.is-active a:focus,
+.netInfoBody > .tabs .tabs-menu-item.is-active:hover a {
+ background-color: var(--theme-body-background);
+ border: 1px solid transparent;
+ border-bottom-color: var(--theme-highlight-bluegrey);
+ color: var(--theme-highlight-bluegrey);
+}
+
+.netInfoBody > .tabs .tabs-menu-item:hover a {
+ border: 1px solid transparent;
+ border-bottom: 1px solid var(--net-border);
+ background-color: var(--theme-body-background);
+}
+
+
+/******************************************************************************/
+/* Themes */
+
+.theme-firebug .netInfoBody > .tabs .tab-panel {
+ border-color: var(--net-border);
+}
+
+.theme-firebug .netInfoBody > .tabs .tabs-menu-item.is-active a,
+.theme-firebug .netInfoBody > .tabs .tabs-menu-item.is-active:hover a,
+.theme-firebug .netInfoBody > .tabs .tabs-menu-item.is-active a:focus {
+ border: 1px solid var(--net-border);
+ border-bottom-color: transparent;
+}
+
+.theme-firebug .netInfoBody > .tabs .tabs-menu-item:hover a {
+ border-bottom-color: transparent;
+}
diff --git a/devtools/client/webconsole/net/components/net-info-body.js b/devtools/client/webconsole/net/components/net-info-body.js
new file mode 100644
index 000000000..c5eccd458
--- /dev/null
+++ b/devtools/client/webconsole/net/components/net-info-body.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 React = require("devtools/client/shared/vendor/react");
+const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
+const { Tabs, TabPanel } = createFactories(require("devtools/client/shared/components/tabs/tabs"));
+
+// Network
+const HeadersTab = React.createFactory(require("./headers-tab"));
+const ResponseTab = React.createFactory(require("./response-tab"));
+const ParamsTab = React.createFactory(require("./params-tab"));
+const CookiesTab = React.createFactory(require("./cookies-tab"));
+const PostTab = React.createFactory(require("./post-tab"));
+const StackTraceTab = React.createFactory(require("./stacktrace-tab"));
+const NetUtils = require("../utils/net");
+
+// Shortcuts
+const PropTypes = React.PropTypes;
+
+/**
+ * This template renders the basic Network log info body. It's not
+ * visible by default, the user needs to expand the network log
+ * to see it.
+ *
+ * This is the set of tabs displaying details about network events:
+ * 1) Headers - request and response headers
+ * 2) Params - URL parameters
+ * 3) Response - response body
+ * 4) Cookies - request and response cookies
+ * 5) Post - posted data
+ */
+var NetInfoBody = React.createClass({
+ propTypes: {
+ tabActive: PropTypes.number.isRequired,
+ actions: PropTypes.object.isRequired,
+ data: PropTypes.shape({
+ request: PropTypes.object.isRequired,
+ response: PropTypes.object.isRequired
+ })
+ },
+
+ displayName: "NetInfoBody",
+
+ getDefaultProps() {
+ return {
+ tabActive: 0
+ };
+ },
+
+ getInitialState() {
+ return {
+ data: {
+ request: {},
+ response: {}
+ },
+ tabActive: this.props.tabActive,
+ };
+ },
+
+ onTabChanged(index) {
+ this.setState({tabActive: index});
+ },
+
+ hasCookies() {
+ let {request, response} = this.state.data;
+ return this.state.hasCookies ||
+ NetUtils.getHeaderValue(request.headers, "Cookie") ||
+ NetUtils.getHeaderValue(response.headers, "Set-Cookie");
+ },
+
+ hasStackTrace() {
+ let {cause} = this.state.data;
+ return cause && cause.stacktrace && cause.stacktrace.length > 0;
+ },
+
+ getTabPanels() {
+ let actions = this.props.actions;
+ let data = this.state.data;
+ let {request} = data;
+
+ // Flags for optional tabs. Some tabs are visible only if there
+ // are data to display.
+ let hasParams = request.queryString && request.queryString.length;
+ let hasPostData = request.bodySize > 0;
+
+ let panels = [];
+
+ // Headers tab
+ panels.push(
+ TabPanel({
+ className: "headers",
+ key: "headers",
+ title: Locale.$STR("netRequest.headers")},
+ HeadersTab({data: data, actions: actions})
+ )
+ );
+
+ // URL parameters tab
+ if (hasParams) {
+ panels.push(
+ TabPanel({
+ className: "params",
+ key: "params",
+ title: Locale.$STR("netRequest.params")},
+ ParamsTab({data: data, actions: actions})
+ )
+ );
+ }
+
+ // Posted data tab
+ if (hasPostData) {
+ panels.push(
+ TabPanel({
+ className: "post",
+ key: "post",
+ title: Locale.$STR("netRequest.post")},
+ PostTab({data: data, actions: actions})
+ )
+ );
+ }
+
+ // Response tab
+ panels.push(
+ TabPanel({className: "response", key: "response",
+ title: Locale.$STR("netRequest.response")},
+ ResponseTab({data: data, actions: actions})
+ )
+ );
+
+ // Cookies tab
+ if (this.hasCookies()) {
+ panels.push(
+ TabPanel({
+ className: "cookies",
+ key: "cookies",
+ title: Locale.$STR("netRequest.cookies")},
+ CookiesTab({
+ data: data,
+ actions: actions
+ })
+ )
+ );
+ }
+
+ // Stacktrace tab
+ if (this.hasStackTrace()) {
+ panels.push(
+ TabPanel({
+ className: "stacktrace-tab",
+ key: "stacktrace",
+ title: Locale.$STR("netRequest.callstack")},
+ StackTraceTab({
+ data: data,
+ actions: actions
+ })
+ )
+ );
+ }
+
+ return panels;
+ },
+
+ render() {
+ let tabActive = this.state.tabActive;
+ let tabPanels = this.getTabPanels();
+ return (
+ Tabs({
+ tabActive: tabActive,
+ onAfterChange: this.onTabChanged},
+ tabPanels
+ )
+ );
+ }
+});
+
+// Exports from this module
+module.exports = NetInfoBody;
diff --git a/devtools/client/webconsole/net/components/net-info-group-list.js b/devtools/client/webconsole/net/components/net-info-group-list.js
new file mode 100644
index 000000000..247a23bb7
--- /dev/null
+++ b/devtools/client/webconsole/net/components/net-info-group-list.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 React = require("devtools/client/shared/vendor/react");
+const NetInfoGroup = React.createFactory(require("./net-info-group"));
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template is responsible for rendering sections/groups inside tabs.
+ * It's used e.g to display Response and Request headers as separate groups.
+ */
+var NetInfoGroupList = React.createClass({
+ propTypes: {
+ groups: PropTypes.array.isRequired,
+ },
+
+ displayName: "NetInfoGroupList",
+
+ render() {
+ let groups = this.props.groups;
+
+ // Filter out empty groups.
+ groups = groups.filter(group => {
+ return group && ((group.params && group.params.length) || group.content);
+ });
+
+ // Render groups
+ groups = groups.map(group => {
+ group.type = group.key;
+ return NetInfoGroup(group);
+ });
+
+ return (
+ DOM.div({className: "netInfoGroupList"},
+ groups
+ )
+ );
+ }
+});
+
+// Exports from this module
+module.exports = NetInfoGroupList;
diff --git a/devtools/client/webconsole/net/components/net-info-group.css b/devtools/client/webconsole/net/components/net-info-group.css
new file mode 100644
index 000000000..43800019f
--- /dev/null
+++ b/devtools/client/webconsole/net/components/net-info-group.css
@@ -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/. */
+
+/******************************************************************************/
+/* Net Info Group */
+
+.netInfoBody .netInfoGroup {
+ padding-bottom: 6px;
+}
+
+/* Last group doesn't need bottom padding */
+.netInfoBody .netInfoGroup:last-child {
+ padding-bottom: 0;
+}
+
+.netInfoBody .netInfoGroup:last-child .netInfoGroupContent {
+ padding-bottom: 0;
+}
+
+.netInfoBody .netInfoGroupTitle {
+ cursor: pointer;
+ font-weight: bold;
+ -moz-user-select: none;
+ cursor: pointer;
+ padding-left: 3px;
+}
+
+.netInfoBody .netInfoGroupTwisty {
+ background-image: url("chrome://devtools/skin/images/controls.png");
+ background-size: 56px 28px;
+ background-position: 0 -14px;
+ background-repeat: no-repeat;
+ width: 14px;
+ height: 14px;
+ cursor: pointer;
+ display: inline-block;
+ vertical-align: middle;
+}
+
+.netInfoBody .netInfoGroup.opened .netInfoGroupTwisty {
+ background-position: -14px -14px;
+}
+
+/* Group content is expandable/collapsible by clicking on the title */
+.netInfoBody .netInfoGroupContent {
+ padding-top: 7px;
+ margin-top: 3px;
+ padding-bottom: 14px;
+ border-top: 1px solid var(--net-border);
+ display: none;
+}
+
+/* Toggle group visibility */
+.netInfoBody .netInfoGroup.opened .netInfoGroupContent {
+ display: block;
+}
+
+/******************************************************************************/
+/* Themes */
+
+.theme-dark .netInfoBody .netInfoGroup {
+ color: var(--theme-body-color);
+}
+
+.theme-dark .netInfoBody .netInfoGroup .netInfoGroupTwisty {
+ filter: invert(1);
+}
+
+/* Twisties */
+.theme-firebug .netInfoBody .netInfoGroup .netInfoGroupTwisty {
+ background-image: url("chrome://devtools/skin/images/firebug/twisty-closed-firebug.svg");
+ background-position: 0 2px;
+ background-size: 11px 11px;
+ width: 15px;
+}
+
+.theme-firebug .netInfoBody .netInfoGroup.opened .netInfoGroupTwisty {
+ background-image: url("chrome://devtools/skin/images/firebug/twisty-open-firebug.svg");
+}
diff --git a/devtools/client/webconsole/net/components/net-info-group.js b/devtools/client/webconsole/net/components/net-info-group.js
new file mode 100644
index 000000000..d9794652e
--- /dev/null
+++ b/devtools/client/webconsole/net/components/net-info-group.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";
+
+const React = require("devtools/client/shared/vendor/react");
+const NetInfoParams = React.createFactory(require("./net-info-params"));
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template represents a group of data within a tab. For example,
+ * Headers tab has two groups 'Request Headers' and 'Response Headers'
+ * The Response tab can also have two groups 'Raw Data' and 'JSON'
+ */
+var NetInfoGroup = React.createClass({
+ propTypes: {
+ type: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ params: PropTypes.array,
+ content: PropTypes.element,
+ open: PropTypes.bool
+ },
+
+ displayName: "NetInfoGroup",
+
+ getDefaultProps() {
+ return {
+ open: true,
+ };
+ },
+
+ getInitialState() {
+ return {
+ open: this.props.open,
+ };
+ },
+
+ onToggle(event) {
+ this.setState({
+ open: !this.state.open
+ });
+ },
+
+ render() {
+ let content = this.props.content;
+
+ if (!content && this.props.params) {
+ content = NetInfoParams({
+ params: this.props.params
+ });
+ }
+
+ let open = this.state.open;
+ let className = open ? "opened" : "";
+
+ return (
+ DOM.div({className: "netInfoGroup" + " " + className + " " +
+ this.props.type},
+ DOM.span({
+ className: "netInfoGroupTwisty",
+ onClick: this.onToggle
+ }),
+ DOM.span({
+ className: "netInfoGroupTitle",
+ onClick: this.onToggle},
+ this.props.name
+ ),
+ DOM.div({className: "netInfoGroupContent"},
+ content
+ )
+ )
+ );
+ }
+});
+
+// Exports from this module
+module.exports = NetInfoGroup;
diff --git a/devtools/client/webconsole/net/components/net-info-params.css b/devtools/client/webconsole/net/components/net-info-params.css
new file mode 100644
index 000000000..4ec7140f8
--- /dev/null
+++ b/devtools/client/webconsole/net/components/net-info-params.css
@@ -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/. */
+
+/******************************************************************************/
+/* Net Info Params */
+
+.netInfoBody .netInfoParamName {
+ padding: 0 10px 0 0;
+ font-weight: bold;
+ vertical-align: top;
+ text-align: right;
+ white-space: nowrap;
+}
+
+.netInfoBody .netInfoParamValue {
+ width: 100%;
+ word-wrap: break-word;
+}
+
+.netInfoBody .netInfoParamValue > code {
+ font-family: var(--monospace-font-family);
+}
diff --git a/devtools/client/webconsole/net/components/net-info-params.js b/devtools/client/webconsole/net/components/net-info-params.js
new file mode 100644
index 000000000..573257b28
--- /dev/null
+++ b/devtools/client/webconsole/net/components/net-info-params.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 React = require("devtools/client/shared/vendor/react");
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template renders list of parameters within a group.
+ * It's essentially a list of name + value pairs.
+ */
+var NetInfoParams = React.createClass({
+ displayName: "NetInfoParams",
+
+ propTypes: {
+ params: PropTypes.arrayOf(PropTypes.shape({
+ name: PropTypes.string.isRequired,
+ value: PropTypes.string.isRequired
+ })).isRequired,
+ },
+
+ render() {
+ let params = this.props.params || [];
+
+ params.sort(function (a, b) {
+ return a.name > b.name ? 1 : -1;
+ });
+
+ let rows = [];
+ params.forEach((param, index) => {
+ rows.push(
+ DOM.tr({key: index},
+ DOM.td({className: "netInfoParamName"},
+ DOM.span({title: param.name}, param.name)
+ ),
+ DOM.td({className: "netInfoParamValue"},
+ DOM.code({}, param.value)
+ )
+ )
+ );
+ });
+
+ return (
+ DOM.table({cellPadding: 0, cellSpacing: 0},
+ DOM.tbody({},
+ rows
+ )
+ )
+ );
+ }
+});
+
+// Exports from this module
+module.exports = NetInfoParams;
diff --git a/devtools/client/webconsole/net/components/params-tab.js b/devtools/client/webconsole/net/components/params-tab.js
new file mode 100644
index 000000000..c3fefc669
--- /dev/null
+++ b/devtools/client/webconsole/net/components/params-tab.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 React = require("devtools/client/shared/vendor/react");
+const NetInfoParams = React.createFactory(require("./net-info-params"));
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template represents 'Params' tab displayed when the user
+ * expands network log in the Console panel. It's responsible for
+ * displaying URL parameters (query string).
+ */
+var ParamsTab = React.createClass({
+ propTypes: {
+ data: PropTypes.shape({
+ request: PropTypes.object.isRequired
+ })
+ },
+
+ displayName: "ParamsTab",
+
+ render() {
+ let data = this.props.data;
+
+ return (
+ DOM.div({className: "paramsTabBox"},
+ DOM.div({className: "panelContent"},
+ NetInfoParams({params: data.request.queryString})
+ )
+ )
+ );
+ }
+});
+
+// Exports from this module
+module.exports = ParamsTab;
diff --git a/devtools/client/webconsole/net/components/post-tab.js b/devtools/client/webconsole/net/components/post-tab.js
new file mode 100644
index 000000000..6d06eb40b
--- /dev/null
+++ b/devtools/client/webconsole/net/components/post-tab.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 React = require("devtools/client/shared/vendor/react");
+
+// Reps
+const { createFactories, parseURLEncodedText } = require("devtools/client/shared/components/reps/rep-utils");
+const TreeView = React.createFactory(require("devtools/client/shared/components/tree/tree-view"));
+const { Rep } = createFactories(require("devtools/client/shared/components/reps/rep"));
+
+// Network
+const NetInfoParams = React.createFactory(require("./net-info-params"));
+const NetInfoGroupList = React.createFactory(require("./net-info-group-list"));
+const Spinner = React.createFactory(require("./spinner"));
+const SizeLimit = React.createFactory(require("./size-limit"));
+const NetUtils = require("../utils/net");
+const Json = require("../utils/json");
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template represents 'Post' tab displayed when the user
+ * expands network log in the Console panel. It's responsible for
+ * displaying posted data (HTTP post body).
+ */
+var PostTab = React.createClass({
+ propTypes: {
+ data: PropTypes.shape({
+ request: PropTypes.object.isRequired
+ }),
+ actions: PropTypes.object.isRequired
+ },
+
+ displayName: "PostTab",
+
+ isJson(file) {
+ let text = file.request.postData.text;
+ let value = NetUtils.getHeaderValue(file.request.headers, "content-type");
+ return Json.isJSON(value, text);
+ },
+
+ parseJson(file) {
+ let postData = file.request.postData;
+ if (!postData) {
+ return null;
+ }
+
+ let jsonString = new String(postData.text);
+ return Json.parseJSONString(jsonString);
+ },
+
+ /**
+ * Render JSON post data as an expandable tree.
+ */
+ renderJson(file) {
+ let text = file.request.postData.text;
+ if (!text || isLongString(text)) {
+ return null;
+ }
+
+ if (!this.isJson(file)) {
+ return null;
+ }
+
+ let json = this.parseJson(file);
+ if (!json) {
+ return null;
+ }
+
+ return {
+ key: "json",
+ content: TreeView({
+ columns: [{id: "value"}],
+ object: json,
+ mode: "tiny",
+ renderValue: props => Rep(Object.assign({}, props, {
+ cropLimit: 50,
+ })),
+ }),
+ name: Locale.$STR("jsonScopeName")
+ };
+ },
+
+ parseXml(file) {
+ let text = file.request.postData.text;
+ if (isLongString(text)) {
+ return null;
+ }
+
+ return NetUtils.parseXml({
+ mimeType: NetUtils.getHeaderValue(file.request.headers, "content-type"),
+ text: text,
+ });
+ },
+
+ isXml(file) {
+ if (isLongString(file.request.postData.text)) {
+ return false;
+ }
+
+ let value = NetUtils.getHeaderValue(file.request.headers, "content-type");
+ if (!value) {
+ return false;
+ }
+
+ return NetUtils.isHTML(value);
+ },
+
+ renderXml(file) {
+ let text = file.request.postData.text;
+ if (!text || isLongString(text)) {
+ return null;
+ }
+
+ if (!this.isXml(file)) {
+ return null;
+ }
+
+ let doc = this.parseXml(file);
+ if (!doc) {
+ return null;
+ }
+
+ // Proper component for rendering XML should be used (see bug 1247392)
+ return null;
+ },
+
+ /**
+ * Multipart post data are parsed and nicely rendered
+ * as an expandable tree of individual parts.
+ */
+ renderMultiPart(file) {
+ let text = file.request.postData.text;
+ if (!text || isLongString(text)) {
+ return;
+ }
+
+ if (NetUtils.isMultiPartRequest(file)) {
+ // TODO: render multi part request (bug: 1247423)
+ }
+
+ return;
+ },
+
+ /**
+ * URL encoded post data are nicely rendered as a list
+ * of parameters.
+ */
+ renderUrlEncoded(file) {
+ let text = file.request.postData.text;
+ if (!text || isLongString(text)) {
+ return null;
+ }
+
+ if (!NetUtils.isURLEncodedRequest(file)) {
+ return null;
+ }
+
+ let lines = text.split("\n");
+ let params = parseURLEncodedText(lines[lines.length - 1]);
+
+ return {
+ key: "url-encoded",
+ content: NetInfoParams({params: params}),
+ name: Locale.$STR("netRequest.params")
+ };
+ },
+
+ renderRawData(file) {
+ let text = file.request.postData.text;
+
+ let group;
+
+ // The post body might reached the limit, so check if we are
+ // dealing with a long string.
+ if (typeof text == "object") {
+ group = {
+ key: "raw-longstring",
+ name: Locale.$STR("netRequest.rawData"),
+ content: DOM.div({className: "netInfoResponseContent"},
+ sanitize(text.initial),
+ SizeLimit({
+ actions: this.props.actions,
+ data: file.request.postData,
+ message: Locale.$STR("netRequest.sizeLimitMessage"),
+ link: Locale.$STR("netRequest.sizeLimitMessageLink")
+ })
+ )
+ };
+ } else {
+ group = {
+ key: "raw",
+ name: Locale.$STR("netRequest.rawData"),
+ content: DOM.div({className: "netInfoResponseContent"},
+ sanitize(text)
+ )
+ };
+ }
+
+ return group;
+ },
+
+ componentDidMount() {
+ let { actions, data: file } = this.props;
+
+ if (!file.request.postData) {
+ // TODO: use async action objects as soon as Redux is in place
+ actions.requestData("requestPostData");
+ }
+ },
+
+ render() {
+ let { actions, data: file } = this.props;
+
+ if (file.discardRequestBody) {
+ return DOM.span({className: "netInfoBodiesDiscarded"},
+ Locale.$STR("netRequest.requestBodyDiscarded")
+ );
+ }
+
+ if (!file.request.postData) {
+ return (
+ Spinner()
+ );
+ }
+
+ // Render post body data. The right representation of the data
+ // is picked according to the content type.
+ let groups = [];
+ groups.push(this.renderUrlEncoded(file));
+ // TODO: render multi part request (bug: 1247423)
+ // groups.push(this.renderMultiPart(file));
+ groups.push(this.renderJson(file));
+ groups.push(this.renderXml(file));
+ groups.push(this.renderRawData(file));
+
+ // Filter out empty groups.
+ groups = groups.filter(group => group);
+
+ // The raw response is collapsed by default if a nice formatted
+ // version is available.
+ if (groups.length > 1) {
+ groups[groups.length - 1].open = false;
+ }
+
+ return (
+ DOM.div({className: "postTabBox"},
+ DOM.div({className: "panelContent"},
+ NetInfoGroupList({
+ groups: groups
+ })
+ )
+ )
+ );
+ }
+});
+
+// Helpers
+
+/**
+ * Workaround for a "not well-formed" error that react
+ * reports when there's multipart data passed to render.
+ */
+function sanitize(text) {
+ text = JSON.stringify(text);
+ text = text.replace(/\\r\\n/g, "\r\n").replace(/\\"/g, "\"");
+ return text.slice(1, text.length - 1);
+}
+
+function isLongString(text) {
+ return typeof text == "object";
+}
+
+// Exports from this module
+module.exports = PostTab;
diff --git a/devtools/client/webconsole/net/components/response-tab.css b/devtools/client/webconsole/net/components/response-tab.css
new file mode 100644
index 000000000..e1c31fca4
--- /dev/null
+++ b/devtools/client/webconsole/net/components/response-tab.css
@@ -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/. */
+
+/******************************************************************************/
+/* Response Tab */
+
+.netInfoBody .netInfoBodiesDiscarded {
+ font-style: italic;
+ color: gray;
+}
+
+.netInfoBody .netInfoResponseContent {
+ font-family: var(--monospace-font-family);
+ word-wrap: break-word;
+}
+
+.netInfoBody .responseTabBox img {
+ max-width: 300px;
+ max-height: 300px;
+}
diff --git a/devtools/client/webconsole/net/components/response-tab.js b/devtools/client/webconsole/net/components/response-tab.js
new file mode 100644
index 000000000..78d8b2f77
--- /dev/null
+++ b/devtools/client/webconsole/net/components/response-tab.js
@@ -0,0 +1,277 @@
+/* This Source Code Form is subject to the terms of 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 React = require("devtools/client/shared/vendor/react");
+
+// Reps
+const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
+const TreeView = React.createFactory(require("devtools/client/shared/components/tree/tree-view"));
+const { Rep } = createFactories(require("devtools/client/shared/components/reps/rep"));
+
+// Network
+const SizeLimit = React.createFactory(require("./size-limit"));
+const NetInfoGroupList = React.createFactory(require("./net-info-group-list"));
+const Spinner = React.createFactory(require("./spinner"));
+const Json = require("../utils/json");
+const NetUtils = require("../utils/net");
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template represents 'Response' tab displayed when the user
+ * expands network log in the Console panel. It's responsible for
+ * rendering HTTP response body.
+ *
+ * In case of supported response mime-type (e.g. application/json,
+ * text/xml, etc.), the response is parsed using appropriate parser
+ * and rendered accordingly.
+ */
+var ResponseTab = React.createClass({
+ propTypes: {
+ data: PropTypes.shape({
+ request: PropTypes.object.isRequired,
+ response: PropTypes.object.isRequired
+ }),
+ actions: PropTypes.object.isRequired
+ },
+
+ displayName: "ResponseTab",
+
+ // Response Types
+
+ isJson(content) {
+ if (isLongString(content.text)) {
+ return false;
+ }
+
+ return Json.isJSON(content.mimeType, content.text);
+ },
+
+ parseJson(file) {
+ let content = file.response.content;
+ if (isLongString(content.text)) {
+ return null;
+ }
+
+ let jsonString = new String(content.text);
+ return Json.parseJSONString(jsonString);
+ },
+
+ isImage(content) {
+ if (isLongString(content.text)) {
+ return false;
+ }
+
+ return NetUtils.isImage(content.mimeType);
+ },
+
+ isXml(content) {
+ if (isLongString(content.text)) {
+ return false;
+ }
+
+ return NetUtils.isHTML(content.mimeType);
+ },
+
+ parseXml(file) {
+ let content = file.response.content;
+ if (isLongString(content.text)) {
+ return null;
+ }
+
+ return NetUtils.parseXml(content);
+ },
+
+ // Rendering
+
+ renderJson(file) {
+ let content = file.response.content;
+ if (!this.isJson(content)) {
+ return null;
+ }
+
+ let json = this.parseJson(file);
+ if (!json) {
+ return null;
+ }
+
+ return {
+ key: "json",
+ content: TreeView({
+ columns: [{id: "value"}],
+ object: json,
+ mode: "tiny",
+ renderValue: props => Rep(Object.assign({}, props, {
+ cropLimit: 50,
+ })),
+ }),
+ name: Locale.$STR("jsonScopeName")
+ };
+ },
+
+ renderImage(file) {
+ let content = file.response.content;
+ if (!this.isImage(content)) {
+ return null;
+ }
+
+ let dataUri = "data:" + content.mimeType + ";base64," + content.text;
+ return {
+ key: "image",
+ content: DOM.img({src: dataUri}),
+ name: Locale.$STR("netRequest.image")
+ };
+ },
+
+ renderXml(file) {
+ let content = file.response.content;
+ if (!this.isXml(content)) {
+ return null;
+ }
+
+ let doc = this.parseXml(file);
+ if (!doc) {
+ return null;
+ }
+
+ // Proper component for rendering XML should be used (see bug 1247392)
+ return null;
+ },
+
+ /**
+ * If full response text is available, let's try to parse and
+ * present nicely according to the underlying format.
+ */
+ renderFormattedResponse(file) {
+ let content = file.response.content;
+ if (typeof content.text == "object") {
+ return null;
+ }
+
+ let group = this.renderJson(file);
+ if (group) {
+ return group;
+ }
+
+ group = this.renderImage(file);
+ if (group) {
+ return group;
+ }
+
+ group = this.renderXml(file);
+ if (group) {
+ return group;
+ }
+ },
+
+ renderRawResponse(file) {
+ let group;
+ let content = file.response.content;
+
+ // The response might reached the limit, so check if we are
+ // dealing with a long string.
+ if (typeof content.text == "object") {
+ group = {
+ key: "raw-longstring",
+ name: Locale.$STR("netRequest.rawData"),
+ content: DOM.div({className: "netInfoResponseContent"},
+ content.text.initial,
+ SizeLimit({
+ actions: this.props.actions,
+ data: content,
+ message: Locale.$STR("netRequest.sizeLimitMessage"),
+ link: Locale.$STR("netRequest.sizeLimitMessageLink")
+ })
+ )
+ };
+ } else {
+ group = {
+ key: "raw",
+ name: Locale.$STR("netRequest.rawData"),
+ content: DOM.div({className: "netInfoResponseContent"},
+ content.text
+ )
+ };
+ }
+
+ return group;
+ },
+
+ componentDidMount() {
+ let { actions, data: file } = this.props;
+ let content = file.response.content;
+
+ if (!content || typeof (content.text) == "undefined") {
+ // TODO: use async action objects as soon as Redux is in place
+ actions.requestData("responseContent");
+ }
+ },
+
+ /**
+ * The response panel displays two groups:
+ *
+ * 1) Formatted response (in case of supported format, e.g. JSON, XML, etc.)
+ * 2) Raw response data (always displayed if not discarded)
+ */
+ render() {
+ let { actions, data: file } = this.props;
+
+ // If response bodies are discarded (not collected) let's just
+ // display a info message indicating what to do to collect even
+ // response bodies.
+ if (file.discardResponseBody) {
+ return DOM.span({className: "netInfoBodiesDiscarded"},
+ Locale.$STR("netRequest.responseBodyDiscarded")
+ );
+ }
+
+ // Request for the response content is done only if the response
+ // is not fetched yet - i.e. the `content.text` is undefined.
+ // Empty content.text` can also be a valid response either
+ // empty or not available yet.
+ let content = file.response.content;
+ if (!content || typeof (content.text) == "undefined") {
+ return (
+ Spinner()
+ );
+ }
+
+ // Render response body data. The right representation of the data
+ // is picked according to the content type.
+ let groups = [];
+ groups.push(this.renderFormattedResponse(file));
+ groups.push(this.renderRawResponse(file));
+
+ // Filter out empty groups.
+ groups = groups.filter(group => group);
+
+ // The raw response is collapsed by default if a nice formatted
+ // version is available.
+ if (groups.length > 1) {
+ groups[1].open = false;
+ }
+
+ return (
+ DOM.div({className: "responseTabBox"},
+ DOM.div({className: "panelContent"},
+ NetInfoGroupList({
+ groups: groups
+ })
+ )
+ )
+ );
+ }
+});
+
+// Helpers
+
+function isLongString(text) {
+ return typeof text == "object";
+}
+
+// Exports from this module
+module.exports = ResponseTab;
diff --git a/devtools/client/webconsole/net/components/size-limit.css b/devtools/client/webconsole/net/components/size-limit.css
new file mode 100644
index 000000000..a5c214d9e
--- /dev/null
+++ b/devtools/client/webconsole/net/components/size-limit.css
@@ -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/. */
+
+/******************************************************************************/
+/* Response Size Limit */
+
+.netInfoBody .netInfoSizeLimit {
+ font-weight: bold;
+ padding-top: 10px;
+}
+
+.netInfoBody .netInfoSizeLimit .objectLink {
+ color: var(--theme-highlight-blue);
+}
diff --git a/devtools/client/webconsole/net/components/size-limit.js b/devtools/client/webconsole/net/components/size-limit.js
new file mode 100644
index 000000000..de8839314
--- /dev/null
+++ b/devtools/client/webconsole/net/components/size-limit.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";
+
+const React = require("devtools/client/shared/vendor/react");
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template represents a size limit notification message
+ * used e.g. in the Response tab when response body exceeds
+ * size limit. The message contains a link allowing the user
+ * to fetch the rest of the data from the backend (debugger server).
+ */
+var SizeLimit = React.createClass({
+ propTypes: {
+ data: PropTypes.object.isRequired,
+ message: PropTypes.string.isRequired,
+ link: PropTypes.string.isRequired,
+ actions: PropTypes.shape({
+ resolveString: PropTypes.func.isRequired
+ }),
+ },
+
+ displayName: "SizeLimit",
+
+ // Event Handlers
+
+ onClickLimit(event) {
+ let actions = this.props.actions;
+ let content = this.props.data;
+
+ actions.resolveString(content, "text");
+ },
+
+ // Rendering
+
+ render() {
+ let message = this.props.message;
+ let link = this.props.link;
+ let reLink = /^(.*)\{\{link\}\}(.*$)/;
+ let m = message.match(reLink);
+
+ return (
+ DOM.div({className: "netInfoSizeLimit"},
+ DOM.span({}, m[1]),
+ DOM.a({
+ className: "objectLink",
+ onClick: this.onClickLimit},
+ link
+ ),
+ DOM.span({}, m[2])
+ )
+ );
+ }
+});
+
+// Exports from this module
+module.exports = SizeLimit;
diff --git a/devtools/client/webconsole/net/components/spinner.js b/devtools/client/webconsole/net/components/spinner.js
new file mode 100644
index 000000000..fe79f7dd1
--- /dev/null
+++ b/devtools/client/webconsole/net/components/spinner.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 React = require("devtools/client/shared/vendor/react");
+
+// Shortcuts
+const DOM = React.DOM;
+
+/**
+ * This template represents a throbber displayed when the UI
+ * is waiting for data coming from the backend (debugging server).
+ */
+var Spinner = React.createClass({
+ displayName: "Spinner",
+
+ render() {
+ return (
+ DOM.div({className: "devtools-throbber"})
+ );
+ }
+});
+
+// Exports from this module
+module.exports = Spinner;
diff --git a/devtools/client/webconsole/net/components/stacktrace-tab.js b/devtools/client/webconsole/net/components/stacktrace-tab.js
new file mode 100644
index 000000000..51eb7689b
--- /dev/null
+++ b/devtools/client/webconsole/net/components/stacktrace-tab.js
@@ -0,0 +1,29 @@
+/* This Source Code Form is subject to the terms of 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, createClass, createFactory } = require("devtools/client/shared/vendor/react");
+const StackTrace = createFactory(require("devtools/client/shared/components/stack-trace"));
+
+const StackTraceTab = createClass({
+ displayName: "StackTraceTab",
+
+ propTypes: {
+ data: PropTypes.object.isRequired,
+ actions: PropTypes.shape({
+ onViewSourceInDebugger: PropTypes.func.isRequired
+ })
+ },
+
+ render() {
+ let { stacktrace } = this.props.data.cause;
+ let { actions } = this.props;
+ let onViewSourceInDebugger = actions.onViewSourceInDebugger.bind(actions);
+
+ return StackTrace({ stacktrace, onViewSourceInDebugger });
+ }
+});
+
+// Exports from this module
+module.exports = StackTraceTab;
diff --git a/devtools/client/webconsole/net/data-provider.js b/devtools/client/webconsole/net/data-provider.js
new file mode 100644
index 000000000..d8a70d72d
--- /dev/null
+++ b/devtools/client/webconsole/net/data-provider.js
@@ -0,0 +1,66 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const promise = require("promise");
+
+/**
+ * Map of pending requests. Used mainly by tests to wait
+ * till things are ready.
+ */
+var promises = new Map();
+
+/**
+ * This object is used to fetch network data from the backend.
+ * Communication with the chrome scope is based on message
+ * exchange.
+ */
+var DataProvider = {
+ hasPendingRequests: function () {
+ return promises.size > 0;
+ },
+
+ requestData: function (client, actor, method) {
+ let key = actor + ":" + method;
+ let p = promises.get(key);
+ if (p) {
+ return p;
+ }
+
+ let deferred = promise.defer();
+ let realMethodName = "get" + method.charAt(0).toUpperCase() +
+ method.slice(1);
+
+ if (!client[realMethodName]) {
+ return null;
+ }
+
+ client[realMethodName](actor, response => {
+ promises.delete(key);
+ deferred.resolve(response);
+ });
+
+ promises.set(key, deferred.promise);
+ return deferred.promise;
+ },
+
+ resolveString: function (client, stringGrip) {
+ let key = stringGrip.actor + ":getString";
+ let p = promises.get(key);
+ if (p) {
+ return p;
+ }
+
+ p = client.getString(stringGrip).then(result => {
+ promises.delete(key);
+ return result;
+ });
+
+ promises.set(key, p);
+ return p;
+ },
+};
+
+// Exports from this module
+module.exports = DataProvider;
diff --git a/devtools/client/webconsole/net/main.js b/devtools/client/webconsole/net/main.js
new file mode 100644
index 000000000..6fdf9494d
--- /dev/null
+++ b/devtools/client/webconsole/net/main.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/. */
+"use strict";
+
+/* global BrowserLoader */
+
+var { utils: Cu } = Components;
+
+// Initialize module loader and load all modules of the new inline
+// preview feature. The entire code-base doesn't need any extra
+// privileges and runs entirely in content scope.
+const rootUrl = "resource://devtools/client/webconsole/net/";
+const require = BrowserLoader({
+ baseURI: rootUrl,
+ window}).require;
+
+const NetRequest = require("./net-request");
+const { loadSheet } = require("sdk/stylesheet/utils");
+
+// Localization
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/netmonitor.properties");
+
+// Stylesheets
+var styleSheets = [
+ "resource://devtools/client/jsonview/css/toolbar.css",
+ "resource://devtools/client/shared/components/tree/tree-view.css",
+ "resource://devtools/client/shared/components/reps/reps.css",
+ "resource://devtools/client/webconsole/net/net-request.css",
+ "resource://devtools/client/webconsole/net/components/size-limit.css",
+ "resource://devtools/client/webconsole/net/components/net-info-body.css",
+ "resource://devtools/client/webconsole/net/components/net-info-group.css",
+ "resource://devtools/client/webconsole/net/components/net-info-params.css",
+ "resource://devtools/client/webconsole/net/components/response-tab.css"
+];
+
+// Load theme stylesheets into the Console frame. This should be
+// done automatically by UI Components as soon as we have consensus
+// on the right CSS strategy FIXME.
+// It would also be nice to include them using @import.
+styleSheets.forEach(url => {
+ loadSheet(this, url, "author");
+});
+
+// Localization API used by React components
+// accessing strings from *.properties file.
+// Example:
+// let localizedString = Locale.$STR('string-key');
+//
+// Resources:
+// http://l20n.org/
+// https://github.com/yahoo/react-intl
+this.Locale = {
+ $STR: key => {
+ try {
+ return L10N.getStr(key);
+ } catch (err) {
+ console.error(key + ": " + err);
+ }
+ }
+};
+
+// List of NetRequest instances represents the state.
+// As soon as Redux is in place it should be maintained using a reducer.
+var netRequests = new Map();
+
+/**
+ * This function handles network events received from the backend. It's
+ * executed from within the webconsole.js
+ */
+function onNetworkEvent(log) {
+ // The 'from' field is set only in case of a 'networkEventUpdate' packet.
+ // The initial 'networkEvent' packet uses 'actor'.
+ // Check if NetRequest object is already created for this event actor and
+ // if there is none make sure to create one.
+ let response = log.response;
+ let netRequest = response.from ? netRequests.get(response.from) : null;
+ if (!netRequest && !log.update) {
+ netRequest = new NetRequest(log);
+ netRequests.set(response.actor, netRequest);
+ }
+
+ if (!netRequest) {
+ return;
+ }
+
+ if (log.update) {
+ netRequest.updateBody(response);
+ }
+
+ return;
+}
+
+// Make the 'onNetworkEvent' accessible from chrome (see webconsole.js)
+this.NetRequest = {
+ onNetworkEvent: onNetworkEvent
+};
diff --git a/devtools/client/webconsole/net/moz.build b/devtools/client/webconsole/net/moz.build
new file mode 100644
index 000000000..1b9eca7fe
--- /dev/null
+++ b/devtools/client/webconsole/net/moz.build
@@ -0,0 +1,19 @@
+# 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 += [
+ 'components',
+ 'utils'
+]
+
+DevToolsModules(
+ 'data-provider.js',
+ 'main.js',
+ 'net-request.css',
+ 'net-request.js',
+)
+
+XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
+BROWSER_CHROME_MANIFESTS += ['test/mochitest/browser.ini']
diff --git a/devtools/client/webconsole/net/net-request.css b/devtools/client/webconsole/net/net-request.css
new file mode 100644
index 000000000..82b6a027f
--- /dev/null
+++ b/devtools/client/webconsole/net/net-request.css
@@ -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/. */
+
+/******************************************************************************/
+/* General */
+
+:root {
+ --net-border: #d7d7d7;
+}
+
+:root.theme-dark {
+ --net-border: #5f7387;
+}
+
+/******************************************************************************/
+/* Network log */
+
+/* No background if a Net log is opened */
+.netRequest.message.opened,
+.netRequest.message.opened:hover {
+ background: transparent !important;
+}
+
+/******************************************************************************/
+/* Themes */
+
+.theme-dark .netRequest.opened:hover,
+.theme-dark .netRequest.opened {
+ background: transparent;
+}
+
+.theme-firebug .netRequest.message.opened:hover {
+ background-image: linear-gradient(rgba(214, 233, 246, 0.8), rgba(255, 255, 255, 1.6)) !important;
+}
diff --git a/devtools/client/webconsole/net/net-request.js b/devtools/client/webconsole/net/net-request.js
new file mode 100644
index 000000000..48cf66fdd
--- /dev/null
+++ b/devtools/client/webconsole/net/net-request.js
@@ -0,0 +1,323 @@
+/* This Source Code Form is subject to the terms of 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 React = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+
+// Reps
+const { parseURLParams } = require("devtools/client/shared/components/reps/rep-utils");
+
+// Network
+const { cancelEvent, isLeftClick } = require("./utils/events");
+const NetInfoBody = React.createFactory(require("./components/net-info-body"));
+const DataProvider = require("./data-provider");
+
+// Constants
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+/**
+ * This object represents a network log in the Console panel (and in the
+ * Network panel in the future).
+ * It's associated with an existing log and so, also with an existing
+ * element in the DOM.
+ *
+ * The object neither render no request for more data by default. It only
+ * reqisters a click listener to the associated log entry (a network event)
+ * and changes the class attribute of the log entry, so a twisty icon
+ * appears to indicates that there are more details displayed if the
+ * log entry is expanded.
+ *
+ * When the user expands the log, data are requested from the backend
+ * and rendered directly within the Console iframe.
+ */
+function NetRequest(log) {
+ this.initialize(log);
+}
+
+NetRequest.prototype = {
+ initialize: function (log) {
+ this.client = log.consoleFrame.webConsoleClient;
+ this.owner = log.consoleFrame.owner;
+
+ // 'this.file' field is following HAR spec.
+ // http://www.softwareishard.com/blog/har-12-spec/
+ this.file = log.response;
+ this.parentNode = log.node;
+ this.file.request.queryString = parseURLParams(this.file.request.url);
+ this.hasCookies = false;
+
+ // Map of fetched responses (to avoid unnecessary RDP round trip).
+ this.cachedResponses = new Map();
+
+ let doc = this.parentNode.ownerDocument;
+ let twisty = doc.createElementNS(XHTML_NS, "a");
+ twisty.className = "theme-twisty";
+ twisty.href = "#";
+
+ let messageBody = this.parentNode.querySelector(".message-body-wrapper");
+ this.parentNode.insertBefore(twisty, messageBody);
+ this.parentNode.setAttribute("collapsible", true);
+
+ this.parentNode.classList.add("netRequest");
+
+ // Register a click listener.
+ this.addClickListener();
+ },
+
+ addClickListener: function () {
+ // Add an event listener to toggle the expanded state when clicked.
+ // The event bubbling is canceled if the user clicks on the log
+ // itself (not on the expanded body), so opening of the default
+ // modal dialog is avoided.
+ this.parentNode.addEventListener("click", (event) => {
+ if (!isLeftClick(event)) {
+ return;
+ }
+
+ // Clicking on the toggle button or the method expands/collapses
+ // the body with HTTP details.
+ let classList = event.originalTarget.classList;
+ if (!(classList.contains("theme-twisty") ||
+ classList.contains("method"))) {
+ return;
+ }
+
+ // Alright, the user is clicking fine, let's open HTTP details!
+ this.onToggleBody(event);
+
+ // Avoid the default modal dialog
+ cancelEvent(event);
+ }, true);
+ },
+
+ onToggleBody: function (event) {
+ let target = event.currentTarget;
+ let logRow = target.closest(".netRequest");
+ logRow.classList.toggle("opened");
+
+ let twisty = this.parentNode.querySelector(".theme-twisty");
+ if (logRow.classList.contains("opened")) {
+ twisty.setAttribute("open", true);
+ } else {
+ twisty.removeAttribute("open");
+ }
+
+ let isOpen = logRow.classList.contains("opened");
+ if (isOpen) {
+ this.renderBody();
+ } else {
+ this.closeBody();
+ }
+ },
+
+ updateCookies: function(method, response) {
+ // TODO: This code will be part of a reducer.
+ let result;
+ if (response.cookies > 0 &&
+ ["requestCookies", "responseCookies"].includes(method)) {
+ this.hasCookies = true;
+ this.refresh();
+ }
+ },
+
+ /**
+ * Executed when 'networkEventUpdate' is received from the backend.
+ */
+ updateBody: function (response) {
+ // 'networkEventUpdate' event indicates that there are new data
+ // available on the backend. The following logic checks the response
+ // cache and if this data has been already requested before they
+ // need to be updated now (re-requested).
+ let method = response.updateType;
+ this.updateCookies(method, response);
+ if (this.cachedResponses.get(method)) {
+ this.cachedResponses.delete(method);
+ this.requestData(method);
+ }
+ },
+
+ /**
+ * Close network inline preview body.
+ */
+ closeBody: function () {
+ this.netInfoBodyBox.parentNode.removeChild(this.netInfoBodyBox);
+ },
+
+ /**
+ * Render network inline preview body.
+ */
+ renderBody: function () {
+ let messageBody = this.parentNode.querySelector(".message-body-wrapper");
+
+ // Create box for all markup rendered by ReactJS. Since we are
+ // rendering within webconsole.xul (i.e. XUL document) we need
+ // to explicitly specify XHTML namespace.
+ let doc = messageBody.ownerDocument;
+ this.netInfoBodyBox = doc.createElementNS(XHTML_NS, "div");
+ this.netInfoBodyBox.classList.add("netInfoBody");
+ messageBody.appendChild(this.netInfoBodyBox);
+
+ // As soon as Redux is in place state and actions will come from
+ // separate modules.
+ let body = NetInfoBody({
+ actions: this
+ });
+
+ // Render net info body!
+ this.body = ReactDOM.render(body, this.netInfoBodyBox);
+
+ this.refresh();
+ },
+
+ /**
+ * Render top level ReactJS component.
+ */
+ refresh: function () {
+ if (!this.netInfoBodyBox) {
+ return;
+ }
+
+ // TODO: As soon as Redux is in place there will be reducer
+ // computing a new state.
+ let newState = Object.assign({}, this.body.state, {
+ data: this.file,
+ hasCookies: this.hasCookies
+ });
+
+ this.body.setState(newState);
+ },
+
+ // Communication with the backend
+
+ requestData: function (method) {
+ // If the response has already been received bail out.
+ let response = this.cachedResponses.get(method);
+ if (response) {
+ return;
+ }
+
+ // Set an attribute indicating that this net log is waiting for
+ // data coming from the backend. Intended mainly for tests.
+ this.parentNode.setAttribute("loading", "true");
+
+ let actor = this.file.actor;
+ DataProvider.requestData(this.client, actor, method).then(args => {
+ this.cachedResponses.set(method, args);
+ this.onRequestData(method, args);
+
+ if (!DataProvider.hasPendingRequests()) {
+ this.parentNode.removeAttribute("loading");
+
+ // Fire an event indicating that all pending requests for
+ // data from the backend has finished. Intended for tests.
+ // Do it asynchronously so, it's done after all handlers
+ // for the current promise are executed.
+ setTimeout(() => {
+ let event = document.createEvent("Event");
+ event.initEvent("netlog-no-pending-requests", true, true);
+ this.parentNode.dispatchEvent(event);
+ });
+ }
+ });
+ },
+
+ onRequestData: function (method, response) {
+ // TODO: This code will be part of a reducer.
+ let result;
+ switch (method) {
+ case "requestHeaders":
+ result = this.onRequestHeaders(response);
+ break;
+ case "responseHeaders":
+ result = this.onResponseHeaders(response);
+ break;
+ case "requestCookies":
+ result = this.onRequestCookies(response);
+ break;
+ case "responseCookies":
+ result = this.onResponseCookies(response);
+ break;
+ case "responseContent":
+ result = this.onResponseContent(response);
+ break;
+ case "requestPostData":
+ result = this.onRequestPostData(response);
+ break;
+ }
+
+ result.then(() => {
+ this.refresh();
+ });
+ },
+
+ onRequestHeaders: function (response) {
+ this.file.request.headers = response.headers;
+
+ return this.resolveHeaders(this.file.request.headers);
+ },
+
+ onResponseHeaders: function (response) {
+ this.file.response.headers = response.headers;
+
+ return this.resolveHeaders(this.file.response.headers);
+ },
+
+ onResponseContent: function (response) {
+ let content = response.content;
+
+ for (let p in content) {
+ this.file.response.content[p] = content[p];
+ }
+
+ return Promise.resolve();
+ },
+
+ onRequestPostData: function (response) {
+ this.file.request.postData = response.postData;
+ return Promise.resolve();
+ },
+
+ onRequestCookies: function (response) {
+ this.file.request.cookies = response.cookies;
+ return this.resolveHeaders(this.file.request.cookies);
+ },
+
+ onResponseCookies: function (response) {
+ this.file.response.cookies = response.cookies;
+ return this.resolveHeaders(this.file.response.cookies);
+ },
+
+ onViewSourceInDebugger: function (frame) {
+ this.owner.viewSourceInDebugger(frame.source, frame.line);
+ },
+
+ resolveHeaders: function (headers) {
+ let promises = [];
+
+ for (let header of headers) {
+ if (typeof header.value == "object") {
+ promises.push(this.resolveString(header.value).then(value => {
+ header.value = value;
+ }));
+ }
+ }
+
+ return Promise.all(promises);
+ },
+
+ resolveString: function (object, propName) {
+ let stringGrip = object[propName];
+ if (typeof stringGrip == "object") {
+ DataProvider.resolveString(this.client, stringGrip).then(args => {
+ object[propName] = args;
+ this.refresh();
+ });
+ }
+ }
+};
+
+// Exports from this module
+module.exports = NetRequest;
diff --git a/devtools/client/webconsole/net/test/mochitest/.eslintrc.js b/devtools/client/webconsole/net/test/mochitest/.eslintrc.js
new file mode 100644
index 000000000..76904829d
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../../../.eslintrc.mochitests.js",
+};
diff --git a/devtools/client/webconsole/net/test/mochitest/browser.ini b/devtools/client/webconsole/net/test/mochitest/browser.ini
new file mode 100644
index 000000000..9414414c6
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/browser.ini
@@ -0,0 +1,22 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ page_basic.html
+ test.json
+ test.json^headers^
+ test-cookies.json
+ test-cookies.json^headers^
+ test.txt
+ test.xml
+ test.xml^headers^
+ !/devtools/client/webconsole/test/head.js
+ !/devtools/client/framework/test/shared-head.js
+
+[browser_net_basic.js]
+[browser_net_cookies.js]
+[browser_net_headers.js]
+[browser_net_params.js]
+[browser_net_post.js]
+[browser_net_response.js]
diff --git a/devtools/client/webconsole/net/test/mochitest/browser_net_basic.js b/devtools/client/webconsole/net/test/mochitest/browser_net_basic.js
new file mode 100644
index 000000000..57273bec0
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/browser_net_basic.js
@@ -0,0 +1,33 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PAGE_URL = URL_ROOT + "page_basic.html";
+const JSON_XHR_URL = URL_ROOT + "test.json";
+
+/**
+ * Basic test that generates XHR in the content and
+ * checks the related log in the Console panel can
+ * be expanded.
+ */
+add_task(function* () {
+ info("Test XHR Spy basic started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "GET",
+ url: JSON_XHR_URL
+ });
+
+ ok(netInfoBody, "The network details must be available");
+
+ // There should be at least two tabs: Headers and Response
+ ok(netInfoBody.querySelector(".tabs .tabs-menu-item.headers"),
+ "Headers tab must be available");
+ ok(netInfoBody.querySelector(".tabs .tabs-menu-item.response"),
+ "Response tab must be available");
+});
diff --git a/devtools/client/webconsole/net/test/mochitest/browser_net_cookies.js b/devtools/client/webconsole/net/test/mochitest/browser_net_cookies.js
new file mode 100644
index 000000000..cfd85c2ed
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/browser_net_cookies.js
@@ -0,0 +1,54 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PAGE_URL = URL_ROOT + "page_basic.html";
+const JSON_XHR_URL = URL_ROOT + "test-cookies.json";
+
+/**
+ * This test generates XHR requests in the page, expands
+ * networks details in the Console panel and checks that
+ * Cookies are properly displayed.
+ */
+add_task(function* () {
+ info("Test XHR Spy cookies started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "GET",
+ url: JSON_XHR_URL
+ });
+
+ // Select "Cookies" tab
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "cookies");
+
+ let requestCookieName = tabBody.querySelector(
+ ".netInfoGroup.requestCookies .netInfoParamName > span[title='bar']");
+
+ // Verify request cookies (name and value)
+ ok(requestCookieName, "Request Cookie name must exist");
+ is(requestCookieName.textContent, "bar",
+ "The cookie name must have proper value");
+
+ let requestCookieValue = requestCookieName.parentNode.nextSibling;
+ ok(requestCookieValue, "Request Cookie value must exist");
+ is(requestCookieValue.textContent, "foo",
+ "The cookie value must have proper value");
+
+ let responseCookieName = tabBody.querySelector(
+ ".netInfoGroup.responseCookies .netInfoParamName > span[title='test']");
+
+ // Verify response cookies (name and value)
+ ok(responseCookieName, "Response Cookie name must exist");
+ is(responseCookieName.textContent, "test",
+ "The cookie name must have proper value");
+
+ let responseCookieValue = responseCookieName.parentNode.nextSibling;
+ ok(responseCookieValue, "Response Cookie value must exist");
+ is(responseCookieValue.textContent, "abc",
+ "The cookie value must have proper value");
+});
diff --git a/devtools/client/webconsole/net/test/mochitest/browser_net_headers.js b/devtools/client/webconsole/net/test/mochitest/browser_net_headers.js
new file mode 100644
index 000000000..4a47074ee
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/browser_net_headers.js
@@ -0,0 +1,40 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PAGE_URL = URL_ROOT + "page_basic.html";
+const JSON_XHR_URL = URL_ROOT + "test.json";
+
+/**
+ * This test generates XHR requests in the page, expands
+ * networks details in the Console panel and checks that
+ * HTTP headers are there.
+ */
+add_task(function* () {
+ info("Test XHR Spy headers started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "GET",
+ url: JSON_XHR_URL
+ });
+
+ // Select "Headers" tab
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "headers");
+ let paramName = tabBody.querySelector(
+ ".netInfoParamName > span[title='Content-Type']");
+
+ // Verify "Content-Type" header (name and value)
+ ok(paramName, "Header name must exist");
+ is(paramName.textContent, "Content-Type",
+ "The header name must have proper value");
+
+ let paramValue = paramName.parentNode.nextSibling;
+ ok(paramValue, "Header value must exist");
+ is(paramValue.textContent, "application/json; charset=utf-8",
+ "The header value must have proper value");
+});
diff --git a/devtools/client/webconsole/net/test/mochitest/browser_net_params.js b/devtools/client/webconsole/net/test/mochitest/browser_net_params.js
new file mode 100644
index 000000000..d8b0e2c84
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/browser_net_params.js
@@ -0,0 +1,69 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PAGE_URL = URL_ROOT + "page_basic.html";
+const JSON_XHR_URL = URL_ROOT + "test.json";
+
+/**
+ * This test generates XHR requests in the page, expands
+ * networks details in the Console panel and checks that
+ * HTTP parameters (query string) are there.
+ */
+add_task(function* () {
+ info("Test XHR Spy params started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "GET",
+ url: JSON_XHR_URL,
+ queryString: "?foo=bar"
+ });
+
+ // Check headers
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "params");
+
+ let paramName = tabBody.querySelector(
+ ".netInfoParamName > span[title='foo']");
+
+ // Verify "Content-Type" header (name and value)
+ ok(paramName, "Header name must exist");
+ is(paramName.textContent, "foo",
+ "The param name must have proper value");
+
+ let paramValue = paramName.parentNode.nextSibling;
+ ok(paramValue, "param value must exist");
+ is(paramValue.textContent, "bar",
+ "The param value must have proper value");
+});
+
+/**
+ * Test URL parameters with the same name.
+ */
+add_task(function* () {
+ info("Test XHR Spy params started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "GET",
+ url: JSON_XHR_URL,
+ queryString: "?box[]=123&box[]=456"
+ });
+
+ // Check headers
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "params");
+
+ let params = tabBody.querySelectorAll(
+ ".netInfoParamName > span[title='box[]']");
+ is(params.length, 2, "Two URI parameters must exist");
+
+ let values = tabBody.querySelectorAll(
+ ".netInfoParamValue > code");
+ is(values[0].textContent, 123, "First value must match");
+ is(values[1].textContent, 456, "Second value must match");
+});
diff --git a/devtools/client/webconsole/net/test/mochitest/browser_net_post.js b/devtools/client/webconsole/net/test/mochitest/browser_net_post.js
new file mode 100644
index 000000000..f6e776ef0
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/browser_net_post.js
@@ -0,0 +1,88 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PAGE_URL = URL_ROOT + "page_basic.html";
+const JSON_XHR_URL = URL_ROOT + "test.json";
+
+const plainPostBody = "test-data";
+const jsonData = "{\"bar\": \"baz\"}";
+const jsonRendered = "bar\"baz\"";
+const xmlPostBody = "<xml><name>John</name></xml>";
+
+/**
+ * This test generates XHR requests in the page, expands
+ * networks details in the Console panel and checks that
+ * Post data are properly rendered.
+ */
+add_task(function* () {
+ info("Test XHR Spy post plain body started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "POST",
+ url: JSON_XHR_URL,
+ body: plainPostBody
+ });
+
+ // Check post body data
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "post");
+ let postContent = tabBody.querySelector(
+ ".netInfoGroup.raw.opened .netInfoGroupContent");
+ is(postContent.textContent, plainPostBody,
+ "Post body must be properly rendered");
+});
+
+add_task(function* () {
+ info("Test XHR Spy post JSON body started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "POST",
+ url: JSON_XHR_URL,
+ body: jsonData,
+ requestHeaders: [{
+ name: "Content-Type",
+ value: "application/json"
+ }]
+ });
+
+ // Check post body data
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "post");
+ let postContent = tabBody.querySelector(
+ ".netInfoGroup.json.opened .netInfoGroupContent");
+ is(postContent.textContent, jsonRendered,
+ "Post body must be properly rendered");
+
+ let rawPostContent = tabBody.querySelector(
+ ".netInfoGroup.raw.opened .netInfoGroupContent");
+ ok(!rawPostContent, "Raw response group must be collapsed");
+});
+
+add_task(function* () {
+ info("Test XHR Spy post XML body started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "POST",
+ url: JSON_XHR_URL,
+ body: xmlPostBody,
+ requestHeaders: [{
+ name: "Content-Type",
+ value: "application/xml"
+ }]
+ });
+
+ // Check post body data
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "post");
+ let rawPostContent = tabBody.querySelector(
+ ".netInfoGroup.raw.opened .netInfoGroupContent");
+ is(rawPostContent.textContent, xmlPostBody,
+ "Raw response group must not be collapsed");
+});
diff --git a/devtools/client/webconsole/net/test/mochitest/browser_net_response.js b/devtools/client/webconsole/net/test/mochitest/browser_net_response.js
new file mode 100644
index 000000000..ec5543043
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/browser_net_response.js
@@ -0,0 +1,86 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PAGE_URL = URL_ROOT + "page_basic.html";
+const TEXT_XHR_URL = URL_ROOT + "test.txt";
+const JSON_XHR_URL = URL_ROOT + "test.json";
+const XML_XHR_URL = URL_ROOT + "test.xml";
+
+const textResponseBody = "this is a response";
+const jsonResponseBody = "name\"John\"";
+
+// Individual tests below generate XHR request in the page, expand
+// network details in the Console panel and checks various types
+// of response bodies.
+
+/**
+ * Validate plain text response
+ */
+add_task(function* () {
+ info("Test XHR Spy respone plain body started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "GET",
+ url: TEXT_XHR_URL,
+ });
+
+ // Check response body data
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "response");
+ let responseContent = tabBody.querySelector(
+ ".netInfoGroup.raw.opened .netInfoGroupContent");
+
+ ok(responseContent.textContent.indexOf(textResponseBody) > -1,
+ "Response body must be properly rendered");
+});
+
+/**
+ * Validate XML response
+ */
+add_task(function* () {
+ info("Test XHR Spy response XML body started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "GET",
+ url: XML_XHR_URL,
+ });
+
+ // Check response body data
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "response");
+ let rawResponseContent = tabBody.querySelector(
+ ".netInfoGroup.raw.opened .netInfoGroupContent");
+ ok(rawResponseContent, "Raw response group must not be collapsed");
+});
+
+/**
+ * Validate JSON response
+ */
+add_task(function* () {
+ info("Test XHR Spy response JSON body started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "GET",
+ url: JSON_XHR_URL,
+ });
+
+ // Check response body data
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "response");
+ let responseContent = tabBody.querySelector(
+ ".netInfoGroup.json .netInfoGroupContent");
+
+ is(responseContent.textContent, jsonResponseBody,
+ "Response body must be properly rendered");
+
+ let rawResponseContent = tabBody.querySelector(
+ ".netInfoGroup.raw.opened .netInfoGroupContent");
+ ok(!rawResponseContent, "Raw response group must be collapsed");
+});
diff --git a/devtools/client/webconsole/net/test/mochitest/head.js b/devtools/client/webconsole/net/test/mochitest/head.js
new file mode 100644
index 000000000..c01206948
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/head.js
@@ -0,0 +1,209 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
+/* import-globals-from ../../../test/head.js */
+
+"use strict";
+
+// Load Web Console head.js, it implements helper console test API
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/webconsole/test/head.js", this);
+
+const FRAME_SCRIPT_UTILS_URL =
+ "chrome://devtools/content/shared/frame-script-utils.js";
+
+const NET_INFO_PREF = "devtools.webconsole.filter.networkinfo";
+const NET_XHR_PREF = "devtools.webconsole.filter.netxhr";
+
+// Enable XHR logging for the test
+Services.prefs.setBoolPref(NET_INFO_PREF, true);
+Services.prefs.setBoolPref(NET_XHR_PREF, true);
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(NET_INFO_PREF, true);
+ Services.prefs.clearUserPref(NET_XHR_PREF, true);
+});
+
+// Use the old webconsole since the new one doesn't yet support
+// XHR spy. See Bug 1304794.
+Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.webconsole.new-frontend-enabled");
+});
+
+/**
+ * Add a new test tab in the browser and load the given url.
+ * @param {String} url The url to be loaded in the new tab
+ * @return a promise that resolves to the tab object when the url is loaded
+ */
+function addTestTab(url) {
+ info("Adding a new JSON tab with URL: '" + url + "'");
+
+ return Task.spawn(function* () {
+ let tab = yield addTab(url);
+
+ // Load devtools/shared/frame-script-utils.js
+ loadCommonFrameScript(tab);
+
+ // Open the Console panel
+ let hud = yield openConsole();
+
+ return {
+ tab: tab,
+ browser: tab.linkedBrowser,
+ hud: hud
+ };
+ });
+}
+
+/**
+ *
+ * @param hud
+ * @param options
+ */
+function executeAndInspectXhr(hud, options) {
+ hud.jsterm.clearOutput();
+
+ options.queryString = options.queryString || "";
+
+ // Execute XHR in the content scope.
+ performRequestsInContent({
+ method: options.method,
+ url: options.url + options.queryString,
+ body: options.body,
+ nocache: options.nocache,
+ requestHeaders: options.requestHeaders
+ });
+
+ return Task.spawn(function* () {
+ // Wait till the appropriate Net log appears in the Console panel.
+ let rules = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: options.url,
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_INFO,
+ isXhr: true,
+ }]
+ });
+
+ // The log is here, get its parent element (className: 'message').
+ let msg = [...rules[0].matched][0];
+ let body = msg.querySelector(".message-body");
+
+ // Open XHR HTTP details body and wait till the UI fetches
+ // all necessary data from the backend. All RPD requests
+ // needs to be finished before we can continue testing.
+ yield synthesizeMouseClickSoon(hud, body);
+ yield waitForBackend(msg);
+ let netInfoBody = body.querySelector(".netInfoBody");
+ ok(netInfoBody, "Net info body must exist");
+ return netInfoBody;
+ });
+}
+
+/**
+ * Wait till XHR data are fetched from the backend (i.e. there are
+ * no pending RDP requests.
+ */
+function waitForBackend(element) {
+ if (!element.hasAttribute("loading")) {
+ return;
+ }
+ return once(element, "netlog-no-pending-requests", true);
+}
+
+/**
+ * Select specific tab in XHR info body.
+ *
+ * @param netInfoBody The main XHR info body
+ * @param tabId Tab ID (possible values: 'headers', 'cookies', 'params',
+ * 'post', 'response');
+ *
+ * @returns Tab body element.
+ */
+function selectNetInfoTab(hud, netInfoBody, tabId) {
+ let tab = netInfoBody.querySelector(".tabs-menu-item." + tabId);
+ ok(tab, "Tab must exist " + tabId);
+
+ // Click to select specified tab and wait till its
+ // UI is populated with data from the backend.
+ // There must be no pending RDP requests before we can
+ // continue testing the UI.
+ return Task.spawn(function* () {
+ yield synthesizeMouseClickSoon(hud, tab);
+ let msg = getAncestorByClass(netInfoBody, "message");
+ yield waitForBackend(msg);
+ let tabBody = netInfoBody.querySelector("." + tabId + "TabBox");
+ ok(tabBody, "Tab body must exist");
+ return tabBody;
+ });
+}
+
+/**
+ * Return parent node with specified class.
+ *
+ * @param node A child element
+ * @param className Specified class name.
+ *
+ * @returns A parent element.
+ */
+function getAncestorByClass(node, className) {
+ for (let parent = node; parent; parent = parent.parentNode) {
+ if (parent.classList && parent.classList.contains(className)) {
+ return parent;
+ }
+ }
+ return null;
+}
+
+/**
+ * Synthesize asynchronous click event (with clean stack trace).
+ */
+function synthesizeMouseClickSoon(hud, element) {
+ return new Promise((resolve) => {
+ executeSoon(() => {
+ EventUtils.synthesizeMouse(element, 2, 2, {}, hud.iframeWindow);
+ resolve();
+ });
+ });
+}
+
+/**
+ * Execute XHR in the content scope.
+ */
+function performRequestsInContent(requests) {
+ info("Performing requests in the context of the content.");
+ return executeInContent("devtools:test:xhr", requests);
+}
+
+function executeInContent(name, data = {}, objects = {},
+ expectResponse = true) {
+ let mm = gBrowser.selectedBrowser.messageManager;
+
+ mm.sendAsyncMessage(name, data, objects);
+ if (expectResponse) {
+ return waitForContentMessage(name);
+ }
+
+ return Promise.resolve();
+}
+
+function waitForContentMessage(name) {
+ info("Expecting message " + name + " from content");
+
+ let mm = gBrowser.selectedBrowser.messageManager;
+
+ return new Promise((resolve) => {
+ mm.addMessageListener(name, function onMessage(msg) {
+ mm.removeMessageListener(name, onMessage);
+ resolve(msg.data);
+ });
+ });
+}
+
+function loadCommonFrameScript(tab) {
+ let browser = tab ? tab.linkedBrowser : gBrowser.selectedBrowser;
+ browser.messageManager.loadFrameScript(FRAME_SCRIPT_UTILS_URL, false);
+}
diff --git a/devtools/client/webconsole/net/test/mochitest/page_basic.html b/devtools/client/webconsole/net/test/mochitest/page_basic.html
new file mode 100644
index 000000000..da7158492
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/page_basic.html
@@ -0,0 +1,14 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>XHR Spy test page</title>
+ </head>
+ <body>
+ <script type="text/javascript">
+ document.cookie = "bar=foo";
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/net/test/mochitest/test-cookies.json b/devtools/client/webconsole/net/test/mochitest/test-cookies.json
new file mode 100644
index 000000000..b5e739025
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/test-cookies.json
@@ -0,0 +1 @@
+{"name":"Cookies Test"}
diff --git a/devtools/client/webconsole/net/test/mochitest/test-cookies.json^headers^ b/devtools/client/webconsole/net/test/mochitest/test-cookies.json^headers^
new file mode 100644
index 000000000..94a8c0c69
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/test-cookies.json^headers^
@@ -0,0 +1,2 @@
+Content-Type: application/json; charset=utf-8
+Set-Cookie: test=abc
diff --git a/devtools/client/webconsole/net/test/mochitest/test.json b/devtools/client/webconsole/net/test/mochitest/test.json
new file mode 100644
index 000000000..6548f8e3e
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/test.json
@@ -0,0 +1 @@
+{"name":"John"}
diff --git a/devtools/client/webconsole/net/test/mochitest/test.json^headers^ b/devtools/client/webconsole/net/test/mochitest/test.json^headers^
new file mode 100644
index 000000000..6010bfd18
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/test.json^headers^
@@ -0,0 +1 @@
+Content-Type: application/json; charset=utf-8
diff --git a/devtools/client/webconsole/net/test/mochitest/test.txt b/devtools/client/webconsole/net/test/mochitest/test.txt
new file mode 100644
index 000000000..af7014e11
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/test.txt
@@ -0,0 +1 @@
+this is a response
diff --git a/devtools/client/webconsole/net/test/mochitest/test.xml b/devtools/client/webconsole/net/test/mochitest/test.xml
new file mode 100644
index 000000000..3749c8e5a
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/test.xml
@@ -0,0 +1 @@
+<xml><name>John</name></xml>
diff --git a/devtools/client/webconsole/net/test/mochitest/test.xml^headers^ b/devtools/client/webconsole/net/test/mochitest/test.xml^headers^
new file mode 100644
index 000000000..10ecdf5f4
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/test.xml^headers^
@@ -0,0 +1 @@
+Content-Type: application/xml; charset=utf-8
diff --git a/devtools/client/webconsole/net/test/unit/.eslintrc.js b/devtools/client/webconsole/net/test/unit/.eslintrc.js
new file mode 100644
index 000000000..54a9a6361
--- /dev/null
+++ b/devtools/client/webconsole/net/test/unit/.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/net/test/unit/test_json-utils.js b/devtools/client/webconsole/net/test/unit/test_json-utils.js
new file mode 100644
index 000000000..f8ccdf3aa
--- /dev/null
+++ b/devtools/client/webconsole/net/test/unit/test_json-utils.js
@@ -0,0 +1,45 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var Cu = Components.utils;
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { parseJSONString, isJSON } = require("devtools/client/webconsole/net/utils/json");
+
+// Test data
+const simpleJson = '{"name":"John"}';
+const jsonInFunc = 'someFunc({"name":"John"})';
+
+const json1 = "{'a': 1}";
+const json2 = " {'a': 1}";
+const json3 = "\t {'a': 1}";
+const json4 = "\n\n\t {'a': 1}";
+const json5 = "\n\n\t ";
+
+const textMimeType = "text/plain";
+const jsonMimeType = "text/javascript";
+const unknownMimeType = "text/unknown";
+
+/**
+ * Testing API provided by webconsole/net/utils/json.js
+ */
+function run_test() {
+ // parseJSONString
+ equal(parseJSONString(simpleJson).name, "John");
+ equal(parseJSONString(jsonInFunc).name, "John");
+
+ // isJSON
+ equal(isJSON(textMimeType, json1), true);
+ equal(isJSON(textMimeType, json2), true);
+ equal(isJSON(jsonMimeType, json3), true);
+ equal(isJSON(jsonMimeType, json4), true);
+
+ equal(isJSON(unknownMimeType, json1), true);
+ equal(isJSON(textMimeType, json1), true);
+
+ equal(isJSON(unknownMimeType), false);
+ equal(isJSON(unknownMimeType, json5), false);
+}
diff --git a/devtools/client/webconsole/net/test/unit/test_net-utils.js b/devtools/client/webconsole/net/test/unit/test_net-utils.js
new file mode 100644
index 000000000..512ebcbc7
--- /dev/null
+++ b/devtools/client/webconsole/net/test/unit/test_net-utils.js
@@ -0,0 +1,77 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var Cu = Components.utils;
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {
+ isImage,
+ isHTML,
+ getHeaderValue,
+ isURLEncodedRequest,
+ isMultiPartRequest
+} = require("devtools/client/webconsole/net/utils/net");
+
+// Test data
+const imageMimeTypes = ["image/jpeg", "image/jpg", "image/gif",
+ "image/png", "image/bmp"];
+
+const htmlMimeTypes = ["text/html", "text/xml", "application/xml",
+ "application/rss+xml", "application/atom+xml", "application/xhtml+xml",
+ "application/mathml+xml", "application/rdf+xml"];
+
+const headers = [{name: "headerName", value: "value1"}];
+
+const har1 = {
+ request: {
+ postData: {
+ text: "content-type: application/x-www-form-urlencoded"
+ }
+ }
+};
+
+const har2 = {
+ request: {
+ headers: [{
+ name: "content-type",
+ value: "application/x-www-form-urlencoded"
+ }]
+ }
+};
+
+const har3 = {
+ request: {
+ headers: [{
+ name: "content-type",
+ value: "multipart/form-data"
+ }]
+ }
+};
+
+/**
+ * Testing API provided by webconsole/net/utils/net.js
+ */
+function run_test() {
+ // isImage
+ imageMimeTypes.forEach(mimeType => {
+ ok(isImage(mimeType));
+ });
+
+ // isHTML
+ htmlMimeTypes.forEach(mimeType => {
+ ok(isHTML(mimeType));
+ });
+
+ // getHeaderValue
+ equal(getHeaderValue(headers, "headerName"), "value1");
+
+ // isURLEncodedRequest
+ ok(isURLEncodedRequest(har1));
+ ok(isURLEncodedRequest(har2));
+
+ // isMultiPartRequest
+ ok(isMultiPartRequest(har3));
+}
diff --git a/devtools/client/webconsole/net/test/unit/xpcshell.ini b/devtools/client/webconsole/net/test/unit/xpcshell.ini
new file mode 100644
index 000000000..d988a2ad0
--- /dev/null
+++ b/devtools/client/webconsole/net/test/unit/xpcshell.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+tags = devtools
+head =
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+[test_json-utils.js]
+[test_net-utils.js]
diff --git a/devtools/client/webconsole/net/utils/events.js b/devtools/client/webconsole/net/utils/events.js
new file mode 100644
index 000000000..9f8705593
--- /dev/null
+++ b/devtools/client/webconsole/net/utils/events.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";
+
+function isLeftClick(event, allowKeyModifiers) {
+ return event.button === 0 && (allowKeyModifiers || noKeyModifiers(event));
+}
+
+function noKeyModifiers(event) {
+ return !event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey;
+}
+
+function cancelEvent(event) {
+ event.stopPropagation();
+ event.preventDefault();
+}
+
+// Exports from this module
+exports.isLeftClick = isLeftClick;
+exports.cancelEvent = cancelEvent;
diff --git a/devtools/client/webconsole/net/utils/json.js b/devtools/client/webconsole/net/utils/json.js
new file mode 100644
index 000000000..70d733f28
--- /dev/null
+++ b/devtools/client/webconsole/net/utils/json.js
@@ -0,0 +1,234 @@
+/* This Source Code Form is subject to the terms of 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";
+
+// List of JSON content types.
+const contentTypes = {
+ "text/plain": 1,
+ "text/javascript": 1,
+ "text/x-javascript": 1,
+ "text/json": 1,
+ "text/x-json": 1,
+ "application/json": 1,
+ "application/x-json": 1,
+ "application/javascript": 1,
+ "application/x-javascript": 1,
+ "application/json-rpc": 1
+};
+
+// Implementation
+var Json = {};
+
+/**
+ * Parsing JSON
+ */
+Json.parseJSONString = function (jsonString) {
+ if (!jsonString.length) {
+ return null;
+ }
+
+ let regex, matches;
+
+ let first = firstNonWs(jsonString);
+ if (first !== "[" && first !== "{") {
+ // This (probably) isn't pure JSON. Let's try to strip various sorts
+ // of XSSI protection/wrapping and see if that works better.
+
+ // Prototype-style secure requests
+ regex = /^\s*\/\*-secure-([\s\S]*)\*\/\s*$/;
+ matches = regex.exec(jsonString);
+ if (matches) {
+ jsonString = matches[1];
+
+ if (jsonString[0] === "\\" && jsonString[1] === "n") {
+ jsonString = jsonString.substr(2);
+ }
+
+ if (jsonString[jsonString.length - 2] === "\\" &&
+ jsonString[jsonString.length - 1] === "n") {
+ jsonString = jsonString.substr(0, jsonString.length - 2);
+ }
+ }
+
+ // Google-style (?) delimiters
+ if (jsonString.indexOf("&&&START&&&") !== -1) {
+ regex = /&&&START&&&([\s\S]*)&&&END&&&/;
+ matches = regex.exec(jsonString);
+ if (matches) {
+ jsonString = matches[1];
+ }
+ }
+
+ // while(1);, for(;;);, and )]}'
+ regex = /^\s*(\)\]\}[^\n]*\n|while\s*\(1\);|for\s*\(;;\);)([\s\S]*)/;
+ matches = regex.exec(jsonString);
+ if (matches) {
+ jsonString = matches[2];
+ }
+
+ // JSONP
+ regex = /^\s*([A-Za-z0-9_$.]+\s*(?:\[.*\]|))\s*\(([\s\S]*)\)/;
+ matches = regex.exec(jsonString);
+ if (matches) {
+ jsonString = matches[2];
+ }
+ }
+
+ try {
+ return JSON.parse(jsonString);
+ } catch (err) {
+ // eslint-disable-line no-empty
+ }
+
+ // Give up if we don't have valid start, to avoid some unnecessary overhead.
+ first = firstNonWs(jsonString);
+ if (first !== "[" && first !== "{" && isNaN(first) && first !== '"') {
+ return null;
+ }
+
+ // Remove JavaScript comments, quote non-quoted identifiers, and merge
+ // multi-line structures like |{"a": 1} \n {"b": 2}| into a single JSON
+ // object [{"a": 1}, {"b": 2}].
+ jsonString = pseudoJsonToJson(jsonString);
+
+ try {
+ return JSON.parse(jsonString);
+ } catch (err) {
+ // eslint-disable-line no-empty
+ }
+
+ return null;
+};
+
+function firstNonWs(str) {
+ for (let i = 0, len = str.length; i < len; i++) {
+ let ch = str[i];
+ if (ch !== " " && ch !== "\n" && ch !== "\t" && ch !== "\r") {
+ return ch;
+ }
+ }
+ return "";
+}
+
+function pseudoJsonToJson(json) {
+ let ret = "";
+ let at = 0, lasti = 0, lastch = "", hasMultipleParts = false;
+ for (let i = 0, len = json.length; i < len; ++i) {
+ let ch = json[i];
+ if (/\s/.test(ch)) {
+ continue;
+ }
+
+ if (ch === '"') {
+ // Consume a string.
+ ++i;
+ while (i < len) {
+ if (json[i] === "\\") {
+ ++i;
+ } else if (json[i] === '"') {
+ break;
+ }
+ ++i;
+ }
+ } else if (ch === "'") {
+ // Convert an invalid string into a valid one.
+ ret += json.slice(at, i) + "\"";
+ at = i + 1;
+ ++i;
+
+ while (i < len) {
+ if (json[i] === "\\") {
+ ++i;
+ } else if (json[i] === "'") {
+ break;
+ }
+ ++i;
+ }
+
+ if (i < len) {
+ ret += json.slice(at, i) + "\"";
+ at = i + 1;
+ }
+ } else if ((ch === "[" || ch === "{") &&
+ (lastch === "]" || lastch === "}")) {
+ // Multiple JSON messages in one... Make it into a single array by
+ // inserting a comma and setting the "multiple parts" flag.
+ ret += json.slice(at, i) + ",";
+ hasMultipleParts = true;
+ at = i;
+ } else if (lastch === "," && (ch === "]" || ch === "}")) {
+ // Trailing commas in arrays/objects.
+ ret += json.slice(at, lasti);
+ at = i;
+ } else if (lastch === "/" && lasti === i - 1) {
+ // Some kind of comment; remove it.
+ if (ch === "/") {
+ ret += json.slice(at, i - 1);
+ at = i + json.slice(i).search(/\n|\r|$/);
+ i = at - 1;
+ } else if (ch === "*") {
+ ret += json.slice(at, i - 1);
+ at = json.indexOf("*/", i + 1) + 2;
+ if (at === 1) {
+ at = len;
+ }
+ i = at - 1;
+ }
+ ch = "\0";
+ } else if (/[a-zA-Z$_]/.test(ch) && lastch !== ":") {
+ // Non-quoted identifier. Quote it.
+ ret += json.slice(at, i) + "\"";
+ at = i;
+ i = i + json.slice(i).search(/[^a-zA-Z0-9$_]|$/);
+ ret += json.slice(at, i) + "\"";
+ at = i;
+ }
+
+ lastch = ch;
+ lasti = i;
+ }
+
+ ret += json.slice(at);
+ if (hasMultipleParts) {
+ ret = "[" + ret + "]";
+ }
+
+ return ret;
+}
+
+Json.isJSON = function (contentType, data) {
+ // Workaround for JSON responses without proper content type
+ // Let's consider all responses starting with "{" as JSON. In the worst
+ // case there will be an exception when parsing. This means that no-JSON
+ // responses (and post data) (with "{") can be parsed unnecessarily,
+ // which represents a little overhead, but this happens only if the request
+ // is actually expanded by the user in the UI (Net & Console panels).
+ // Do a manual string search instead of checking (data.strip()[0] === "{")
+ // to improve performance/memory usage.
+ let len = data ? data.length : 0;
+ for (let i = 0; i < len; i++) {
+ let ch = data.charAt(i);
+ if (ch === "{") {
+ return true;
+ }
+
+ if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r") {
+ continue;
+ }
+
+ break;
+ }
+
+ if (!contentType) {
+ return false;
+ }
+
+ contentType = contentType.split(";")[0];
+ contentType = contentType.trim();
+ return !!contentTypes[contentType];
+};
+
+// Exports from this module
+module.exports = Json;
+
diff --git a/devtools/client/webconsole/net/utils/moz.build b/devtools/client/webconsole/net/utils/moz.build
new file mode 100644
index 000000000..3fdc458e3
--- /dev/null
+++ b/devtools/client/webconsole/net/utils/moz.build
@@ -0,0 +1,11 @@
+# -*- 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/.
+
+DevToolsModules(
+ 'events.js',
+ 'json.js',
+ 'net.js',
+)
diff --git a/devtools/client/webconsole/net/utils/net.js b/devtools/client/webconsole/net/utils/net.js
new file mode 100644
index 000000000..782ec032a
--- /dev/null
+++ b/devtools/client/webconsole/net/utils/net.js
@@ -0,0 +1,134 @@
+/* This Source Code Form is subject to the terms of 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 mimeCategoryMap = {
+ "text/plain": "txt",
+ "application/octet-stream": "bin",
+ "text/html": "html",
+ "text/xml": "html",
+ "application/xml": "html",
+ "application/rss+xml": "html",
+ "application/atom+xml": "html",
+ "application/xhtml+xml": "html",
+ "application/mathml+xml": "html",
+ "application/rdf+xml": "html",
+ "text/css": "css",
+ "application/x-javascript": "js",
+ "text/javascript": "js",
+ "application/javascript": "js",
+ "text/ecmascript": "js",
+ "application/ecmascript": "js",
+ "image/jpeg": "image",
+ "image/jpg": "image",
+ "image/gif": "image",
+ "image/png": "image",
+ "image/bmp": "image",
+ "application/x-shockwave-flash": "plugin",
+ "application/x-silverlight-app": "plugin",
+ "video/x-flv": "media",
+ "audio/mpeg3": "media",
+ "audio/x-mpeg-3": "media",
+ "video/mpeg": "media",
+ "video/x-mpeg": "media",
+ "video/webm": "media",
+ "video/mp4": "media",
+ "video/ogg": "media",
+ "audio/ogg": "media",
+ "application/ogg": "media",
+ "application/x-ogg": "media",
+ "application/x-midi": "media",
+ "audio/midi": "media",
+ "audio/x-mid": "media",
+ "audio/x-midi": "media",
+ "music/crescendo": "media",
+ "audio/wav": "media",
+ "audio/x-wav": "media",
+ "application/x-woff": "font",
+ "application/font-woff": "font",
+ "application/x-font-woff": "font",
+ "application/x-ttf": "font",
+ "application/x-font-ttf": "font",
+ "font/ttf": "font",
+ "font/woff": "font",
+ "application/x-otf": "font",
+ "application/x-font-otf": "font"
+};
+
+var NetUtils = {};
+
+NetUtils.isImage = function (contentType) {
+ if (!contentType) {
+ return false;
+ }
+
+ contentType = contentType.split(";")[0];
+ contentType = contentType.trim();
+ return mimeCategoryMap[contentType] == "image";
+};
+
+NetUtils.isHTML = function (contentType) {
+ if (!contentType) {
+ return false;
+ }
+
+ contentType = contentType.split(";")[0];
+ contentType = contentType.trim();
+ return mimeCategoryMap[contentType] == "html";
+};
+
+NetUtils.getHeaderValue = function (headers, name) {
+ if (!headers) {
+ return null;
+ }
+
+ name = name.toLowerCase();
+ for (let i = 0; i < headers.length; ++i) {
+ let headerName = headers[i].name.toLowerCase();
+ if (headerName == name) {
+ return headers[i].value;
+ }
+ }
+};
+
+NetUtils.parseXml = function (content) {
+ let contentType = content.mimeType.split(";")[0];
+ contentType = contentType.trim();
+
+ let parser = new DOMParser();
+ let doc = parser.parseFromString(content.text, contentType);
+ let root = doc.documentElement;
+
+ // Error handling
+ let nsURI = "http://www.mozilla.org/newlayout/xml/parsererror.xml";
+ if (root.namespaceURI == nsURI && root.nodeName == "parsererror") {
+ return null;
+ }
+
+ return doc;
+};
+
+NetUtils.isURLEncodedRequest = function (file) {
+ let mimeType = "application/x-www-form-urlencoded";
+
+ let postData = file.request.postData;
+ if (postData && postData.text) {
+ let text = postData.text.toLowerCase();
+ if (text.startsWith("content-type: " + mimeType)) {
+ return true;
+ }
+ }
+
+ let value = NetUtils.getHeaderValue(file.request.headers, "content-type");
+ return value && value.startsWith(mimeType);
+};
+
+NetUtils.isMultiPartRequest = function (file) {
+ let mimeType = "multipart/form-data";
+ let value = NetUtils.getHeaderValue(file.request.headers, "content-type");
+ return value && value.startsWith(mimeType);
+};
+
+// Exports from this module
+module.exports = NetUtils;
diff --git a/devtools/client/webconsole/new-console-output/actions/enhancers.js b/devtools/client/webconsole/new-console-output/actions/enhancers.js
new file mode 100644
index 000000000..5553942e2
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/actions/enhancers.js
@@ -0,0 +1,20 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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";
+
+const { BATCH_ACTIONS } = require("../constants");
+
+function batchActions(batchedActions) {
+ return {
+ type: BATCH_ACTIONS,
+ actions: batchedActions,
+ };
+}
+
+module.exports = {
+ batchActions
+};
diff --git a/devtools/client/webconsole/new-console-output/actions/filters.js b/devtools/client/webconsole/new-console-output/actions/filters.js
new file mode 100644
index 000000000..05d080219
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/actions/filters.js
@@ -0,0 +1,55 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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";
+
+const { getAllFilters } = require("devtools/client/webconsole/new-console-output/selectors/filters");
+const Services = require("Services");
+
+const {
+ FILTER_TEXT_SET,
+ FILTER_TOGGLE,
+ FILTERS_CLEAR,
+ PREFS,
+} = require("devtools/client/webconsole/new-console-output/constants");
+
+function filterTextSet(text) {
+ return {
+ type: FILTER_TEXT_SET,
+ text
+ };
+}
+
+function filterToggle(filter) {
+ return (dispatch, getState) => {
+ dispatch({
+ type: FILTER_TOGGLE,
+ filter,
+ });
+ const filterState = getAllFilters(getState());
+ Services.prefs.setBoolPref(PREFS.FILTER[filter.toUpperCase()],
+ filterState.get(filter));
+ };
+}
+
+function filtersClear() {
+ return (dispatch, getState) => {
+ dispatch({
+ type: FILTERS_CLEAR,
+ });
+
+ const filterState = getAllFilters(getState());
+ for (let filter in filterState) {
+ Services.prefs.clearUserPref(PREFS.FILTER[filter.toUpperCase()]);
+ }
+ };
+}
+
+module.exports = {
+ filterTextSet,
+ filterToggle,
+ filtersClear
+};
diff --git a/devtools/client/webconsole/new-console-output/actions/index.js b/devtools/client/webconsole/new-console-output/actions/index.js
new file mode 100644
index 000000000..5ce76a402
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/actions/index.js
@@ -0,0 +1,18 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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";
+
+const actionModules = [
+ "enhancers",
+ "filters",
+ "messages",
+ "ui",
+].map(filename => require(`./${filename}`));
+
+const actions = Object.assign({}, ...actionModules);
+
+module.exports = actions;
diff --git a/devtools/client/webconsole/new-console-output/actions/messages.js b/devtools/client/webconsole/new-console-output/actions/messages.js
new file mode 100644
index 000000000..467e27503
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/actions/messages.js
@@ -0,0 +1,100 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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";
+
+const {
+ prepareMessage
+} = require("devtools/client/webconsole/new-console-output/utils/messages");
+const { IdGenerator } = require("devtools/client/webconsole/new-console-output/utils/id-generator");
+const { batchActions } = require("devtools/client/webconsole/new-console-output/actions/enhancers");
+const {
+ MESSAGE_ADD,
+ MESSAGES_CLEAR,
+ MESSAGE_OPEN,
+ MESSAGE_CLOSE,
+ MESSAGE_TYPE,
+ MESSAGE_TABLE_RECEIVE,
+} = require("../constants");
+
+const defaultIdGenerator = new IdGenerator();
+
+function messageAdd(packet, idGenerator = null) {
+ if (idGenerator == null) {
+ idGenerator = defaultIdGenerator;
+ }
+ let message = prepareMessage(packet, idGenerator);
+ const addMessageAction = {
+ type: MESSAGE_ADD,
+ message
+ };
+
+ if (message.type === MESSAGE_TYPE.CLEAR) {
+ return batchActions([
+ messagesClear(),
+ addMessageAction,
+ ]);
+ }
+ return addMessageAction;
+}
+
+function messagesClear() {
+ return {
+ type: MESSAGES_CLEAR
+ };
+}
+
+function messageOpen(id) {
+ return {
+ type: MESSAGE_OPEN,
+ id
+ };
+}
+
+function messageClose(id) {
+ return {
+ type: MESSAGE_CLOSE,
+ id
+ };
+}
+
+function messageTableDataGet(id, client, dataType) {
+ return (dispatch) => {
+ let fetchObjectActorData;
+ if (["Map", "WeakMap", "Set", "WeakSet"].includes(dataType)) {
+ fetchObjectActorData = (cb) => client.enumEntries(cb);
+ } else {
+ fetchObjectActorData = (cb) => client.enumProperties({
+ ignoreNonIndexedProperties: dataType === "Array"
+ }, cb);
+ }
+
+ fetchObjectActorData(enumResponse => {
+ const {iterator} = enumResponse;
+ iterator.slice(0, iterator.count, sliceResponse => {
+ let {ownProperties} = sliceResponse;
+ dispatch(messageTableDataReceive(id, ownProperties));
+ });
+ });
+ };
+}
+
+function messageTableDataReceive(id, data) {
+ return {
+ type: MESSAGE_TABLE_RECEIVE,
+ id,
+ data
+ };
+}
+
+module.exports = {
+ messageAdd,
+ messagesClear,
+ messageOpen,
+ messageClose,
+ messageTableDataGet,
+};
+
diff --git a/devtools/client/webconsole/new-console-output/actions/moz.build b/devtools/client/webconsole/new-console-output/actions/moz.build
new file mode 100644
index 000000000..c7a8ed52c
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/actions/moz.build
@@ -0,0 +1,12 @@
+# 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(
+ 'enhancers.js',
+ 'filters.js',
+ 'index.js',
+ 'messages.js',
+ 'ui.js',
+)
diff --git a/devtools/client/webconsole/new-console-output/actions/ui.js b/devtools/client/webconsole/new-console-output/actions/ui.js
new file mode 100644
index 000000000..cf9814d79
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/actions/ui.js
@@ -0,0 +1,27 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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";
+
+const { getAllUi } = require("devtools/client/webconsole/new-console-output/selectors/ui");
+const Services = require("Services");
+
+const {
+ FILTER_BAR_TOGGLE,
+ PREFS,
+} = require("devtools/client/webconsole/new-console-output/constants");
+
+function filterBarToggle(show) {
+ return (dispatch, getState) => {
+ dispatch({
+ type: FILTER_BAR_TOGGLE
+ });
+ const uiState = getAllUi(getState());
+ Services.prefs.setBoolPref(PREFS.UI.FILTER_BAR, uiState.get("filterBarVisible"));
+ };
+}
+
+exports.filterBarToggle = filterBarToggle;
diff --git a/devtools/client/webconsole/new-console-output/components/collapse-button.js b/devtools/client/webconsole/new-console-output/components/collapse-button.js
new file mode 100644
index 000000000..ab72fcf4d
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/collapse-button.js
@@ -0,0 +1,50 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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 {
+ createClass,
+ DOM: dom,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+
+const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages");
+
+const CollapseButton = createClass({
+
+ displayName: "CollapseButton",
+
+ propTypes: {
+ open: PropTypes.bool.isRequired,
+ title: PropTypes.string,
+ },
+
+ getDefaultProps: function () {
+ return {
+ title: l10n.getStr("messageToggleDetails")
+ };
+ },
+
+ render: function () {
+ const { open, onClick, title } = this.props;
+
+ let classes = ["theme-twisty"];
+
+ if (open) {
+ classes.push("open");
+ }
+
+ return dom.a({
+ className: classes.join(" "),
+ onClick,
+ title: title,
+ });
+ }
+});
+
+module.exports = CollapseButton;
diff --git a/devtools/client/webconsole/new-console-output/components/console-output.js b/devtools/client/webconsole/new-console-output/components/console-output.js
new file mode 100644
index 000000000..1ba7f8dda
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/console-output.js
@@ -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/. */
+"use strict";
+
+const {
+ createClass,
+ createFactory,
+ DOM: dom,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+
+const {
+ getAllMessages,
+ getAllMessagesUiById,
+ getAllMessagesTableDataById,
+ getAllGroupsById,
+} = require("devtools/client/webconsole/new-console-output/selectors/messages");
+const { getScrollSetting } = require("devtools/client/webconsole/new-console-output/selectors/ui");
+const MessageContainer = createFactory(require("devtools/client/webconsole/new-console-output/components/message-container").MessageContainer);
+
+const ConsoleOutput = createClass({
+
+ displayName: "ConsoleOutput",
+
+ propTypes: {
+ messages: PropTypes.object.isRequired,
+ messagesUi: PropTypes.object.isRequired,
+ serviceContainer: PropTypes.shape({
+ attachRefToHud: PropTypes.func.isRequired,
+ }),
+ autoscroll: PropTypes.bool.isRequired,
+ },
+
+ componentDidMount() {
+ scrollToBottom(this.outputNode);
+ this.props.serviceContainer.attachRefToHud("outputScroller", this.outputNode);
+ },
+
+ componentWillUpdate(nextProps, nextState) {
+ if (!this.outputNode) {
+ return;
+ }
+
+ const outputNode = this.outputNode;
+
+ // Figure out if we are at the bottom. If so, then any new message should be scrolled
+ // into view.
+ if (this.props.autoscroll && outputNode.lastChild) {
+ this.shouldScrollBottom = isScrolledToBottom(outputNode.lastChild, outputNode);
+ }
+ },
+
+ componentDidUpdate() {
+ if (this.shouldScrollBottom) {
+ scrollToBottom(this.outputNode);
+ }
+ },
+
+ render() {
+ let {
+ dispatch,
+ autoscroll,
+ messages,
+ messagesUi,
+ messagesTableData,
+ serviceContainer,
+ groups,
+ } = this.props;
+
+ let messageNodes = messages.map((message) => {
+ const parentGroups = message.groupId ? (
+ (groups.get(message.groupId) || [])
+ .concat([message.groupId])
+ ) : [];
+
+ return (
+ MessageContainer({
+ dispatch,
+ message,
+ key: message.id,
+ serviceContainer,
+ open: messagesUi.includes(message.id),
+ tableData: messagesTableData.get(message.id),
+ autoscroll,
+ indent: parentGroups.length,
+ })
+ );
+ });
+ return (
+ dom.div({
+ className: "webconsole-output",
+ ref: node => {
+ this.outputNode = node;
+ },
+ }, messageNodes
+ )
+ );
+ }
+});
+
+function scrollToBottom(node) {
+ node.scrollTop = node.scrollHeight;
+}
+
+function isScrolledToBottom(outputNode, scrollNode) {
+ let lastNodeHeight = outputNode.lastChild ?
+ outputNode.lastChild.clientHeight : 0;
+ return scrollNode.scrollTop + scrollNode.clientHeight >=
+ scrollNode.scrollHeight - lastNodeHeight / 2;
+}
+
+function mapStateToProps(state, props) {
+ return {
+ messages: getAllMessages(state),
+ messagesUi: getAllMessagesUiById(state),
+ messagesTableData: getAllMessagesTableDataById(state),
+ autoscroll: getScrollSetting(state),
+ groups: getAllGroupsById(state),
+ };
+}
+
+module.exports = connect(mapStateToProps)(ConsoleOutput);
diff --git a/devtools/client/webconsole/new-console-output/components/console-table.js b/devtools/client/webconsole/new-console-output/components/console-table.js
new file mode 100644
index 000000000..bf8fdcbd8
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/console-table.js
@@ -0,0 +1,202 @@
+/* This Source Code Form is subject to the terms of 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 {
+ createClass,
+ createFactory,
+ DOM: dom,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+const { ObjectClient } = require("devtools/shared/client/main");
+const actions = require("devtools/client/webconsole/new-console-output/actions/messages");
+const {l10n} = require("devtools/client/webconsole/new-console-output/utils/messages");
+const GripMessageBody = createFactory(require("devtools/client/webconsole/new-console-output/components/grip-message-body"));
+
+const TABLE_ROW_MAX_ITEMS = 1000;
+const TABLE_COLUMN_MAX_ITEMS = 10;
+
+const ConsoleTable = createClass({
+
+ displayName: "ConsoleTable",
+
+ propTypes: {
+ dispatch: PropTypes.func.isRequired,
+ parameters: PropTypes.array.isRequired,
+ serviceContainer: PropTypes.shape({
+ hudProxyClient: PropTypes.object.isRequired,
+ }),
+ id: PropTypes.string.isRequired,
+ },
+
+ componentWillMount: function () {
+ const {id, dispatch, serviceContainer, parameters} = this.props;
+
+ if (!Array.isArray(parameters) || parameters.length === 0) {
+ return;
+ }
+
+ const client = new ObjectClient(serviceContainer.hudProxyClient, parameters[0]);
+ let dataType = getParametersDataType(parameters);
+
+ // Get all the object properties.
+ dispatch(actions.messageTableDataGet(id, client, dataType));
+ },
+
+ getHeaders: function (columns) {
+ let headerItems = [];
+ columns.forEach((value, key) => headerItems.push(dom.th({}, value)));
+ return headerItems;
+ },
+
+ getRows: function (columns, items) {
+ return items.map(item => {
+ let cells = [];
+ columns.forEach((value, key) => {
+ cells.push(
+ dom.td(
+ {},
+ GripMessageBody({
+ grip: item[key]
+ })
+ )
+ );
+ });
+ return dom.tr({}, cells);
+ });
+ },
+
+ render: function () {
+ const {parameters, tableData} = this.props;
+ const headersGrip = parameters[1];
+ const headers = headersGrip && headersGrip.preview ? headersGrip.preview.items : null;
+
+ // if tableData is nullable, we don't show anything.
+ if (!tableData) {
+ return null;
+ }
+
+ const {columns, items} = getTableItems(
+ tableData,
+ getParametersDataType(parameters),
+ headers
+ );
+
+ return (
+ dom.table({className: "new-consoletable devtools-monospace"},
+ dom.thead({}, this.getHeaders(columns)),
+ dom.tbody({}, this.getRows(columns, items))
+ )
+ );
+ }
+});
+
+function getParametersDataType(parameters = null) {
+ if (!Array.isArray(parameters) || parameters.length === 0) {
+ return null;
+ }
+ return parameters[0].class;
+}
+
+function getTableItems(data = {}, type, headers = null) {
+ const INDEX_NAME = "_index";
+ const VALUE_NAME = "_value";
+ const namedIndexes = {
+ [INDEX_NAME]: (
+ ["Object", "Array"].includes(type) ?
+ l10n.getStr("table.index") : l10n.getStr("table.iterationIndex")
+ ),
+ [VALUE_NAME]: l10n.getStr("table.value"),
+ key: l10n.getStr("table.key")
+ };
+
+ let columns = new Map();
+ let items = [];
+
+ let addItem = function (item) {
+ items.push(item);
+ Object.keys(item).forEach(key => addColumn(key));
+ };
+
+ let addColumn = function (columnIndex) {
+ let columnExists = columns.has(columnIndex);
+ let hasMaxColumns = columns.size == TABLE_COLUMN_MAX_ITEMS;
+ let hasCustomHeaders = Array.isArray(headers);
+
+ if (
+ !columnExists &&
+ !hasMaxColumns && (
+ !hasCustomHeaders ||
+ headers.includes(columnIndex) ||
+ columnIndex === INDEX_NAME
+ )
+ ) {
+ columns.set(columnIndex, namedIndexes[columnIndex] || columnIndex);
+ }
+ };
+
+ for (let index of Object.keys(data)) {
+ if (type !== "Object" && index == parseInt(index, 10)) {
+ index = parseInt(index, 10);
+ }
+
+ let item = {
+ [INDEX_NAME]: index
+ };
+
+ let property = data[index].value;
+
+ if (property.preview) {
+ let {preview} = property;
+ let entries = preview.ownProperties || preview.items;
+ if (entries) {
+ for (let key of Object.keys(entries)) {
+ let entry = entries[key];
+ item[key] = entry.value || entry;
+ }
+ } else {
+ if (preview.key) {
+ item.key = preview.key;
+ }
+
+ item[VALUE_NAME] = preview.value || property;
+ }
+ } else {
+ item[VALUE_NAME] = property;
+ }
+
+ 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 (Array.isArray(headers)) {
+ headers.forEach(header => addColumn(header));
+ }
+
+ // We want to always have the index column first
+ if (columns.has(INDEX_NAME)) {
+ let 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)) {
+ let 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/new-console-output/components/filter-bar.js b/devtools/client/webconsole/new-console-output/components/filter-bar.js
new file mode 100644
index 000000000..a386a414a
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/filter-bar.js
@@ -0,0 +1,170 @@
+/* This Source Code Form is subject to the terms of 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,
+ createClass,
+ DOM: dom,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { getAllFilters } = require("devtools/client/webconsole/new-console-output/selectors/filters");
+const { getAllUi } = require("devtools/client/webconsole/new-console-output/selectors/ui");
+const { filterTextSet, filtersClear } = require("devtools/client/webconsole/new-console-output/actions/index");
+const { messagesClear } = require("devtools/client/webconsole/new-console-output/actions/index");
+const uiActions = require("devtools/client/webconsole/new-console-output/actions/index");
+const {
+ MESSAGE_LEVEL
+} = require("../constants");
+const FilterButton = createFactory(require("devtools/client/webconsole/new-console-output/components/filter-button"));
+
+const FilterBar = createClass({
+
+ displayName: "FilterBar",
+
+ propTypes: {
+ filter: PropTypes.object.isRequired,
+ serviceContainer: PropTypes.shape({
+ attachRefToHud: PropTypes.func.isRequired,
+ }).isRequired,
+ ui: PropTypes.object.isRequired
+ },
+
+ componentDidMount() {
+ this.props.serviceContainer.attachRefToHud("filterBox",
+ this.wrapperNode.querySelector(".text-filter"));
+ },
+
+ onClickMessagesClear: function () {
+ this.props.dispatch(messagesClear());
+ },
+
+ onClickFilterBarToggle: function () {
+ this.props.dispatch(uiActions.filterBarToggle());
+ },
+
+ onClickFiltersClear: function () {
+ this.props.dispatch(filtersClear());
+ },
+
+ onSearchInput: function (e) {
+ this.props.dispatch(filterTextSet(e.target.value));
+ },
+
+ render() {
+ const {dispatch, filter, ui} = this.props;
+ let filterBarVisible = ui.filterBarVisible;
+ let children = [];
+
+ children.push(dom.div({className: "devtools-toolbar webconsole-filterbar-primary"},
+ dom.button({
+ className: "devtools-button devtools-clear-icon",
+ title: "Clear output",
+ onClick: this.onClickMessagesClear
+ }),
+ dom.button({
+ className: "devtools-button devtools-filter-icon" + (
+ filterBarVisible ? " checked" : ""),
+ title: "Toggle filter bar",
+ onClick: this.onClickFilterBarToggle
+ }),
+ dom.input({
+ className: "devtools-plaininput text-filter",
+ type: "search",
+ value: filter.text,
+ placeholder: "Filter output",
+ onInput: this.onSearchInput
+ })
+ ));
+
+ if (filterBarVisible) {
+ children.push(
+ dom.div({className: "devtools-toolbar webconsole-filterbar-secondary"},
+ FilterButton({
+ active: filter.error,
+ label: "Errors",
+ filterKey: MESSAGE_LEVEL.ERROR,
+ dispatch
+ }),
+ FilterButton({
+ active: filter.warn,
+ label: "Warnings",
+ filterKey: MESSAGE_LEVEL.WARN,
+ dispatch
+ }),
+ FilterButton({
+ active: filter.log,
+ label: "Logs",
+ filterKey: MESSAGE_LEVEL.LOG,
+ dispatch
+ }),
+ FilterButton({
+ active: filter.info,
+ label: "Info",
+ filterKey: MESSAGE_LEVEL.INFO,
+ dispatch
+ }),
+ FilterButton({
+ active: filter.debug,
+ label: "Debug",
+ filterKey: MESSAGE_LEVEL.DEBUG,
+ dispatch
+ }),
+ dom.span({
+ className: "devtools-separator",
+ }),
+ FilterButton({
+ active: filter.netxhr,
+ label: "XHR",
+ filterKey: "netxhr",
+ dispatch
+ }),
+ FilterButton({
+ active: filter.net,
+ label: "Requests",
+ filterKey: "net",
+ dispatch
+ })
+ )
+ );
+ }
+
+ if (ui.filteredMessageVisible) {
+ children.push(
+ dom.div({className: "devtools-toolbar"},
+ dom.span({
+ className: "clear"},
+ "You have filters set that may hide some results. " +
+ "Learn more about our filtering syntax ",
+ dom.a({}, "here"),
+ "."),
+ dom.button({
+ className: "menu-filter-button",
+ onClick: this.onClickFiltersClear
+ }, "Remove filters")
+ )
+ );
+ }
+
+ return (
+ dom.div({
+ className: "webconsole-filteringbar-wrapper",
+ ref: node => {
+ this.wrapperNode = node;
+ }
+ }, ...children
+ )
+ );
+ }
+});
+
+function mapStateToProps(state) {
+ return {
+ filter: getAllFilters(state),
+ ui: getAllUi(state)
+ };
+}
+
+module.exports = connect(mapStateToProps)(FilterBar);
diff --git a/devtools/client/webconsole/new-console-output/components/filter-button.js b/devtools/client/webconsole/new-console-output/components/filter-button.js
new file mode 100644
index 000000000..4116bb524
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/filter-button.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";
+
+const {
+ createClass,
+ DOM: dom,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+const actions = require("devtools/client/webconsole/new-console-output/actions/index");
+
+const FilterButton = createClass({
+
+ displayName: "FilterButton",
+
+ propTypes: {
+ label: PropTypes.string.isRequired,
+ filterKey: PropTypes.string.isRequired,
+ active: PropTypes.bool.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ },
+
+ onClick: function () {
+ this.props.dispatch(actions.filterToggle(this.props.filterKey));
+ },
+
+ render() {
+ const {active, label, filterKey} = this.props;
+
+ let classList = [
+ "menu-filter-button",
+ filterKey,
+ ];
+ if (active) {
+ classList.push("checked");
+ }
+
+ return dom.button({
+ className: classList.join(" "),
+ onClick: this.onClick
+ }, label);
+ }
+});
+
+module.exports = FilterButton;
diff --git a/devtools/client/webconsole/new-console-output/components/grip-message-body.js b/devtools/client/webconsole/new-console-output/components/grip-message-body.js
new file mode 100644
index 000000000..29c2e6a4f
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/grip-message-body.js
@@ -0,0 +1,102 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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";
+
+// If this is being run from Mocha, then the browser loader hasn't set up
+// define. We need to do that before loading Rep.
+if (typeof define === "undefined") {
+ require("amd-loader");
+}
+
+// React
+const {
+ createFactory,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
+const { Rep } = createFactories(require("devtools/client/shared/components/reps/rep"));
+const StringRep = createFactories(require("devtools/client/shared/components/reps/string").StringRep).rep;
+const VariablesViewLink = createFactory(require("devtools/client/webconsole/new-console-output/components/variables-view-link"));
+const { Grip } = require("devtools/client/shared/components/reps/grip");
+
+GripMessageBody.displayName = "GripMessageBody";
+
+GripMessageBody.propTypes = {
+ grip: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number,
+ PropTypes.object,
+ ]).isRequired,
+ serviceContainer: PropTypes.shape({
+ createElement: PropTypes.func.isRequired,
+ }),
+ userProvidedStyle: PropTypes.string,
+};
+
+function GripMessageBody(props) {
+ const { grip, userProvidedStyle, serviceContainer } = props;
+
+ let styleObject;
+ if (userProvidedStyle && userProvidedStyle !== "") {
+ styleObject = cleanupStyle(userProvidedStyle, serviceContainer.createElement);
+ }
+
+ return (
+ // @TODO once there is a longString rep, also turn off quotes for those.
+ typeof grip === "string"
+ ? StringRep({
+ object: grip,
+ useQuotes: false,
+ mode: props.mode,
+ style: styleObject
+ })
+ : Rep({
+ object: grip,
+ objectLink: VariablesViewLink,
+ defaultRep: Grip,
+ mode: props.mode,
+ })
+ );
+}
+
+function cleanupStyle(userProvidedStyle, createElement) {
+ // Regular expression that matches the allowed CSS property names.
+ const allowedStylesRegex = new RegExp(
+ "^(?:-moz-)?(?:background|border|box|clear|color|cursor|display|float|font|line|" +
+ "margin|padding|text|transition|outline|white-space|word|writing|" +
+ "(?:min-|max-)?width|(?:min-|max-)?height)"
+ );
+
+ // Regular expression that matches the forbidden CSS property values.
+ const forbiddenValuesRegexs = [
+ // url(), -moz-element()
+ /\b(?:url|(?:-moz-)?element)[\s('"]+/gi,
+
+ // various URL protocols
+ /['"(]*(?:chrome|resource|about|app|data|https?|ftp|file):+\/*/gi,
+ ];
+
+ // Use a dummy element to parse the style string.
+ let dummy = createElement("div");
+ dummy.style = userProvidedStyle;
+
+ // Return a style object as expected by React DOM components, e.g.
+ // {color: "red"}
+ // without forbidden properties and values.
+ return [...dummy.style]
+ .filter(name => {
+ return allowedStylesRegex.test(name)
+ && !forbiddenValuesRegexs.some(regex => regex.test(dummy.style[name]));
+ })
+ .reduce((object, name) => {
+ return Object.assign({
+ [name]: dummy.style[name]
+ }, object);
+ }, {});
+}
+
+module.exports = GripMessageBody;
diff --git a/devtools/client/webconsole/new-console-output/components/message-container.js b/devtools/client/webconsole/new-console-output/components/message-container.js
new file mode 100644
index 000000000..115e9e291
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/message-container.js
@@ -0,0 +1,92 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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 {
+ createClass,
+ createFactory,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+
+const {
+ MESSAGE_SOURCE,
+ MESSAGE_TYPE
+} = require("devtools/client/webconsole/new-console-output/constants");
+
+const componentMap = new Map([
+ ["ConsoleApiCall", require("./message-types/console-api-call")],
+ ["ConsoleCommand", require("./message-types/console-command")],
+ ["DefaultRenderer", require("./message-types/default-renderer")],
+ ["EvaluationResult", require("./message-types/evaluation-result")],
+ ["NetworkEventMessage", require("./message-types/network-event-message")],
+ ["PageError", require("./message-types/page-error")]
+]);
+
+const MessageContainer = createClass({
+ displayName: "MessageContainer",
+
+ propTypes: {
+ message: PropTypes.object.isRequired,
+ open: PropTypes.bool.isRequired,
+ serviceContainer: PropTypes.object.isRequired,
+ autoscroll: PropTypes.bool.isRequired,
+ indent: PropTypes.number.isRequired,
+ },
+
+ getDefaultProps: function () {
+ return {
+ open: false,
+ indent: 0,
+ };
+ },
+
+ shouldComponentUpdate(nextProps, nextState) {
+ const repeatChanged = this.props.message.repeat !== nextProps.message.repeat;
+ const openChanged = this.props.open !== nextProps.open;
+ const tableDataChanged = this.props.tableData !== nextProps.tableData;
+ return repeatChanged || openChanged || tableDataChanged;
+ },
+
+ render() {
+ const { message } = this.props;
+
+ let MessageComponent = createFactory(getMessageComponent(message));
+ return MessageComponent(this.props);
+ }
+});
+
+function getMessageComponent(message) {
+ switch (message.source) {
+ case MESSAGE_SOURCE.CONSOLE_API:
+ return componentMap.get("ConsoleApiCall");
+ case MESSAGE_SOURCE.NETWORK:
+ return componentMap.get("NetworkEventMessage");
+ case MESSAGE_SOURCE.JAVASCRIPT:
+ switch (message.type) {
+ case MESSAGE_TYPE.COMMAND:
+ return componentMap.get("ConsoleCommand");
+ case MESSAGE_TYPE.RESULT:
+ return componentMap.get("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 componentMap.get("PageError");
+ default:
+ return componentMap.get("DefaultRenderer");
+ }
+ }
+
+ return componentMap.get("DefaultRenderer");
+}
+
+module.exports.MessageContainer = MessageContainer;
+
+// Exported so we can test it with unit tests.
+module.exports.getMessageComponent = getMessageComponent;
diff --git a/devtools/client/webconsole/new-console-output/components/message-icon.js b/devtools/client/webconsole/new-console-output/components/message-icon.js
new file mode 100644
index 000000000..b4c32fda0
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/message-icon.js
@@ -0,0 +1,32 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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 {
+ DOM: dom,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+const {l10n} = require("devtools/client/webconsole/new-console-output/utils/messages");
+
+MessageIcon.displayName = "MessageIcon";
+
+MessageIcon.propTypes = {
+ level: PropTypes.string.isRequired,
+};
+
+function MessageIcon(props) {
+ const { level } = props;
+
+ const title = l10n.getStr("level." + level);
+ return dom.div({
+ className: "icon",
+ title
+ });
+}
+
+module.exports = MessageIcon;
diff --git a/devtools/client/webconsole/new-console-output/components/message-indent.js b/devtools/client/webconsole/new-console-output/components/message-indent.js
new file mode 100644
index 000000000..354e13589
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/message-indent.js
@@ -0,0 +1,37 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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 {
+ createClass,
+ DOM: dom,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+
+const INDENT_WIDTH = 12;
+const MessageIndent = createClass({
+
+ displayName: "MessageIndent",
+
+ propTypes: {
+ indent: PropTypes.number.isRequired,
+ },
+
+ render: function () {
+ const { indent } = this.props;
+ return dom.span({
+ className: "indent",
+ style: {"width": indent * INDENT_WIDTH}
+ });
+ }
+});
+
+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/new-console-output/components/message-repeat.js b/devtools/client/webconsole/new-console-output/components/message-repeat.js
new file mode 100644
index 000000000..1820340ea
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/message-repeat.js
@@ -0,0 +1,36 @@
+
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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 {
+ DOM: dom,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+const { PluralForm } = require("devtools/shared/plural-form");
+const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages");
+
+MessageRepeat.displayName = "MessageRepeat";
+
+MessageRepeat.propTypes = {
+ repeat: PropTypes.number.isRequired
+};
+
+function MessageRepeat(props) {
+ const { repeat } = props;
+ const visibility = repeat > 1 ? "visible" : "hidden";
+
+ return dom.span({
+ className: "message-repeats",
+ style: {visibility},
+ title: PluralForm.get(repeat, l10n.getStr("messageRepeats.tooltip2"))
+ .replace("#1", repeat)
+ }, repeat);
+}
+
+module.exports = MessageRepeat;
diff --git a/devtools/client/webconsole/new-console-output/components/message-types/console-api-call.js b/devtools/client/webconsole/new-console-output/components/message-types/console-api-call.js
new file mode 100644
index 000000000..7200648fa
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/message-types/console-api-call.js
@@ -0,0 +1,132 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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,
+ DOM: dom,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+const GripMessageBody = createFactory(require("devtools/client/webconsole/new-console-output/components/grip-message-body"));
+const ConsoleTable = createFactory(require("devtools/client/webconsole/new-console-output/components/console-table"));
+const {isGroupType, l10n} = require("devtools/client/webconsole/new-console-output/utils/messages");
+
+const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message"));
+
+ConsoleApiCall.displayName = "ConsoleApiCall";
+
+ConsoleApiCall.propTypes = {
+ message: PropTypes.object.isRequired,
+ open: PropTypes.bool,
+ serviceContainer: PropTypes.object.isRequired,
+ indent: PropTypes.number.isRequired,
+};
+
+ConsoleApiCall.defaultProps = {
+ open: false,
+ indent: 0,
+};
+
+function ConsoleApiCall(props) {
+ const {
+ dispatch,
+ message,
+ open,
+ tableData,
+ serviceContainer,
+ indent,
+ } = props;
+ const {
+ id: messageId,
+ source,
+ type,
+ level,
+ repeat,
+ stacktrace,
+ frame,
+ parameters,
+ messageText,
+ userProvidedStyles,
+ } = message;
+
+ let messageBody;
+ if (type === "trace") {
+ messageBody = dom.span({className: "cm-variable"}, "console.trace()");
+ } else if (type === "assert") {
+ let reps = formatReps(parameters);
+ messageBody = dom.span({ className: "cm-variable" }, "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(parameters, userProvidedStyles, serviceContainer);
+ } else {
+ messageBody = messageText;
+ }
+
+ let attachment = null;
+ if (type === "table") {
+ attachment = ConsoleTable({
+ dispatch,
+ id: message.id,
+ serviceContainer,
+ parameters: message.parameters,
+ tableData
+ });
+ }
+
+ 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,
+ });
+}
+
+function formatReps(parameters, userProvidedStyles, serviceContainer) {
+ return (
+ parameters
+ // Get all the grips.
+ .map((grip, key) => GripMessageBody({
+ grip,
+ key,
+ userProvidedStyle: userProvidedStyles ? userProvidedStyles[key] : null,
+ serviceContainer
+ }))
+ // Interleave spaces.
+ .reduce((arr, v, i) => {
+ return i + 1 < parameters.length
+ ? arr.concat(v, dom.span({}, " "))
+ : arr.concat(v);
+ }, [])
+ );
+}
+
+module.exports = ConsoleApiCall;
+
diff --git a/devtools/client/webconsole/new-console-output/components/message-types/console-command.js b/devtools/client/webconsole/new-console-output/components/message-types/console-command.js
new file mode 100644
index 000000000..d87229fa9
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/message-types/console-command.js
@@ -0,0 +1,57 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message"));
+
+ConsoleCommand.displayName = "ConsoleCommand";
+
+ConsoleCommand.propTypes = {
+ message: PropTypes.object.isRequired,
+ autoscroll: PropTypes.bool.isRequired,
+ indent: PropTypes.number.isRequired,
+};
+
+ConsoleCommand.defaultProps = {
+ indent: 0,
+};
+
+/**
+ * Displays input from the console.
+ */
+function ConsoleCommand(props) {
+ const { autoscroll, indent, message } = props;
+ const {
+ source,
+ type,
+ level,
+ messageText: messageBody,
+ } = message;
+
+ const {
+ serviceContainer,
+ } = props;
+
+ const childProps = {
+ source,
+ type,
+ level,
+ topLevelClasses: [],
+ messageBody,
+ scrollToMessage: autoscroll,
+ serviceContainer,
+ indent: indent,
+ };
+ return Message(childProps);
+}
+
+module.exports = ConsoleCommand;
diff --git a/devtools/client/webconsole/new-console-output/components/message-types/default-renderer.js b/devtools/client/webconsole/new-console-output/components/message-types/default-renderer.js
new file mode 100644
index 000000000..d07089531
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/message-types/default-renderer.js
@@ -0,0 +1,22 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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 {
+ DOM: dom,
+} = require("devtools/client/shared/vendor/react");
+
+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/new-console-output/components/message-types/evaluation-result.js b/devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js
new file mode 100644
index 000000000..992dc62cf
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js
@@ -0,0 +1,64 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message"));
+const GripMessageBody = createFactory(require("devtools/client/webconsole/new-console-output/components/grip-message-body"));
+
+EvaluationResult.displayName = "EvaluationResult";
+
+EvaluationResult.propTypes = {
+ message: PropTypes.object.isRequired,
+ indent: PropTypes.number.isRequired,
+};
+
+EvaluationResult.defaultProps = {
+ indent: 0,
+};
+
+function EvaluationResult(props) {
+ const { message, serviceContainer, indent } = props;
+ const {
+ source,
+ type,
+ level,
+ id: messageId,
+ exceptionDocURL,
+ frame,
+ } = message;
+
+ let messageBody;
+ if (message.messageText) {
+ messageBody = message.messageText;
+ } else {
+ messageBody = GripMessageBody({grip: message.parameters});
+ }
+
+ const topLevelClasses = ["cm-s-mozilla"];
+
+ const childProps = {
+ source,
+ type,
+ level,
+ indent,
+ topLevelClasses,
+ messageBody,
+ messageId,
+ scrollToMessage: props.autoscroll,
+ serviceContainer,
+ exceptionDocURL,
+ frame,
+ };
+ return Message(childProps);
+}
+
+module.exports = EvaluationResult;
diff --git a/devtools/client/webconsole/new-console-output/components/message-types/moz.build b/devtools/client/webconsole/new-console-output/components/message-types/moz.build
new file mode 100644
index 000000000..9b9f72017
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/message-types/moz.build
@@ -0,0 +1,13 @@
+# 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(
+ 'console-api-call.js',
+ 'console-command.js',
+ 'default-renderer.js',
+ 'evaluation-result.js',
+ 'network-event-message.js',
+ 'page-error.js',
+)
diff --git a/devtools/client/webconsole/new-console-output/components/message-types/network-event-message.js b/devtools/client/webconsole/new-console-output/components/message-types/network-event-message.js
new file mode 100644
index 000000000..e3c81a487
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/message-types/network-event-message.js
@@ -0,0 +1,63 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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,
+ DOM: dom,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message"));
+const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages");
+
+NetworkEventMessage.displayName = "NetworkEventMessage";
+
+NetworkEventMessage.propTypes = {
+ message: PropTypes.object.isRequired,
+ serviceContainer: PropTypes.shape({
+ openNetworkPanel: PropTypes.func.isRequired,
+ }),
+ indent: PropTypes.number.isRequired,
+};
+
+NetworkEventMessage.defaultProps = {
+ indent: 0,
+};
+
+function NetworkEventMessage(props) {
+ const { message, serviceContainer, indent } = props;
+ const { actor, source, type, level, request, isXHR } = message;
+
+ const topLevelClasses = [ "cm-s-mozilla" ];
+
+ function onUrlClick() {
+ serviceContainer.openNetworkPanel(actor);
+ }
+
+ const method = dom.span({className: "method" }, request.method);
+ const xhr = isXHR
+ ? dom.span({ className: "xhr" }, l10n.getStr("webConsoleXhrIndicator"))
+ : null;
+ const url = dom.a({ className: "url", title: request.url, onClick: onUrlClick },
+ request.url.replace(/\?.+/, ""));
+
+ const messageBody = dom.span({}, method, xhr, url);
+
+ const childProps = {
+ source,
+ type,
+ level,
+ indent,
+ topLevelClasses,
+ messageBody,
+ serviceContainer,
+ };
+ return Message(childProps);
+}
+
+module.exports = NetworkEventMessage;
diff --git a/devtools/client/webconsole/new-console-output/components/message-types/page-error.js b/devtools/client/webconsole/new-console-output/components/message-types/page-error.js
new file mode 100644
index 000000000..77ea75ff7
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/message-types/page-error.js
@@ -0,0 +1,69 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message"));
+
+PageError.displayName = "PageError";
+
+PageError.propTypes = {
+ message: PropTypes.object.isRequired,
+ open: PropTypes.bool,
+ indent: PropTypes.number.isRequired,
+};
+
+PageError.defaultProps = {
+ open: false,
+ indent: 0,
+};
+
+function PageError(props) {
+ const {
+ dispatch,
+ message,
+ open,
+ serviceContainer,
+ indent,
+ } = props;
+ const {
+ id: messageId,
+ source,
+ type,
+ level,
+ messageText: messageBody,
+ repeat,
+ stacktrace,
+ frame,
+ exceptionDocURL,
+ } = message;
+
+ const childProps = {
+ dispatch,
+ messageId,
+ open,
+ collapsible: Array.isArray(stacktrace),
+ source,
+ type,
+ level,
+ topLevelClasses: [],
+ indent,
+ messageBody,
+ repeat,
+ frame,
+ stacktrace,
+ serviceContainer,
+ exceptionDocURL,
+ };
+ return Message(childProps);
+}
+
+module.exports = PageError;
diff --git a/devtools/client/webconsole/new-console-output/components/message.js b/devtools/client/webconsole/new-console-output/components/message.js
new file mode 100644
index 000000000..f36bff7e4
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/message.js
@@ -0,0 +1,176 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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 {
+ createClass,
+ createFactory,
+ DOM: dom,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages");
+const actions = require("devtools/client/webconsole/new-console-output/actions/index");
+const CollapseButton = createFactory(require("devtools/client/webconsole/new-console-output/components/collapse-button"));
+const MessageIndent = createFactory(require("devtools/client/webconsole/new-console-output/components/message-indent").MessageIndent);
+const MessageIcon = createFactory(require("devtools/client/webconsole/new-console-output/components/message-icon"));
+const MessageRepeat = createFactory(require("devtools/client/webconsole/new-console-output/components/message-repeat"));
+const FrameView = createFactory(require("devtools/client/shared/components/frame"));
+const StackTrace = createFactory(require("devtools/client/shared/components/stack-trace"));
+
+const Message = createClass({
+ displayName: "Message",
+
+ propTypes: {
+ open: PropTypes.bool,
+ collapsible: PropTypes.bool,
+ collapseTitle: PropTypes.string,
+ source: PropTypes.string.isRequired,
+ type: PropTypes.string.isRequired,
+ level: PropTypes.string.isRequired,
+ indent: PropTypes.number.isRequired,
+ 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,
+ serviceContainer: PropTypes.shape({
+ emitNewMessage: PropTypes.func.isRequired,
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ sourceMapService: PropTypes.any,
+ }),
+ },
+
+ getDefaultProps: function () {
+ return {
+ indent: 0
+ };
+ },
+
+ componentDidMount() {
+ if (this.messageNode) {
+ if (this.props.scrollToMessage) {
+ this.messageNode.scrollIntoView();
+ }
+ // Event used in tests. Some message types don't pass it in because existing tests
+ // did not emit for them.
+ if (this.props.serviceContainer) {
+ this.props.serviceContainer.emitNewMessage(this.messageNode, this.props.messageId);
+ }
+ }
+ },
+
+ onLearnMoreClick: function () {
+ let {exceptionDocURL} = this.props;
+ this.props.serviceContainer.openLink(exceptionDocURL);
+ },
+
+ render() {
+ const {
+ messageId,
+ open,
+ collapsible,
+ collapseTitle,
+ source,
+ type,
+ level,
+ indent,
+ topLevelClasses,
+ messageBody,
+ frame,
+ stacktrace,
+ serviceContainer,
+ dispatch,
+ exceptionDocURL,
+ } = this.props;
+
+ topLevelClasses.push("message", source, type, level);
+ if (open) {
+ topLevelClasses.push("open");
+ }
+
+ const icon = MessageIcon({level});
+
+ // 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) {
+ const child = open ? StackTrace({
+ stacktrace: stacktrace,
+ onViewSourceInDebugger: serviceContainer.onViewSourceInDebugger
+ }) : null;
+ attachment = dom.div({ className: "stacktrace devtools-monospace" }, child);
+ }
+
+ // If there is an expandable part, make it collapsible.
+ let collapse = null;
+ if (collapsible) {
+ collapse = CollapseButton({
+ open,
+ title: collapseTitle,
+ onClick: function () {
+ if (open) {
+ dispatch(actions.messageClose(messageId));
+ } else {
+ dispatch(actions.messageOpen(messageId));
+ }
+ },
+ });
+ }
+
+ const repeat = this.props.repeat ? MessageRepeat({repeat: this.props.repeat}) : null;
+
+ // Configure the location.
+ const location = dom.span({ className: "message-location devtools-monospace" },
+ frame ? FrameView({
+ frame,
+ onClick: serviceContainer ? serviceContainer.onViewSourceInDebugger : undefined,
+ showEmptyPathAsHost: true,
+ sourceMapService: serviceContainer ? serviceContainer.sourceMapService : undefined
+ }) : null
+ );
+
+ let learnMore;
+ if (exceptionDocURL) {
+ learnMore = dom.a({
+ className: "learn-more-link webconsole-learn-more-link",
+ title: exceptionDocURL.split("?")[0],
+ onClick: this.onLearnMoreClick,
+ }, `[${l10n.getStr("webConsoleMoreInfoLabel")}]`);
+ }
+
+ return dom.div({
+ className: topLevelClasses.join(" "),
+ ref: node => {
+ this.messageNode = node;
+ }
+ },
+ // @TODO add timestamp
+ MessageIndent({indent}),
+ icon,
+ collapse,
+ dom.span({ className: "message-body-wrapper" },
+ dom.span({ className: "message-flex-body" },
+ dom.span({ className: "message-body devtools-monospace" },
+ messageBody,
+ learnMore
+ ),
+ repeat,
+ location
+ ),
+ attachment
+ )
+ );
+ }
+});
+
+module.exports = Message;
diff --git a/devtools/client/webconsole/new-console-output/components/moz.build b/devtools/client/webconsole/new-console-output/components/moz.build
new file mode 100644
index 000000000..8c0022314
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/moz.build
@@ -0,0 +1,23 @@
+# 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(
+ 'collapse-button.js',
+ 'console-output.js',
+ 'console-table.js',
+ 'filter-bar.js',
+ 'filter-button.js',
+ 'grip-message-body.js',
+ 'message-container.js',
+ 'message-icon.js',
+ 'message-indent.js',
+ 'message-repeat.js',
+ 'message.js',
+ 'variables-view-link.js'
+)
diff --git a/devtools/client/webconsole/new-console-output/components/variables-view-link.js b/devtools/client/webconsole/new-console-output/components/variables-view-link.js
new file mode 100644
index 000000000..4d79c322f
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/variables-view-link.js
@@ -0,0 +1,34 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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 {
+ DOM: dom,
+ PropTypes
+} = require("devtools/client/shared/vendor/react");
+const {openVariablesView} = require("devtools/client/webconsole/new-console-output/utils/variables-view");
+
+VariablesViewLink.displayName = "VariablesViewLink";
+
+VariablesViewLink.propTypes = {
+ object: PropTypes.object.isRequired
+};
+
+function VariablesViewLink(props) {
+ const { object, children } = props;
+
+ return (
+ dom.a({
+ onClick: openVariablesView.bind(null, object),
+ className: "cm-variable",
+ draggable: false,
+ }, children)
+ );
+}
+
+module.exports = VariablesViewLink;
diff --git a/devtools/client/webconsole/new-console-output/constants.js b/devtools/client/webconsole/new-console-output/constants.js
new file mode 100644
index 000000000..ef11d6eb8
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/constants.js
@@ -0,0 +1,81 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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";
+
+const actionTypes = {
+ BATCH_ACTIONS: "BATCH_ACTIONS",
+ MESSAGE_ADD: "MESSAGE_ADD",
+ MESSAGES_CLEAR: "MESSAGES_CLEAR",
+ MESSAGE_OPEN: "MESSAGE_OPEN",
+ MESSAGE_CLOSE: "MESSAGE_CLOSE",
+ MESSAGE_TABLE_RECEIVE: "MESSAGE_TABLE_RECEIVE",
+ FILTER_TOGGLE: "FILTER_TOGGLE",
+ FILTER_TEXT_SET: "FILTER_TEXT_SET",
+ FILTERS_CLEAR: "FILTERS_CLEAR",
+ FILTER_BAR_TOGGLE: "FILTER_BAR_TOGGLE",
+};
+
+const prefs = {
+ PREFS: {
+ FILTER: {
+ ERROR: "devtools.webconsole.filter.error",
+ WARN: "devtools.webconsole.filter.warn",
+ INFO: "devtools.webconsole.filter.info",
+ LOG: "devtools.webconsole.filter.log",
+ DEBUG: "devtools.webconsole.filter.debug",
+ NET: "devtools.webconsole.filter.net",
+ NETXHR: "devtools.webconsole.filter.netxhr",
+ },
+ UI: {
+ FILTER_BAR: "devtools.webconsole.ui.filterbar"
+ }
+ }
+};
+
+const chromeRDPEnums = {
+ MESSAGE_SOURCE: {
+ XML: "xml",
+ JAVASCRIPT: "javascript",
+ NETWORK: "network",
+ CONSOLE_API: "console-api",
+ STORAGE: "storage",
+ APPCACHE: "appcache",
+ RENDERING: "rendering",
+ SECURITY: "security",
+ OTHER: "other",
+ DEPRECATION: "deprecation"
+ },
+ MESSAGE_TYPE: {
+ LOG: "log",
+ DIR: "dir",
+ TABLE: "table",
+ TRACE: "trace",
+ CLEAR: "clear",
+ START_GROUP: "startGroup",
+ START_GROUP_COLLAPSED: "startGroupCollapsed",
+ END_GROUP: "endGroup",
+ ASSERT: "assert",
+ 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",
+ },
+ MESSAGE_LEVEL: {
+ LOG: "log",
+ ERROR: "error",
+ WARN: "warn",
+ DEBUG: "debug",
+ INFO: "info"
+ }
+};
+
+// Combine into a single constants object
+module.exports = Object.assign({}, actionTypes, prefs, chromeRDPEnums);
diff --git a/devtools/client/webconsole/new-console-output/main.js b/devtools/client/webconsole/new-console-output/main.js
new file mode 100644
index 000000000..29db5e337
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/main.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/. */
+
+ /* global BrowserLoader */
+
+"use strict";
+
+var { utils: Cu } = Components;
+
+const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+const { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
+
+// Initialize module loader and load all modules of the new inline
+// preview feature. The entire code-base doesn't need any extra
+// privileges and runs entirely in content scope.
+const NewConsoleOutputWrapper = BrowserLoader({
+ baseURI: "resource://devtools/client/webconsole/new-console-output/",
+ window}).require("./new-console-output-wrapper");
+
+this.NewConsoleOutput = function (parentNode, jsterm, toolbox, owner, serviceContainer) {
+ return new NewConsoleOutputWrapper(parentNode, jsterm, toolbox, owner, serviceContainer);
+};
diff --git a/devtools/client/webconsole/new-console-output/moz.build b/devtools/client/webconsole/new-console-output/moz.build
new file mode 100644
index 000000000..7d0905aaa
--- /dev/null
+++ b/devtools/client/webconsole/new-console-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 += [
+ 'actions',
+ 'components',
+ 'reducers',
+ 'selectors',
+ 'test',
+ 'utils',
+]
+
+DevToolsModules(
+ 'constants.js',
+ 'main.js',
+ 'new-console-output-wrapper.js',
+ 'store.js',
+ 'types.js',
+)
diff --git a/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js b/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
new file mode 100644
index 000000000..17c1e767d
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
@@ -0,0 +1,134 @@
+/* This Source Code Form is subject to the terms of 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 React = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+const { Provider } = require("devtools/client/shared/vendor/react-redux");
+
+const actions = require("devtools/client/webconsole/new-console-output/actions/index");
+const { configureStore } = require("devtools/client/webconsole/new-console-output/store");
+
+const ConsoleOutput = React.createFactory(require("devtools/client/webconsole/new-console-output/components/console-output"));
+const FilterBar = React.createFactory(require("devtools/client/webconsole/new-console-output/components/filter-bar"));
+
+const store = configureStore();
+let queuedActions = [];
+let throttledDispatchTimeout = false;
+
+function NewConsoleOutputWrapper(parentNode, jsterm, toolbox, owner, document) {
+ this.parentNode = parentNode;
+ this.jsterm = jsterm;
+ this.toolbox = toolbox;
+ this.owner = owner;
+ this.document = document;
+
+ this.init = this.init.bind(this);
+}
+
+NewConsoleOutputWrapper.prototype = {
+ init: function () {
+ const attachRefToHud = (id, node) => {
+ this.jsterm.hud[id] = node;
+ };
+
+ let childComponent = ConsoleOutput({
+ serviceContainer: {
+ attachRefToHud,
+ emitNewMessage: (node, messageId) => {
+ this.jsterm.hud.emit("new-messages", new Set([{
+ node,
+ messageId,
+ }]));
+ },
+ hudProxyClient: this.jsterm.hud.proxy.client,
+ onViewSourceInDebugger: frame => this.toolbox.viewSourceInDebugger.call(
+ this.toolbox,
+ frame.url,
+ frame.line
+ ),
+ openNetworkPanel: (requestId) => {
+ return this.toolbox.selectTool("netmonitor").then(panel => {
+ return panel.panelWin.NetMonitorController.inspectRequest(requestId);
+ });
+ },
+ sourceMapService: this.toolbox ? this.toolbox._sourceMapService : null,
+ openLink: url => this.jsterm.hud.owner.openLink.call(this.jsterm.hud.owner, url),
+ createElement: nodename => {
+ return this.document.createElementNS("http://www.w3.org/1999/xhtml", nodename);
+ }
+ }
+ });
+ let filterBar = FilterBar({
+ serviceContainer: {
+ attachRefToHud
+ }
+ });
+ let provider = React.createElement(
+ Provider,
+ { store },
+ React.DOM.div(
+ {className: "webconsole-output-wrapper"},
+ filterBar,
+ childComponent
+ ));
+
+ this.body = ReactDOM.render(provider, this.parentNode);
+ },
+
+ dispatchMessageAdd: function (message, waitForResponse) {
+ let action = actions.messageAdd(message);
+ batchedMessageAdd(action);
+
+ // Wait for the message to render to resolve with the DOM node.
+ // This is just for backwards compatibility with old tests, and should
+ // be removed once it's not needed anymore.
+ // Can only wait for response if the action contains a valid message.
+ if (waitForResponse && action.message) {
+ let messageId = action.message.get("id");
+ return new Promise(resolve => {
+ let jsterm = this.jsterm;
+ jsterm.hud.on("new-messages", function onThisMessage(e, messages) {
+ for (let m of messages) {
+ if (m.messageId == messageId) {
+ resolve(m.node);
+ jsterm.hud.off("new-messages", onThisMessage);
+ return;
+ }
+ }
+ });
+ });
+ }
+
+ return Promise.resolve();
+ },
+
+ dispatchMessagesAdd: function (messages) {
+ const batchedActions = messages.map(message => actions.messageAdd(message));
+ store.dispatch(actions.batchActions(batchedActions));
+ },
+
+ dispatchMessagesClear: function () {
+ store.dispatch(actions.messagesClear());
+ },
+ // Should be used for test purpose only.
+ getStore: function () {
+ return store;
+ }
+};
+
+function batchedMessageAdd(action) {
+ queuedActions.push(action);
+ if (!throttledDispatchTimeout) {
+ throttledDispatchTimeout = setTimeout(() => {
+ store.dispatch(actions.batchActions(queuedActions));
+ queuedActions = [];
+ throttledDispatchTimeout = null;
+ }, 50);
+ }
+}
+
+// Exports from this module
+module.exports = NewConsoleOutputWrapper;
diff --git a/devtools/client/webconsole/new-console-output/reducers/filters.js b/devtools/client/webconsole/new-console-output/reducers/filters.js
new file mode 100644
index 000000000..cd5f4bf7c
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/reducers/filters.js
@@ -0,0 +1,39 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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";
+
+const Immutable = require("devtools/client/shared/vendor/immutable");
+const constants = require("devtools/client/webconsole/new-console-output/constants");
+
+const FilterState = Immutable.Record({
+ debug: true,
+ error: true,
+ info: true,
+ log: true,
+ net: false,
+ netxhr: false,
+ text: "",
+ warn: true,
+});
+
+function filters(state = new FilterState(), action) {
+ switch (action.type) {
+ case constants.FILTER_TOGGLE:
+ const {filter} = action;
+ const active = !state.get(filter);
+ return state.set(filter, active);
+ case constants.FILTERS_CLEAR:
+ return new FilterState();
+ case constants.FILTER_TEXT_SET:
+ let {text} = action;
+ return state.set("text", text);
+ }
+
+ return state;
+}
+
+exports.FilterState = FilterState;
+exports.filters = filters;
diff --git a/devtools/client/webconsole/new-console-output/reducers/index.js b/devtools/client/webconsole/new-console-output/reducers/index.js
new file mode 100644
index 000000000..6ab10d565
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/reducers/index.js
@@ -0,0 +1,18 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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";
+
+const { filters } = require("./filters");
+const { messages } = require("./messages");
+const { prefs } = require("./prefs");
+const { ui } = require("./ui");
+
+exports.reducers = {
+ filters,
+ messages,
+ prefs,
+ ui,
+};
diff --git a/devtools/client/webconsole/new-console-output/reducers/messages.js b/devtools/client/webconsole/new-console-output/reducers/messages.js
new file mode 100644
index 000000000..0693fed60
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/reducers/messages.js
@@ -0,0 +1,135 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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";
+
+const Immutable = require("devtools/client/shared/vendor/immutable");
+const constants = require("devtools/client/webconsole/new-console-output/constants");
+const {isGroupType} = require("devtools/client/webconsole/new-console-output/utils/messages");
+
+const MessageState = Immutable.Record({
+ // List of all the messages added to the console.
+ messagesById: Immutable.List(),
+ // List of the message ids which are opened.
+ messagesUiById: Immutable.List(),
+ // Map of the form {messageId : tableData}, which represent the data passed
+ // as an argument in console.table calls.
+ messagesTableDataById: Immutable.Map(),
+ // Map of the form {groupMessageId : groupArray},
+ // where groupArray is the list of of all the parent groups' ids of the groupMessageId.
+ groupsById: Immutable.Map(),
+ // Message id of the current group (no corresponding console.groupEnd yet).
+ currentGroup: null,
+});
+
+function messages(state = new MessageState(), action) {
+ const {
+ messagesById,
+ messagesUiById,
+ messagesTableDataById,
+ groupsById,
+ currentGroup
+ } = state;
+
+ switch (action.type) {
+ case constants.MESSAGE_ADD:
+ let newMessage = action.message;
+
+ 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.
+ return state.set("currentGroup", getNewCurrentGroup(currentGroup, groupsById));
+ }
+
+ if (newMessage.allowRepeating && messagesById.size > 0) {
+ let lastMessage = messagesById.last();
+ if (lastMessage.repeatId === newMessage.repeatId) {
+ return state.withMutations(function (record) {
+ record.set("messagesById", messagesById.pop().push(
+ newMessage.set("repeat", lastMessage.repeat + 1)
+ ));
+ });
+ }
+ }
+
+ return state.withMutations(function (record) {
+ // Add the new message with a reference to the parent group.
+ record.set(
+ "messagesById",
+ messagesById.push(newMessage.set("groupId", currentGroup))
+ );
+
+ if (newMessage.type === "trace") {
+ // We want the stacktrace to be open by default.
+ record.set("messagesUiById", messagesUiById.push(newMessage.id));
+ } else if (isGroupType(newMessage.type)) {
+ record.set("currentGroup", newMessage.id);
+ record.set("groupsById",
+ groupsById.set(
+ newMessage.id,
+ getParentGroups(currentGroup, groupsById)
+ )
+ );
+
+ if (newMessage.type === constants.MESSAGE_TYPE.START_GROUP) {
+ // We want the group to be open by default.
+ record.set("messagesUiById", messagesUiById.push(newMessage.id));
+ }
+ }
+ });
+ case constants.MESSAGES_CLEAR:
+ return state.withMutations(function (record) {
+ record.set("messagesById", Immutable.List());
+ record.set("messagesUiById", Immutable.List());
+ record.set("groupsById", Immutable.Map());
+ record.set("currentGroup", null);
+ });
+ case constants.MESSAGE_OPEN:
+ return state.set("messagesUiById", messagesUiById.push(action.id));
+ case constants.MESSAGE_CLOSE:
+ let index = state.messagesUiById.indexOf(action.id);
+ return state.deleteIn(["messagesUiById", index]);
+ case constants.MESSAGE_TABLE_RECEIVE:
+ const {id, data} = action;
+ return state.set("messagesTableDataById", messagesTableDataById.set(id, data));
+ }
+
+ return state;
+}
+
+function getNewCurrentGroup(currentGoup, groupsById) {
+ let newCurrentGroup = null;
+ if (currentGoup) {
+ // Retrieve the parent groups of the current group.
+ let parents = groupsById.get(currentGoup);
+ if (Array.isArray(parents) && parents.length > 0) {
+ // If there's at least one parent, make the first one the new currentGroup.
+ newCurrentGroup = parents[0];
+ }
+ }
+ return newCurrentGroup;
+}
+
+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.
+ let parentGroups = groupsById.get(currentGroup);
+ if (Array.isArray(parentGroups) && parentGroups.length > 0) {
+ groups = groups.concat(parentGroups);
+ }
+ }
+
+ return groups;
+}
+
+exports.messages = messages;
diff --git a/devtools/client/webconsole/new-console-output/reducers/moz.build b/devtools/client/webconsole/new-console-output/reducers/moz.build
new file mode 100644
index 000000000..651512f85
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/reducers/moz.build
@@ -0,0 +1,12 @@
+# 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(
+ 'filters.js',
+ 'index.js',
+ 'messages.js',
+ 'prefs.js',
+ 'ui.js',
+)
diff --git a/devtools/client/webconsole/new-console-output/reducers/prefs.js b/devtools/client/webconsole/new-console-output/reducers/prefs.js
new file mode 100644
index 000000000..0707105e1
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/reducers/prefs.js
@@ -0,0 +1,18 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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";
+
+const Immutable = require("devtools/client/shared/vendor/immutable");
+const PrefState = Immutable.Record({
+ logLimit: 1000
+});
+
+function prefs(state = new PrefState(), action) {
+ return state;
+}
+
+exports.PrefState = PrefState;
+exports.prefs = prefs;
diff --git a/devtools/client/webconsole/new-console-output/reducers/ui.js b/devtools/client/webconsole/new-console-output/reducers/ui.js
new file mode 100644
index 000000000..aa91dceeb
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/reducers/ui.js
@@ -0,0 +1,39 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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";
+
+const {
+ FILTER_BAR_TOGGLE,
+ MESSAGE_ADD,
+} = require("devtools/client/webconsole/new-console-output/constants");
+const Immutable = require("devtools/client/shared/vendor/immutable");
+
+const UiState = Immutable.Record({
+ filterBarVisible: false,
+ filteredMessageVisible: false,
+ autoscroll: true,
+});
+
+function ui(state = new UiState(), action) {
+ // Autoscroll should be set for all action types. If the last action was not message
+ // add, then turn it off. This prevents us from scrolling after someone toggles a
+ // filter, or to the bottom of the attachement when an expandable message at the bottom
+ // of the list is expanded. It does depend on the MESSAGE_ADD action being the last in
+ // its batch, though.
+ state = state.set("autoscroll", action.type == MESSAGE_ADD);
+
+ switch (action.type) {
+ case FILTER_BAR_TOGGLE:
+ return state.set("filterBarVisible", !state.filterBarVisible);
+ }
+
+ return state;
+}
+
+module.exports = {
+ UiState,
+ ui,
+};
diff --git a/devtools/client/webconsole/new-console-output/selectors/filters.js b/devtools/client/webconsole/new-console-output/selectors/filters.js
new file mode 100644
index 000000000..36afa60cc
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/selectors/filters.js
@@ -0,0 +1,12 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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";
+
+function getAllFilters(state) {
+ return state.filters;
+}
+
+exports.getAllFilters = getAllFilters;
diff --git a/devtools/client/webconsole/new-console-output/selectors/messages.js b/devtools/client/webconsole/new-console-output/selectors/messages.js
new file mode 100644
index 000000000..c4b1aee28
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/selectors/messages.js
@@ -0,0 +1,168 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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";
+
+const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages");
+const { getAllFilters } = require("devtools/client/webconsole/new-console-output/selectors/filters");
+const { getLogLimit } = require("devtools/client/webconsole/new-console-output/selectors/prefs");
+const {
+ MESSAGE_TYPE,
+ MESSAGE_SOURCE
+} = require("devtools/client/webconsole/new-console-output/constants");
+
+function getAllMessages(state) {
+ let messages = getAllMessagesById(state);
+ let logLimit = getLogLimit(state);
+ let filters = getAllFilters(state);
+
+ let groups = getAllGroupsById(state);
+ let messagesUI = getAllMessagesUiById(state);
+
+ return prune(
+ messages.filter(message => {
+ return (
+ isInOpenedGroup(message, groups, messagesUI)
+ && (
+ isUnfilterable(message)
+ || (
+ matchLevelFilters(message, filters)
+ && matchNetworkFilters(message, filters)
+ && matchSearchFilters(message, filters)
+ )
+ )
+ );
+ }),
+ logLimit
+ );
+}
+
+function getAllMessagesById(state) {
+ return state.messages.messagesById;
+}
+
+function getAllMessagesUiById(state) {
+ return state.messages.messagesUiById;
+}
+
+function getAllMessagesTableDataById(state) {
+ return state.messages.messagesTableDataById;
+}
+
+function getAllGroupsById(state) {
+ return state.messages.groupsById;
+}
+
+function getCurrentGroup(state) {
+ return state.messages.currentGroup;
+}
+
+function isUnfilterable(message) {
+ return [
+ MESSAGE_TYPE.COMMAND,
+ MESSAGE_TYPE.RESULT,
+ MESSAGE_TYPE.START_GROUP,
+ MESSAGE_TYPE.START_GROUP_COLLAPSED,
+ ].includes(message.type);
+}
+
+function isInOpenedGroup(message, groups, messagesUI) {
+ return !message.groupId
+ || (
+ !isGroupClosed(message.groupId, messagesUI)
+ && !hasClosedParentGroup(groups.get(message.groupId), messagesUI)
+ );
+}
+
+function hasClosedParentGroup(group, messagesUI) {
+ return group.some(groupId => isGroupClosed(groupId, messagesUI));
+}
+
+function isGroupClosed(groupId, messagesUI) {
+ return messagesUI.includes(groupId) === false;
+}
+
+function matchLevelFilters(message, filters) {
+ return filters.get(message.level) === true;
+}
+
+function matchNetworkFilters(message, filters) {
+ return (
+ message.source !== MESSAGE_SOURCE.NETWORK
+ || (filters.get("net") === true && message.isXHR === false)
+ || (filters.get("netxhr") === true && message.isXHR === true)
+ );
+}
+
+function matchSearchFilters(message, filters) {
+ let text = filters.text || "";
+ return (
+ text === ""
+ // @TODO currently we return true for any object grip. We should find a way to
+ // search object grips.
+ || (message.parameters !== null && !Array.isArray(message.parameters))
+ // Look for a match in location.
+ || isTextInFrame(text, message.frame)
+ // Look for a match in stacktrace.
+ || (
+ Array.isArray(message.stacktrace) &&
+ message.stacktrace.some(frame => isTextInFrame(text,
+ // isTextInFrame expect the properties of the frame object to be in the same
+ // order they are rendered in the Frame component.
+ {
+ functionName: frame.functionName ||
+ l10n.getStr("stacktrace.anonymousFunction"),
+ filename: frame.filename,
+ lineNumber: frame.lineNumber,
+ columnNumber: frame.columnNumber
+ }))
+ )
+ // Look for a match in messageText.
+ || (message.messageText !== null
+ && message.messageText.toLocaleLowerCase().includes(text.toLocaleLowerCase()))
+ // Look for a match in parameters. Currently only checks value grips.
+ || (message.parameters !== null
+ && message.parameters.join("").toLocaleLowerCase()
+ .includes(text.toLocaleLowerCase()))
+ );
+}
+
+function isTextInFrame(text, frame) {
+ if (!frame) {
+ return false;
+ }
+ // @TODO Change this to Object.values once it's supported in Node's version of V8
+ return Object.keys(frame)
+ .map(key => frame[key])
+ .join(":")
+ .toLocaleLowerCase()
+ .includes(text.toLocaleLowerCase());
+}
+
+function prune(messages, logLimit) {
+ let messageCount = messages.count();
+ if (messageCount > logLimit) {
+ // If the second non-pruned message is in a group,
+ // we want to return the group as the first non-pruned message.
+ let firstIndex = messages.size - logLimit;
+ let groupId = messages.get(firstIndex + 1).groupId;
+
+ if (groupId) {
+ return messages.splice(0, firstIndex + 1)
+ .unshift(
+ messages.findLast((message) => message.id === groupId)
+ );
+ }
+ return messages.splice(0, firstIndex);
+ }
+
+ return messages;
+}
+
+exports.getAllMessages = getAllMessages;
+exports.getAllMessagesUiById = getAllMessagesUiById;
+exports.getAllMessagesTableDataById = getAllMessagesTableDataById;
+exports.getAllGroupsById = getAllGroupsById;
+exports.getCurrentGroup = getCurrentGroup;
diff --git a/devtools/client/webconsole/new-console-output/selectors/moz.build b/devtools/client/webconsole/new-console-output/selectors/moz.build
new file mode 100644
index 000000000..547f53542
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/selectors/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(
+ 'filters.js',
+ 'messages.js',
+ 'prefs.js',
+ 'ui.js',
+)
diff --git a/devtools/client/webconsole/new-console-output/selectors/prefs.js b/devtools/client/webconsole/new-console-output/selectors/prefs.js
new file mode 100644
index 000000000..18d8b678c
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/selectors/prefs.js
@@ -0,0 +1,12 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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";
+
+function getLogLimit(state) {
+ return state.prefs.logLimit;
+}
+
+exports.getLogLimit = getLogLimit;
diff --git a/devtools/client/webconsole/new-console-output/selectors/ui.js b/devtools/client/webconsole/new-console-output/selectors/ui.js
new file mode 100644
index 000000000..c9729e92d
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/selectors/ui.js
@@ -0,0 +1,20 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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";
+
+function getAllUi(state) {
+ return state.ui;
+}
+
+function getScrollSetting(state) {
+ return getAllUi(state).autoscroll;
+}
+
+module.exports = {
+ getAllUi,
+ getScrollSetting,
+};
diff --git a/devtools/client/webconsole/new-console-output/store.js b/devtools/client/webconsole/new-console-output/store.js
new file mode 100644
index 000000000..8ad7947e9
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/store.js
@@ -0,0 +1,74 @@
+/* This Source Code Form is subject to the terms of 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 {FilterState} = require("devtools/client/webconsole/new-console-output/reducers/filters");
+const {PrefState} = require("devtools/client/webconsole/new-console-output/reducers/prefs");
+const {UiState} = require("devtools/client/webconsole/new-console-output/reducers/ui");
+const {
+ applyMiddleware,
+ combineReducers,
+ compose,
+ createStore
+} = require("devtools/client/shared/vendor/redux");
+const { thunk } = require("devtools/client/shared/redux/middleware/thunk");
+const {
+ BATCH_ACTIONS,
+ PREFS,
+} = require("devtools/client/webconsole/new-console-output/constants");
+const { reducers } = require("./reducers/index");
+const Services = require("Services");
+
+function configureStore() {
+ const initialState = {
+ prefs: new PrefState({
+ logLimit: Math.max(Services.prefs.getIntPref("devtools.hud.loglimit"), 1),
+ }),
+ filters: new FilterState({
+ error: Services.prefs.getBoolPref(PREFS.FILTER.ERROR),
+ warn: Services.prefs.getBoolPref(PREFS.FILTER.WARN),
+ info: Services.prefs.getBoolPref(PREFS.FILTER.INFO),
+ log: Services.prefs.getBoolPref(PREFS.FILTER.LOG),
+ net: Services.prefs.getBoolPref(PREFS.FILTER.NET),
+ netxhr: Services.prefs.getBoolPref(PREFS.FILTER.NETXHR),
+ }),
+ ui: new UiState({
+ filterBarVisible: Services.prefs.getBoolPref(PREFS.UI.FILTER_BAR),
+ })
+ };
+
+ return createStore(
+ combineReducers(reducers),
+ initialState,
+ compose(applyMiddleware(thunk), enableBatching())
+ );
+}
+
+/**
+ * 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);
+ };
+}
+
+// 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/new-console-output/test/.eslintrc.js b/devtools/client/webconsole/new-console-output/test/.eslintrc.js
new file mode 100644
index 000000000..e010df386
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ "extends": ["../../../../.eslintrc.xpcshell.js"]
+};
diff --git a/devtools/client/webconsole/new-console-output/test/chrome/chrome.ini b/devtools/client/webconsole/new-console-output/test/chrome/chrome.ini
new file mode 100644
index 000000000..0543ae5c6
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/chrome/chrome.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+
+support-files =
+ head.js
+
+[test_render_perf.html]
+skip-if = true # Bug 1306783
diff --git a/devtools/client/webconsole/new-console-output/test/chrome/head.js b/devtools/client/webconsole/new-console-output/test/chrome/head.js
new file mode 100644
index 000000000..e8a5fd22e
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/chrome/head.js
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var { utils: Cu } = Components;
+
+var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var { Assert } = require("resource://testing-common/Assert.jsm");
+var { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
+var { Task } = require("devtools/shared/task");
+
+var { require: browserRequire } = BrowserLoader({
+ baseURI: "resource://devtools/client/webconsole/",
+ window
+});
diff --git a/devtools/client/webconsole/new-console-output/test/chrome/test_render_perf.html b/devtools/client/webconsole/new-console-output/test/chrome/test_render_perf.html
new file mode 100644
index 000000000..d22819a2b
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/chrome/test_render_perf.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+ <meta charset="utf8">
+ <title>Test for getRepeatId()</title>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript;version=1.8" 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;version=1.8">
+const testPackets = [];
+const numMessages = 1000;
+for (let id = 0; id < numMessages; id++) {
+ let message = "Odd text";
+ if (id % 2 === 0) {
+ message = "Even text";
+ }
+ testPackets.push({
+ "from": "server1.conn4.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "foobar",
+ message,
+ 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"
+ }
+ });
+}
+
+function timeit(cb) {
+ // Return a Promise that resolves the number of seconds cb takes.
+ return new Promise(resolve => {
+ let start = performance.now();
+ cb();
+ let elapsed = performance.now() - start;
+ resolve(elapsed / 1000);
+ });
+}
+
+window.onload = Task.async(function* () {
+ const { configureStore } = browserRequire("devtools/client/webconsole/new-console-output/store");
+ const { filterTextSet, filtersClear } = browserRequire("devtools/client/webconsole/new-console-output/actions/index");
+ const NewConsoleOutputWrapper = browserRequire("devtools/client/webconsole/new-console-output/new-console-output-wrapper");
+ const wrapper = new NewConsoleOutputWrapper(document.querySelector("#output"), {});
+
+ const store = configureStore();
+
+ let time = yield timeit(() => {
+ testPackets.forEach((message) => {
+ wrapper.dispatchMessageAdd(message);
+ });
+ });
+ info("took " + time + " seconds to render messages");
+
+ time = yield timeit(() => {
+ store.dispatch(filterTextSet("Odd text"));
+ });
+ info("took " + time + " seconds to search filter half the messages");
+
+ time = yield timeit(() => {
+ store.dispatch(filtersClear());
+ });
+ info("took " + time + " seconds to clear the filter");
+
+ ok(true, "Yay, it didn't time out!");
+
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/devtools/client/webconsole/new-console-output/test/components/console-api-call.test.js b/devtools/client/webconsole/new-console-output/test/components/console-api-call.test.js
new file mode 100644
index 000000000..3b4e2b196
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/components/console-api-call.test.js
@@ -0,0 +1,230 @@
+/* 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("devtools/client/shared/vendor/react");
+const Provider = createFactory(require("react-redux").Provider);
+const { setupStore } = require("devtools/client/webconsole/new-console-output/test/helpers");
+
+// Components under test.
+const ConsoleApiCall = createFactory(require("devtools/client/webconsole/new-console-output/components/message-types/console-api-call"));
+const {
+ MESSAGE_OPEN,
+ MESSAGE_CLOSE,
+} = require("devtools/client/webconsole/new-console-output/constants");
+const { INDENT_WIDTH } = require("devtools/client/webconsole/new-console-output/components/message-indent");
+
+// Test fakes.
+const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
+const serviceContainer = require("devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer");
+
+const tempfilePath = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.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);
+ expect(wrapper.find("div.message.cm-s-mozilla span span.message-flex-body span.message-body.devtools-monospace").length).toBe(1);
+
+ // There should be the location
+ const locationLink = wrapper.find(`.message-location`);
+ expect(locationLink.length).toBe(1);
+ expect(locationLink.text()).toBe("test-tempfile.js:1:27");
+ });
+
+ 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`);
+ // Forbidden styles are not applied.
+ expect(secondElementStyle.background).toBe(undefined);
+ });
+
+ it("renders repeat node", () => {
+ const message =
+ stubPreparedMessages.get("console.log('foobar', 'test')")
+ .set("repeat", 107);
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-repeats").text()).toBe("107");
+ expect(wrapper.find(".message-repeats").prop("title")).toBe("107 repeats");
+
+ expect(wrapper.find("span > span.message-flex-body > span.message-body.devtools-monospace + span.message-repeats").length).toBe(1);
+ });
+
+ it("has the expected indent", () => {
+ const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+
+ const indent = 10;
+ let wrapper = render(ConsoleApiCall({ message, serviceContainer, indent }));
+ expect(wrapper.find(".indent").prop("style").width)
+ .toBe(`${indent * INDENT_WIDTH}px`);
+
+ wrapper = render(ConsoleApiCall({ message, serviceContainer}));
+ expect(wrapper.find(".indent").prop("style").width).toBe(`0`);
+ });
+ });
+
+ describe("console.count", () => {
+ it("renders", () => {
+ const message = stubPreparedMessages.get("console.count('bar')");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text()).toBe("bar: 1");
+ });
+ });
+
+ describe("console.assert", () => {
+ it("renders", () => {
+ const message = stubPreparedMessages.get("console.assert(false, {message: 'foobar'})");
+ const wrapper = render(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("");
+ });
+ });
+
+ 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$/);
+ });
+ });
+
+ describe("console.trace", () => {
+ it("renders", () => {
+ const message = stubPreparedMessages.get("console.trace()");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer, open: true }));
+ const filepath = `${tempfilePath}`;
+
+ expect(wrapper.find(".message-body").text()).toBe("console.trace()");
+
+ const frameLinks = wrapper.find(`.stack-trace span.frame-link[data-url='${filepath}']`);
+ 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);
+ });
+ });
+
+ 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(message.messageText);
+ expect(wrapper.find(".theme-twisty.open").length).toBe(1);
+ });
+
+ 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(".theme-twisty.open").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(".theme-twisty").simulate("click");
+ call = store.dispatch.getCall(1);
+ expect(call.args[0]).toEqual({
+ id: message.id,
+ type: MESSAGE_OPEN
+ });
+ });
+ });
+
+ 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(message.messageText);
+ expect(wrapper.find(".theme-twisty:not(.open)").length).toBe(1);
+ });
+ });
+
+ describe("console.dirxml", () => {
+ it("renders", () => {
+ const message = stubPreparedMessages.get("console.dirxml(window)");
+ const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text())
+ .toBe("Window http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html");
+ });
+ });
+});
diff --git a/devtools/client/webconsole/new-console-output/test/components/evaluation-result.test.js b/devtools/client/webconsole/new-console-output/test/components/evaluation-result.test.js
new file mode 100644
index 000000000..4d7890807
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/components/evaluation-result.test.js
@@ -0,0 +1,84 @@
+/* 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("devtools/client/shared/vendor/react");
+const Provider = createFactory(require("react-redux").Provider);
+const { setupStore } = require("devtools/client/webconsole/new-console-output/test/helpers");
+
+// Components under test.
+const EvaluationResult = createFactory(require("devtools/client/webconsole/new-console-output/components/message-types/evaluation-result"));
+const { INDENT_WIDTH } = require("devtools/client/webconsole/new-console-output/components/message-indent");
+
+// Test fakes.
+const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
+const serviceContainer = require("devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer");
+
+describe("EvaluationResult component:", () => {
+ it("renders a grip result", () => {
+ const message = stubPreparedMessages.get("new Date(0)");
+ const wrapper = render(EvaluationResult({ message }));
+
+ expect(wrapper.find(".message-body").text()).toBe("Date 1970-01-01T00:00:00.000Z");
+
+ expect(wrapper.find(".message.log").length).toBe(1);
+ });
+
+ it("renders an error", () => {
+ const message = stubPreparedMessages.get("asdf()");
+ const wrapper = render(EvaluationResult({ message }));
+
+ expect(wrapper.find(".message-body").text())
+ .toBe("ReferenceError: asdf is not defined[Learn More]");
+
+ expect(wrapper.find(".message.error").length).toBe(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})
+ ));
+
+ 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");
+ let 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;
+ let wrapper = render(EvaluationResult({ message, indent}));
+ expect(wrapper.find(".indent").prop("style").width)
+ .toBe(`${indent * INDENT_WIDTH}px`);
+
+ wrapper = render(EvaluationResult({ message}));
+ expect(wrapper.find(".indent").prop("style").width).toBe(`0`);
+ });
+
+ it("has location information", () => {
+ const message = stubPreparedMessages.get("1 + @");
+ const wrapper = render(EvaluationResult({ message }));
+
+ const locationLink = wrapper.find(`.message-location`);
+ expect(locationLink.length).toBe(1);
+ expect(locationLink.text()).toBe("debugger eval code:1:4");
+ });
+});
diff --git a/devtools/client/webconsole/new-console-output/test/components/filter-bar.test.js b/devtools/client/webconsole/new-console-output/test/components/filter-bar.test.js
new file mode 100644
index 000000000..23f958cd9
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/components/filter-bar.test.js
@@ -0,0 +1,96 @@
+/* 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 } = require("enzyme");
+
+const { createFactory } = require("devtools/client/shared/vendor/react");
+const Provider = createFactory(require("react-redux").Provider);
+
+const FilterButton = createFactory(require("devtools/client/webconsole/new-console-output/components/filter-button"));
+const FilterBar = createFactory(require("devtools/client/webconsole/new-console-output/components/filter-bar"));
+const { getAllUi } = require("devtools/client/webconsole/new-console-output/selectors/ui");
+const {
+ MESSAGES_CLEAR,
+ MESSAGE_LEVEL
+} = require("devtools/client/webconsole/new-console-output/constants");
+
+const { setupStore } = require("devtools/client/webconsole/new-console-output/test/helpers");
+const serviceContainer = require("devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer");
+
+describe("FilterBar component:", () => {
+ it("initial render", () => {
+ const store = setupStore([]);
+
+ const wrapper = render(Provider({store}, FilterBar({ serviceContainer })));
+ const toolbar = wrapper.find(
+ ".devtools-toolbar.webconsole-filterbar-primary"
+ );
+
+ // Clear button
+ expect(toolbar.children().eq(0).attr("class"))
+ .toBe("devtools-button devtools-clear-icon");
+ expect(toolbar.children().eq(0).attr("title")).toBe("Clear output");
+
+ // Filter bar toggle
+ expect(toolbar.children().eq(1).attr("class"))
+ .toBe("devtools-button devtools-filter-icon");
+ expect(toolbar.children().eq(1).attr("title")).toBe("Toggle filter bar");
+
+ // Text filter
+ expect(toolbar.children().eq(2).attr("class")).toBe("devtools-plaininput text-filter");
+ expect(toolbar.children().eq(2).attr("placeholder")).toBe("Filter output");
+ expect(toolbar.children().eq(2).attr("type")).toBe("search");
+ expect(toolbar.children().eq(2).attr("value")).toBe("");
+ });
+
+ it("displays filter bar when button is clicked", () => {
+ const store = setupStore([]);
+
+ expect(getAllUi(store.getState()).filterBarVisible).toBe(false);
+
+ const wrapper = mount(Provider({store}, FilterBar({ serviceContainer })));
+ wrapper.find(".devtools-filter-icon").simulate("click");
+
+ expect(getAllUi(store.getState()).filterBarVisible).toBe(true);
+
+ // Buttons are displayed
+ const buttonProps = {
+ active: true,
+ dispatch: store.dispatch
+ };
+ const logButton = FilterButton(Object.assign({}, buttonProps,
+ { label: "Logs", filterKey: MESSAGE_LEVEL.LOG }));
+ const debugButton = FilterButton(Object.assign({}, buttonProps,
+ { label: "Debug", filterKey: MESSAGE_LEVEL.DEBUG }));
+ const infoButton = FilterButton(Object.assign({}, buttonProps,
+ { label: "Info", filterKey: MESSAGE_LEVEL.INFO }));
+ const warnButton = FilterButton(Object.assign({}, buttonProps,
+ { label: "Warnings", filterKey: MESSAGE_LEVEL.WARN }));
+ const errorButton = FilterButton(Object.assign({}, buttonProps,
+ { label: "Errors", filterKey: MESSAGE_LEVEL.ERROR }));
+ expect(wrapper.contains([errorButton, warnButton, logButton, infoButton, debugButton])).toBe(true);
+ });
+
+ it("fires MESSAGES_CLEAR action when clear button is clicked", () => {
+ const store = setupStore([]);
+ store.dispatch = sinon.spy();
+
+ const wrapper = mount(Provider({store}, FilterBar({ serviceContainer })));
+ 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}, FilterBar({ serviceContainer })));
+ wrapper.find(".devtools-plaininput").simulate("input", { target: { value: "a" } });
+ expect(store.getState().filters.text).toBe("a");
+ });
+});
diff --git a/devtools/client/webconsole/new-console-output/test/components/filter-button.test.js b/devtools/client/webconsole/new-console-output/test/components/filter-button.test.js
new file mode 100644
index 000000000..3774da0b8
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/components/filter-button.test.js
@@ -0,0 +1,34 @@
+/* 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("devtools/client/shared/vendor/react");
+
+const FilterButton = createFactory(require("devtools/client/webconsole/new-console-output/components/filter-button"));
+const { MESSAGE_LEVEL } = require("devtools/client/webconsole/new-console-output/constants");
+
+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.html()).toBe(
+ "<button class=\"menu-filter-button error checked\">Error</button>"
+ );
+ });
+
+ it("displays as inactive when turned off", () => {
+ const inactiveProps = Object.assign({}, props, { active: false });
+ const wrapper = render(FilterButton(inactiveProps));
+ expect(wrapper.html()).toBe(
+ "<button class=\"menu-filter-button error\">Error</button>"
+ );
+ });
+});
diff --git a/devtools/client/webconsole/new-console-output/test/components/message-container.test.js b/devtools/client/webconsole/new-console-output/test/components/message-container.test.js
new file mode 100644
index 000000000..2377af906
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/components/message-container.test.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test utils.
+const expect = require("expect");
+const {
+ renderComponent,
+ shallowRenderComponent
+} = require("devtools/client/webconsole/new-console-output/test/helpers");
+
+// Components under test.
+const { MessageContainer } = require("devtools/client/webconsole/new-console-output/components/message-container");
+const ConsoleApiCall = require("devtools/client/webconsole/new-console-output/components/message-types/console-api-call");
+const EvaluationResult = require("devtools/client/webconsole/new-console-output/components/message-types/evaluation-result");
+const PageError = require("devtools/client/webconsole/new-console-output/components/message-types/page-error");
+
+// Test fakes.
+const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
+const serviceContainer = require("devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer");
+
+describe("MessageContainer component:", () => {
+ it("pipes data to children as expected", () => {
+ const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+ const rendered = renderComponent(MessageContainer, {message, serviceContainer});
+
+ expect(rendered.textContent.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")
+ }
+ ];
+
+ messageTypes.forEach(info => {
+ const { component, message } = info;
+ const rendered = shallowRenderComponent(MessageContainer, {
+ message,
+ serviceContainer,
+ });
+ expect(rendered.type).toBe(component);
+ });
+ });
+});
diff --git a/devtools/client/webconsole/new-console-output/test/components/message-icon.test.js b/devtools/client/webconsole/new-console-output/test/components/message-icon.test.js
new file mode 100644
index 000000000..0244f08cf
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/components/message-icon.test.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const {
+ MESSAGE_LEVEL,
+} = require("devtools/client/webconsole/new-console-output/constants");
+const MessageIcon = require("devtools/client/webconsole/new-console-output/components/message-icon");
+
+const expect = require("expect");
+
+const {
+ renderComponent
+} = require("devtools/client/webconsole/new-console-output/test/helpers");
+
+describe("MessageIcon component:", () => {
+ it("renders icon based on level", () => {
+ const rendered = renderComponent(MessageIcon, { level: MESSAGE_LEVEL.ERROR });
+
+ expect(rendered.classList.contains("icon")).toBe(true);
+ expect(rendered.getAttribute("title")).toBe("Error");
+ });
+});
diff --git a/devtools/client/webconsole/new-console-output/test/components/message-repeat.test.js b/devtools/client/webconsole/new-console-output/test/components/message-repeat.test.js
new file mode 100644
index 000000000..0257a3aad
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/components/message-repeat.test.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const MessageRepeat = require("devtools/client/webconsole/new-console-output/components/message-repeat");
+
+const expect = require("expect");
+
+const {
+ renderComponent
+} = require("devtools/client/webconsole/new-console-output/test/helpers");
+
+describe("MessageRepeat component:", () => {
+ it("renders repeated value correctly", () => {
+ const rendered = renderComponent(MessageRepeat, { repeat: 99 });
+ expect(rendered.classList.contains("message-repeats")).toBe(true);
+ expect(rendered.style.visibility).toBe("visible");
+ expect(rendered.textContent).toBe("99");
+ });
+
+ it("renders an un-repeated value correctly", () => {
+ const rendered = renderComponent(MessageRepeat, { repeat: 1 });
+ expect(rendered.style.visibility).toBe("hidden");
+ });
+});
diff --git a/devtools/client/webconsole/new-console-output/test/components/network-event-message.test.js b/devtools/client/webconsole/new-console-output/test/components/network-event-message.test.js
new file mode 100644
index 000000000..8d0c5307e
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/components/network-event-message.test.js
@@ -0,0 +1,74 @@
+/* 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("devtools/client/shared/vendor/react");
+
+// Components under test.
+const NetworkEventMessage = createFactory(require("devtools/client/webconsole/new-console-output/components/message-types/network-event-message"));
+const { INDENT_WIDTH } = require("devtools/client/webconsole/new-console-output/components/message-indent");
+
+// Test fakes.
+const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
+const serviceContainer = require("devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer");
+
+const EXPECTED_URL = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/inexistent.html";
+
+describe("NetworkEventMessage component:", () => {
+ describe("GET request", () => {
+ it("renders as expected", () => {
+ const message = stubPreparedMessages.get("GET request");
+ const wrapper = render(NetworkEventMessage({ message, serviceContainer }));
+
+ 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("div.message.cm-s-mozilla span.message-body.devtools-monospace").length).toBe(1);
+ });
+
+ it("has the expected indent", () => {
+ const message = stubPreparedMessages.get("GET request");
+
+ const indent = 10;
+ let wrapper = render(NetworkEventMessage({ message, serviceContainer, indent}));
+ expect(wrapper.find(".indent").prop("style").width)
+ .toBe(`${indent * INDENT_WIDTH}px`);
+
+ wrapper = render(NetworkEventMessage({ message, serviceContainer }));
+ expect(wrapper.find(".indent").prop("style").width).toBe(`0`);
+ });
+ });
+
+ describe("XHR GET request", () => {
+ it("renders as expected", () => {
+ const message = stubPreparedMessages.get("XHR GET request");
+ const wrapper = render(NetworkEventMessage({ message, serviceContainer }));
+
+ 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("div.message.cm-s-mozilla span.message-body.devtools-monospace").length).toBe(1);
+ });
+ });
+
+ describe("XHR POST request", () => {
+ it("renders as expected", () => {
+ const message = stubPreparedMessages.get("XHR POST request");
+ const wrapper = render(NetworkEventMessage({ message, serviceContainer }));
+
+ 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("div.message.cm-s-mozilla span.message-body.devtools-monospace").length).toBe(1);
+ });
+ });
+});
diff --git a/devtools/client/webconsole/new-console-output/test/components/page-error.test.js b/devtools/client/webconsole/new-console-output/test/components/page-error.test.js
new file mode 100644
index 000000000..93f3a9ea5
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/components/page-error.test.js
@@ -0,0 +1,126 @@
+/* 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("devtools/client/shared/vendor/react");
+const Provider = createFactory(require("react-redux").Provider);
+const { setupStore } = require("devtools/client/webconsole/new-console-output/test/helpers");
+
+// Components under test.
+const PageError = require("devtools/client/webconsole/new-console-output/components/message-types/page-error");
+const {
+ MESSAGE_OPEN,
+ MESSAGE_CLOSE,
+} = require("devtools/client/webconsole/new-console-output/constants");
+const { INDENT_WIDTH } = require("devtools/client/webconsole/new-console-output/components/message-indent");
+
+// Test fakes.
+const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
+const serviceContainer = require("devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer");
+
+describe("PageError component:", () => {
+ it("renders", () => {
+ const message = stubPreparedMessages.get("ReferenceError: asdf is not defined");
+ const wrapper = render(PageError({ message, serviceContainer }));
+
+ expect(wrapper.find(".message-body").text())
+ .toBe("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 https://github.com/devtools-html/gecko-dev/issues/285
+ expect(locationLink.text()).toBe("test-tempfile.js:3:5");
+ });
+
+ 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})
+ ));
+
+ // 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");
+ let call = serviceContainer.openLink.getCall(0);
+ expect(call.args[0]).toEqual(message.exceptionDocURL);
+ });
+
+ it("has a stacktrace which can be openned", () => {
+ 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(".theme-twisty.open").length).toBe(1);
+
+ // There should be three stacktrace items.
+ const frameLinks = wrapper.find(`.stack-trace span.frame-link`);
+ expect(frameLinks.length).toBe(3);
+ });
+
+ it("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(".theme-twisty.open").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(".theme-twisty").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, serviceContainer, indent}));
+ expect(wrapper.find(".indent").prop("style").width)
+ .toBe(`${indent * INDENT_WIDTH}px`);
+
+ wrapper = render(PageError({ message, serviceContainer}));
+ expect(wrapper.find(".indent").prop("style").width).toBe(`0`);
+ });
+});
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/L10n.js b/devtools/client/webconsole/new-console-output/test/fixtures/L10n.js
new file mode 100644
index 000000000..bb34bb477
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/L10n.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// @TODO Load the actual strings from webconsole.properties instead.
+class L10n {
+ getStr(str) {
+ switch (str) {
+ case "level.error":
+ return "Error";
+ case "consoleCleared":
+ return "Console was cleared.";
+ case "webConsoleXhrIndicator":
+ return "XHR";
+ case "webConsoleMoreInfoLabel":
+ return "Learn More";
+ }
+ return str;
+ }
+
+ getFormatStr(str) {
+ return this.getStr(str);
+ }
+}
+
+module.exports = L10n;
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/LocalizationHelper.js b/devtools/client/webconsole/new-console-output/test/fixtures/LocalizationHelper.js
new file mode 100644
index 000000000..8e6e9428c
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/LocalizationHelper.js
@@ -0,0 +1,10 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const LocalizationHelper = require("devtools/client/webconsole/new-console-output/test/fixtures/L10n");
+
+module.exports = {
+ LocalizationHelper
+};
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/ObjectClient.js b/devtools/client/webconsole/new-console-output/test/fixtures/ObjectClient.js
new file mode 100644
index 000000000..87a058d5c
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/ObjectClient.js
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+class ObjectClient {
+}
+
+module.exports = ObjectClient;
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/PluralForm.js b/devtools/client/webconsole/new-console-output/test/fixtures/PluralForm.js
new file mode 100644
index 000000000..9ab3ad3ec
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/PluralForm.js
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+module.exports = {
+ PluralForm: {
+ get: function (occurence, str) {
+ // @TODO Remove when loading the actual strings from webconsole.properties
+ // is done in the L10n fixture.
+ if (str === "messageRepeats.tooltip2") {
+ return `${occurence} repeats`;
+ }
+
+ return str;
+ }
+ }
+};
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/Services.js b/devtools/client/webconsole/new-console-output/test/fixtures/Services.js
new file mode 100644
index 000000000..61b3d5e13
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/Services.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { PREFS } = require("devtools/client/webconsole/new-console-output/constants");
+
+module.exports = {
+ prefs: {
+ getIntPref: pref => {
+ switch (pref) {
+ case "devtools.hud.loglimit":
+ return 1000;
+ }
+ },
+ getBoolPref: pref => {
+ const falsey = [
+ PREFS.FILTER.NET,
+ PREFS.FILTER.NETXHR,
+ PREFS.UI.FILTER_BAR,
+ ];
+ return !falsey.includes(pref);
+ },
+ setBoolPref: () => {},
+ clearUserPref: () => {},
+ }
+};
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/WebConsoleUtils.js b/devtools/client/webconsole/new-console-output/test/fixtures/WebConsoleUtils.js
new file mode 100644
index 000000000..5ab1c0bb4
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/WebConsoleUtils.js
@@ -0,0 +1,14 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const L10n = require("devtools/client/webconsole/new-console-output/test/fixtures/L10n");
+
+const Utils = {
+ L10n
+};
+
+module.exports = {
+ Utils
+};
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/moz.build b/devtools/client/webconsole/new-console-output/test/fixtures/moz.build
new file mode 100644
index 000000000..ff41d6c80
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/moz.build
@@ -0,0 +1,9 @@
+# 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 += [
+ 'stub-generators',
+ 'stubs'
+]
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer.js b/devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer.js
new file mode 100644
index 000000000..04b15c88b
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/serviceContainer.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+module.exports = {
+ attachRefToHud: () => {},
+ emitNewMessage: () => {},
+ hudProxyClient: {},
+ onViewSourceInDebugger: () => {},
+ openNetworkPanel: () => {},
+ sourceMapService: {
+ subscribe: () => {},
+ },
+ openLink: () => {},
+ createElement: tagName => document.createElement(tagName)
+};
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser.ini b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser.ini
new file mode 100644
index 000000000..9f348544f
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser.ini
@@ -0,0 +1,18 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ !/devtools/client/framework/test/shared-head.js
+ test-console-api.html
+ test-network-event.html
+ test-tempfile.js
+
+[browser_webconsole_update_stubs_console_api.js]
+skip-if=true # This is only used to update stubs. It is not an actual test.
+[browser_webconsole_update_stubs_evaluation_result.js]
+skip-if=true # This is only used to update stubs. It is not an actual test.
+[browser_webconsole_update_stubs_network_event.js]
+skip-if=true # This is only used to update stubs. It is not an actual test.
+[browser_webconsole_update_stubs_page_error.js]
+skip-if=true # This is only used to update stubs. It is not an actual test.
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_console_api.js b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_console_api.js
new file mode 100644
index 000000000..fc859a002
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_console_api.js
@@ -0,0 +1,56 @@
+/* -*- 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/ */
+
+"use strict";
+requestLongerTimeout(2)
+
+Cu.import("resource://gre/modules/osfile.jsm");
+const { consoleApi: snippets } = require("devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js");
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html";
+
+let stubs = {
+ preparedMessages: [],
+ packets: [],
+};
+
+add_task(function* () {
+ for (var [key, {keys, code}] of snippets) {
+ yield OS.File.writeAtomic(TEMP_FILE_PATH, `function triggerPacket() {${code}}`);
+
+ let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole");
+ let {ui} = toolbox.getCurrentPanel().hud;
+
+ ok(ui.jsterm, "jsterm exists");
+ ok(ui.newConsoleOutput, "newConsoleOutput exists");
+
+ let received = new Promise(resolve => {
+ let i = 0;
+ let listener = (type, res) => {
+ stubs.packets.push(formatPacket(keys[i], res));
+ stubs.preparedMessages.push(formatStub(keys[i], res));
+ if(++i === keys.length ){
+ toolbox.target.client.removeListener("consoleAPICall", listener);
+ resolve();
+ }
+ };
+ toolbox.target.client.addListener("consoleAPICall", listener);
+ });
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, key, function(key) {
+ var script = content.document.createElement("script");
+ script.src = "test-tempfile.js?key=" + encodeURIComponent(key);
+ script.onload = function() { content.wrappedJSObject.triggerPacket(); }
+ content.document.body.appendChild(script);
+ });
+
+ yield received;
+
+ yield closeTabAndToolbox();
+ }
+ let filePath = OS.Path.join(`${BASE_PATH}/stubs`, "consoleApi.js");
+ OS.File.writeAtomic(filePath, formatFile(stubs));
+ OS.File.writeAtomic(TEMP_FILE_PATH, "");
+});
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_evaluation_result.js b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_evaluation_result.js
new file mode 100644
index 000000000..507201a24
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_evaluation_result.js
@@ -0,0 +1,32 @@
+/* -*- 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/ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/osfile.jsm");
+const TEST_URI = "data:text/html;charset=utf-8,stub generation";
+
+const { evaluationResult: snippets} = require("devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js");
+
+let stubs = {
+ preparedMessages: [],
+ packets: [],
+};
+
+add_task(function* () {
+ let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole");
+ ok(true, "make the test not fail");
+
+ for (var [code,key] of snippets) {
+ const packet = yield new Promise(resolve => {
+ toolbox.target.activeConsole.evaluateJS(code, resolve);
+ });
+ stubs.packets.push(formatPacket(key, packet));
+ stubs.preparedMessages.push(formatStub(key, packet));
+ }
+
+ let filePath = OS.Path.join(`${BASE_PATH}/stubs`, "evaluationResult.js");
+ OS.File.writeAtomic(filePath, formatFile(stubs));
+});
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_network_event.js b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_network_event.js
new file mode 100644
index 000000000..cc018f634
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_network_event.js
@@ -0,0 +1,47 @@
+/* -*- 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/ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/osfile.jsm");
+const TARGET = "networkEvent";
+const { [TARGET]: snippets } = require("devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js");
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-network-event.html";
+
+let stubs = {
+ preparedMessages: [],
+ packets: [],
+};
+
+add_task(function* () {
+ for (var [key, {keys, code}] of snippets) {
+ OS.File.writeAtomic(TEMP_FILE_PATH, `function triggerPacket() {${code}}`);
+ let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole");
+ let {ui} = toolbox.getCurrentPanel().hud;
+
+ ok(ui.jsterm, "jsterm exists");
+ ok(ui.newConsoleOutput, "newConsoleOutput exists");
+
+ let received = new Promise(resolve => {
+ let i = 0;
+ toolbox.target.client.addListener(TARGET, (type, res) => {
+ stubs.packets.push(formatPacket(keys[i], res));
+ stubs.preparedMessages.push(formatNetworkStub(keys[i], res));
+ if(++i === keys.length ){
+ resolve();
+ }
+ });
+ });
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
+ content.wrappedJSObject.triggerPacket();
+ });
+
+ yield received;
+ }
+ let filePath = OS.Path.join(`${BASE_PATH}/stubs/${TARGET}.js`);
+ OS.File.writeAtomic(filePath, formatFile(stubs));
+ OS.File.writeAtomic(TEMP_FILE_PATH, "");
+});
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_page_error.js b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_page_error.js
new file mode 100644
index 000000000..9323e0031
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_page_error.js
@@ -0,0 +1,48 @@
+/* -*- 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/ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/osfile.jsm");
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html";
+
+const { pageError: snippets} = require("devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js");
+
+let stubs = {
+ preparedMessages: [],
+ packets: [],
+};
+
+add_task(function* () {
+ let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole");
+ ok(true, "make the test not fail");
+
+ for (var [key,code] of snippets) {
+ OS.File.writeAtomic(TEMP_FILE_PATH, `${code}`);
+ let received = new Promise(resolve => {
+ toolbox.target.client.addListener("pageError", function onPacket(e, packet) {
+ toolbox.target.client.removeListener("pageError", onPacket);
+ info("Received page error:" + e + " " + JSON.stringify(packet, null, "\t"));
+
+ let message = prepareMessage(packet, {getNextId: () => 1});
+ stubs.packets.push(formatPacket(message.messageText, packet));
+ stubs.preparedMessages.push(formatStub(message.messageText, packet));
+ resolve();
+ });
+ });
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, key, function(key) {
+ var script = content.document.createElement("script");
+ script.src = "test-tempfile.js?key=" + encodeURIComponent(key);
+ content.document.body.appendChild(script);
+ });
+
+ yield received;
+ }
+
+ let filePath = OS.Path.join(`${BASE_PATH}/stubs`, "pageError.js");
+ OS.File.writeAtomic(filePath, formatFile(stubs));
+ OS.File.writeAtomic(TEMP_FILE_PATH, "");
+});
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/head.js b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/head.js
new file mode 100644
index 000000000..be988b9d8
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/head.js
@@ -0,0 +1,192 @@
+/* -*- 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/ */
+/* import-globals-from ../../../../framework/test/shared-head.js */
+
+"use strict";
+
+// shared-head.js handles imports, constants, and utility functions
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
+ this);
+
+Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.webconsole.new-frontend-enabled");
+});
+
+const { prepareMessage } = require("devtools/client/webconsole/new-console-output/utils/messages");
+const { stubPackets } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index.js");
+
+const BASE_PATH = "../../../../devtools/client/webconsole/new-console-output/test/fixtures";
+const TEMP_FILE_PATH = OS.Path.join(`${BASE_PATH}/stub-generators`, "test-tempfile.js");
+
+let cachedPackets = {};
+
+function getCleanedPacket(key, packet) {
+ if(Object.keys(cachedPackets).includes(key)) {
+ return cachedPackets[key];
+ }
+
+ // Strip escaped characters.
+ let safeKey = key
+ .replace(/\\n/g, "\n")
+ .replace(/\\r/g, "\r")
+ .replace(/\\\"/g, `\"`)
+ .replace(/\\\'/g, `\'`);
+
+ // If the stub already exist, we want to ignore irrelevant properties
+ // (actor, timeStamp, timer, ...) that might changed and "pollute"
+ // the diff resulting from this stub generation.
+ let res;
+ if(stubPackets.has(safeKey)) {
+
+ let existingPacket = stubPackets.get(safeKey);
+ res = Object.assign({}, packet, {
+ from: existingPacket.from
+ });
+
+ // Clean root timestamp.
+ if(res.timestamp) {
+ res.timestamp = existingPacket.timestamp;
+ }
+
+ if (res.message) {
+ // Clean timeStamp on the message prop.
+ res.message.timeStamp = existingPacket.message.timeStamp;
+ if (res.message.timer) {
+ // Clean timer properties on the message.
+ // Those properties are found on console.time and console.timeEnd calls,
+ // and those time can vary, which is why we need to clean them.
+ if (res.message.timer.started) {
+ res.message.timer.started = existingPacket.message.timer.started;
+ }
+ if (res.message.timer.duration) {
+ res.message.timer.duration = existingPacket.message.timer.duration;
+ }
+ }
+
+ if(Array.isArray(res.message.arguments)) {
+ // Clean actor ids on each message.arguments item.
+ res.message.arguments.forEach((argument, i) => {
+ if (argument && argument.actor) {
+ argument.actor = existingPacket.message.arguments[i].actor;
+ }
+ });
+ }
+ }
+
+ if (res.result) {
+ // Clean actor ids on evaluation result messages.
+ res.result.actor = existingPacket.result.actor;
+ if (res.result.preview) {
+ if(res.result.preview.timestamp) {
+ // Clean timestamp there too.
+ res.result.preview.timestamp = existingPacket.result.preview.timestamp;
+ }
+ }
+ }
+
+ if (res.exception) {
+ // Clean actor ids on exception messages.
+ res.exception.actor = existingPacket.exception.actor;
+ if (res.exception.preview) {
+ if(res.exception.preview.timestamp) {
+ // Clean timestamp there too.
+ res.exception.preview.timestamp = existingPacket.exception.preview.timestamp;
+ }
+ }
+ }
+
+ if (res.eventActor) {
+ // Clean actor ids, timeStamp and startedDateTime on network messages.
+ res.eventActor.actor = existingPacket.eventActor.actor;
+ res.eventActor.startedDateTime = existingPacket.eventActor.startedDateTime;
+ res.eventActor.timeStamp = existingPacket.eventActor.timeStamp;
+ }
+
+ if (res.pageError) {
+ // Clean timeStamp on pageError messages.
+ res.pageError.timeStamp = existingPacket.pageError.timeStamp;
+ }
+
+ } else {
+ res = packet;
+ }
+
+ cachedPackets[key] = res;
+ return res;
+}
+
+function formatPacket(key, packet) {
+ return `
+stubPackets.set("${key}", ${JSON.stringify(getCleanedPacket(key, packet), null, "\t")});
+`;
+}
+
+function formatStub(key, packet) {
+ let prepared = prepareMessage(
+ getCleanedPacket(key, packet),
+ {getNextId: () => "1"}
+ );
+
+ return `
+stubPreparedMessages.set("${key}", new ConsoleMessage(${JSON.stringify(prepared, null, "\t")}));
+`;
+}
+
+function formatNetworkStub(key, packet) {
+ let actor = packet.eventActor;
+ let networkInfo = {
+ _type: "NetworkEvent",
+ timeStamp: actor.timeStamp,
+ node: null,
+ actor: actor.actor,
+ discardRequestBody: true,
+ discardResponseBody: true,
+ startedDateTime: actor.startedDateTime,
+ request: {
+ url: actor.url,
+ method: actor.method,
+ },
+ isXHR: actor.isXHR,
+ cause: actor.cause,
+ response: {},
+ timings: {},
+ // track the list of network event updates
+ updates: [],
+ private: actor.private,
+ fromCache: actor.fromCache,
+ fromServiceWorker: actor.fromServiceWorker
+ };
+ let prepared = prepareMessage(networkInfo, {getNextId: () => "1"});
+ return `
+stubPreparedMessages.set("${key}", new NetworkEventMessage(${JSON.stringify(prepared, null, "\t")}));
+`;
+}
+
+function formatFile(stubs) {
+ return `/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/*
+ * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN TESTS IN FIXTURES/ TO UPDATE.
+ */
+
+const { ConsoleMessage, NetworkEventMessage } = require("devtools/client/webconsole/new-console-output/types");
+
+let stubPreparedMessages = new Map();
+let stubPackets = new Map();
+
+${stubs.preparedMessages.join("")}
+${stubs.packets.join("")}
+
+module.exports = {
+ stubPreparedMessages,
+ stubPackets,
+}`;
+}
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/moz.build b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/moz.build
new file mode 100644
index 000000000..4b4e8a1d8
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/moz.build
@@ -0,0 +1,8 @@
+# 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(
+ 'stub-snippets.js',
+)
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js
new file mode 100644
index 000000000..f79548e7b
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js
@@ -0,0 +1,148 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var {DebuggerServer} = require("devtools/server/main");
+var longString = (new Array(DebuggerServer.LONG_STRING_LENGTH + 4)).join("a");
+var initialString = longString.substring(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH);
+
+// Console API
+
+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('hello \\nfrom \\rthe \\\"string world!')",
+ "console.log('\xFA\u1E47\u0129\xE7\xF6d\xEA \u021B\u0115\u0219\u0165')",
+ "console.dirxml(window)",
+];
+
+let consoleApi = new Map(consoleApiCommands.map(
+ cmd => [cmd, {keys: [cmd], code: cmd}]));
+
+consoleApi.set("console.trace()", {
+ keys: ["console.trace()"],
+ code: `
+function testStacktraceFiltering() {
+ console.trace()
+}
+function foo() {
+ testStacktraceFiltering()
+}
+
+foo()
+`});
+
+consoleApi.set("console.time('bar')", {
+ keys: ["console.time('bar')", "console.timeEnd('bar')"],
+ code: `
+console.time("bar");
+console.timeEnd("bar");
+`});
+
+consoleApi.set("console.table('bar')", {
+ keys: ["console.table('bar')"],
+ code: `
+console.table('bar');
+`});
+
+consoleApi.set("console.table(['a', 'b', 'c'])", {
+ keys: ["console.table(['a', 'b', 'c'])"],
+ code: `
+console.table(['a', 'b', 'c']);
+`});
+
+consoleApi.set("console.group('bar')", {
+ keys: ["console.group('bar')", "console.groupEnd('bar')"],
+ code: `
+console.group("bar");
+console.groupEnd("bar");
+`});
+
+consoleApi.set("console.groupCollapsed('foo')", {
+ keys: ["console.groupCollapsed('foo')", "console.groupEnd('foo')"],
+ code: `
+console.groupCollapsed("foo");
+console.groupEnd("foo");
+`});
+
+consoleApi.set("console.group()", {
+ keys: ["console.group()", "console.groupEnd()"],
+ code: `
+console.group();
+console.groupEnd();
+`});
+
+consoleApi.set("console.log(%cfoobar)", {
+ keys: ["console.log(%cfoobar)"],
+ code: `
+console.log(
+ "%cfoo%cbar",
+ "color:blue;font-size:1.3em;background:url('http://example.com/test');position:absolute;top:10px",
+ "color:red;background:\\165rl('http://example.com/test')");
+`});
+
+// Evaluation Result
+const evaluationResultCommands = [
+ "new Date(0)",
+ "asdf()",
+ "1 + @"
+];
+
+let evaluationResult = new Map(evaluationResultCommands.map(cmd => [cmd, cmd]));
+
+// Network Event
+
+let networkEvent = new Map();
+
+networkEvent.set("GET request", {
+ keys: ["GET request"],
+ code: `
+let i = document.createElement("img");
+i.src = "inexistent.html";
+`});
+
+networkEvent.set("XHR GET request", {
+ keys: ["XHR GET request"],
+ code: `
+const xhr = new XMLHttpRequest();
+xhr.open("GET", "inexistent.html");
+xhr.send();
+`});
+
+networkEvent.set("XHR POST request", {
+ keys: ["XHR POST request"],
+ code: `
+const xhr = new XMLHttpRequest();
+xhr.open("POST", "inexistent.html");
+xhr.send();
+`});
+
+// Page Error
+
+let pageError = new Map();
+
+pageError.set("Reference Error", `
+ function bar() {
+ asdf()
+ }
+ function foo() {
+ bar()
+ }
+
+ foo()
+`);
+
+module.exports = {
+ consoleApi,
+ evaluationResult,
+ networkEvent,
+ pageError,
+};
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html
new file mode 100644
index 000000000..3246cff15
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Stub generator</title>
+ </head>
+ <body>
+ <p>Stub generator</p>
+ <script src="test-tempfile.js"></script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-network-event.html b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-network-event.html
new file mode 100644
index 000000000..c234acea6
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-network-event.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Stub generator for network event</title>
+ </head>
+ <body>
+ <p>Stub generator for network event</p>
+ <script src="test-tempfile.js"></script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stubs/consoleApi.js b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/consoleApi.js
new file mode 100644
index 000000000..26e95fe39
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/consoleApi.js
@@ -0,0 +1,1482 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/*
+ * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN TESTS IN FIXTURES/ TO UPDATE.
+ */
+
+const { ConsoleMessage, NetworkEventMessage } = require("devtools/client/webconsole/new-console-output/types");
+
+let stubPreparedMessages = new Map();
+let stubPackets = new Map();
+
+
+stubPreparedMessages.set("console.log('foobar', 'test')", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "log",
+ "level": "log",
+ "messageText": null,
+ "parameters": [
+ "foobar",
+ "test"
+ ],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"foobar\",\"test\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27foobar%27%2C%20%27test%27)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27foobar%27%2C%20%27test%27)",
+ "line": 1,
+ "column": 27
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.log(undefined)", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "log",
+ "level": "log",
+ "messageText": null,
+ "parameters": [
+ {
+ "type": "undefined"
+ }
+ ],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[{\"type\":\"undefined\"}],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(undefined)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(undefined)",
+ "line": 1,
+ "column": 27
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.warn('danger, will robinson!')", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "warn",
+ "level": "warn",
+ "messageText": null,
+ "parameters": [
+ "danger, will robinson!"
+ ],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"warn\",\"level\":\"warn\",\"messageText\":null,\"parameters\":[\"danger, will robinson!\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.warn(%27danger%2C%20will%20robinson!%27)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.warn(%27danger%2C%20will%20robinson!%27)",
+ "line": 1,
+ "column": 27
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.log(NaN)", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "log",
+ "level": "log",
+ "messageText": null,
+ "parameters": [
+ {
+ "type": "NaN"
+ }
+ ],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[{\"type\":\"NaN\"}],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(NaN)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(NaN)",
+ "line": 1,
+ "column": 27
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.log(null)", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "log",
+ "level": "log",
+ "messageText": null,
+ "parameters": [
+ {
+ "type": "null"
+ }
+ ],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[{\"type\":\"null\"}],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(null)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(null)",
+ "line": 1,
+ "column": 27
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.log('鼬')", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "log",
+ "level": "log",
+ "messageText": null,
+ "parameters": [
+ "鼬"
+ ],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"鼬\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27%E9%BC%AC%27)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27%E9%BC%AC%27)",
+ "line": 1,
+ "column": 27
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.clear()", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "clear",
+ "level": "log",
+ "messageText": null,
+ "parameters": [
+ "Console was cleared."
+ ],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"clear\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"Console was cleared.\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.clear()\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.clear()",
+ "line": 1,
+ "column": 27
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.count('bar')", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "log",
+ "level": "debug",
+ "messageText": "bar: 1",
+ "parameters": null,
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"debug\",\"messageText\":\"bar: 1\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.count(%27bar%27)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.count(%27bar%27)",
+ "line": 1,
+ "column": 27
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.assert(false, {message: 'foobar'})", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "assert",
+ "level": "error",
+ "messageText": null,
+ "parameters": [
+ {
+ "type": "object",
+ "actor": "server1.conn8.child1/obj31",
+ "class": "Object",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "message": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "foobar"
+ }
+ },
+ "ownPropertiesLength": 1,
+ "safeGetterValues": {}
+ }
+ }
+ ],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"assert\",\"level\":\"error\",\"messageText\":null,\"parameters\":[{\"type\":\"object\",\"actor\":\"server1.conn8.child1/obj31\",\"class\":\"Object\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":1,\"preview\":{\"kind\":\"Object\",\"ownProperties\":{\"message\":{\"configurable\":true,\"enumerable\":true,\"writable\":true,\"value\":\"foobar\"}},\"ownPropertiesLength\":1,\"safeGetterValues\":{}}}],\"repeatId\":null,\"stacktrace\":[{\"columnNumber\":27,\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.assert(false%2C%20%7Bmessage%3A%20%27foobar%27%7D)\",\"functionName\":\"triggerPacket\",\"language\":2,\"lineNumber\":1}],\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.assert(false%2C%20%7Bmessage%3A%20%27foobar%27%7D)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": [
+ {
+ "columnNumber": 27,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.assert(false%2C%20%7Bmessage%3A%20%27foobar%27%7D)",
+ "functionName": "triggerPacket",
+ "language": 2,
+ "lineNumber": 1
+ }
+ ],
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.assert(false%2C%20%7Bmessage%3A%20%27foobar%27%7D)",
+ "line": 1,
+ "column": 27
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.log('hello \nfrom \rthe \"string world!')", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "log",
+ "level": "log",
+ "messageText": null,
+ "parameters": [
+ "hello \nfrom \rthe \"string world!"
+ ],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"hello \\nfrom \\rthe \\\"string world!\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27hello%20%5Cnfrom%20%5Crthe%20%5C%22string%20world!%27)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27hello%20%5Cnfrom%20%5Crthe%20%5C%22string%20world!%27)",
+ "line": 1,
+ "column": 27
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.log('úṇĩçödê țĕșť')", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "log",
+ "level": "log",
+ "messageText": null,
+ "parameters": [
+ "úṇĩçödê țĕșť"
+ ],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"úṇĩçödê țĕșť\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27%C3%BA%E1%B9%87%C4%A9%C3%A7%C3%B6d%C3%AA%20%C8%9B%C4%95%C8%99%C5%A5%27)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27%C3%BA%E1%B9%87%C4%A9%C3%A7%C3%B6d%C3%AA%20%C8%9B%C4%95%C8%99%C5%A5%27)",
+ "line": 1,
+ "column": 27
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.dirxml(window)", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "log",
+ "level": "log",
+ "messageText": null,
+ "parameters": [
+ {
+ "type": "object",
+ "actor": "server1.conn11.child1/obj31",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 804,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html"
+ }
+ }
+ ],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[{\"type\":\"object\",\"actor\":\"server1.conn11.child1/obj31\",\"class\":\"Window\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":804,\"preview\":{\"kind\":\"ObjectWithURL\",\"url\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\"}}],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.dirxml(window)\",\"line\":1,\"column\":27},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.dirxml(window)",
+ "line": 1,
+ "column": 27
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.trace()", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "trace",
+ "level": "log",
+ "messageText": null,
+ "parameters": [],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"trace\",\"level\":\"log\",\"messageText\":null,\"parameters\":[],\"repeatId\":null,\"stacktrace\":[{\"columnNumber\":3,\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()\",\"functionName\":\"testStacktraceFiltering\",\"language\":2,\"lineNumber\":3},{\"columnNumber\":3,\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()\",\"functionName\":\"foo\",\"language\":2,\"lineNumber\":6},{\"columnNumber\":1,\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()\",\"functionName\":\"triggerPacket\",\"language\":2,\"lineNumber\":9}],\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()\",\"line\":3,\"column\":3},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": [
+ {
+ "columnNumber": 3,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()",
+ "functionName": "testStacktraceFiltering",
+ "language": 2,
+ "lineNumber": 3
+ },
+ {
+ "columnNumber": 3,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()",
+ "functionName": "foo",
+ "language": 2,
+ "lineNumber": 6
+ },
+ {
+ "columnNumber": 1,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()",
+ "functionName": "triggerPacket",
+ "language": 2,
+ "lineNumber": 9
+ }
+ ],
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()",
+ "line": 3,
+ "column": 3
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.time('bar')", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "nullMessage",
+ "level": "log",
+ "messageText": null,
+ "parameters": null,
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"nullMessage\",\"level\":\"log\",\"messageText\":null,\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.time(%27bar%27)\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.time(%27bar%27)",
+ "line": 2,
+ "column": 1
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.timeEnd('bar')", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "timeEnd",
+ "level": "log",
+ "messageText": "bar: 1.36ms",
+ "parameters": null,
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"timeEnd\",\"level\":\"log\",\"messageText\":\"bar: 1.36ms\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.time(%27bar%27)\",\"line\":3,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.time(%27bar%27)",
+ "line": 3,
+ "column": 1
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.table('bar')", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "log",
+ "level": "log",
+ "messageText": null,
+ "parameters": [
+ "bar"
+ ],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"bar\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.table(%27bar%27)\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.table(%27bar%27)",
+ "line": 2,
+ "column": 1
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.table(['a', 'b', 'c'])", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "table",
+ "level": "log",
+ "messageText": null,
+ "parameters": [
+ {
+ "type": "object",
+ "actor": "server1.conn15.child1/obj31",
+ "class": "Array",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 3,
+ "items": [
+ "a",
+ "b",
+ "c"
+ ]
+ }
+ }
+ ],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"table\",\"level\":\"log\",\"messageText\":null,\"parameters\":[{\"type\":\"object\",\"actor\":\"server1.conn15.child1/obj31\",\"class\":\"Array\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":4,\"preview\":{\"kind\":\"ArrayLike\",\"length\":3,\"items\":[\"a\",\"b\",\"c\"]}}],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.table(%5B%27a%27%2C%20%27b%27%2C%20%27c%27%5D)\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.table(%5B%27a%27%2C%20%27b%27%2C%20%27c%27%5D)",
+ "line": 2,
+ "column": 1
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.group('bar')", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "startGroup",
+ "level": "log",
+ "messageText": "bar",
+ "parameters": null,
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"startGroup\",\"level\":\"log\",\"messageText\":\"bar\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group(%27bar%27)\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group(%27bar%27)",
+ "line": 2,
+ "column": 1
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.groupEnd('bar')", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "endGroup",
+ "level": "log",
+ "messageText": null,
+ "parameters": null,
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"endGroup\",\"level\":\"log\",\"messageText\":null,\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group(%27bar%27)\",\"line\":3,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group(%27bar%27)",
+ "line": 3,
+ "column": 1
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.groupCollapsed('foo')", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "startGroupCollapsed",
+ "level": "log",
+ "messageText": "foo",
+ "parameters": null,
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"startGroupCollapsed\",\"level\":\"log\",\"messageText\":\"foo\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.groupCollapsed(%27foo%27)\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.groupCollapsed(%27foo%27)",
+ "line": 2,
+ "column": 1
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.groupEnd('foo')", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "endGroup",
+ "level": "log",
+ "messageText": null,
+ "parameters": null,
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"endGroup\",\"level\":\"log\",\"messageText\":null,\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.groupCollapsed(%27foo%27)\",\"line\":3,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.groupCollapsed(%27foo%27)",
+ "line": 3,
+ "column": 1
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.group()", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "startGroup",
+ "level": "log",
+ "messageText": "<no group label>",
+ "parameters": null,
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"startGroup\",\"level\":\"log\",\"messageText\":\"<no group label>\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group()\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group()",
+ "line": 2,
+ "column": 1
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.groupEnd()", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "endGroup",
+ "level": "log",
+ "messageText": null,
+ "parameters": null,
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"endGroup\",\"level\":\"log\",\"messageText\":null,\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group()\",\"line\":3,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group()",
+ "line": 3,
+ "column": 1
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": []
+}));
+
+stubPreparedMessages.set("console.log(%cfoobar)", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "console-api",
+ "type": "log",
+ "level": "log",
+ "messageText": null,
+ "parameters": [
+ "foo",
+ "bar"
+ ],
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"foo\",\"bar\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%25cfoobar)\",\"line\":2,\"column\":1},\"groupId\":null,\"exceptionDocURL\":null,\"userProvidedStyles\":[\"color:blue;font-size:1.3em;background:url('http://example.com/test');position:absolute;top:10px\",\"color:red;background:url('http://example.com/test')\"]}",
+ "stacktrace": null,
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%25cfoobar)",
+ "line": 2,
+ "column": 1
+ },
+ "groupId": null,
+ "exceptionDocURL": null,
+ "userProvidedStyles": [
+ "color:blue;font-size:1.3em;background:url('http://example.com/test');position:absolute;top:10px",
+ "color:red;background:url('http://example.com/test')"
+ ]
+}));
+
+
+stubPackets.set("console.log('foobar', 'test')", {
+ "from": "server1.conn0.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "foobar",
+ "test"
+ ],
+ "columnNumber": 27,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27foobar%27%2C%20%27test%27)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "log",
+ "lineNumber": 1,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "styles": [],
+ "timeStamp": 1477086261590,
+ "timer": null,
+ "workerType": "none",
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.log(undefined)", {
+ "from": "server1.conn1.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ {
+ "type": "undefined"
+ }
+ ],
+ "columnNumber": 27,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(undefined)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "log",
+ "lineNumber": 1,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "styles": [],
+ "timeStamp": 1477086264886,
+ "timer": null,
+ "workerType": "none",
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.warn('danger, will robinson!')", {
+ "from": "server1.conn2.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "danger, will robinson!"
+ ],
+ "columnNumber": 27,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.warn(%27danger%2C%20will%20robinson!%27)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "warn",
+ "lineNumber": 1,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "styles": [],
+ "timeStamp": 1477086267284,
+ "timer": null,
+ "workerType": "none",
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.log(NaN)", {
+ "from": "server1.conn3.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ {
+ "type": "NaN"
+ }
+ ],
+ "columnNumber": 27,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(NaN)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "log",
+ "lineNumber": 1,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "styles": [],
+ "timeStamp": 1477086269484,
+ "timer": null,
+ "workerType": "none",
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.log(null)", {
+ "from": "server1.conn4.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ {
+ "type": "null"
+ }
+ ],
+ "columnNumber": 27,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(null)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "log",
+ "lineNumber": 1,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "styles": [],
+ "timeStamp": 1477086271418,
+ "timer": null,
+ "workerType": "none",
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.log('鼬')", {
+ "from": "server1.conn5.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "鼬"
+ ],
+ "columnNumber": 27,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27%E9%BC%AC%27)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "log",
+ "lineNumber": 1,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "styles": [],
+ "timeStamp": 1477086273549,
+ "timer": null,
+ "workerType": "none",
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.clear()", {
+ "from": "server1.conn6.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [],
+ "columnNumber": 27,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.clear()",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "clear",
+ "lineNumber": 1,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "timeStamp": 1477086275587,
+ "timer": null,
+ "workerType": "none",
+ "styles": [],
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.count('bar')", {
+ "from": "server1.conn7.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "bar"
+ ],
+ "columnNumber": 27,
+ "counter": {
+ "count": 1,
+ "label": "bar"
+ },
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.count(%27bar%27)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "count",
+ "lineNumber": 1,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "timeStamp": 1477086277812,
+ "timer": null,
+ "workerType": "none",
+ "styles": [],
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.assert(false, {message: 'foobar'})", {
+ "from": "server1.conn8.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ {
+ "type": "object",
+ "actor": "server1.conn8.child1/obj31",
+ "class": "Object",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "message": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "foobar"
+ }
+ },
+ "ownPropertiesLength": 1,
+ "safeGetterValues": {}
+ }
+ }
+ ],
+ "columnNumber": 27,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.assert(false%2C%20%7Bmessage%3A%20%27foobar%27%7D)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "assert",
+ "lineNumber": 1,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "styles": [],
+ "timeStamp": 1477086280131,
+ "timer": null,
+ "stacktrace": [
+ {
+ "columnNumber": 27,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.assert(false%2C%20%7Bmessage%3A%20%27foobar%27%7D)",
+ "functionName": "triggerPacket",
+ "language": 2,
+ "lineNumber": 1
+ }
+ ],
+ "workerType": "none",
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.log('hello \nfrom \rthe \"string world!')", {
+ "from": "server1.conn9.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "hello \nfrom \rthe \"string world!"
+ ],
+ "columnNumber": 27,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27hello%20%5Cnfrom%20%5Crthe%20%5C%22string%20world!%27)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "log",
+ "lineNumber": 1,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "styles": [],
+ "timeStamp": 1477086281936,
+ "timer": null,
+ "workerType": "none",
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.log('úṇĩçödê țĕșť')", {
+ "from": "server1.conn10.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "úṇĩçödê țĕșť"
+ ],
+ "columnNumber": 27,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%27%C3%BA%E1%B9%87%C4%A9%C3%A7%C3%B6d%C3%AA%20%C8%9B%C4%95%C8%99%C5%A5%27)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "log",
+ "lineNumber": 1,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "styles": [],
+ "timeStamp": 1477086283713,
+ "timer": null,
+ "workerType": "none",
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.dirxml(window)", {
+ "from": "server1.conn11.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ {
+ "type": "object",
+ "actor": "server1.conn11.child1/obj31",
+ "class": "Window",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 804,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html"
+ }
+ }
+ ],
+ "columnNumber": 27,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.dirxml(window)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "dirxml",
+ "lineNumber": 1,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "timeStamp": 1477086285483,
+ "timer": null,
+ "workerType": "none",
+ "styles": [],
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.trace()", {
+ "from": "server1.conn12.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [],
+ "columnNumber": 3,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()",
+ "functionName": "testStacktraceFiltering",
+ "groupName": "",
+ "level": "trace",
+ "lineNumber": 3,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "timeStamp": 1477086287286,
+ "timer": null,
+ "stacktrace": [
+ {
+ "columnNumber": 3,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()",
+ "functionName": "testStacktraceFiltering",
+ "language": 2,
+ "lineNumber": 3
+ },
+ {
+ "columnNumber": 3,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()",
+ "functionName": "foo",
+ "language": 2,
+ "lineNumber": 6
+ },
+ {
+ "columnNumber": 1,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()",
+ "functionName": "triggerPacket",
+ "language": 2,
+ "lineNumber": 9
+ }
+ ],
+ "workerType": "none",
+ "styles": [],
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.time('bar')", {
+ "from": "server1.conn13.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "bar"
+ ],
+ "columnNumber": 1,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.time(%27bar%27)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "time",
+ "lineNumber": 2,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "timeStamp": 1477086289137,
+ "timer": {
+ "name": "bar",
+ "started": 1166.305
+ },
+ "workerType": "none",
+ "styles": [],
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.timeEnd('bar')", {
+ "from": "server1.conn13.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "bar"
+ ],
+ "columnNumber": 1,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.time(%27bar%27)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "timeEnd",
+ "lineNumber": 3,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "timeStamp": 1477086289138,
+ "timer": {
+ "duration": 1.3550000000000182,
+ "name": "bar"
+ },
+ "workerType": "none",
+ "styles": [],
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.table('bar')", {
+ "from": "server1.conn14.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "bar"
+ ],
+ "columnNumber": 1,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.table(%27bar%27)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "table",
+ "lineNumber": 2,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "timeStamp": 1477086290984,
+ "timer": null,
+ "workerType": "none",
+ "styles": [],
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.table(['a', 'b', 'c'])", {
+ "from": "server1.conn15.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ {
+ "type": "object",
+ "actor": "server1.conn15.child1/obj31",
+ "class": "Array",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 3,
+ "items": [
+ "a",
+ "b",
+ "c"
+ ]
+ }
+ }
+ ],
+ "columnNumber": 1,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.table(%5B%27a%27%2C%20%27b%27%2C%20%27c%27%5D)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "table",
+ "lineNumber": 2,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "timeStamp": 1477086292762,
+ "timer": null,
+ "workerType": "none",
+ "styles": [],
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.group('bar')", {
+ "from": "server1.conn16.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "bar"
+ ],
+ "columnNumber": 1,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group(%27bar%27)",
+ "functionName": "triggerPacket",
+ "groupName": "bar",
+ "level": "group",
+ "lineNumber": 2,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "timeStamp": 1477086294628,
+ "timer": null,
+ "workerType": "none",
+ "styles": [],
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.groupEnd('bar')", {
+ "from": "server1.conn16.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "bar"
+ ],
+ "columnNumber": 1,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group(%27bar%27)",
+ "functionName": "triggerPacket",
+ "groupName": "bar",
+ "level": "groupEnd",
+ "lineNumber": 3,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "timeStamp": 1477086294630,
+ "timer": null,
+ "workerType": "none",
+ "styles": [],
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.groupCollapsed('foo')", {
+ "from": "server1.conn17.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "foo"
+ ],
+ "columnNumber": 1,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.groupCollapsed(%27foo%27)",
+ "functionName": "triggerPacket",
+ "groupName": "foo",
+ "level": "groupCollapsed",
+ "lineNumber": 2,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "timeStamp": 1477086296567,
+ "timer": null,
+ "workerType": "none",
+ "styles": [],
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.groupEnd('foo')", {
+ "from": "server1.conn17.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "foo"
+ ],
+ "columnNumber": 1,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.groupCollapsed(%27foo%27)",
+ "functionName": "triggerPacket",
+ "groupName": "foo",
+ "level": "groupEnd",
+ "lineNumber": 3,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "timeStamp": 1477086296570,
+ "timer": null,
+ "workerType": "none",
+ "styles": [],
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.group()", {
+ "from": "server1.conn18.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [],
+ "columnNumber": 1,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group()",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "group",
+ "lineNumber": 2,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "timeStamp": 1477086298462,
+ "timer": null,
+ "workerType": "none",
+ "styles": [],
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.groupEnd()", {
+ "from": "server1.conn18.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [],
+ "columnNumber": 1,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.group()",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "groupEnd",
+ "lineNumber": 3,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "timeStamp": 1477086298464,
+ "timer": null,
+ "workerType": "none",
+ "styles": [],
+ "category": "webdev"
+ }
+});
+
+stubPackets.set("console.log(%cfoobar)", {
+ "from": "server1.conn19.child1/consoleActor2",
+ "type": "consoleAPICall",
+ "message": {
+ "arguments": [
+ "foo",
+ "bar"
+ ],
+ "columnNumber": 1,
+ "counter": null,
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.log(%25cfoobar)",
+ "functionName": "triggerPacket",
+ "groupName": "",
+ "level": "log",
+ "lineNumber": 2,
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "firstPartyDomain": "",
+ "inIsolatedMozBrowser": false,
+ "privateBrowsingId": 0,
+ "userContextId": 0
+ },
+ "private": false,
+ "styles": [
+ "color:blue;font-size:1.3em;background:url('http://example.com/test');position:absolute;top:10px",
+ "color:red;background:url('http://example.com/test')"
+ ],
+ "timeStamp": 1477086300265,
+ "timer": null,
+ "workerType": "none",
+ "category": "webdev"
+ }
+});
+
+
+module.exports = {
+ stubPreparedMessages,
+ stubPackets,
+} \ No newline at end of file
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stubs/evaluationResult.js b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/evaluationResult.js
new file mode 100644
index 000000000..098086044
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/evaluationResult.js
@@ -0,0 +1,182 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/*
+ * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN TESTS IN FIXTURES/ TO UPDATE.
+ */
+
+const { ConsoleMessage, NetworkEventMessage } = require("devtools/client/webconsole/new-console-output/types");
+
+let stubPreparedMessages = new Map();
+let stubPackets = new Map();
+
+
+stubPreparedMessages.set("new Date(0)", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "javascript",
+ "type": "result",
+ "level": "log",
+ "parameters": {
+ "type": "object",
+ "actor": "server1.conn0.child1/obj30",
+ "class": "Date",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "timestamp": 0
+ }
+ },
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"javascript\",\"type\":\"result\",\"level\":\"log\",\"parameters\":{\"type\":\"object\",\"actor\":\"server1.conn0.child1/obj30\",\"class\":\"Date\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":0,\"preview\":{\"timestamp\":0}},\"repeatId\":null,\"stacktrace\":null,\"frame\":null,\"groupId\":null,\"userProvidedStyles\":null}",
+ "stacktrace": null,
+ "frame": null,
+ "groupId": null,
+ "userProvidedStyles": null
+}));
+
+stubPreparedMessages.set("asdf()", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "javascript",
+ "type": "result",
+ "level": "error",
+ "messageText": "ReferenceError: asdf is not defined",
+ "parameters": {
+ "type": "undefined"
+ },
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"javascript\",\"type\":\"result\",\"level\":\"error\",\"messageText\":\"ReferenceError: asdf is not defined\",\"parameters\":{\"type\":\"undefined\"},\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"debugger eval code\",\"line\":1,\"column\":1},\"groupId\":null,\"exceptionDocURL\":\"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default\",\"userProvidedStyles\":null}",
+ "stacktrace": null,
+ "frame": {
+ "source": "debugger eval code",
+ "line": 1,
+ "column": 1
+ },
+ "groupId": null,
+ "exceptionDocURL": "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default",
+ "userProvidedStyles": null
+}));
+
+stubPreparedMessages.set("1 + @", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "javascript",
+ "type": "result",
+ "level": "error",
+ "messageText": "SyntaxError: illegal character",
+ "parameters": {
+ "type": "undefined"
+ },
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"javascript\",\"type\":\"result\",\"level\":\"error\",\"messageText\":\"SyntaxError: illegal character\",\"parameters\":{\"type\":\"undefined\"},\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"debugger eval code\",\"line\":1,\"column\":4},\"groupId\":null,\"userProvidedStyles\":null}",
+ "stacktrace": null,
+ "frame": {
+ "source": "debugger eval code",
+ "line": 1,
+ "column": 4
+ },
+ "groupId": null,
+ "userProvidedStyles": null
+}));
+
+
+stubPackets.set("new Date(0)", {
+ "from": "server1.conn0.child1/consoleActor2",
+ "input": "new Date(0)",
+ "result": {
+ "type": "object",
+ "actor": "server1.conn0.child1/obj30",
+ "class": "Date",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "timestamp": 0
+ }
+ },
+ "timestamp": 1476573073424,
+ "exception": null,
+ "frame": null,
+ "helperResult": null
+});
+
+stubPackets.set("asdf()", {
+ "from": "server1.conn0.child1/consoleActor2",
+ "input": "asdf()",
+ "result": {
+ "type": "undefined"
+ },
+ "timestamp": 1476573073442,
+ "exception": {
+ "type": "object",
+ "actor": "server1.conn0.child1/obj32",
+ "class": "Error",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "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
+ }
+ },
+ "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",
+ "frame": {
+ "source": "debugger eval code",
+ "line": 1,
+ "column": 1
+ },
+ "helperResult": null
+});
+
+stubPackets.set("1 + @", {
+ "from": "server1.conn0.child1/consoleActor2",
+ "input": "1 + @",
+ "result": {
+ "type": "undefined"
+ },
+ "timestamp": 1478755616654,
+ "exception": {
+ "type": "object",
+ "actor": "server1.conn0.child1/obj33",
+ "class": "Error",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "Error",
+ "name": "SyntaxError",
+ "message": "illegal character",
+ "stack": "",
+ "fileName": "debugger eval code",
+ "lineNumber": 1,
+ "columnNumber": 4
+ }
+ },
+ "exceptionMessage": "SyntaxError: illegal character",
+ "frame": {
+ "source": "debugger eval code",
+ "line": 1,
+ "column": 4
+ },
+ "helperResult": null
+});
+
+
+module.exports = {
+ stubPreparedMessages,
+ stubPackets,
+} \ No newline at end of file
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stubs/index.js b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/index.js
new file mode 100644
index 000000000..59b420180
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/index.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let maps = [];
+
+[
+ "consoleApi",
+ "evaluationResult",
+ "networkEvent",
+ "pageError",
+].forEach((filename) => {
+ maps[filename] = require(`./${filename}`);
+});
+
+// Combine all the maps into a single map.
+module.exports = {
+ stubPreparedMessages: new Map([
+ ...maps.consoleApi.stubPreparedMessages,
+ ...maps.evaluationResult.stubPreparedMessages,
+ ...maps.networkEvent.stubPreparedMessages,
+ ...maps.pageError.stubPreparedMessages, ]),
+ stubPackets: new Map([
+ ...maps.consoleApi.stubPackets,
+ ...maps.evaluationResult.stubPackets,
+ ...maps.networkEvent.stubPackets,
+ ...maps.pageError.stubPackets, ]),
+};
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stubs/moz.build b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/moz.build
new file mode 100644
index 000000000..88e9c46df
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/moz.build
@@ -0,0 +1,11 @@
+# This Source Code Form is subject to the terms of 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(
+ 'consoleApi.js',
+ 'evaluationResult.js',
+ 'index.js',
+ 'networkEvent.js',
+ 'pageError.js',
+)
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stubs/networkEvent.js b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/networkEvent.js
new file mode 100644
index 000000000..58a40d30b
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/networkEvent.js
@@ -0,0 +1,189 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/*
+ * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN TESTS IN FIXTURES/ TO UPDATE.
+ */
+
+const { ConsoleMessage, NetworkEventMessage } = require("devtools/client/webconsole/new-console-output/types");
+
+let stubPreparedMessages = new Map();
+let stubPackets = new Map();
+
+
+stubPreparedMessages.set("GET request", new NetworkEventMessage({
+ "id": "1",
+ "actor": "server1.conn0.child1/netEvent29",
+ "level": "log",
+ "isXHR": false,
+ "request": {
+ "url": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/inexistent.html",
+ "method": "GET"
+ },
+ "response": {},
+ "source": "network",
+ "type": "log",
+ "groupId": null
+}));
+
+stubPreparedMessages.set("XHR GET request", new NetworkEventMessage({
+ "id": "1",
+ "actor": "server1.conn1.child1/netEvent29",
+ "level": "log",
+ "isXHR": true,
+ "request": {
+ "url": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/inexistent.html",
+ "method": "GET"
+ },
+ "response": {},
+ "source": "network",
+ "type": "log",
+ "groupId": null
+}));
+
+stubPreparedMessages.set("XHR POST request", new NetworkEventMessage({
+ "id": "1",
+ "actor": "server1.conn2.child1/netEvent29",
+ "level": "log",
+ "isXHR": true,
+ "request": {
+ "url": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/inexistent.html",
+ "method": "POST"
+ },
+ "response": {},
+ "source": "network",
+ "type": "log",
+ "groupId": null
+}));
+
+
+stubPackets.set("GET request", {
+ "from": "server1.conn0.child1/consoleActor2",
+ "type": "networkEvent",
+ "eventActor": {
+ "actor": "server1.conn0.child1/netEvent29",
+ "startedDateTime": "2016-10-15T23:12:04.196Z",
+ "timeStamp": 1476573124196,
+ "url": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/inexistent.html",
+ "method": "GET",
+ "isXHR": false,
+ "cause": {
+ "type": 3,
+ "loadingDocumentUri": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-network-event.html",
+ "stacktrace": [
+ {
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js",
+ "lineNumber": 3,
+ "columnNumber": 1,
+ "functionName": "triggerPacket",
+ "asyncCause": null
+ },
+ {
+ "filename": "chrome://mochikit/content/tests/BrowserTestUtils/content-task.js line 52 > eval",
+ "lineNumber": 4,
+ "columnNumber": 7,
+ "functionName": null,
+ "asyncCause": null
+ },
+ {
+ "filename": "chrome://mochikit/content/tests/BrowserTestUtils/content-task.js",
+ "lineNumber": 53,
+ "columnNumber": 20,
+ "functionName": null,
+ "asyncCause": null
+ }
+ ]
+ },
+ "private": false
+ }
+});
+
+stubPackets.set("XHR GET request", {
+ "from": "server1.conn1.child1/consoleActor2",
+ "type": "networkEvent",
+ "eventActor": {
+ "actor": "server1.conn1.child1/netEvent29",
+ "startedDateTime": "2016-10-15T23:12:05.690Z",
+ "timeStamp": 1476573125690,
+ "url": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/inexistent.html",
+ "method": "GET",
+ "isXHR": true,
+ "cause": {
+ "type": 11,
+ "loadingDocumentUri": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-network-event.html",
+ "stacktrace": [
+ {
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js",
+ "lineNumber": 4,
+ "columnNumber": 1,
+ "functionName": "triggerPacket",
+ "asyncCause": null
+ },
+ {
+ "filename": "chrome://mochikit/content/tests/BrowserTestUtils/content-task.js line 52 > eval",
+ "lineNumber": 4,
+ "columnNumber": 7,
+ "functionName": null,
+ "asyncCause": null
+ },
+ {
+ "filename": "chrome://mochikit/content/tests/BrowserTestUtils/content-task.js",
+ "lineNumber": 53,
+ "columnNumber": 20,
+ "functionName": null,
+ "asyncCause": null
+ }
+ ]
+ },
+ "private": false
+ }
+});
+
+stubPackets.set("XHR POST request", {
+ "from": "server1.conn2.child1/consoleActor2",
+ "type": "networkEvent",
+ "eventActor": {
+ "actor": "server1.conn2.child1/netEvent29",
+ "startedDateTime": "2016-10-15T23:12:07.158Z",
+ "timeStamp": 1476573127158,
+ "url": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/inexistent.html",
+ "method": "POST",
+ "isXHR": true,
+ "cause": {
+ "type": 11,
+ "loadingDocumentUri": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-network-event.html",
+ "stacktrace": [
+ {
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js",
+ "lineNumber": 4,
+ "columnNumber": 1,
+ "functionName": "triggerPacket",
+ "asyncCause": null
+ },
+ {
+ "filename": "chrome://mochikit/content/tests/BrowserTestUtils/content-task.js line 52 > eval",
+ "lineNumber": 4,
+ "columnNumber": 7,
+ "functionName": null,
+ "asyncCause": null
+ },
+ {
+ "filename": "chrome://mochikit/content/tests/BrowserTestUtils/content-task.js",
+ "lineNumber": 53,
+ "columnNumber": 20,
+ "functionName": null,
+ "asyncCause": null
+ }
+ ]
+ },
+ "private": false
+ }
+});
+
+
+module.exports = {
+ stubPreparedMessages,
+ stubPackets,
+} \ No newline at end of file
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stubs/pageError.js b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/pageError.js
new file mode 100644
index 000000000..eda8e8b83
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/pageError.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/*
+ * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN TESTS IN FIXTURES/ TO UPDATE.
+ */
+
+const { ConsoleMessage, NetworkEventMessage } = require("devtools/client/webconsole/new-console-output/types");
+
+let stubPreparedMessages = new Map();
+let stubPackets = new Map();
+
+
+stubPreparedMessages.set("ReferenceError: asdf is not defined", new ConsoleMessage({
+ "id": "1",
+ "allowRepeating": true,
+ "source": "javascript",
+ "type": "log",
+ "level": "error",
+ "messageText": "ReferenceError: asdf is not defined",
+ "parameters": null,
+ "repeat": 1,
+ "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"javascript\",\"type\":\"log\",\"level\":\"error\",\"messageText\":\"ReferenceError: asdf is not defined\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":[{\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error\",\"lineNumber\":3,\"columnNumber\":5,\"functionName\":\"bar\"},{\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error\",\"lineNumber\":6,\"columnNumber\":5,\"functionName\":\"foo\"},{\"filename\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error\",\"lineNumber\":9,\"columnNumber\":3,\"functionName\":null}],\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error\",\"line\":3,\"column\":5},\"groupId\":null,\"exceptionDocURL\":\"https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default\"}",
+ "stacktrace": [
+ {
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error",
+ "lineNumber": 3,
+ "columnNumber": 5,
+ "functionName": "bar"
+ },
+ {
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error",
+ "lineNumber": 6,
+ "columnNumber": 5,
+ "functionName": "foo"
+ },
+ {
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error",
+ "lineNumber": 9,
+ "columnNumber": 3,
+ "functionName": null
+ }
+ ],
+ "frame": {
+ "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error",
+ "line": 3,
+ "column": 5
+ },
+ "groupId": null,
+ "exceptionDocURL": "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default"
+}));
+
+
+stubPackets.set("ReferenceError: asdf is not defined", {
+ "from": "server1.conn0.child1/consoleActor2",
+ "type": "pageError",
+ "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": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error",
+ "lineText": "",
+ "lineNumber": 3,
+ "columnNumber": 5,
+ "category": "content javascript",
+ "timeStamp": 1476573167137,
+ "warning": false,
+ "error": false,
+ "exception": true,
+ "strict": false,
+ "info": false,
+ "private": false,
+ "stacktrace": [
+ {
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error",
+ "lineNumber": 3,
+ "columnNumber": 5,
+ "functionName": "bar"
+ },
+ {
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error",
+ "lineNumber": 6,
+ "columnNumber": 5,
+ "functionName": "foo"
+ },
+ {
+ "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=Reference%20Error",
+ "lineNumber": 9,
+ "columnNumber": 3,
+ "functionName": null
+ }
+ ]
+ }
+});
+
+
+module.exports = {
+ stubPreparedMessages,
+ stubPackets,
+} \ No newline at end of file
diff --git a/devtools/client/webconsole/new-console-output/test/helpers.js b/devtools/client/webconsole/new-console-output/test/helpers.js
new file mode 100644
index 000000000..39807eaed
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/helpers.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let ReactDOM = require("devtools/client/shared/vendor/react-dom");
+let React = require("devtools/client/shared/vendor/react");
+var TestUtils = React.addons.TestUtils;
+
+const actions = require("devtools/client/webconsole/new-console-output/actions/index");
+const { configureStore } = require("devtools/client/webconsole/new-console-output/store");
+const { IdGenerator } = require("devtools/client/webconsole/new-console-output/utils/id-generator");
+const { stubPackets } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
+
+/**
+ * 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({}, actions);
+
+ const idGenerator = new IdGenerator();
+ wrappedActions.messageAdd = (packet) => {
+ return actions.messageAdd(packet, idGenerator);
+ };
+
+ return wrappedActions;
+}
+
+/**
+ * Prepare the store for use in testing.
+ */
+function setupStore(input) {
+ const store = configureStore();
+
+ // Add the messages from the input commands to the store.
+ input.forEach((cmd) => {
+ store.dispatch(actions.messageAdd(stubPackets.get(cmd)));
+ });
+
+ return store;
+}
+
+function renderComponent(component, props) {
+ const el = React.createElement(component, props, {});
+ // By default, renderIntoDocument() won't work for stateless components, but
+ // it will work if the stateless component is wrapped in a stateful one.
+ // See https://github.com/facebook/react/issues/4839
+ const wrappedEl = React.DOM.span({}, [el]);
+ const renderedComponent = TestUtils.renderIntoDocument(wrappedEl);
+ return ReactDOM.findDOMNode(renderedComponent).children[0];
+}
+
+function shallowRenderComponent(component, props) {
+ const el = React.createElement(component, props);
+ const renderer = TestUtils.createRenderer();
+ renderer.render(el, {});
+ return renderer.getRenderOutput();
+}
+
+module.exports = {
+ setupActions,
+ setupStore,
+ renderComponent,
+ shallowRenderComponent
+};
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini b/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
new file mode 100644
index 000000000..9881d0559
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
@@ -0,0 +1,21 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ test-batching.html
+ test-console.html
+ test-console-filters.html
+ test-console-group.html
+ test-console-table.html
+ !/devtools/client/framework/test/shared-head.js
+
+[browser_webconsole_batching.js]
+[browser_webconsole_console_group.js]
+[browser_webconsole_console_table.js]
+[browser_webconsole_filters.js]
+[browser_webconsole_init.js]
+[browser_webconsole_input_focus.js]
+[browser_webconsole_keyboard_accessibility.js]
+[browser_webconsole_observer_notifications.js]
+[browser_webconsole_vview_close_on_esc_key.js]
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_batching.js b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_batching.js
new file mode 100644
index 000000000..0bfdccc3c
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_batching.js
@@ -0,0 +1,51 @@
+/* -*- 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/ */
+
+"use strict";
+
+// Check adding console calls as batch keep the order of the message.
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/mochitest/test-batching.html";
+const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages");
+
+add_task(function* () {
+ let hud = yield openNewTabAndConsole(TEST_URI);
+ const messageNumber = 100;
+ yield testSimpleBatchLogging(hud, messageNumber);
+ yield testBatchLoggingAndClear(hud, messageNumber);
+});
+
+function* testSimpleBatchLogging(hud, messageNumber) {
+ yield ContentTask.spawn(gBrowser.selectedBrowser, messageNumber,
+ function (numMessages) {
+ content.wrappedJSObject.batchLog(numMessages);
+ }
+ );
+
+ for (let i = 0; i < messageNumber; i++) {
+ let node = yield waitFor(() => findMessageAtIndex(hud, i, i));
+ is(node.textContent, i.toString(), `message at index "${i}" is the expected one`);
+ }
+}
+
+function* testBatchLoggingAndClear(hud, messageNumber) {
+ yield ContentTask.spawn(gBrowser.selectedBrowser, messageNumber,
+ function (numMessages) {
+ content.wrappedJSObject.batchLogAndClear(numMessages);
+ }
+ );
+ yield waitFor(() => findMessage(hud, l10n.getStr("consoleCleared")));
+ ok(true, "console cleared message is displayed");
+
+ // Passing the text argument as an empty string will returns all the message,
+ // whatever their content is.
+ const messages = findMessages(hud, "");
+ is(messages.length, 1, "console was cleared as expected");
+}
+
+function findMessageAtIndex(hud, text, index) {
+ const selector = `.message:nth-of-type(${index + 1}) .message-body`;
+ return findMessage(hud, text, selector);
+}
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_group.js b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_group.js
new file mode 100644
index 000000000..94de78f13
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_group.js
@@ -0,0 +1,91 @@
+/* -*- 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/ */
+
+"use strict";
+
+// Check console.group, console.groupCollapsed and console.groupEnd calls
+// behave as expected.
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/mochitest/test-console-group.html";
+const { INDENT_WIDTH } = require("devtools/client/webconsole/new-console-output/components/message-indent");
+
+add_task(function* () {
+ let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole");
+ let hud = toolbox.getCurrentPanel().hud;
+
+ const store = hud.ui.newConsoleOutput.getStore();
+ // Adding loggin each time the store is modified in order to check
+ // the store state in case of failure.
+ store.subscribe(() => {
+ const messages = store.getState().messages.messagesById.toJS()
+ .map(message => {
+ return {
+ id: message.id,
+ type: message.type,
+ parameters: message.parameters,
+ messageText: message.messageText
+ };
+ }
+ );
+ info("messages : " + JSON.stringify(messages));
+ });
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function () {
+ content.wrappedJSObject.doLog();
+ });
+
+ info("Test a group at root level");
+ let node = yield waitFor(() => findMessage(hud, "group-1"));
+ testClass(node, "startGroup");
+ testIndent(node, 0);
+
+ info("Test a message in a 1 level deep group");
+ node = yield waitFor(() => findMessage(hud, "log-1"));
+ testClass(node, "log");
+ testIndent(node, 1);
+
+ info("Test a group in a 1 level deep group");
+ node = yield waitFor(() => findMessage(hud, "group-2"));
+ testClass(node, "startGroup");
+ testIndent(node, 1);
+
+ info("Test a message in a 2 level deep group");
+ node = yield waitFor(() => findMessage(hud, "log-2"));
+ testClass(node, "log");
+ testIndent(node, 2);
+
+ info("Test a message in a 1 level deep group, after closing a 2 level deep group");
+ node = yield waitFor(() => findMessage(hud, "log-3"));
+ testClass(node, "log");
+ testIndent(node, 1);
+
+ info("Test a message at root level, after closing all the groups");
+ node = yield waitFor(() => findMessage(hud, "log-4"));
+ testClass(node, "log");
+ testIndent(node, 0);
+
+ info("Test a collapsed group at root level");
+ node = yield waitFor(() => findMessage(hud, "group-3"));
+ testClass(node, "startGroupCollapsed");
+ testIndent(node, 0);
+
+ info("Test a message at root level, after closing a collapsed group");
+ node = yield waitFor(() => findMessage(hud, "log-6"));
+ testClass(node, "log");
+ testIndent(node, 0);
+
+ let nodes = hud.ui.experimentalOutputNode.querySelectorAll(".message");
+ is(nodes.length, 8, "expected number of messages are displayed");
+});
+
+function testClass(node, className) {
+ ok(node.classList.contains(className), `message has the expected "${className}" class`);
+}
+
+function testIndent(node, indent) {
+ indent = `${indent * INDENT_WIDTH}px`;
+ is(node.querySelector(".indent").style.width, indent,
+ "message has the expected level of indentation");
+}
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_table.js b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_table.js
new file mode 100644
index 000000000..a90ae1af1
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_table.js
@@ -0,0 +1,173 @@
+/* -*- 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/ */
+
+"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/new-console-output/test/mochitest/test-console-table.html";
+
+add_task(function* () {
+ let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole");
+ let hud = toolbox.getCurrentPanel().hud;
+
+ function Person(firstName, lastName) {
+ this.firstName = firstName;
+ this.lastName = lastName;
+ }
+
+ 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 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 restricting the columns displayed",
+ input: [
+ new Person("Sam", "Wright"),
+ new Person("Elena", "Bartz"),
+ ],
+ headers: ["firstName"],
+ expected: {
+ columns: ["(index)", "firstName"],
+ rows: [
+ ["0", "Sam"],
+ ["1", "Elena"],
+ ]
+ }
+ }];
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, testCases, function (tests) {
+ tests.forEach((test) => {
+ content.wrappedJSObject.doConsoleTable(test.input, test.headers);
+ });
+ });
+
+ let nodes = [];
+ for (let testCase of testCases) {
+ let node = yield waitFor(
+ () => findConsoleTable(hud.ui.experimentalOutputNode, testCases.indexOf(testCase))
+ );
+ nodes.push(node);
+ }
+
+ let consoleTableNodes = hud.ui.experimentalOutputNode.querySelectorAll(
+ ".message .new-consoletable");
+
+ is(consoleTableNodes.length, testCases.length,
+ "console has the expected number of consoleTable items");
+
+ testCases.forEach((testCase, index) => {
+ info(testCase.info);
+
+ let node = nodes[index];
+ let columns = Array.from(node.querySelectorAll("thead th"));
+ let rows = Array.from(node.querySelectorAll("tbody tr"));
+
+ is(
+ JSON.stringify(testCase.expected.columns),
+ JSON.stringify(columns.map(column => column.textContent)),
+ "table has the expected columns"
+ );
+
+ is(testCase.expected.rows.length, rows.length,
+ "table has the expected number of rows");
+
+ testCase.expected.rows.forEach((expectedRow, rowIndex) => {
+ let row = rows[rowIndex];
+ let cells = row.querySelectorAll("td");
+ is(expectedRow.length, cells.length, "row has the expected number of cells");
+
+ expectedRow.forEach((expectedCell, cellIndex) => {
+ let cell = cells[cellIndex];
+ is(expectedCell, cell.textContent, "cell has the expected content");
+ });
+ });
+ });
+});
+
+function findConsoleTable(node, index) {
+ let condition = node.querySelector(
+ `.message:nth-of-type(${index + 1}) .new-consoletable`);
+ return condition;
+}
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_filters.js b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_filters.js
new file mode 100644
index 000000000..8eb536926
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_filters.js
@@ -0,0 +1,72 @@
+/* -*- 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 filters.
+
+"use strict";
+
+const { MESSAGE_LEVEL } = require("devtools/client/webconsole/new-console-output/constants");
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/mochitest/test-console-filters.html";
+
+add_task(function* () {
+ let hud = yield openNewTabAndConsole(TEST_URI);
+ const outputNode = hud.ui.experimentalOutputNode;
+
+ const toolbar = yield waitFor(() => {
+ return outputNode.querySelector(".webconsole-filterbar-primary");
+ });
+ ok(toolbar, "Toolbar found");
+
+ // Show the filter bar
+ toolbar.querySelector(".devtools-filter-icon").click();
+ const filterBar = yield waitFor(() => {
+ return outputNode.querySelector(".webconsole-filterbar-secondary");
+ });
+ ok(filterBar, "Filter bar is shown when filter icon is clicked.");
+
+ // Check defaults.
+ Object.values(MESSAGE_LEVEL).forEach(level => {
+ ok(filterIsEnabled(filterBar.querySelector(`.${level}`)),
+ `Filter button for ${level} is on by default`);
+ });
+ ["net", "netxhr"].forEach(category => {
+ ok(!filterIsEnabled(filterBar.querySelector(`.${category}`)),
+ `Filter button for ${category} is off by default`);
+ });
+
+ // Check that messages are shown as expected. This depends on cached messages being
+ // shown.
+ ok(findMessages(hud, "").length == 5,
+ "Messages of all levels shown when filters are on.");
+
+ // Check that messages are not shown when their filter is turned off.
+ filterBar.querySelector(".error").click();
+ yield waitFor(() => findMessages(hud, "").length == 4);
+ ok(true, "When a filter is turned off, its messages are not shown.");
+
+ // Check that the ui settings were persisted.
+ yield closeTabAndToolbox();
+ yield testFilterPersistence();
+});
+
+function filterIsEnabled(button) {
+ return button.classList.contains("checked");
+}
+
+function* testFilterPersistence() {
+ let hud = yield openNewTabAndConsole(TEST_URI);
+ const outputNode = hud.ui.experimentalOutputNode;
+ const filterBar = yield 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(".error")),
+ "Filter button setting is persisted");
+ ok(findMessages(hud, "").length == 4,
+ "Messages of all levels shown when filters are on.");
+}
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_init.js b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_init.js
new file mode 100644
index 000000000..4280270dd
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_init.js
@@ -0,0 +1,35 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/mochitest/test-console.html";
+
+add_task(function* () {
+ let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole");
+ let hud = toolbox.getCurrentPanel().hud;
+ let {ui} = hud;
+
+ ok(ui.jsterm, "jsterm exists");
+ ok(ui.newConsoleOutput, "newConsoleOutput exists");
+
+ // @TODO: fix proptype errors
+ let receievedMessages = waitForMessages({
+ hud,
+ messages: [{
+ text: '0',
+ }, {
+ text: '1',
+ }, {
+ text: '2',
+ }],
+ });
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
+ content.wrappedJSObject.doLogs(3);
+ });
+
+ yield receievedMessages;
+});
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_input_focus.js b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_input_focus.js
new file mode 100644
index 000000000..7660df238
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_input_focus.js
@@ -0,0 +1,57 @@
+/* -*- 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 the input field is focused when the console is opened.
+
+"use strict";
+
+const TEST_URI =
+ `data:text/html;charset=utf-8,Test input focused
+ <script>
+ console.log("console message 1");
+ </script>`;
+
+add_task(function* () {
+ let hud = yield openNewTabAndConsole(TEST_URI);
+ hud.jsterm.clearOutput();
+
+ let inputNode = hud.jsterm.inputNode;
+ ok(inputNode.getAttribute("focused"), "input node is focused after output is cleared");
+
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.wrappedJSObject.console.log("console message 2");
+ });
+ let msg = yield waitFor(() => findMessage(hud, "console message 2"));
+ let outputItem = msg.querySelector(".message-body");
+
+ inputNode = hud.jsterm.inputNode;
+ ok(inputNode.getAttribute("focused"), "input node is focused, first");
+
+ yield waitForBlurredInput(inputNode);
+
+ EventUtils.sendMouseEvent({type: "click"}, hud.outputNode);
+ ok(inputNode.getAttribute("focused"), "input node is focused, second time");
+
+ yield waitForBlurredInput(inputNode);
+
+ info("Setting a text selection and making sure a click does not re-focus");
+ let selection = hud.iframeWindow.getSelection();
+ selection.selectAllChildren(outputItem);
+
+ EventUtils.sendMouseEvent({type: "click"}, hud.outputNode);
+ ok(!inputNode.getAttribute("focused"),
+ "input node focused after text is selected");
+});
+
+function waitForBlurredInput(inputNode) {
+ return new Promise(resolve => {
+ let lostFocus = () => {
+ ok(!inputNode.getAttribute("focused"), "input node is not focused");
+ resolve();
+ };
+ inputNode.addEventListener("blur", lostFocus, { once: true });
+ document.getElementById("urlbar").click();
+ });
+}
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_keyboard_accessibility.js b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_keyboard_accessibility.js
new file mode 100644
index 000000000..1038194b9
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_keyboard_accessibility.js
@@ -0,0 +1,71 @@
+/* -*- 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/ */
+
+// Check that basic keyboard shortcuts work in the web console.
+
+"use strict";
+
+const TEST_URI =
+ `data:text/html;charset=utf-8,<p>Test keyboard accessibility</p>
+ <script>
+ for (let i = 1; i <= 100; i++) {
+ console.log("console message " + i);
+ }
+ </script>
+ `;
+
+add_task(function* () {
+ let hud = yield openNewTabAndConsole(TEST_URI);
+ info("Web Console opened");
+
+ const outputScroller = hud.ui.outputScroller;
+
+ yield waitFor(() => findMessages(hud, "").length == 100);
+
+ let currentPosition = outputScroller.scrollTop;
+ const bottom = currentPosition;
+
+ EventUtils.sendMouseEvent({type: "click"}, hud.jsterm.inputNode);
+
+ // Page up.
+ EventUtils.synthesizeKey("VK_PAGE_UP", {});
+ isnot(outputScroller.scrollTop, currentPosition,
+ "scroll position changed after page up");
+
+ // Page down.
+ currentPosition = outputScroller.scrollTop;
+ EventUtils.synthesizeKey("VK_PAGE_DOWN", {});
+ ok(outputScroller.scrollTop > currentPosition,
+ "scroll position now at bottom");
+
+ // Home
+ EventUtils.synthesizeKey("VK_HOME", {});
+ is(outputScroller.scrollTop, 0, "scroll position now at top");
+
+ // End
+ EventUtils.synthesizeKey("VK_END", {});
+ let scrollTop = outputScroller.scrollTop;
+ ok(scrollTop > 0 && Math.abs(scrollTop - bottom) <= 5,
+ "scroll position now at bottom");
+
+ // Clear output
+ info("try ctrl-l to clear output");
+ let clearShortcut;
+ if (Services.appinfo.OS === "Darwin") {
+ clearShortcut = WCUL10n.getStr("webconsole.clear.keyOSX");
+ } else {
+ clearShortcut = WCUL10n.getStr("webconsole.clear.key");
+ }
+ synthesizeKeyShortcut(clearShortcut);
+ yield waitFor(() => findMessages(hud, "").length == 0);
+ is(hud.jsterm.inputNode.getAttribute("focused"), "true", "jsterm input is focused");
+
+ // Focus filter
+ info("try ctrl-f to focus filter");
+ synthesizeKeyShortcut(WCUL10n.getStr("webconsole.find.key"));
+ ok(!hud.jsterm.inputNode.getAttribute("focused"), "jsterm input is not focused");
+ is(hud.ui.filterBox, outputScroller.ownerDocument.activeElement,
+ "filter input is focused");
+});
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_observer_notifications.js b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_observer_notifications.js
new file mode 100644
index 000000000..5225a6ac1
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_observer_notifications.js
@@ -0,0 +1,47 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>Web Console test for " +
+ "obeserver notifications";
+
+let created = false;
+let destroyed = false;
+
+add_task(function* () {
+ setupObserver();
+ yield openNewTabAndConsole(TEST_URI);
+ yield waitFor(() => created);
+
+ yield closeTabAndToolbox(gBrowser.selectedTab);
+ yield waitFor(() => destroyed);
+});
+
+function setupObserver() {
+ const observer = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+ observe: function observe(subject, topic) {
+ subject = subject.QueryInterface(Ci.nsISupportsString);
+
+ switch (topic) {
+ case "web-console-created":
+ ok(HUDService.getHudReferenceById(subject.data), "We have a hud reference");
+ Services.obs.removeObserver(observer, "web-console-created");
+ created = true;
+ break;
+ case "web-console-destroyed":
+ ok(!HUDService.getHudReferenceById(subject.data), "We do not have a hud reference");
+ Services.obs.removeObserver(observer, "web-console-destroyed");
+ destroyed = true;
+ break;
+ }
+ },
+ };
+
+ Services.obs.addObserver(observer, "web-console-created", false);
+ Services.obs.addObserver(observer, "web-console-destroyed", false);
+}
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_vview_close_on_esc_key.js b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_vview_close_on_esc_key.js
new file mode 100644
index 000000000..712a990b4
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_vview_close_on_esc_key.js
@@ -0,0 +1,46 @@
+/* -*- 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/ */
+
+// Check that the variables view sidebar can be closed by pressing Escape in the
+// web console.
+
+"use strict";
+
+const TEST_URI =
+ "data:text/html;charset=utf8,<script>let fooObj = {testProp: 'testValue'}</script>";
+
+add_task(function* () {
+ let hud = yield openNewTabAndConsole(TEST_URI);
+ let jsterm = hud.jsterm;
+ let vview;
+
+ yield openSidebar("fooObj", 'testProp: "testValue"');
+ vview.window.focus();
+
+ let sidebarClosed = jsterm.once("sidebar-closed");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield sidebarClosed;
+
+ function* openSidebar(objName, expectedText) {
+ yield jsterm.execute(objName);
+ info("JSTerm executed");
+
+ let msg = yield waitFor(() => findMessage(hud, "Object"));
+ ok(msg, "Message found");
+
+ let anchor = msg.querySelector("a");
+ let body = msg.querySelector(".message-body");
+ ok(anchor, "object anchor");
+ ok(body, "message body");
+ ok(body.textContent.includes(expectedText), "message text check");
+
+ msg.scrollIntoView();
+ yield EventUtils.synthesizeMouse(anchor, 2, 2, {}, hud.iframeWindow);
+
+ let vviewVar = yield jsterm.once("variablesview-fetched");
+ vview = vviewVar._variablesView;
+ ok(vview, "variables view object exists");
+ }
+});
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/head.js b/devtools/client/webconsole/new-console-output/test/mochitest/head.js
new file mode 100644
index 000000000..b71eaec4f
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/head.js
@@ -0,0 +1,137 @@
+/* -*- 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/ */
+/* import-globals-from ../../../../framework/test/shared-head.js */
+
+"use strict";
+
+// shared-head.js handles imports, constants, and utility functions
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
+ this);
+
+var {Utils: WebConsoleUtils} = require("devtools/client/webconsole/utils");
+const WEBCONSOLE_STRINGS_URI = "devtools/client/locales/webconsole.properties";
+var WCUL10n = new WebConsoleUtils.L10n(WEBCONSOLE_STRINGS_URI);
+
+Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", true);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.webconsole.new-frontend-enabled");
+
+ let browserConsole = HUDService.getBrowserConsole();
+ if (browserConsole) {
+ if (browserConsole.jsterm) {
+ browserConsole.jsterm.clearOutput(true);
+ }
+ yield HUDService.toggleBrowserConsole();
+ }
+});
+
+/**
+ * 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.
+ * @return Promise
+ * Resolves when the tab has been added, loaded and the toolbox has been opened.
+ * Resolves to the toolbox.
+ */
+var openNewTabAndConsole = Task.async(function* (url) {
+ let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole");
+ let hud = toolbox.getCurrentPanel().hud;
+ hud.jsterm._lazyVariablesView = false;
+ return hud;
+});
+
+/**
+ * Wait for messages in the web console output, resolving once they are receieved.
+ *
+ * @param object options
+ * - hud: the webconsole
+ * - messages: Array[Object]. An array of messages to match. Current supported options:
+ * - text: Exact text match in .message-body
+ */
+function waitForMessages({ hud, messages }) {
+ return new Promise(resolve => {
+ let numMatched = 0;
+ let receivedLog = hud.ui.on("new-messages", function messagesReceieved(e, newMessages) {
+ for (let message of messages) {
+ if (message.matched) {
+ continue;
+ }
+
+ for (let newMessage of newMessages) {
+ if (newMessage.node.querySelector(".message-body").textContent == message.text) {
+ numMatched++;
+ message.matched = true;
+ info("Matched a message with text: " + message.text + ", still waiting for " + (messages.length - numMatched) + " messages");
+ break;
+ }
+ }
+
+ if (numMatched === messages.length) {
+ hud.ui.off("new-messages", messagesReceieved);
+ resolve(receivedLog);
+ return;
+ }
+ }
+ });
+ });
+}
+
+/**
+ * Wait for a predicate to return a result.
+ *
+ * @param function condition
+ * Invoked once in a while until it returns a truthy value. This should be an
+ * idempotent function, since we have to run it a second time after it returns
+ * true in order to return the value.
+ * @param string message [optional]
+ * A message to output if the condition failes.
+ * @param number interval [optional]
+ * How often the predicate is invoked, in milliseconds.
+ * @return object
+ * A promise that is resolved with the result of the condition.
+ */
+function* waitFor(condition, message = "waitFor", interval = 10, maxTries = 500) {
+ return new Promise(resolve => {
+ BrowserTestUtils.waitForCondition(condition, message, interval, maxTries)
+ .then(() => resolve(condition()));
+ });
+}
+
+/**
+ * Find a message in the output.
+ *
+ * @param object hud
+ * The web console.
+ * @param string text
+ * A substring that can be found in the message.
+ * @param selector [optional]
+ * The selector to use in finding the message.
+ */
+function findMessage(hud, text, selector = ".message") {
+ const elements = findMessages(hud, text, selector);
+ return elements.pop();
+}
+
+/**
+ * Find multiple messages in the output.
+ *
+ * @param object hud
+ * The web console.
+ * @param string text
+ * A substring that can be found in the message.
+ * @param selector [optional]
+ * The selector to use in finding the message.
+ */
+function findMessages(hud, text, selector = ".message") {
+ const messages = hud.ui.experimentalOutputNode.querySelectorAll(selector);
+ const elements = Array.prototype.filter.call(
+ messages,
+ (el) => el.textContent.includes(text)
+ );
+ return elements;
+}
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/test-batching.html b/devtools/client/webconsole/new-console-output/test/mochitest/test-batching.html
new file mode 100644
index 000000000..9d122387a
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/test-batching.html
@@ -0,0 +1,28 @@
+<!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>
+ "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/new-console-output/test/mochitest/test-console-filters.html b/devtools/client/webconsole/new-console-output/test/mochitest/test-console-filters.html
new file mode 100644
index 000000000..293421549
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/test-console-filters.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Webconsole filters test page</title>
+ </head>
+ <body>
+ <p>Webconsole filters test page</p>
+ <script>
+ 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/new-console-output/test/mochitest/test-console-group.html b/devtools/client/webconsole/new-console-output/test/mochitest/test-console-group.html
new file mode 100644
index 000000000..47373d3b9
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/test-console-group.html
@@ -0,0 +1,28 @@
+<!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>
+ "use strict";
+
+ function doLog() {
+ console.group("group-1");
+ console.log("log-1");
+ console.group("group-2");
+ console.log("log-2");
+ console.groupEnd("group-2");
+ console.log("log-3");
+ console.groupEnd("group-1");
+ console.log("log-4");
+ console.groupCollapsed("group-3");
+ console.log("log-5");
+ console.groupEnd("group-3");
+ console.log("log-6");
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/test-console-table.html b/devtools/client/webconsole/new-console-output/test/mochitest/test-console-table.html
new file mode 100644
index 000000000..b7666e50b
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/test-console-table.html
@@ -0,0 +1,19 @@
+<!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>
+ function doConsoleTable(data, constrainedHeaders = null) {
+ if (constrainedHeaders) {
+ console.table(data, constrainedHeaders);
+ } else {
+ console.table(data);
+ }
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/new-console-output/test/mochitest/test-console.html b/devtools/client/webconsole/new-console-output/test/mochitest/test-console.html
new file mode 100644
index 000000000..7ef09d9a1
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/test-console.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Simple webconsole test page</title>
+ </head>
+ <body>
+ <p>Simple webconsole test page</p>
+ <script>
+ function doLogs(num) {
+ num = num || 1;
+ for (var i = 0; i < num; i++) {
+ console.log(i);
+ }
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/new-console-output/test/moz.build b/devtools/client/webconsole/new-console-output/test/moz.build
new file mode 100644
index 000000000..da06c3162
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/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/.
+
+BROWSER_CHROME_MANIFESTS += [
+ 'fixtures/stub-generators/browser.ini',
+ 'mochitest/browser.ini',
+]
+
+DIRS += [
+ 'fixtures'
+]
+
+MOCHITEST_CHROME_MANIFESTS += [
+ 'chrome/chrome.ini',
+]
diff --git a/devtools/client/webconsole/new-console-output/test/requireHelper.js b/devtools/client/webconsole/new-console-output/test/requireHelper.js
new file mode 100644
index 000000000..ac6205808
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/requireHelper.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const requireHacker = require("require-hacker");
+
+requireHacker.global_hook("default", path => {
+ switch (path) {
+ // For Enzyme
+ case "react-dom/server":
+ return `const React = require('react-dev'); module.exports = React`;
+ case "react-addons-test-utils":
+ return `const React = require('react-dev'); module.exports = React.addons.TestUtils`;
+ // Use react-dev. This would be handled by browserLoader in Firefox.
+ case "react":
+ case "devtools/client/shared/vendor/react":
+ return `const React = require('react-dev'); module.exports = React`;
+ // For Rep's use of AMD
+ case "devtools/client/shared/vendor/react.default":
+ return `const React = require('react-dev'); module.exports = React`;
+ }
+
+ // Some modules depend on Chrome APIs which don't work in mocha. When such a module
+ // is required, replace it with a mock version.
+ switch (path) {
+ case "devtools/client/webconsole/utils":
+ return `module.exports = require("devtools/client/webconsole/new-console-output/test/fixtures/WebConsoleUtils")`;
+ case "devtools/shared/l10n":
+ return `module.exports = require("devtools/client/webconsole/new-console-output/test/fixtures/LocalizationHelper")`;
+ case "devtools/shared/plural-form":
+ return `module.exports = require("devtools/client/webconsole/new-console-output/test/fixtures/PluralForm")`;
+ case "Services":
+ case "Services.default":
+ return `module.exports = require("devtools/client/webconsole/new-console-output/test/fixtures/Services")`;
+ case "devtools/shared/client/main":
+ return `module.exports = require("devtools/client/webconsole/new-console-output/test/fixtures/ObjectClient")`;
+ }
+});
diff --git a/devtools/client/webconsole/new-console-output/test/store/filters.test.js b/devtools/client/webconsole/new-console-output/test/store/filters.test.js
new file mode 100644
index 000000000..3c38a255a
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/store/filters.test.js
@@ -0,0 +1,215 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const expect = require("expect");
+
+const actions = require("devtools/client/webconsole/new-console-output/actions/index");
+const { messageAdd } = require("devtools/client/webconsole/new-console-output/actions/index");
+const { ConsoleCommand } = require("devtools/client/webconsole/new-console-output/types");
+const { getAllMessages } = require("devtools/client/webconsole/new-console-output/selectors/messages");
+const { getAllFilters } = require("devtools/client/webconsole/new-console-output/selectors/filters");
+const { setupStore } = require("devtools/client/webconsole/new-console-output/test/helpers");
+const { MESSAGE_LEVEL } = require("devtools/client/webconsole/new-console-output/constants");
+const { stubPackets } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
+const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
+
+describe("Filtering", () => {
+ let store;
+ let numMessages;
+ // Number of messages in prepareBaseStore which are not filtered out, i.e. Evaluation
+ // Results, console commands and console.groups .
+ const numUnfilterableMessages = 3;
+
+ beforeEach(() => {
+ store = prepareBaseStore();
+ store.dispatch(actions.filtersClear());
+ numMessages = getAllMessages(store.getState()).size;
+ });
+
+ describe("Level filter", () => {
+ it("filters log messages", () => {
+ store.dispatch(actions.filterToggle(MESSAGE_LEVEL.LOG));
+
+ let messages = getAllMessages(store.getState());
+ expect(messages.size).toEqual(numMessages - 3);
+ });
+
+ it("filters debug messages", () => {
+ store.dispatch(actions.filterToggle(MESSAGE_LEVEL.DEBUG));
+
+ let messages = getAllMessages(store.getState());
+ expect(messages.size).toEqual(numMessages - 1);
+ });
+
+ // @TODO add info stub
+ it("filters info messages");
+
+ it("filters warning messages", () => {
+ store.dispatch(actions.filterToggle(MESSAGE_LEVEL.WARN));
+
+ let messages = getAllMessages(store.getState());
+ expect(messages.size).toEqual(numMessages - 1);
+ });
+
+ it("filters error messages", () => {
+ store.dispatch(actions.filterToggle(MESSAGE_LEVEL.ERROR));
+
+ let messages = getAllMessages(store.getState());
+ expect(messages.size).toEqual(numMessages - 1);
+ });
+
+ it("filters xhr messages", () => {
+ let message = stubPreparedMessages.get("XHR GET request");
+ store.dispatch(messageAdd(message));
+
+ let messages = getAllMessages(store.getState());
+ expect(messages.size).toEqual(numMessages);
+
+ store.dispatch(actions.filterToggle("netxhr"));
+ messages = getAllMessages(store.getState());
+ expect(messages.size).toEqual(numMessages + 1);
+ });
+
+ it("filters network messages", () => {
+ let message = stubPreparedMessages.get("GET request");
+ store.dispatch(messageAdd(message));
+
+ let messages = getAllMessages(store.getState());
+ expect(messages.size).toEqual(numMessages);
+
+ store.dispatch(actions.filterToggle("net"));
+ messages = getAllMessages(store.getState());
+ expect(messages.size).toEqual(numMessages + 1);
+ });
+ });
+
+ describe("Text filter", () => {
+ it("matches on value grips", () => {
+ store.dispatch(actions.filterTextSet("danger"));
+
+ let messages = getAllMessages(store.getState());
+ expect(messages.size - numUnfilterableMessages).toEqual(1);
+ });
+
+ it("matches unicode values", () => {
+ store.dispatch(actions.filterTextSet("鼬"));
+
+ let messages = getAllMessages(store.getState());
+ expect(messages.size - numUnfilterableMessages).toEqual(1);
+ });
+
+ it("matches locations", () => {
+ // Add a message with a different filename.
+ let locationMsg =
+ Object.assign({}, stubPackets.get("console.log('foobar', 'test')"));
+ locationMsg.message =
+ Object.assign({}, locationMsg.message, { filename: "search-location-test.js" });
+ store.dispatch(messageAdd(locationMsg));
+
+ store.dispatch(actions.filterTextSet("search-location-test.js"));
+
+ let messages = getAllMessages(store.getState());
+ expect(messages.size - numUnfilterableMessages).toEqual(1);
+ });
+
+ it("matches stacktrace functionName", () => {
+ let traceMessage = stubPackets.get("console.trace()");
+ store.dispatch(messageAdd(traceMessage));
+
+ store.dispatch(actions.filterTextSet("testStacktraceFiltering"));
+
+ let messages = getAllMessages(store.getState());
+ expect(messages.size - numUnfilterableMessages).toEqual(1);
+ });
+
+ it("matches stacktrace location", () => {
+ let traceMessage = stubPackets.get("console.trace()");
+ traceMessage.message =
+ Object.assign({}, traceMessage.message, {
+ filename: "search-location-test.js",
+ lineNumber: 85,
+ columnNumber: 13
+ });
+
+ store.dispatch(messageAdd(traceMessage));
+
+ store.dispatch(actions.filterTextSet("search-location-test.js:85:13"));
+
+ let messages = getAllMessages(store.getState());
+ expect(messages.size - numUnfilterableMessages).toEqual(1);
+ });
+
+ it("restores all messages once text is cleared", () => {
+ store.dispatch(actions.filterTextSet("danger"));
+ store.dispatch(actions.filterTextSet(""));
+
+ let messages = getAllMessages(store.getState());
+ expect(messages.size).toEqual(numMessages);
+ });
+ });
+
+ describe("Combined filters", () => {
+ // @TODO add test
+ it("filters");
+ });
+});
+
+describe("Clear filters", () => {
+ it("clears all filters", () => {
+ const store = setupStore([]);
+
+ // Setup test case
+ store.dispatch(actions.filterToggle(MESSAGE_LEVEL.ERROR));
+ store.dispatch(actions.filterToggle("netxhr"));
+ store.dispatch(actions.filterTextSet("foobar"));
+
+ let filters = getAllFilters(store.getState());
+ expect(filters.toJS()).toEqual({
+ "debug": true,
+ "error": false,
+ "info": true,
+ "log": true,
+ "net": false,
+ "netxhr": true,
+ "warn": true,
+ "text": "foobar"
+ });
+
+ store.dispatch(actions.filtersClear());
+
+ filters = getAllFilters(store.getState());
+ expect(filters.toJS()).toEqual({
+ "debug": true,
+ "error": true,
+ "info": true,
+ "log": true,
+ "net": false,
+ "netxhr": false,
+ "warn": true,
+ "text": ""
+ });
+ });
+});
+
+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",
+ "console.group('bar')"
+ ]);
+
+ // Console Command - never filtered
+ store.dispatch(messageAdd(new ConsoleCommand({ messageText: `console.warn("x")` })));
+
+ return store;
+}
diff --git a/devtools/client/webconsole/new-console-output/test/store/messages.test.js b/devtools/client/webconsole/new-console-output/test/store/messages.test.js
new file mode 100644
index 000000000..582ca36e3
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/store/messages.test.js
@@ -0,0 +1,353 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const {
+ getAllMessages,
+ getAllMessagesUiById,
+ getAllGroupsById,
+ getCurrentGroup,
+} = require("devtools/client/webconsole/new-console-output/selectors/messages");
+const {
+ setupActions,
+ setupStore
+} = require("devtools/client/webconsole/new-console-output/test/helpers");
+const { stubPackets, stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
+const {
+ MESSAGE_TYPE,
+} = require("devtools/client/webconsole/new-console-output/constants");
+
+const expect = require("expect");
+
+describe("Message reducer:", () => {
+ let actions;
+
+ before(() => {
+ actions = setupActions();
+ });
+
+ describe("messagesById", () => {
+ it("adds a message to an empty store", () => {
+ const { dispatch, getState } = setupStore([]);
+
+ const packet = stubPackets.get("console.log('foobar', 'test')");
+ const message = stubPreparedMessages.get("console.log('foobar', 'test')");
+ dispatch(actions.messageAdd(packet));
+
+ const messages = getAllMessages(getState());
+
+ expect(messages.first()).toEqual(message);
+ });
+
+ it("increments repeat on a repeating message", () => {
+ const { dispatch, getState } = setupStore([
+ "console.log('foobar', 'test')",
+ "console.log('foobar', 'test')"
+ ]);
+
+ const packet = stubPackets.get("console.log('foobar', 'test')");
+ dispatch(actions.messageAdd(packet));
+ dispatch(actions.messageAdd(packet));
+
+ const messages = getAllMessages(getState());
+
+ expect(messages.size).toBe(1);
+ expect(messages.first().repeat).toBe(4);
+ });
+
+ it("does not clobber a unique message", () => {
+ const { dispatch, getState } = setupStore([
+ "console.log('foobar', 'test')",
+ "console.log('foobar', 'test')"
+ ]);
+
+ const packet = stubPackets.get("console.log('foobar', 'test')");
+ dispatch(actions.messageAdd(packet));
+
+ const packet2 = stubPackets.get("console.log(undefined)");
+ dispatch(actions.messageAdd(packet2));
+
+ const messages = getAllMessages(getState());
+
+ expect(messages.size).toBe(2);
+ expect(messages.first().repeat).toBe(3);
+ expect(messages.last().repeat).toBe(1);
+ });
+
+ it("adds a message in response to console.clear()", () => {
+ const { dispatch, getState } = setupStore([]);
+
+ dispatch(actions.messageAdd(stubPackets.get("console.clear()")));
+
+ const messages = getAllMessages(getState());
+
+ expect(messages.size).toBe(1);
+ expect(messages.first().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(undefined)"
+ ]);
+
+ dispatch(actions.messagesClear());
+
+ const messages = getAllMessages(getState());
+ expect(messages.size).toBe(0);
+ });
+
+ it("limits the number of messages displayed", () => {
+ const { dispatch, getState } = setupStore([]);
+
+ const logLimit = 1000;
+ const packet = stubPackets.get("console.log(undefined)");
+ for (let i = 1; i <= logLimit + 1; i++) {
+ packet.message.arguments = [`message num ${i}`];
+ dispatch(actions.messageAdd(packet));
+ }
+
+ const messages = getAllMessages(getState());
+ expect(messages.count()).toBe(logLimit);
+ expect(messages.first().parameters[0]).toBe(`message num 2`);
+ expect(messages.last().parameters[0]).toBe(`message num ${logLimit + 1}`);
+ });
+
+ it("does not add null messages to the store", () => {
+ const { dispatch, getState } = setupStore([]);
+
+ const message = stubPackets.get("console.time('bar')");
+ dispatch(actions.messageAdd(message));
+
+ const messages = getAllMessages(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.messageAdd(packet));
+
+ const messages = getAllMessages(getState());
+ const tableMessage = messages.last();
+ 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.messageAdd(message));
+
+ const messages = getAllMessages(getState());
+ expect(messages.size).toBe(1);
+ });
+
+ it("sets groupId property as expected", () => {
+ const { dispatch, getState } = setupStore([]);
+
+ dispatch(actions.messageAdd(
+ stubPackets.get("console.group('bar')")));
+
+ const packet = stubPackets.get("console.log('foobar', 'test')");
+ dispatch(actions.messageAdd(packet));
+
+ const messages = getAllMessages(getState());
+ expect(messages.size).toBe(2);
+ expect(messages.last().groupId).toBe(messages.first().id);
+ });
+
+ it("does not display console.groupEnd messages to the store", () => {
+ const { dispatch, getState } = setupStore([]);
+
+ const message = stubPackets.get("console.groupEnd('bar')");
+ dispatch(actions.messageAdd(message));
+
+ const messages = getAllMessages(getState());
+ expect(messages.size).toBe(0);
+ });
+
+ it("filters out message added after a console.groupCollapsed message", () => {
+ const { dispatch, getState } = setupStore([]);
+
+ const message = stubPackets.get("console.groupCollapsed('foo')");
+ dispatch(actions.messageAdd(message));
+
+ dispatch(actions.messageAdd(
+ stubPackets.get("console.log('foobar', 'test')")));
+
+ const messages = getAllMessages(getState());
+ expect(messages.size).toBe(1);
+ });
+
+ it("shows the group of the first displayed message when messages are pruned", () => {
+ const { dispatch, getState } = setupStore([]);
+
+ const logLimit = 1000;
+
+ const groupMessage = stubPreparedMessages.get("console.group('bar')");
+ dispatch(actions.messageAdd(
+ stubPackets.get("console.group('bar')")));
+
+ const packet = stubPackets.get("console.log(undefined)");
+ for (let i = 1; i <= logLimit + 1; i++) {
+ packet.message.arguments = [`message num ${i}`];
+ dispatch(actions.messageAdd(packet));
+ }
+
+ const messages = getAllMessages(getState());
+ expect(messages.count()).toBe(logLimit);
+ expect(messages.first().messageText).toBe(groupMessage.messageText);
+ expect(messages.get(1).parameters[0]).toBe(`message num 3`);
+ expect(messages.last().parameters[0]).toBe(`message num ${logLimit + 1}`);
+ });
+
+ it("adds console.dirxml call as console.log", () => {
+ const { dispatch, getState } = setupStore([]);
+
+ const packet = stubPackets.get("console.dirxml(window)");
+ dispatch(actions.messageAdd(packet));
+
+ const messages = getAllMessages(getState());
+ const dirxmlMessage = messages.last();
+ expect(dirxmlMessage.level).toEqual(MESSAGE_TYPE.LOG);
+ });
+ });
+
+ describe("messagesUiById", () => {
+ it("opens console.trace messages when they are added", () => {
+ const { dispatch, getState } = setupStore([]);
+
+ const message = stubPackets.get("console.trace()");
+ dispatch(actions.messageAdd(message));
+
+ const messages = getAllMessages(getState());
+ const messagesUi = getAllMessagesUiById(getState());
+ expect(messagesUi.size).toBe(1);
+ expect(messagesUi.first()).toBe(messages.first().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.messageAdd(traceMessage));
+
+ dispatch(actions.messagesClear());
+
+ const messagesUi = getAllMessagesUiById(getState());
+ expect(messagesUi.size).toBe(0);
+ });
+
+ it("opens console.group messages when they are added", () => {
+ const { dispatch, getState } = setupStore([]);
+
+ const message = stubPackets.get("console.group('bar')");
+ dispatch(actions.messageAdd(message));
+
+ const messages = getAllMessages(getState());
+ const messagesUi = getAllMessagesUiById(getState());
+ expect(messagesUi.size).toBe(1);
+ expect(messagesUi.first()).toBe(messages.first().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.messageAdd(message));
+
+ const messagesUi = getAllMessagesUiById(getState());
+ expect(messagesUi.size).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.messageAdd(packet));
+
+ const messages = getAllMessages(getState());
+ const currentGroup = getCurrentGroup(getState());
+ expect(currentGroup).toBe(messages.first().id);
+ });
+
+ it("sets currentGroup to expected value when console.groupEnd is added", () => {
+ const { dispatch, getState } = setupStore([
+ "console.group('bar')",
+ "console.groupCollapsed('foo')"
+ ]);
+
+ let messages = getAllMessages(getState());
+ let currentGroup = getCurrentGroup(getState());
+ expect(currentGroup).toBe(messages.last().id);
+
+ const endFooPacket = stubPackets.get("console.groupEnd('foo')");
+ dispatch(actions.messageAdd(endFooPacket));
+ messages = getAllMessages(getState());
+ currentGroup = getCurrentGroup(getState());
+ expect(currentGroup).toBe(messages.first().id);
+
+ const endBarPacket = stubPackets.get("console.groupEnd('foo')");
+ dispatch(actions.messageAdd(endBarPacket));
+ messages = getAllMessages(getState());
+ 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.messageAdd(barPacket));
+
+ let messages = getAllMessages(getState());
+ let groupsById = getAllGroupsById(getState());
+ expect(groupsById.size).toBe(1);
+ expect(groupsById.has(messages.first().id)).toBe(true);
+ expect(groupsById.get(messages.first().id)).toEqual([]);
+
+ const fooPacket = stubPackets.get("console.groupCollapsed('foo')");
+ dispatch(actions.messageAdd(fooPacket));
+ messages = getAllMessages(getState());
+ groupsById = getAllGroupsById(getState());
+ expect(groupsById.size).toBe(2);
+ expect(groupsById.has(messages.last().id)).toBe(true);
+ expect(groupsById.get(messages.last().id)).toEqual([messages.first().id]);
+ });
+
+ it("resets groupsById in response to MESSAGES_CLEAR action", () => {
+ const { dispatch, getState } = setupStore([
+ "console.group('bar')",
+ "console.groupCollapsed('foo')",
+ ]);
+
+ let groupsById = getAllGroupsById(getState());
+ expect(groupsById.size).toBe(2);
+
+ dispatch(actions.messagesClear());
+
+ groupsById = getAllGroupsById(getState());
+ expect(groupsById.size).toBe(0);
+ });
+ });
+});
diff --git a/devtools/client/webconsole/new-console-output/test/utils/getRepeatId.test.js b/devtools/client/webconsole/new-console-output/test/utils/getRepeatId.test.js
new file mode 100644
index 000000000..d27238e14
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/utils/getRepeatId.test.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const { getRepeatId } = require("devtools/client/webconsole/new-console-output/utils/messages");
+const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
+
+const expect = require("expect");
+
+describe("getRepeatId:", () => {
+ it("returns same repeatId for duplicate values", () => {
+ const message1 = stubPreparedMessages.get("console.log('foobar', 'test')");
+ const message2 = message1.set("repeat", 3);
+ expect(getRepeatId(message1)).toEqual(getRepeatId(message2));
+ });
+
+ it("returns different repeatIds for different values", () => {
+ const message1 = stubPreparedMessages.get("console.log('foobar', 'test')");
+ const message2 = message1.set("parameters", ["funny", "monkey"]);
+ expect(getRepeatId(message1)).toNotEqual(getRepeatId(message2));
+ });
+
+ it("returns different repeatIds for different severities", () => {
+ const message1 = stubPreparedMessages.get("console.log('foobar', 'test')");
+ const message2 = message1.set("level", "error");
+ expect(getRepeatId(message1)).toNotEqual(getRepeatId(message2));
+ });
+
+ it("handles falsy values distinctly", () => {
+ const messageNaN = stubPreparedMessages.get("console.log(NaN)");
+ const messageUnd = stubPreparedMessages.get("console.log(undefined)");
+ const messageNul = stubPreparedMessages.get("console.log(null)");
+
+ const repeatIds = new Set([
+ getRepeatId(messageNaN),
+ getRepeatId(messageUnd),
+ getRepeatId(messageNul)]
+ );
+ expect(repeatIds.size).toEqual(3);
+ });
+});
diff --git a/devtools/client/webconsole/new-console-output/types.js b/devtools/client/webconsole/new-console-output/types.js
new file mode 100644
index 000000000..897ae5d3a
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/types.js
@@ -0,0 +1,53 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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";
+
+const Immutable = require("devtools/client/shared/vendor/immutable");
+
+const {
+ MESSAGE_SOURCE,
+ MESSAGE_TYPE,
+ MESSAGE_LEVEL
+} = require("devtools/client/webconsole/new-console-output/constants");
+
+exports.ConsoleCommand = Immutable.Record({
+ id: null,
+ allowRepeating: false,
+ messageText: null,
+ source: MESSAGE_SOURCE.JAVASCRIPT,
+ type: MESSAGE_TYPE.COMMAND,
+ level: MESSAGE_LEVEL.LOG,
+ groupId: null,
+});
+
+exports.ConsoleMessage = Immutable.Record({
+ id: null,
+ allowRepeating: true,
+ source: null,
+ type: null,
+ level: null,
+ messageText: null,
+ parameters: null,
+ repeat: 1,
+ repeatId: null,
+ stacktrace: null,
+ frame: null,
+ groupId: null,
+ exceptionDocURL: null,
+ userProvidedStyles: null,
+});
+
+exports.NetworkEventMessage = Immutable.Record({
+ id: null,
+ actor: null,
+ level: MESSAGE_LEVEL.LOG,
+ isXHR: false,
+ request: null,
+ response: null,
+ source: MESSAGE_SOURCE.NETWORK,
+ type: MESSAGE_TYPE.LOG,
+ groupId: null,
+});
diff --git a/devtools/client/webconsole/new-console-output/utils/id-generator.js b/devtools/client/webconsole/new-console-output/utils/id-generator.js
new file mode 100644
index 000000000..7d875b750
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/utils/id-generator.js
@@ -0,0 +1,22 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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";
+
+exports.IdGenerator = class IdGenerator {
+ constructor() {
+ this.messageId = 1;
+ }
+
+ getNextId() {
+ // Return the next message id, as a string.
+ return "" + this.messageId++;
+ }
+
+ getCurrentId() {
+ return this.messageId;
+ }
+};
diff --git a/devtools/client/webconsole/new-console-output/utils/messages.js b/devtools/client/webconsole/new-console-output/utils/messages.js
new file mode 100644
index 000000000..f91209e9d
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/utils/messages.js
@@ -0,0 +1,283 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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";
+
+const WebConsoleUtils = require("devtools/client/webconsole/utils").Utils;
+const STRINGS_URI = "devtools/client/locales/webconsole.properties";
+const l10n = new WebConsoleUtils.L10n(STRINGS_URI);
+
+const {
+ MESSAGE_SOURCE,
+ MESSAGE_TYPE,
+ MESSAGE_LEVEL,
+} = require("../constants");
+const {
+ ConsoleMessage,
+ NetworkEventMessage,
+} = require("../types");
+
+function prepareMessage(packet, idGenerator) {
+ // This packet is already in the expected packet structure. Simply return.
+ if (!packet.source) {
+ packet = transformPacket(packet);
+ }
+
+ if (packet.allowRepeating) {
+ packet = packet.set("repeatId", getRepeatId(packet));
+ }
+ return packet.set("id", idGenerator.getNextId());
+}
+
+/**
+ * Transforms a packet from Firefox RDP structure to Chrome RDP structure.
+ */
+function transformPacket(packet) {
+ if (packet._type) {
+ packet = convertCachedPacket(packet);
+ }
+
+ switch (packet.type) {
+ case "consoleAPICall": {
+ let { message } = packet;
+
+ let parameters = message.arguments;
+ let type = message.level;
+ let level = getLevelFromType(type);
+ let messageText = null;
+ const timer = message.timer;
+
+ // Special per-type conversion.
+ switch (type) {
+ case "clear":
+ // We show a message to users when calls console.clear() is called.
+ parameters = [l10n.getStr("consoleCleared")];
+ break;
+ case "count":
+ // Chrome RDP doesn't have a special type for count.
+ type = MESSAGE_TYPE.LOG;
+ let {counter} = message;
+ let label = counter.label ? counter.label : l10n.getStr("noCounterLabel");
+ messageText = `${label}: ${counter.count}`;
+ parameters = null;
+ break;
+ case "time":
+ // We don't show anything for console.time calls to match Chrome's behaviour.
+ parameters = null;
+ type = MESSAGE_TYPE.NULL_MESSAGE;
+ break;
+ case "timeEnd":
+ parameters = null;
+ if (timer) {
+ // We show the duration to users when calls console.timeEnd() is called,
+ // if corresponding console.time() was called before.
+ let duration = Math.round(timer.duration * 100) / 100;
+ messageText = l10n.getFormatStr("timeEnd", [timer.name, duration]);
+ } else {
+ // If the `timer` property does not exists, we don't output anything.
+ type = MESSAGE_TYPE.NULL_MESSAGE;
+ }
+ break;
+ case "table":
+ const supportedClasses = [
+ "Array", "Object", "Map", "Set", "WeakMap", "WeakSet"];
+ if (
+ !Array.isArray(parameters) ||
+ parameters.length === 0 ||
+ !supportedClasses.includes(parameters[0].class)
+ ) {
+ // 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;
+ parameters = null;
+ messageText = message.groupName || l10n.getStr("noGroupLabel");
+ break;
+ case "groupCollapsed":
+ type = MESSAGE_TYPE.START_GROUP_COLLAPSED;
+ parameters = null;
+ messageText = message.groupName || 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,
+ line: message.lineNumber,
+ column: message.columnNumber,
+ } : null;
+
+ return new ConsoleMessage({
+ source: MESSAGE_SOURCE.CONSOLE_API,
+ type,
+ level,
+ parameters,
+ messageText,
+ stacktrace: message.stacktrace ? message.stacktrace : null,
+ frame,
+ userProvidedStyles: message.styles,
+ });
+ }
+
+ case "navigationMessage": {
+ let { message } = packet;
+ return new ConsoleMessage({
+ source: MESSAGE_SOURCE.CONSOLE_API,
+ type: MESSAGE_TYPE.LOG,
+ level: MESSAGE_LEVEL.LOG,
+ messageText: "Navigated to " + message.url,
+ });
+ }
+
+ case "pageError": {
+ let { pageError } = packet;
+ let level = MESSAGE_LEVEL.ERROR;
+ if (pageError.warning || pageError.strict) {
+ level = MESSAGE_LEVEL.WARN;
+ } else if (pageError.info) {
+ level = MESSAGE_LEVEL.INFO;
+ }
+
+ const frame = pageError.sourceName ? {
+ source: pageError.sourceName,
+ line: pageError.lineNumber,
+ column: pageError.columnNumber
+ } : null;
+
+ return new ConsoleMessage({
+ source: MESSAGE_SOURCE.JAVASCRIPT,
+ type: MESSAGE_TYPE.LOG,
+ level,
+ messageText: pageError.errorMessage,
+ stacktrace: pageError.stacktrace ? pageError.stacktrace : null,
+ frame,
+ exceptionDocURL: pageError.exceptionDocURL,
+ });
+ }
+
+ case "networkEvent": {
+ let { networkEvent } = packet;
+
+ return new NetworkEventMessage({
+ actor: networkEvent.actor,
+ isXHR: networkEvent.isXHR,
+ request: networkEvent.request,
+ response: networkEvent.response,
+ });
+ }
+
+ case "evaluationResult":
+ default: {
+ let {
+ exceptionMessage: messageText,
+ exceptionDocURL,
+ frame,
+ result: parameters
+ } = packet;
+
+ const level = messageText ? MESSAGE_LEVEL.ERROR : MESSAGE_LEVEL.LOG;
+ return new ConsoleMessage({
+ source: MESSAGE_SOURCE.JAVASCRIPT,
+ type: MESSAGE_TYPE.RESULT,
+ level,
+ messageText,
+ parameters,
+ exceptionDocURL,
+ frame,
+ });
+ }
+ }
+}
+
+// Helpers
+function getRepeatId(message) {
+ message = message.toJS();
+ delete message.repeat;
+ return JSON.stringify(message);
+}
+
+function convertCachedPacket(packet) {
+ // The devtools server provides cached message packets in a different shape, so we
+ // transform them here.
+ let convertPacket = {};
+ if (packet._type === "ConsoleAPI") {
+ convertPacket.message = packet;
+ convertPacket.type = "consoleAPICall";
+ } else if (packet._type === "PageError") {
+ convertPacket.pageError = packet;
+ convertPacket.type = "pageError";
+ } else if ("_navPayload" in packet) {
+ convertPacket.type = "navigationMessage";
+ convertPacket.message = packet;
+ } else if (packet._type === "NetworkEvent") {
+ convertPacket.networkEvent = packet;
+ convertPacket.type = "networkEvent";
+ } else {
+ throw new Error("Unexpected packet type");
+ }
+ return convertPacket;
+}
+
+/**
+ * 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,
+ 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_LOG,
+ 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_DEBUG,
+ };
+
+ return levelMap[type] || MESSAGE_TYPE.LOG;
+}
+
+function isGroupType(type) {
+ return [
+ MESSAGE_TYPE.START_GROUP,
+ MESSAGE_TYPE.START_GROUP_COLLAPSED
+ ].includes(type);
+}
+
+exports.prepareMessage = prepareMessage;
+// Export for use in testing.
+exports.getRepeatId = getRepeatId;
+
+exports.l10n = l10n;
+exports.isGroupType = isGroupType;
diff --git a/devtools/client/webconsole/new-console-output/utils/moz.build b/devtools/client/webconsole/new-console-output/utils/moz.build
new file mode 100644
index 000000000..00378baa4
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/utils/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(
+ 'id-generator.js',
+ 'messages.js',
+ 'variables-view.js',
+)
diff --git a/devtools/client/webconsole/new-console-output/utils/variables-view.js b/devtools/client/webconsole/new-console-output/utils/variables-view.js
new file mode 100644
index 000000000..3cfee875a
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/utils/variables-view.js
@@ -0,0 +1,20 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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/. */
+
+/* global window */
+"use strict";
+
+/**
+ * @TODO Remove this.
+ *
+ * Once JSTerm is also written in React/Redux, these will be actions.
+ */
+exports.openVariablesView = (objectActor) => {
+ window.jsterm.openVariablesView({
+ objectActor,
+ autofocus: true,
+ });
+};
diff --git a/devtools/client/webconsole/package.json b/devtools/client/webconsole/package.json
new file mode 100644
index 000000000..6349f2057
--- /dev/null
+++ b/devtools/client/webconsole/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "webconsole",
+ "version": "0.0.1",
+ "devDependencies": {
+ "amd-loader": "0.0.5",
+ "babel-preset-es2015": "^6.6.0",
+ "babel-register": "^6.7.2",
+ "enzyme": "^2.4.1",
+ "expect": "^1.16.0",
+ "jsdom": "^9.4.1",
+ "jsdom-global": "^2.0.0",
+ "mocha": "^2.5.3",
+ "require-hacker": "^2.1.4",
+ "sinon": "^1.17.5"
+ },
+ "scripts": {
+ "postinstall": "cd ../ && npm install && cd webconsole",
+ "test": "NODE_PATH=`pwd`/../../../:`pwd`/../../../devtools/client/shared/vendor/ mocha new-console-output/test/**/*.test.js --compilers js:babel-register -r jsdom-global/register -r ./new-console-output/test/requireHelper.js"
+ }
+}
diff --git a/devtools/client/webconsole/panel.js b/devtools/client/webconsole/panel.js
new file mode 100644
index 000000000..3e3a4f4b9
--- /dev/null
+++ b/devtools/client/webconsole/panel.js
@@ -0,0 +1,118 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft= javascript ts=2 et sw=2 tw=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";
+
+const promise = require("promise");
+
+loader.lazyGetter(this, "HUDService", () => require("devtools/client/webconsole/hudservice"));
+loader.lazyGetter(this, "EventEmitter", () => require("devtools/shared/event-emitter"));
+
+/**
+ * A DevToolPanel that controls the Web Console.
+ */
+function WebConsolePanel(iframeWindow, toolbox) {
+ this._frameWindow = iframeWindow;
+ this._toolbox = toolbox;
+ 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: function () {
+ this.hud.jsterm.focus();
+ },
+
+ /**
+ * Open is effectively an asynchronous constructor.
+ *
+ * @return object
+ * A promise that is resolved when the Web Console completes opening.
+ */
+ open: function () {
+ let parentDoc = this._toolbox.doc;
+ let iframe = parentDoc.getElementById("toolbox-panel-iframe-webconsole");
+
+ // Make sure the iframe content window is ready.
+ let deferredIframe = promise.defer();
+ let win, doc;
+ if ((win = iframe.contentWindow) &&
+ (doc = win.document) &&
+ doc.readyState == "complete") {
+ deferredIframe.resolve(null);
+ } else {
+ iframe.addEventListener("load", function onIframeLoad() {
+ iframe.removeEventListener("load", onIframeLoad, true);
+ deferredIframe.resolve(null);
+ }, true);
+ }
+
+ // Local debugging needs to make the target remote.
+ let promiseTarget;
+ if (!this.target.isRemote) {
+ promiseTarget = this.target.makeRemote();
+ } else {
+ promiseTarget = promise.resolve(this.target);
+ }
+
+ // 1. Wait for the iframe to load.
+ // 2. Wait for the remote target.
+ // 3. Open the Web Console.
+ return deferredIframe.promise
+ .then(() => promiseTarget)
+ .then((target) => {
+ this._frameWindow._remoteTarget = target;
+
+ let webConsoleUIWindow = iframe.contentWindow.wrappedJSObject;
+ let chromeWindow = iframe.ownerDocument.defaultView;
+ return HUDService.openWebConsole(this.target, webConsoleUIWindow,
+ chromeWindow);
+ })
+ .then((webConsole) => {
+ this.hud = webConsole;
+ this._isReady = true;
+ this.emit("ready");
+ return this;
+ }, (reason) => {
+ let msg = "WebConsolePanel open failed. " +
+ reason.error + ": " + reason.message;
+ dump(msg + "\n");
+ console.error(msg);
+ });
+ },
+
+ get target() {
+ return this._toolbox.target;
+ },
+
+ _isReady: false,
+ get isReady() {
+ return this._isReady;
+ },
+
+ destroy: function () {
+ if (this._destroyer) {
+ return this._destroyer;
+ }
+
+ this._destroyer = this.hud.destroy();
+ this._destroyer.then(() => {
+ this._frameWindow = null;
+ this._toolbox = null;
+ this.emit("destroyed");
+ });
+
+ return this._destroyer;
+ },
+};
diff --git a/devtools/client/webconsole/test/.eslintrc.js b/devtools/client/webconsole/test/.eslintrc.js
new file mode 100644
index 000000000..8d15a76d9
--- /dev/null
+++ b/devtools/client/webconsole/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/webconsole/test/browser.ini b/devtools/client/webconsole/test/browser.ini
new file mode 100644
index 000000000..918411182
--- /dev/null
+++ b/devtools/client/webconsole/test/browser.ini
@@ -0,0 +1,396 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ test-bug-585956-console-trace.html
+ test-bug-593003-iframe-wrong-hud-iframe.html
+ test-bug-593003-iframe-wrong-hud.html
+ test-bug-595934-canvas-css.html
+ test-bug-595934-canvas-css.js
+ test-bug-595934-css-loader.css
+ test-bug-595934-css-loader.css^headers^
+ test-bug-595934-css-loader.html
+ test-bug-595934-css-parser.css
+ test-bug-595934-css-parser.html
+ test-bug-595934-empty-getelementbyid.html
+ test-bug-595934-empty-getelementbyid.js
+ test-bug-595934-html.html
+ test-bug-595934-image.html
+ test-bug-595934-image.jpg
+ test-bug-595934-imagemap.html
+ test-bug-595934-malformedxml-external.html
+ test-bug-595934-malformedxml-external.xml
+ test-bug-595934-malformedxml.xhtml
+ test-bug-595934-svg.xhtml
+ test-bug-595934-workers.html
+ test-bug-595934-workers.js
+ test-bug-597136-external-script-errors.html
+ test-bug-597136-external-script-errors.js
+ test-bug-597756-reopen-closed-tab.html
+ test-bug-599725-response-headers.sjs
+ test-bug-600183-charset.html
+ test-bug-600183-charset.html^headers^
+ test-bug-601177-log-levels.html
+ test-bug-601177-log-levels.js
+ test-bug-603750-websocket.html
+ test-bug-603750-websocket.js
+ test-bug-613013-console-api-iframe.html
+ test-bug-618078-network-exceptions.html
+ test-bug-621644-jsterm-dollar.html
+ test-bug-630733-response-redirect-headers.sjs
+ test-bug-632275-getters.html
+ test-bug-632347-iterators-generators.html
+ test-bug-644419-log-limits.html
+ test-bug-646025-console-file-location.html
+ test-bug-658368-time-methods.html
+ test-bug-737873-mixedcontent.html
+ test-bug-752559-ineffective-iframe-sandbox-warning0.html
+ test-bug-752559-ineffective-iframe-sandbox-warning1.html
+ test-bug-752559-ineffective-iframe-sandbox-warning2.html
+ test-bug-752559-ineffective-iframe-sandbox-warning3.html
+ test-bug-752559-ineffective-iframe-sandbox-warning4.html
+ test-bug-752559-ineffective-iframe-sandbox-warning5.html
+ test-bug-752559-ineffective-iframe-sandbox-warning-inner.html
+ test-bug-752559-ineffective-iframe-sandbox-warning-nested1.html
+ test-bug-752559-ineffective-iframe-sandbox-warning-nested2.html
+ test-bug-762593-insecure-passwords-about-blank-web-console-warning.html
+ test-bug-762593-insecure-passwords-web-console-warning.html
+ test-bug-766001-console-log.js
+ test-bug-766001-js-console-links.html
+ test-bug-766001-js-errors.js
+ test-bug-782653-css-errors-1.css
+ test-bug-782653-css-errors-2.css
+ test-bug-782653-css-errors.html
+ test-bug-837351-security-errors.html
+ test-bug-859170-longstring-hang.html
+ test-bug-869003-iframe.html
+ test-bug-869003-top-window.html
+ test-closure-optimized-out.html
+ test-closures.html
+ test-console-assert.html
+ test-console-clear.html
+ test-console-count.html
+ test-console-count-external-file.js
+ test-console-extras.html
+ test-console-replaced-api.html
+ test-console-server-logging.sjs
+ test-console-server-logging-array.sjs
+ test-console.html
+ test-console-workers.html
+ test-console-table.html
+ test-console-output-02.html
+ test-console-output-03.html
+ test-console-output-04.html
+ test-console-output-dom-elements.html
+ test-console-output-events.html
+ test-console-output-regexp.html
+ test-console-column.html
+ test-consoleiframes.html
+ test-console-trace-async.html
+ test-certificate-messages.html
+ test-cu-reporterror.js
+ test-data.json
+ test-data.json^headers^
+ test-duplicate-error.html
+ test-encoding-ISO-8859-1.html
+ test-error.html
+ test-eval-in-stackframe.html
+ test-file-location.js
+ test-filter.html
+ test-for-of.html
+ test_hpkp-invalid-headers.sjs
+ test_hsts-invalid-headers.sjs
+ test-iframe-762593-insecure-form-action.html
+ test-iframe-762593-insecure-frame.html
+ test-iframe1.html
+ test-iframe2.html
+ test-iframe3.html
+ test-image.png
+ test-mixedcontent-securityerrors.html
+ test-mutation.html
+ test-network-request.html
+ test-network.html
+ test-observe-http-ajax.html
+ test-own-console.html
+ test-property-provider.html
+ test-repeated-messages.html
+ test-result-format-as-string.html
+ test-trackingprotection-securityerrors.html
+ test-webconsole-error-observer.html
+ test_bug_770099_violation.html
+ test_bug_770099_violation.html^headers^
+ test-autocomplete-in-stackframe.html
+ testscript.js
+ test-bug_923281_console_log_filter.html
+ test-bug_923281_test1.js
+ test-bug_923281_test2.js
+ test-bug_939783_console_trace_duplicates.html
+ test-bug-952277-highlight-nodes-in-vview.html
+ test-bug-609872-cd-iframe-parent.html
+ test-bug-609872-cd-iframe-child.html
+ test-bug-989025-iframe-parent.html
+ test-bug_1050691_click_function_to_source.html
+ test-bug_1050691_click_function_to_source.js
+ test-console-api-stackframe.html
+ test-exception-stackframe.html
+ test_bug_1010953_cspro.html^headers^
+ test_bug_1010953_cspro.html
+ test_bug1045902_console_csp_ignore_reflected_xss_message.html^headers^
+ test_bug1045902_console_csp_ignore_reflected_xss_message.html
+ test_bug1092055_shouldwarn.js^headers^
+ test_bug1092055_shouldwarn.js
+ test_bug1092055_shouldwarn.html
+ test_bug_1247459_violation.html
+ !/devtools/client/framework/test/shared-head.js
+ !/devtools/client/netmonitor/test/sjs_cors-test-server.sjs
+ !/image/test/mochitest/blue.png
+
+[browser_bug1045902_console_csp_ignore_reflected_xss_message.js]
+skip-if = (e10s && debug) || (e10s && os == 'win') # Bug 1221499 enabled these on windows
+[browser_bug664688_sandbox_update_after_navigation.js]
+[browser_bug_638949_copy_link_location.js]
+subsuite = clipboard
+[browser_bug_862916_console_dir_and_filter_off.js]
+skip-if = (e10s && (os == 'win' || os == 'mac')) # Bug 1243976
+[browser_bug_865288_repeat_different_objects.js]
+[browser_bug_865871_variables_view_close_on_esc_key.js]
+[browser_bug_869003_inspect_cross_domain_object.js]
+[browser_bug_871156_ctrlw_close_tab.js]
+[browser_cached_messages.js]
+[browser_console.js]
+[browser_console_addonsdk_loader_exception.js]
+[browser_console_clear_method.js]
+[browser_console_clear_on_reload.js]
+[browser_console_click_focus.js]
+[browser_console_consolejsm_output.js]
+[browser_console_copy_command.js]
+subsuite = clipboard
+[browser_console_dead_objects.js]
+skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
+[browser_console_copy_entire_message_context_menu.js]
+subsuite = clipboard
+[browser_console_error_source_click.js]
+[browser_console_filters.js]
+[browser_console_iframe_messages.js]
+[browser_console_keyboard_accessibility.js]
+[browser_console_log_inspectable_object.js]
+[browser_console_native_getters.js]
+[browser_console_navigation_marker.js]
+[browser_console_netlogging.js]
+[browser_console_nsiconsolemessage.js]
+[browser_console_optimized_out_vars.js]
+[browser_console_private_browsing.js]
+skip-if = e10s # Bug 1042253 - webconsole e10s tests
+[browser_console_server_logging.js]
+[browser_console_variables_view.js]
+[browser_console_variables_view_filter.js]
+[browser_console_variables_view_dom_nodes.js]
+[browser_console_variables_view_dont_sort_non_sortable_classes_properties.js]
+[browser_console_variables_view_special_names.js]
+[browser_console_variables_view_while_debugging.js]
+[browser_console_variables_view_while_debugging_and_inspecting.js]
+[browser_eval_in_debugger_stackframe.js]
+[browser_eval_in_debugger_stackframe2.js]
+[browser_jsterm_inspect.js]
+skip-if = e10s && debug && (os == 'win' || os == 'mac') # Bug 1243966
+[browser_longstring_hang.js]
+[browser_output_breaks_after_console_dir_uninspectable.js]
+[browser_output_longstring_expand.js]
+[browser_repeated_messages_accuracy.js]
+[browser_result_format_as_string.js]
+[browser_warn_user_about_replaced_api.js]
+[browser_webconsole_allow_mixedcontent_securityerrors.js]
+tags = mcb
+[browser_webconsole_script_errordoc_urls.js]
+skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
+[browser_webconsole_assert.js]
+[browser_webconsole_block_mixedcontent_securityerrors.js]
+tags = mcb
+[browser_webconsole_bug_579412_input_focus.js]
+[browser_webconsole_bug_580001_closing_after_completion.js]
+[browser_webconsole_bug_580030_errors_after_page_reload.js]
+[browser_webconsole_bug_580454_timestamp_l10n.js]
+[browser_webconsole_bug_582201_duplicate_errors.js]
+[browser_webconsole_bug_583816_No_input_and_Tab_key_pressed.js]
+[browser_webconsole_bug_585237_line_limit.js]
+[browser_webconsole_bug_585956_console_trace.js]
+[browser_webconsole_bug_585991_autocomplete_keys.js]
+[browser_webconsole_bug_585991_autocomplete_popup.js]
+[browser_webconsole_bug_586388_select_all.js]
+[browser_webconsole_bug_587617_output_copy.js]
+subsuite = clipboard
+[browser_webconsole_bug_588342_document_focus.js]
+[browser_webconsole_bug_588730_text_node_insertion.js]
+[browser_webconsole_bug_588967_input_expansion.js]
+[browser_webconsole_bug_589162_css_filter.js]
+[browser_webconsole_bug_592442_closing_brackets.js]
+[browser_webconsole_bug_593003_iframe_wrong_hud.js]
+[browser_webconsole_bug_594497_history_arrow_keys.js]
+[browser_webconsole_bug_595223_file_uri.js]
+[browser_webconsole_bug_595350_multiple_windows_and_tabs.js]
+skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
+[browser_webconsole_bug_595934_message_categories.js]
+skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
+[browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js]
+[browser_webconsole_bug_597136_external_script_errors.js]
+[browser_webconsole_bug_597136_network_requests_from_chrome.js]
+[browser_webconsole_bug_597460_filter_scroll.js]
+[browser_webconsole_bug_597756_reopen_closed_tab.js]
+[browser_webconsole_bug_599725_response_headers.js]
+[browser_webconsole_bug_600183_charset.js]
+[browser_webconsole_bug_601177_log_levels.js]
+[browser_webconsole_bug_601352_scroll.js]
+[browser_webconsole_bug_601667_filter_buttons.js]
+[browser_webconsole_bug_603750_websocket.js]
+[browser_webconsole_bug_611795.js]
+[browser_webconsole_bug_613013_console_api_iframe.js]
+[browser_webconsole_bug_613280_jsterm_copy.js]
+subsuite = clipboard
+[browser_webconsole_bug_613642_maintain_scroll.js]
+[browser_webconsole_bug_613642_prune_scroll.js]
+[browser_webconsole_bug_614793_jsterm_scroll.js]
+[browser_webconsole_bug_618078_network_exceptions.js]
+[browser_webconsole_bug_621644_jsterm_dollar.js]
+[browser_webconsole_bug_622303_persistent_filters.js]
+[browser_webconsole_bug_623749_ctrl_a_select_all_winnt.js]
+skip-if = os != "win"
+[browser_webconsole_bug_630733_response_redirect_headers.js]
+[browser_webconsole_bug_632275_getters_document_width.js]
+[browser_webconsole_bug_632347_iterators_generators.js]
+skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
+[browser_webconsole_bug_632817.js]
+skip-if = true # Bug 1244707
+[browser_webconsole_bug_642108_pruneTest.js]
+[browser_webconsole_autocomplete_and_selfxss.js]
+subsuite = clipboard
+[browser_webconsole_bug_644419_log_limits.js]
+[browser_webconsole_bug_646025_console_file_location.js]
+[browser_webconsole_bug_651501_document_body_autocomplete.js]
+[browser_webconsole_bug_653531_highlighter_console_helper.js]
+skip-if = true # Requires direct access to content nodes
+[browser_webconsole_bug_658368_time_methods.js]
+[browser_webconsole_bug_659907_console_dir.js]
+[browser_webconsole_bug_660806_history_nav.js]
+[browser_webconsole_bug_664131_console_group.js]
+[browser_webconsole_bug_686937_autocomplete_JSTerm_helpers.js]
+[browser_webconsole_bug_704295.js]
+[browser_webconsole_bug_734061_No_input_change_and_Tab_key_pressed.js]
+[browser_webconsole_bug_737873_mixedcontent.js]
+tags = mcb
+[browser_webconsole_bug_752559_ineffective_iframe_sandbox_warning.js]
+[browser_webconsole_bug_762593_insecure_passwords_about_blank_web_console_warning.js]
+[browser_webconsole_bug_762593_insecure_passwords_web_console_warning.js]
+skip-if = true # Bug 1110500 - mouse event failure in test
+[browser_webconsole_bug_764572_output_open_url.js]
+skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
+[browser_webconsole_bug_766001_JS_Console_in_Debugger.js]
+[browser_webconsole_bug_770099_violation.js]
+skip-if = e10s && (os == 'win' || os == 'mac') # Bug 1243978
+[browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js]
+[browser_webconsole_bug_804845_ctrl_key_nav.js]
+skip-if = os != "mac"
+[browser_webconsole_bug_817834_add_edited_input_to_history.js]
+[browser_webconsole_bug_837351_securityerrors.js]
+[browser_webconsole_filter_buttons_contextmenu.js]
+[browser_webconsole_bug_1006027_message_timestamps_incorrect.js]
+skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug intermittent)
+[browser_webconsole_bug_1010953_cspro.js]
+skip-if = e10s && (os == 'win' || os == 'mac') # Bug 1243967
+[browser_webconsole_bug_1247459_violation.js]
+skip-if = e10s && (os == 'win') # Bug 1264955
+[browser_webconsole_certificate_messages.js]
+skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
+[browser_webconsole_show_subresource_security_errors.js]
+skip-if = e10s && (os == 'win' || os == 'mac') # Bug 1243987
+[browser_webconsole_cached_autocomplete.js]
+[browser_webconsole_chrome.js]
+[browser_webconsole_clear_method.js]
+[browser_webconsole_clickable_urls.js]
+[browser_webconsole_closure_inspection.js]
+[browser_webconsole_completion.js]
+[browser_webconsole_console_extras.js]
+[browser_webconsole_console_logging_api.js]
+[browser_webconsole_console_logging_workers_api.js]
+[browser_webconsole_console_trace_async.js]
+[browser_webconsole_count.js]
+[browser_webconsole_dont_navigate_on_doubleclick.js]
+[browser_webconsole_execution_scope.js]
+[browser_webconsole_for_of.js]
+[browser_webconsole_history.js]
+[browser_webconsole_hpkp_invalid-headers.js]
+[browser_webconsole_hsts_invalid-headers.js]
+skip-if = e10s # Bug 1042253 - webconsole e10s tests
+[browser_webconsole_input_field_focus_on_panel_select.js]
+[browser_webconsole_inspect-parsed-documents.js]
+[browser_webconsole_js_input_expansion.js]
+[browser_webconsole_jsterm.js]
+skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug timeout)
+[browser_webconsole_live_filtering_of_message_types.js]
+[browser_webconsole_live_filtering_on_search_strings.js]
+[browser_webconsole_message_node_id.js]
+[browser_webconsole_multiline_input.js]
+[browser_webconsole_netlogging.js]
+skip-if = true # Bug 1298364
+[browser_webconsole_netlogging_basic.js]
+[browser_webconsole_netlogging_panel.js]
+[browser_webconsole_netlogging_reset_filter.js]
+[browser_webconsole_notifications.js]
+[browser_webconsole_open-links-without-callback.js]
+[browser_webconsole_promise.js]
+[browser_webconsole_output_copy_newlines.js]
+subsuite = clipboard
+[browser_webconsole_output_order.js]
+[browser_webconsole_property_provider.js]
+skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
+[browser_webconsole_scratchpad_panel_link.js]
+[browser_webconsole_split.js]
+[browser_webconsole_split_escape_key.js]
+[browser_webconsole_split_focus.js]
+[browser_webconsole_split_persist.js]
+[browser_webconsole_trackingprotection_errors.js]
+tags = trackingprotection
+[browser_webconsole_view_source.js]
+[browser_webconsole_reflow.js]
+[browser_webconsole_log_file_filter.js]
+[browser_webconsole_expandable_timestamps.js]
+[browser_webconsole_autocomplete_accessibility.js]
+[browser_webconsole_autocomplete_in_debugger_stackframe.js]
+[browser_webconsole_autocomplete_popup_close_on_tab_switch.js]
+[browser_webconsole_autocomplete-properties-with-non-alphanumeric-names.js]
+[browser_console_hide_jsterm_when_devtools_chrome_enabled_false.js]
+[browser_console_history_persist.js]
+[browser_webconsole_output_01.js]
+[browser_webconsole_output_02.js]
+[browser_webconsole_output_03.js]
+[browser_webconsole_output_04.js]
+[browser_webconsole_output_05.js]
+[browser_webconsole_output_06.js]
+[browser_webconsole_output_dom_elements_01.js]
+[browser_webconsole_output_dom_elements_02.js]
+skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug timeout)
+[browser_webconsole_output_dom_elements_03.js]
+skip-if = e10s # Bug 1241019
+[browser_webconsole_output_dom_elements_04.js]
+skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug timeout)
+[browser_webconsole_output_dom_elements_05.js]
+[browser_webconsole_output_events.js]
+[browser_webconsole_output_regexp.js]
+[browser_webconsole_output_table.js]
+[browser_console_variables_view_highlighter.js]
+[browser_webconsole_start_netmon_first.js]
+[browser_webconsole_console_trace_duplicates.js]
+[browser_webconsole_cd_iframe.js]
+[browser_webconsole_autocomplete_crossdomain_iframe.js]
+[browser_webconsole_console_custom_styles.js]
+[browser_webconsole_console_api_stackframe.js]
+[browser_webconsole_exception_stackframe.js]
+[browser_webconsole_column_numbers.js]
+[browser_console_open_or_focus.js]
+[browser_webconsole_bug_922212_console_dirxml.js]
+[browser_webconsole_shows_reqs_in_netmonitor.js]
+[browser_netmonitor_shows_reqs_in_webconsole.js]
+[browser_webconsole_bug_1050691_click_function_to_source.js]
+[browser_webconsole_context_menu_open_in_var_view.js]
+[browser_webconsole_context_menu_store_as_global.js]
+[browser_webconsole_strict_mode_errors.js]
diff --git a/devtools/client/webconsole/test/browser_bug1045902_console_csp_ignore_reflected_xss_message.js b/devtools/client/webconsole/test/browser_bug1045902_console_csp_ignore_reflected_xss_message.js
new file mode 100644
index 000000000..cfbf61795
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_bug1045902_console_csp_ignore_reflected_xss_message.js
@@ -0,0 +1,52 @@
+/* -*- 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 a file with an unsupported CSP directive ('reflected-xss filter')
+// displays the appropriate message to the console.
+
+"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/test_bug1045902_console_csp_ignore_reflected_xss_" +
+ "message.html";
+
+var hud = undefined;
+
+var TEST_URI = "data:text/html;charset=utf8,Web Console CSP ignoring " +
+ "reflected XSS (bug 1045902)";
+
+add_task(function* () {
+ let { browser } = yield loadTab(TEST_URI);
+
+ hud = yield openConsole();
+
+ yield loadDocument(browser);
+ yield testViolationMessage();
+
+ hud = null;
+});
+
+function loadDocument(browser) {
+ hud.jsterm.clearOutput();
+ browser.loadURI(TEST_FILE);
+ return BrowserTestUtils.browserLoaded(browser);
+}
+
+function testViolationMessage() {
+ let aOutputNode = hud.outputNode;
+
+ return waitForSuccess({
+ name: "Confirming that CSP logs messages to the console when " +
+ "\u2018reflected-xss\u2019 directive is used!",
+ validator: function () {
+ console.log(aOutputNode.textContent);
+ let success = false;
+ success = aOutputNode.textContent.indexOf(EXPECTED_RESULT) > -1;
+ return success;
+ }
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_bug664688_sandbox_update_after_navigation.js b/devtools/client/webconsole/test/browser_bug664688_sandbox_update_after_navigation.js
new file mode 100644
index 000000000..1aacb61c1
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_bug664688_sandbox_update_after_navigation.js
@@ -0,0 +1,92 @@
+/* -*- 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 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.
+
+"use strict";
+
+add_task(function* () {
+ const TEST_URI1 = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+ const TEST_URI2 = "http://example.org/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+ yield loadTab(TEST_URI1);
+ let hud = yield openConsole();
+
+ hud.jsterm.clearOutput();
+ hud.jsterm.execute("window.location.href");
+
+ info("wait for window.location.href");
+
+ let msgForLocation1 = {
+ webconsole: hud,
+ messages: [
+ {
+ name: "window.location.href jsterm input",
+ text: "window.location.href",
+ category: CATEGORY_INPUT,
+ },
+ {
+ name: "window.location.href result is displayed",
+ text: TEST_URI1,
+ category: CATEGORY_OUTPUT,
+ },
+ ],
+ };
+
+ yield waitForMessages(msgForLocation1);
+
+ // load second url
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI2);
+ yield loadBrowser(gBrowser.selectedBrowser);
+
+ is(hud.outputNode.textContent.indexOf("Permission denied"), -1,
+ "no permission denied errors");
+
+ hud.jsterm.clearOutput();
+ hud.jsterm.execute("window.location.href");
+
+ info("wait for window.location.href after page navigation");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "window.location.href jsterm input",
+ text: "window.location.href",
+ category: CATEGORY_INPUT,
+ },
+ {
+ name: "window.location.href result is displayed",
+ text: TEST_URI2,
+ category: CATEGORY_OUTPUT,
+ },
+ ],
+ });
+
+ is(hud.outputNode.textContent.indexOf("Permission denied"), -1,
+ "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).
+ let cleared = hud.jsterm.once("messages-cleared");
+
+ gBrowser.goBack();
+
+ info("Waiting for messages to be cleared due to navigation");
+ yield cleared;
+
+ info("Messages cleared after navigation; checking location");
+ hud.jsterm.execute("window.location.href");
+
+ info("wait for window.location.href after goBack()");
+ yield waitForMessages(msgForLocation1);
+ is(hud.outputNode.textContent.indexOf("Permission denied"), -1,
+ "no permission denied errors");
+});
diff --git a/devtools/client/webconsole/test/browser_bug_638949_copy_link_location.js b/devtools/client/webconsole/test/browser_bug_638949_copy_link_location.js
new file mode 100644
index 000000000..54bdbe499
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_bug_638949_copy_link_location.js
@@ -0,0 +1,107 @@
+/* -*- 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/ */
+
+// Test for the "Copy link location" context menu item shown when you right
+// click network requests in the output.
+
+"use strict";
+
+add_task(function* () {
+ const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html?_date=" + Date.now();
+ const COMMAND_NAME = "consoleCmd_copyURL";
+ const CONTEXT_MENU_ID = "#menu_copyURL";
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.webconsole.filter.networkinfo");
+ });
+
+ Services.prefs.setBoolPref("devtools.webconsole.filter.networkinfo", true);
+
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ let output = hud.outputNode;
+ let menu = hud.iframeWindow.document.getElementById("output-contextmenu");
+
+ hud.jsterm.clearOutput();
+ content.console.log("bug 638949");
+
+ // Test that the "Copy Link Location" command is disabled for non-network
+ // messages.
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "bug 638949",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ output.focus();
+ let message = [...result.matched][0];
+
+ goUpdateCommand(COMMAND_NAME);
+ ok(!isEnabled(), COMMAND_NAME + " is disabled");
+
+ // Test that the "Copy Link Location" menu item is hidden for non-network
+ // messages.
+ yield waitForContextMenu(menu, message, () => {
+ let isHidden = menu.querySelector(CONTEXT_MENU_ID).hidden;
+ ok(isHidden, CONTEXT_MENU_ID + " is hidden");
+ });
+
+ hud.jsterm.clearOutput();
+ // Reloading will produce network logging
+ content.location.reload();
+
+ // Test that the "Copy Link Location" command is enabled and works
+ // as expected for any network-related message.
+ // This command should copy only the URL.
+ [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "test-console.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ output.focus();
+ message = [...result.matched][0];
+ hud.ui.output.selectMessage(message);
+
+ goUpdateCommand(COMMAND_NAME);
+ ok(isEnabled(), COMMAND_NAME + " is enabled");
+
+ info("expected clipboard value: " + message.url);
+
+ let deferred = promise.defer();
+
+ waitForClipboard((aData) => {
+ return aData.trim() == message.url;
+ }, () => {
+ goDoCommand(COMMAND_NAME);
+ }, () => {
+ deferred.resolve(null);
+ }, () => {
+ deferred.reject(null);
+ });
+
+ yield deferred.promise;
+
+ // Test that the "Copy Link Location" menu item is visible for network-related
+ // messages.
+ yield waitForContextMenu(menu, message, () => {
+ let isVisible = !menu.querySelector(CONTEXT_MENU_ID).hidden;
+ ok(isVisible, CONTEXT_MENU_ID + " is visible");
+ });
+
+ // Return whether "Copy Link Location" command is enabled or not.
+ function isEnabled() {
+ let controller = top.document.commandDispatcher
+ .getControllerForCommand(COMMAND_NAME);
+ return controller && controller.isCommandEnabled(COMMAND_NAME);
+ }
+});
diff --git a/devtools/client/webconsole/test/browser_bug_862916_console_dir_and_filter_off.js b/devtools/client/webconsole/test/browser_bug_862916_console_dir_and_filter_off.js
new file mode 100644
index 000000000..9d04076ee
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_bug_862916_console_dir_and_filter_off.js
@@ -0,0 +1,31 @@
+/* -*- 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/ */
+
+// Check that the output for console.dir() works even if Logging filter is off.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<p>test for bug 862916";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ ok(hud, "web console opened");
+
+ hud.setFilterState("log", false);
+ registerCleanupFunction(() => hud.setFilterState("log", true));
+
+ hud.jsterm.execute("window.fooBarz = 'bug862916'; " +
+ "console.dir(window)");
+
+ let varView = yield hud.jsterm.once("variablesview-fetched");
+ ok(varView, "variables view object");
+
+ yield findVariableViewProperties(varView, [
+ { name: "fooBarz", value: "bug862916" },
+ ], { webconsole: hud });
+});
+
diff --git a/devtools/client/webconsole/test/browser_bug_865288_repeat_different_objects.js b/devtools/client/webconsole/test/browser_bug_865288_repeat_different_objects.js
new file mode 100644
index 000000000..86ab5bd39
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_bug_865288_repeat_different_objects.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/ */
+
+// 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 = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-repeated-messages.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ info("waiting for 3 console.log objects");
+
+ hud.jsterm.clearOutput(true);
+ hud.jsterm.execute("window.testConsoleObjects()");
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "3 console.log messages",
+ text: "abba",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ count: 3,
+ repeats: 1,
+ objects: true,
+ }],
+ });
+
+ let msgs = [...result.matched];
+ is(msgs.length, 3, "3 message elements");
+
+ for (let i = 0; i < msgs.length; i++) {
+ info("test message element #" + i);
+
+ let msg = msgs[i];
+ let clickable = msg.querySelector(".message-body a");
+ ok(clickable, "clickable object #" + i);
+
+ msg.scrollIntoView(false);
+ yield clickObject(clickable, i);
+ }
+
+ function* clickObject(obj, i) {
+ executeSoon(() => {
+ EventUtils.synthesizeMouse(obj, 2, 2, {}, hud.iframeWindow);
+ });
+
+ let varView = yield hud.jsterm.once("variablesview-fetched");
+ ok(varView, "variables view fetched #" + i);
+
+ yield findVariableViewProperties(varView, [
+ { name: "id", value: "abba" + i },
+ ], { webconsole: hud });
+ }
+});
+
diff --git a/devtools/client/webconsole/test/browser_bug_865871_variables_view_close_on_esc_key.js b/devtools/client/webconsole/test/browser_bug_865871_variables_view_close_on_esc_key.js
new file mode 100644
index 000000000..044525b28
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_bug_865871_variables_view_close_on_esc_key.js
@@ -0,0 +1,75 @@
+/* -*- 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/ */
+
+// Check that the variables view sidebar can be closed by pressing Escape in the
+// web console.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-eval-in-stackframe.html";
+
+function test() {
+ let hud;
+
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ let {tab} = yield loadTab(TEST_URI);
+ hud = yield openConsole(tab);
+ let jsterm = hud.jsterm;
+ let result;
+ let vview;
+ let msg;
+
+ yield openSidebar("fooObj",
+ 'testProp: "testValue"',
+ { name: "testProp", value: "testValue" });
+
+ let prop = result.matchedProp;
+ ok(prop, "matched the |testProp| property in the variables view");
+
+ vview.window.focus();
+
+ let sidebarClosed = jsterm.once("sidebar-closed");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield sidebarClosed;
+
+ jsterm.clearOutput();
+
+ yield openSidebar("window.location",
+ "Location \u2192 http://example.com/browser/",
+ { name: "host", value: "example.com" });
+
+ vview.window.focus();
+
+ msg.scrollIntoView();
+ sidebarClosed = jsterm.once("sidebar-closed");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield sidebarClosed;
+
+ function* openSidebar(objName, expectedText, expectedObj) {
+ msg = yield jsterm.execute(objName);
+ ok(msg, "output message found");
+
+ let anchor = msg.querySelector("a");
+ let body = msg.querySelector(".message-body");
+ ok(anchor, "object anchor");
+ ok(body, "message body");
+ ok(body.textContent.includes(expectedText), "message text check");
+
+ msg.scrollIntoView();
+ yield EventUtils.synthesizeMouse(anchor, 2, 2, {}, hud.iframeWindow);
+
+ let vviewVar = yield jsterm.once("variablesview-fetched");
+ vview = vviewVar._variablesView;
+ ok(vview, "variables view object exists");
+
+ [result] = yield findVariableViewProperties(vviewVar, [
+ expectedObj,
+ ], { webconsole: hud });
+ }
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_bug_869003_inspect_cross_domain_object.js b/devtools/client/webconsole/test/browser_bug_869003_inspect_cross_domain_object.js
new file mode 100644
index 000000000..685148fc7
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_bug_869003_inspect_cross_domain_object.js
@@ -0,0 +1,77 @@
+/* -*- 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/ */
+
+// Check that users can inspect objects logged from cross-domain iframes -
+// bug 869003.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-869003-top-window.html";
+
+add_task(function* () {
+ // This test is slightly more involved: it opens the web console, then the
+ // variables view for a given object, it updates a property in the view and
+ // checks the result. We can get a timeout with debug builds on slower
+ // machines.
+ requestLongerTimeout(2);
+
+ yield loadTab("data:text/html;charset=utf8,<p>hello");
+ let hud = yield openConsole();
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI);
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "console.log message",
+ text: "foobar",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ objects: true,
+ }],
+ });
+
+ let msg = [...result.matched][0];
+ ok(msg, "message element");
+
+ let body = msg.querySelector(".message-body");
+ ok(body, "message body");
+
+ let clickable = result.clickableElements[0];
+ ok(clickable, "clickable object found");
+ ok(body.textContent.includes('{ hello: "world!",'), "message text check");
+
+ executeSoon(() => {
+ EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow);
+ });
+
+ let aVar = yield hud.jsterm.once("variablesview-fetched");
+ ok(aVar, "variables view fetched");
+ ok(aVar._variablesView, "variables view object");
+
+ [result] = yield findVariableViewProperties(aVar, [
+ { name: "hello", value: "world!" },
+ { name: "bug", value: 869003 },
+ ], { webconsole: hud });
+
+ let prop = result.matchedProp;
+ ok(prop, "matched the |hello| property in the variables view");
+
+ // Check that property value updates work.
+ aVar = yield updateVariablesViewProperty({
+ property: prop,
+ field: "value",
+ string: "'omgtest'",
+ webconsole: hud,
+ });
+
+ info("onFetchAfterUpdate");
+
+ yield findVariableViewProperties(aVar, [
+ { name: "hello", value: "omgtest" },
+ { name: "bug", value: 869003 },
+ ], { webconsole: hud });
+});
diff --git a/devtools/client/webconsole/test/browser_bug_871156_ctrlw_close_tab.js b/devtools/client/webconsole/test/browser_bug_871156_ctrlw_close_tab.js
new file mode 100644
index 000000000..c1698cf91
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_bug_871156_ctrlw_close_tab.js
@@ -0,0 +1,78 @@
+/* -*- 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/ */
+
+// 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(function* () {
+ const TEST_URI = "data:text/html;charset=utf8,<title>bug871156</title>\n" +
+ "<p>hello world";
+ let firstTab = gBrowser.selectedTab;
+
+ Services.prefs.setBoolPref("browser.tabs.animate", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.tabs.animate");
+ });
+
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+ ok(hud, "Web Console opened");
+
+ let tabClosed = promise.defer();
+ let toolboxDestroyed = promise.defer();
+ let tabSelected = promise.defer();
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = gDevTools.getToolbox(target);
+
+ gBrowser.tabContainer.addEventListener("TabClose", function onTabClose() {
+ gBrowser.tabContainer.removeEventListener("TabClose", onTabClose);
+ info("tab closed");
+ tabClosed.resolve(null);
+ });
+
+ gBrowser.tabContainer.addEventListener("TabSelect", function onTabSelect() {
+ gBrowser.tabContainer.removeEventListener("TabSelect", onTabSelect);
+ if (gBrowser.selectedTab == firstTab) {
+ info("tab selected");
+ tabSelected.resolve(null);
+ }
+ });
+
+ toolbox.once("destroyed", () => {
+ info("toolbox destroyed");
+ toolboxDestroyed.resolve(null);
+ });
+
+ // Get out of the web console initialization.
+ executeSoon(() => {
+ EventUtils.synthesizeKey("w", { accelKey: true });
+ });
+
+ yield promise.all([tabClosed.promise, toolboxDestroyed.promise,
+ tabSelected.promise]);
+ info("promise.all resolved. start testing the Browser Console");
+
+ hud = yield HUDService.toggleBrowserConsole();
+ ok(hud, "Browser Console opened");
+
+ let deferred = promise.defer();
+
+ Services.obs.addObserver(function onDestroy() {
+ Services.obs.removeObserver(onDestroy, "web-console-destroyed");
+ ok(true, "the Browser Console closed");
+
+ deferred.resolve(null);
+ }, "web-console-destroyed", false);
+
+ waitForFocus(() => {
+ EventUtils.synthesizeKey("w", { accelKey: true }, hud.iframeWindow);
+ }, hud.iframeWindow);
+
+ yield deferred.promise;
+});
diff --git a/devtools/client/webconsole/test/browser_cached_messages.js b/devtools/client/webconsole/test/browser_cached_messages.js
new file mode 100644
index 000000000..bf69deee3
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_cached_messages.js
@@ -0,0 +1,59 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test to see if the cached messages are displayed when the console UI is
+// opened.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-webconsole-error-observer.html";
+
+// On e10s, the exception is triggered in child process
+// and is ignored by test harness
+if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ loadTab(TEST_URI).then(testOpenUI);
+}
+
+function testOpenUI(aTestReopen) {
+ openConsole().then((hud) => {
+ waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ text: "log Bazzle",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "error Bazzle",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_ERROR,
+ },
+ {
+ text: "bazBug611032",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ },
+ {
+ text: "cssColorBug611032",
+ category: CATEGORY_CSS,
+ severity: SEVERITY_WARNING,
+ },
+ ],
+ }).then(() => {
+ closeConsole(gBrowser.selectedTab).then(() => {
+ aTestReopen && info("will reopen the Web Console");
+ executeSoon(aTestReopen ? testOpenUI : finishTest);
+ });
+ });
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_console.js b/devtools/client/webconsole/test/browser_console.js
new file mode 100644
index 000000000..7bd1ffdc2
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console.js
@@ -0,0 +1,160 @@
+/* -*- 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/ */
+
+// Test the basic features of the Browser Console, bug 587757.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html?" + Date.now();
+const TEST_FILE = "chrome://mochitests/content/browser/devtools/client/" +
+ "webconsole/test/test-cu-reporterror.js";
+
+const TEST_XHR_ERROR_URI = `http://example.com/404.html?${Date.now()}`;
+
+const TEST_IMAGE = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-image.png";
+
+"use strict";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let opened = waitForConsole();
+
+ let hud = HUDService.getBrowserConsole();
+ ok(!hud, "browser console is not open");
+ info("wait for the browser console to open with ctrl-shift-j");
+ EventUtils.synthesizeKey("j", { accelKey: true, shiftKey: true }, window);
+
+ hud = yield opened;
+ ok(hud, "browser console opened");
+
+ yield consoleOpened(hud);
+});
+
+function consoleOpened(hud) {
+ hud.jsterm.clearOutput(true);
+
+ expectUncaughtException();
+ executeSoon(() => {
+ foobarExceptionBug587757();
+ });
+
+ // Add a message from a chrome window.
+ hud.iframeWindow.console.log("bug587757a");
+
+ // Check Cu.reportError stack.
+ // Use another js script to not depend on the test file line numbers.
+ Services.scriptloader.loadSubScript(TEST_FILE, hud.iframeWindow);
+
+ // Add a message from a content window.
+ content.console.log("bug587757b");
+
+ // Test eval.
+ hud.jsterm.execute("document.location.href");
+
+ // Check for network requests.
+ let 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.
+ let 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".
+ fetch(TEST_IMAGE).then(() => { console.log("fetch loaded"); });
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "chrome window console.log() is displayed",
+ text: "bug587757a",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ name: "Cu.reportError is displayed",
+ text: "bug1141222",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ stacktrace: [{
+ file: TEST_FILE,
+ line: 2,
+ }, {
+ file: TEST_FILE,
+ line: 4,
+ },
+ // Ignore the rest of the stack,
+ // just assert Cu.reportError call site
+ // and consoleOpened call
+ ]
+ },
+ {
+ name: "content window console.log() is displayed",
+ text: "bug587757b",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ name: "jsterm eval result",
+ text: "browser.xul",
+ category: CATEGORY_OUTPUT,
+ severity: SEVERITY_LOG,
+ },
+ {
+ name: "exception message",
+ text: "foobarExceptionBug587757",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ },
+ {
+ name: "network message",
+ text: "test-console.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_INFO,
+ isXhr: true,
+ },
+ {
+ name: "xhr error message",
+ text: "404.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_ERROR,
+ isXhr: true,
+ },
+ {
+ name: "network message",
+ text: "test-image.png",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_INFO,
+ isXhr: true,
+ },
+ ],
+ });
+}
+
+function waitForConsole() {
+ let deferred = promise.defer();
+
+ Services.obs.addObserver(function observer(aSubject) {
+ Services.obs.removeObserver(observer, "web-console-created");
+ aSubject.QueryInterface(Ci.nsISupportsString);
+
+ let hud = HUDService.getBrowserConsole();
+ ok(hud, "browser console is open");
+ is(aSubject.data, hud.hudId, "notification hudId is correct");
+
+ executeSoon(() => deferred.resolve(hud));
+ }, "web-console-created", false);
+
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_console_addonsdk_loader_exception.js b/devtools/client/webconsole/test/browser_console_addonsdk_loader_exception.js
new file mode 100644
index 000000000..3eec65de3
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_addonsdk_loader_exception.js
@@ -0,0 +1,92 @@
+/* -*- 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/ */
+
+// Check that exceptions from scripts loaded with the addon-sdk loader are
+// opened correctly in View Source from the Browser Console.
+// See bug 866950.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<p>hello world from bug 866950";
+
+function test() {
+ requestLongerTimeout(2);
+
+ let webconsole, browserconsole;
+
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ let {tab} = yield loadTab(TEST_URI);
+ webconsole = yield openConsole(tab);
+ ok(webconsole, "web console opened");
+
+ browserconsole = yield HUDService.toggleBrowserConsole();
+ ok(browserconsole, "browser console opened");
+
+ // Cause an exception in a script loaded with the addon-sdk loader.
+ let toolbox = gDevTools.getToolbox(webconsole.target);
+ let oldPanels = toolbox._toolPanels;
+ // non-iterable
+ toolbox._toolPanels = {};
+
+ function fixToolbox() {
+ toolbox._toolPanels = oldPanels;
+ }
+
+ info("generate exception and wait for message");
+
+ executeSoon(() => {
+ executeSoon(fixToolbox);
+ expectUncaughtException();
+ toolbox.getToolPanels();
+ });
+
+ let [result] = yield waitForMessages({
+ webconsole: browserconsole,
+ messages: [{
+ text: "TypeError: this._toolPanels is not iterable",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ }],
+ });
+
+ fixToolbox();
+
+ let msg = [...result.matched][0];
+ ok(msg, "message element found");
+ let locationNode = msg
+ .querySelector(".message .message-location > .frame-link");
+ ok(locationNode, "message location element found");
+
+ let url = locationNode.getAttribute("data-url");
+ info("location node url: " + url);
+ ok(url.indexOf("resource://") === 0, "error comes from a subscript");
+
+ let viewSource = browserconsole.viewSource;
+ let URL = null;
+ let clickPromise = promise.defer();
+ browserconsole.viewSourceInDebugger = (sourceURL) => {
+ info("browserconsole.viewSourceInDebugger() was invoked: " + sourceURL);
+ URL = sourceURL;
+ clickPromise.resolve(null);
+ };
+
+ msg.scrollIntoView();
+ EventUtils.synthesizeMouse(locationNode, 2, 2, {},
+ browserconsole.iframeWindow);
+
+ info("wait for click on locationNode");
+ yield clickPromise.promise;
+
+ info("view-source url: " + URL);
+ ok(URL, "we have some source URL after the click");
+ isnot(URL.indexOf("toolbox.js"), -1,
+ "we have the expected view source URL");
+ is(URL.indexOf("->"), -1, "no -> in the URL given to view-source");
+
+ browserconsole.viewSourceInDebugger = viewSource;
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_console_clear_method.js b/devtools/client/webconsole/test/browser_console_clear_method.js
new file mode 100644
index 000000000..33b43850e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_clear_method.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/ */
+
+// Check that console.clear() does not clear the output of the browser console.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<p>Bug 1296870";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield HUDService.toggleBrowserConsole();
+
+ info("Log a new message from the content page");
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.wrappedJSObject.console.log("msg");
+ });
+ yield waitForMessage("msg", hud);
+
+ info("Send a console.clear() from the content page");
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.wrappedJSObject.console.clear();
+ });
+ yield waitForMessage("Console was cleared", hud);
+
+ info("Check that the messages logged after the first clear are still displayed");
+ isnot(hud.outputNode.textContent.indexOf("msg"), -1, "msg is in the output");
+});
+
+function waitForMessage(message, webconsole) {
+ return waitForMessages({
+ webconsole,
+ messages: [{
+ text: message,
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_console_clear_on_reload.js b/devtools/client/webconsole/test/browser_console_clear_on_reload.js
new file mode 100644
index 000000000..223eb028d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_clear_on_reload.js
@@ -0,0 +1,86 @@
+/* -*- 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/ */
+
+// Check that clear output on page reload works - bug 705921.
+// Check that clear output and page reload remove the sidebar - bug 971967.
+
+"use strict";
+
+add_task(function* () {
+ const PREF = "devtools.webconsole.persistlog";
+ const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+ Services.prefs.setBoolPref(PREF, false);
+ registerCleanupFunction(() => Services.prefs.clearUserPref(PREF));
+
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+ ok(hud, "Web Console opened");
+
+ yield openSidebar("fooObj", { name: "testProp", value: "testValue" });
+
+ let sidebarClosed = hud.jsterm.once("sidebar-closed");
+ hud.jsterm.clearOutput();
+ yield sidebarClosed;
+
+ hud.jsterm.execute("console.log('foobarz1')");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foobarz1",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ yield openSidebar("fooObj", { name: "testProp", value: "testValue" });
+
+ BrowserReload();
+
+ sidebarClosed = hud.jsterm.once("sidebar-closed");
+ loadBrowser(gBrowser.selectedBrowser);
+ yield sidebarClosed;
+
+ hud.jsterm.execute("console.log('foobarz2')");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "test-console.html",
+ category: CATEGORY_NETWORK,
+ },
+ {
+ text: "foobarz2",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ is(hud.outputNode.textContent.indexOf("foobarz1"), -1,
+ "foobarz1 has been removed from output");
+
+ function* openSidebar(objName, expectedObj) {
+ let msg = yield hud.jsterm.execute(objName);
+ ok(msg, "output message found");
+
+ let anchor = msg.querySelector("a");
+ let body = msg.querySelector(".message-body");
+ ok(anchor, "object anchor");
+ ok(body, "message body");
+
+ yield EventUtils.synthesizeMouse(anchor, 2, 2, {}, hud.iframeWindow);
+
+ let vviewVar = yield hud.jsterm.once("variablesview-fetched");
+ let vview = vviewVar._variablesView;
+ ok(vview, "variables view object exists");
+
+ yield findVariableViewProperties(vviewVar, [
+ expectedObj,
+ ], { webconsole: hud });
+ }
+});
diff --git a/devtools/client/webconsole/test/browser_console_click_focus.js b/devtools/client/webconsole/test/browser_console_click_focus.js
new file mode 100644
index 000000000..f405f0bbf
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_click_focus.js
@@ -0,0 +1,59 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the input field is focused when the console is opened.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "Dolske Digs Bacon",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ let msg = [...result.matched][0];
+ let outputItem = msg.querySelector(".message-body");
+ ok(outputItem, "found a logged message");
+
+ let inputNode = hud.jsterm.inputNode;
+ ok(inputNode.getAttribute("focused"), "input node is focused, first");
+
+ yield waitForBlurredInput(inputNode);
+
+ EventUtils.sendMouseEvent({type: "click"}, hud.outputNode);
+ ok(inputNode.getAttribute("focused"), "input node is focused, second time");
+
+ yield waitForBlurredInput(inputNode);
+
+ info("Setting a text selection and making sure a click does not re-focus");
+ let selection = hud.iframeWindow.getSelection();
+ selection.selectAllChildren(outputItem);
+
+ EventUtils.sendMouseEvent({type: "click"}, hud.outputNode);
+ ok(!inputNode.getAttribute("focused"),
+ "input node is not focused after drag");
+});
+
+function waitForBlurredInput(inputNode) {
+ return new Promise(resolve => {
+ let lostFocus = () => {
+ inputNode.removeEventListener("blur", lostFocus);
+ ok(!inputNode.getAttribute("focused"), "input node is not focused");
+ resolve();
+ };
+ inputNode.addEventListener("blur", lostFocus);
+ document.getElementById("urlbar").click();
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_console_consolejsm_output.js b/devtools/client/webconsole/test/browser_console_consolejsm_output.js
new file mode 100644
index 000000000..e5b37843e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_consolejsm_output.js
@@ -0,0 +1,285 @@
+/* -*- 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/ */
+
+// Test that Console.jsm outputs messages to the Browser Console, bug 851231.
+
+"use strict";
+
+function onNewMessage(aEvent, aNewMessages) {
+ for (let msg of aNewMessages) {
+ // Messages that shouldn't be output contain the substring FAIL_TEST
+ if (msg.node.textContent.includes("FAIL_TEST")) {
+ ok(false, "Message shouldn't have been output: " + msg.node.textContent);
+ }
+ }
+}
+
+add_task(function* () {
+ let consoleStorage = Cc["@mozilla.org/consoleAPI-storage;1"];
+ let storage = consoleStorage.getService(Ci.nsIConsoleAPIStorage);
+ storage.clearEvents();
+
+ let {console} = Cu.import("resource://gre/modules/Console.jsm", {});
+ console.log("bug861338-log-cached");
+
+ let hud = yield HUDService.toggleBrowserConsole();
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "cached console.log message",
+ text: "bug861338-log-cached",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ hud.jsterm.clearOutput(true);
+
+ function testTrace() {
+ console.trace();
+ }
+
+ console.time("foobarTimer");
+ let 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(document);
+ testTrace();
+ console.timeEnd("foobarTimer");
+
+ info("wait for the Console.jsm messages");
+
+ let results = yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "console.log output",
+ text: "bug851231-log",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ name: "console.info output",
+ text: "bug851231-info",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_INFO,
+ },
+ {
+ name: "console.warn output",
+ text: "bug851231-warn",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_WARNING,
+ },
+ {
+ name: "console.error output",
+ text: /\bbug851231-error\b.+\{\s*bug851231prop:\s"bug851231value"\s*\}/,
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_ERROR,
+ objects: true,
+ },
+ {
+ name: "console.debug output",
+ text: "bug851231-debug",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ name: "console.trace output",
+ consoleTrace: {
+ file: "browser_console_consolejsm_output.js",
+ fn: "testTrace",
+ },
+ },
+ {
+ name: "console.dir output",
+ consoleDir: /XULDocument\s+.+\s+chrome:\/\/.+\/browser\.xul/,
+ },
+ {
+ name: "console.time output",
+ consoleTime: "foobarTimer",
+ },
+ {
+ name: "console.timeEnd output",
+ consoleTimeEnd: "foobarTimer",
+ },
+ ],
+ });
+
+ let consoleErrorMsg = results[3];
+ ok(consoleErrorMsg, "console.error message element found");
+ let clickable = consoleErrorMsg.clickableElements[0];
+ ok(clickable, "clickable object found for console.error");
+
+ let deferred = promise.defer();
+
+ let onFetch = (aEvent, aVar) => {
+ // Skip the notification from console.dir variablesview-fetched.
+ if (aVar._variablesView != hud.jsterm._variablesView) {
+ return;
+ }
+ hud.jsterm.off("variablesview-fetched", onFetch);
+
+ deferred.resolve(aVar);
+ };
+
+ hud.jsterm.on("variablesview-fetched", onFetch);
+
+ clickable.scrollIntoView(false);
+
+ info("wait for variablesview-fetched");
+ executeSoon(() =>
+ EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow));
+
+ let varView = yield deferred.promise;
+ ok(varView, "object inspector opened on click");
+
+ yield findVariableViewProperties(varView, [{
+ name: "bug851231prop",
+ value: "bug851231value",
+ }], { webconsole: hud });
+
+ yield HUDService.toggleBrowserConsole();
+});
+
+add_task(function* testPrefix() {
+ let consoleStorage = Cc["@mozilla.org/consoleAPI-storage;1"];
+ let storage = consoleStorage.getService(Ci.nsIConsoleAPIStorage);
+ storage.clearEvents();
+
+ let {ConsoleAPI} = Cu.import("resource://gre/modules/Console.jsm", {});
+ let consoleOptions = {
+ maxLogLevel: "error",
+ prefix: "Log Prefix",
+ };
+ let console2 = new ConsoleAPI(consoleOptions);
+ console2.error("Testing a prefix");
+ console2.log("FAIL_TEST: Below the maxLogLevel");
+
+ let hud = yield HUDService.toggleBrowserConsole();
+ hud.ui.on("new-messages", onNewMessage);
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "cached console.error message",
+ prefix: "Log Prefix:",
+ severity: SEVERITY_ERROR,
+ text: "Testing a prefix",
+ }],
+ });
+
+ hud.jsterm.clearOutput(true);
+ hud.ui.off("new-messages", onNewMessage);
+ yield HUDService.toggleBrowserConsole();
+});
+
+add_task(function* testMaxLogLevelPrefMissing() {
+ let consoleStorage = Cc["@mozilla.org/consoleAPI-storage;1"];
+ let storage = consoleStorage.getService(Ci.nsIConsoleAPIStorage);
+ storage.clearEvents();
+
+ let {ConsoleAPI} = Cu.import("resource://gre/modules/Console.jsm", {});
+ let consoleOptions = {
+ maxLogLevel: "error",
+ maxLogLevelPref: "testing.maxLogLevel",
+ };
+ let console = new ConsoleAPI(consoleOptions);
+
+ is(Services.prefs.getPrefType(consoleOptions.maxLogLevelPref),
+ Services.prefs.PREF_INVALID,
+ "Check log level pref is missing");
+
+ // Since the maxLogLevelPref doesn't exist, we should fallback to the passed
+ // maxLogLevel of "error".
+ console.warn("FAIL_TEST: Below the maxLogLevel");
+ console.error("Error should be shown");
+
+ let hud = yield HUDService.toggleBrowserConsole();
+
+ hud.ui.on("new-messages", onNewMessage);
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "defaulting to error level",
+ severity: SEVERITY_ERROR,
+ text: "Error should be shown",
+ }],
+ });
+
+ hud.jsterm.clearOutput(true);
+ hud.ui.off("new-messages", onNewMessage);
+ yield HUDService.toggleBrowserConsole();
+});
+
+add_task(function* testMaxLogLevelPref() {
+ let consoleStorage = Cc["@mozilla.org/consoleAPI-storage;1"];
+ let storage = consoleStorage.getService(Ci.nsIConsoleAPIStorage);
+ storage.clearEvents();
+
+ let {ConsoleAPI} = Cu.import("resource://gre/modules/Console.jsm", {});
+ let consoleOptions = {
+ maxLogLevel: "error",
+ maxLogLevelPref: "testing.maxLogLevel",
+ };
+
+ info("Setting the pref to warn");
+ Services.prefs.setCharPref(consoleOptions.maxLogLevelPref, "Warn");
+
+ let console = new ConsoleAPI(consoleOptions);
+
+ is(console.maxLogLevel, "warn", "Check pref was read at initialization");
+
+ console.info("FAIL_TEST: info is below the maxLogLevel");
+ console.error("Error should be shown");
+ console.warn("Warn should be shown due to the initial pref value");
+
+ info("Setting the pref to info");
+ Services.prefs.setCharPref(consoleOptions.maxLogLevelPref, "INFO");
+ is(console.maxLogLevel, "info", "Check pref was lowercased");
+
+ console.info("info should be shown due to the pref change being observed");
+
+ info("Clearing the pref");
+ Services.prefs.clearUserPref(consoleOptions.maxLogLevelPref);
+
+ console.warn("FAIL_TEST: Shouldn't be shown due to defaulting to error");
+ console.error("Should be shown due to defaulting to error");
+
+ let hud = yield HUDService.toggleBrowserConsole();
+ hud.ui.on("new-messages", onNewMessage);
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "error > warn",
+ severity: SEVERITY_ERROR,
+ text: "Error should be shown",
+ },
+ {
+ name: "warn is the inital pref value",
+ severity: SEVERITY_WARNING,
+ text: "Warn should be shown due to the initial pref value",
+ },
+ {
+ name: "pref changed to info",
+ severity: SEVERITY_INFO,
+ text: "info should be shown due to the pref change being observed",
+ },
+ {
+ name: "default to intial maxLogLevel if pref is removed",
+ severity: SEVERITY_ERROR,
+ text: "Should be shown due to defaulting to error",
+ }],
+ });
+
+ hud.jsterm.clearOutput(true);
+ hud.ui.off("new-messages", onNewMessage);
+ yield HUDService.toggleBrowserConsole();
+});
diff --git a/devtools/client/webconsole/test/browser_console_copy_command.js b/devtools/client/webconsole/test/browser_console_copy_command.js
new file mode 100644
index 000000000..c4ed4360f
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_copy_command.js
@@ -0,0 +1,76 @@
+/* -*- 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 the `copy` console helper works as intended.
+
+"use strict";
+
+var gWebConsole, gJSTerm;
+
+var 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();
+
+var ID = "select-me";
+
+add_task(function* init() {
+ yield loadTab("data:text/html;charset=utf-8," +
+ "<body>" +
+ " <div>" +
+ " <h1>Testing copy command</h1>" +
+ " <p>This is some example text</p>" +
+ " <p id='select-me'>" + TEXT + "</p>" +
+ " </div>" +
+ " <div><p></p></div>" +
+ "</body>");
+
+ gWebConsole = yield openConsole();
+ gJSTerm = gWebConsole.jsterm;
+});
+
+add_task(function* testCopy() {
+ let RANDOM = Math.random();
+ let string = "Text: " + RANDOM;
+ let obj = {a: 1, b: "foo", c: RANDOM};
+
+ let samples = [
+ [RANDOM, RANDOM],
+ [JSON.stringify(string), string],
+ [obj.toSource(), JSON.stringify(obj, null, " ")],
+ [
+ "$('#" + ID + "')",
+ content.document.getElementById(ID).outerHTML
+ ]
+ ];
+ for (let [source, reference] of samples) {
+ let deferredResult = promise.defer();
+
+ SimpleTest.waitForClipboard(
+ "" + reference,
+ () => {
+ let command = "copy(" + source + ")";
+ info("Attempting to copy: " + source);
+ info("Executing command: " + command);
+ gJSTerm.execute(command, msg => {
+ is(msg, undefined, "Command success: " + command);
+ });
+ },
+ deferredResult.resolve,
+ deferredResult.reject);
+
+ yield deferredResult.promise;
+ }
+});
+
+add_task(function* cleanup() {
+ gWebConsole = gJSTerm = null;
+ gBrowser.removeTab(gBrowser.selectedTab);
+ finishTest();
+});
diff --git a/devtools/client/webconsole/test/browser_console_copy_entire_message_context_menu.js b/devtools/client/webconsole/test/browser_console_copy_entire_message_context_menu.js
new file mode 100644
index 000000000..bdd4f7179
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_copy_entire_message_context_menu.js
@@ -0,0 +1,97 @@
+/* -*- 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/ */
+
+/* globals goDoCommand */
+
+"use strict";
+
+// Test copying of the entire console message when right-clicked
+// with no other text selected. See Bug 1100562.
+
+add_task(function* () {
+ let hud;
+ let outputNode;
+ let contextMenu;
+
+ const TEST_URI = "http://example.com/browser/devtools/client/webconsole/test/test-console.html";
+
+ const { tab, browser } = yield loadTab(TEST_URI);
+ hud = yield openConsole(tab);
+ outputNode = hud.outputNode;
+ contextMenu = hud.iframeWindow.document.getElementById("output-contextmenu");
+
+ registerCleanupFunction(() => {
+ hud = outputNode = contextMenu = null;
+ });
+
+ hud.jsterm.clearOutput();
+
+ yield ContentTask.spawn(browser, {}, function* () {
+ let button = content.document.getElementById("testTrace");
+ button.click();
+ });
+
+ let results = yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ text: "bug 1100562",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ lines: 1,
+ },
+ {
+ name: "console.trace output",
+ consoleTrace: true,
+ lines: 3,
+ },
+ ]
+ });
+
+ outputNode.focus();
+
+ for (let result of results) {
+ let message = [...result.matched][0];
+
+ yield waitForContextMenu(contextMenu, message, () => {
+ let copyItem = contextMenu.querySelector("#cMenu_copy");
+ copyItem.doCommand();
+
+ let controller = top.document.commandDispatcher
+ .getControllerForCommand("cmd_copy");
+ is(controller.isCommandEnabled("cmd_copy"), true, "cmd_copy is enabled");
+ });
+
+ let clipboardText;
+
+ yield waitForClipboardPromise(
+ () => goDoCommand("cmd_copy"),
+ (str) => {
+ clipboardText = str;
+ return message.textContent == clipboardText;
+ }
+ );
+
+ ok(clipboardText, "Clipboard text was found and saved");
+
+ let lines = clipboardText.split("\n");
+ ok(lines.length > 0, "There is at least one newline in the message");
+ is(lines.pop(), "", "There is a newline at the end");
+ is(lines.length, result.lines, `There are ${result.lines} lines in the message`);
+
+ // Test the first line for "timestamp message repeat file:line"
+ let firstLine = lines.shift();
+ ok(/^[\d:.]+ .+ \d+ .+:\d+$/.test(firstLine),
+ "The message's first line has the right format");
+
+ // Test the remaining lines (stack trace) for "TABfunctionName sourceURL:line:col"
+ for (let line of lines) {
+ ok(/^\t.+ .+:\d+:\d+$/.test(line), "The stack trace line has the right format");
+ }
+ }
+
+ yield closeConsole(tab);
+ yield finishTest();
+});
diff --git a/devtools/client/webconsole/test/browser_console_dead_objects.js b/devtools/client/webconsole/test/browser_console_dead_objects.js
new file mode 100644
index 000000000..46b15d59b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_dead_objects.js
@@ -0,0 +1,88 @@
+/* -*- 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/ */
+
+// Check that Dead Objects do not break the Web/Browser Consoles.
+// See bug 883649.
+// This test does:
+// - opens a new tab,
+// - opens the Browser Console,
+// - stores a reference to the content document of the tab on the chrome
+// window object,
+// - closes the tab,
+// - tries to use the object that was pointing to the now-defunct content
+// document. This is the dead object.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<p>dead objects!";
+
+function test() {
+ let hud = null;
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.chrome.enabled");
+ });
+
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ Services.prefs.setBoolPref("devtools.chrome.enabled", true);
+ yield loadTab(TEST_URI);
+
+ info("open the browser console");
+
+ hud = yield HUDService.toggleBrowserConsole();
+ ok(hud, "browser console opened");
+
+ let jsterm = hud.jsterm;
+
+ jsterm.clearOutput();
+
+ // Add the reference to the content document.
+ yield jsterm.execute("Cu = Components.utils;" +
+ "Cu.import('resource://gre/modules/Services.jsm');" +
+ "chromeWindow = Services.wm.getMostRecentWindow('" +
+ "navigator:browser');" +
+ "foobarzTezt = chromeWindow.content.document;" +
+ "delete chromeWindow");
+
+ gBrowser.removeCurrentTab();
+
+ let msg = yield jsterm.execute("foobarzTezt");
+
+ isnot(hud.outputNode.textContent.indexOf("[object DeadObject]"), -1,
+ "dead object found");
+
+ jsterm.setInputValue("foobarzTezt");
+
+ for (let c of ".hello") {
+ EventUtils.synthesizeKey(c, {}, hud.iframeWindow);
+ }
+
+ yield jsterm.execute();
+
+ isnot(hud.outputNode.textContent.indexOf("can't access dead object"), -1,
+ "'cannot access dead object' message found");
+
+ // Click the second execute output.
+ let clickable = msg.querySelector("a");
+ ok(clickable, "clickable object found");
+ isnot(clickable.textContent.indexOf("[object DeadObject]"), -1,
+ "message text check");
+
+ msg.scrollIntoView();
+
+ executeSoon(() => {
+ EventUtils.synthesizeMouseAtCenter(clickable, {}, hud.iframeWindow);
+ });
+
+ yield jsterm.once("variablesview-fetched");
+ ok(true, "variables view fetched");
+
+ msg = yield jsterm.execute("delete window.foobarzTezt; 2013-26");
+
+ isnot(msg.textContent.indexOf("1987"), -1, "result message found");
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_console_error_source_click.js b/devtools/client/webconsole/test/browser_console_error_source_click.js
new file mode 100644
index 000000000..5839f20d5
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_error_source_click.js
@@ -0,0 +1,79 @@
+/* -*- 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/ */
+
+// Check that JS errors and CSS warnings open view source when their source link
+// is clicked in the Browser Console. See bug 877778.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<p>hello world from bug 877778 " +
+ "<button onclick='foobar.explode()' " +
+ "style='test-color: green-please'>click!</button>";
+
+add_task(function* () {
+ yield new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["devtools.browserconsole.filter.cssparser", true]
+ ]}, resolve);
+ });
+
+ yield loadTab(TEST_URI);
+ let hud = yield HUDService.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");
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let button = content.document.querySelector("button");
+ button.click();
+ });
+
+ let results = yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ text: "ReferenceError: foobar is not defined",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ },
+ {
+ text: "Unknown property \u2018test-color\u2019",
+ category: CATEGORY_CSS,
+ severity: SEVERITY_WARNING,
+ },
+ ],
+ });
+
+ let viewSourceCalled = false;
+
+ let viewSource = hud.viewSource;
+ hud.viewSource = () => {
+ viewSourceCalled = true;
+ };
+
+ for (let result of results) {
+ viewSourceCalled = false;
+
+ let msg = [...result.matched][0];
+ ok(msg, "message element found for: " + result.text);
+ ok(!msg.classList.contains("filtered-by-type"), "message element is not filtered");
+ let selector = ".message .message-location .frame-link-source";
+ let locationNode = msg.querySelector(selector);
+ ok(locationNode, "message location element found");
+
+ locationNode.click();
+
+ ok(viewSourceCalled, "view source opened");
+ }
+
+ hud.viewSource = viewSource;
+
+ yield finishTest();
+});
diff --git a/devtools/client/webconsole/test/browser_console_filters.js b/devtools/client/webconsole/test/browser_console_filters.js
new file mode 100644
index 000000000..072766fdb
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_filters.js
@@ -0,0 +1,60 @@
+/* -*- 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/ */
+
+// Check that the Browser Console does not use the same filter prefs as the Web
+// Console. See bug 878186.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<p>browser console filters";
+const WEB_CONSOLE_PREFIX = "devtools.webconsole.filter.";
+const BROWSER_CONSOLE_PREFIX = "devtools.browserconsole.filter.";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ info("open the web console");
+ let hud = yield openConsole();
+ ok(hud, "web console opened");
+
+ is(Services.prefs.getBoolPref(BROWSER_CONSOLE_PREFIX + "exception"), true,
+ "'exception' filter is enabled (browser console)");
+ is(Services.prefs.getBoolPref(WEB_CONSOLE_PREFIX + "exception"), true,
+ "'exception' filter is enabled (web console)");
+
+ info("toggle 'exception' filter");
+ hud.setFilterState("exception", false);
+
+ is(Services.prefs.getBoolPref(BROWSER_CONSOLE_PREFIX + "exception"), true,
+ "'exception' filter is enabled (browser console)");
+ is(Services.prefs.getBoolPref(WEB_CONSOLE_PREFIX + "exception"), false,
+ "'exception' filter is disabled (web console)");
+
+ hud.setFilterState("exception", true);
+
+ // We need to let the console opening event loop to finish.
+ let deferred = promise.defer();
+ executeSoon(() => closeConsole().then(() => deferred.resolve(null)));
+ yield deferred.promise;
+
+ info("web console closed");
+ hud = yield HUDService.toggleBrowserConsole();
+ ok(hud, "browser console opened");
+
+ is(Services.prefs.getBoolPref(BROWSER_CONSOLE_PREFIX + "exception"), true,
+ "'exception' filter is enabled (browser console)");
+ is(Services.prefs.getBoolPref(WEB_CONSOLE_PREFIX + "exception"), true,
+ "'exception' filter is enabled (web console)");
+
+ info("toggle 'exception' filter");
+ hud.setFilterState("exception", false);
+
+ is(Services.prefs.getBoolPref(BROWSER_CONSOLE_PREFIX + "exception"), false,
+ "'exception' filter is disabled (browser console)");
+ is(Services.prefs.getBoolPref(WEB_CONSOLE_PREFIX + "exception"), true,
+ "'exception' filter is enabled (web console)");
+
+ hud.setFilterState("exception", true);
+});
diff --git a/devtools/client/webconsole/test/browser_console_hide_jsterm_when_devtools_chrome_enabled_false.js b/devtools/client/webconsole/test/browser_console_hide_jsterm_when_devtools_chrome_enabled_false.js
new file mode 100644
index 000000000..d3fdb08be
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_hide_jsterm_when_devtools_chrome_enabled_false.js
@@ -0,0 +1,114 @@
+/* -*- 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/ */
+
+/*
+ * Bug 922161 - 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 not be set.
+ * -webconsole jsterm should be enabled
+ * -webconsole object inspector properties should be set.
+ */
+
+"use strict";
+
+function testObjectInspectorPropertiesAreNotSet(variablesView) {
+ is(variablesView.eval, null, "vview.eval is null");
+ is(variablesView.switch, null, "vview.switch is null");
+ is(variablesView.delete, null, "vview.delete is null");
+}
+
+function* getVariablesView(hud) {
+ function openVariablesView(event, vview) {
+ deferred.resolve(vview._variablesView);
+ }
+
+ let deferred = promise.defer();
+
+ // Filter out other messages to ensure ours stays visible.
+ hud.ui.filterBox.value = "browser_console_hide_jsterm_test";
+
+ hud.jsterm.clearOutput();
+ hud.jsterm.execute("new Object({ browser_console_hide_jsterm_test: true })");
+
+ let [message] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "Object { browser_console_hide_jsterm_test: true }",
+ category: CATEGORY_OUTPUT,
+ }],
+ });
+
+ hud.jsterm.once("variablesview-fetched", openVariablesView);
+
+ let anchor = [...message.matched][0].querySelector("a");
+
+ executeSoon(() =>
+ EventUtils.synthesizeMouse(anchor, 2, 2, {}, hud.iframeWindow)
+ );
+
+ return deferred.promise;
+}
+
+function testJSTermIsVisible(hud) {
+ let inputContainer = hud.ui.window.document
+ .querySelector(".jsterm-input-container");
+ isnot(inputContainer.style.display, "none", "input is visible");
+}
+
+function testObjectInspectorPropertiesAreSet(variablesView) {
+ isnot(variablesView.eval, null, "vview.eval is set");
+ isnot(variablesView.switch, null, "vview.switch is set");
+ isnot(variablesView.delete, null, "vview.delete is set");
+}
+
+function testJSTermIsNotVisible(hud) {
+ let inputContainer = hud.ui.window.document
+ .querySelector(".jsterm-input-container");
+ is(inputContainer.style.display, "none", "input is not visible");
+}
+
+function* testRunner() {
+ let browserConsole, webConsole, variablesView;
+
+ Services.prefs.setBoolPref("devtools.chrome.enabled", true);
+
+ browserConsole = yield HUDService.toggleBrowserConsole();
+ variablesView = yield getVariablesView(browserConsole);
+ testJSTermIsVisible(browserConsole);
+ testObjectInspectorPropertiesAreSet(variablesView);
+
+ let {tab: browserTab} = yield loadTab("data:text/html;charset=utf8,hello world");
+ webConsole = yield openConsole(browserTab);
+ variablesView = yield getVariablesView(webConsole);
+ testJSTermIsVisible(webConsole);
+ testObjectInspectorPropertiesAreSet(variablesView);
+ yield closeConsole(browserTab);
+
+ yield HUDService.toggleBrowserConsole();
+ Services.prefs.setBoolPref("devtools.chrome.enabled", false);
+
+ browserConsole = yield HUDService.toggleBrowserConsole();
+ variablesView = yield getVariablesView(browserConsole);
+ testJSTermIsNotVisible(browserConsole);
+ testObjectInspectorPropertiesAreNotSet(variablesView);
+
+ webConsole = yield openConsole(browserTab);
+ variablesView = yield getVariablesView(webConsole);
+ testJSTermIsVisible(webConsole);
+ testObjectInspectorPropertiesAreSet(variablesView);
+ yield closeConsole(browserTab);
+}
+
+function test() {
+ Task.spawn(testRunner).then(finishTest);
+}
diff --git a/devtools/client/webconsole/test/browser_console_history_persist.js b/devtools/client/webconsole/test/browser_console_history_persist.js
new file mode 100644
index 000000000..61c4cbf4d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_history_persist.js
@@ -0,0 +1,119 @@
+/* -*- 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/ */
+
+// Test that console command input is persisted across toolbox loads.
+// See Bug 943306.
+
+"use strict";
+
+requestLongerTimeout(2);
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for " +
+ "persisting history - bug 943306";
+const INPUT_HISTORY_COUNT = 10;
+
+add_task(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.
+ yield loadTab(TEST_URI);
+ let hud1 = yield openConsole();
+ is(JSON.stringify(hud1.jsterm.history), "[]",
+ "No history on first tab initially");
+ yield populateInputHistory(hud1);
+ is(JSON.stringify(hud1.jsterm.history),
+ '["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.
+ yield loadTab(TEST_URI);
+ let hud2 = yield openConsole();
+ is(JSON.stringify(hud2.jsterm.history),
+ '["0","1","2","3","4","5","6","7","8","9"]',
+ "Second tab has populated history");
+ yield testNaviatingHistoryInUI(hud2);
+ is(JSON.stringify(hud2.jsterm.history),
+ '["0","1","2","3","4","5","6","7","8","9",""]',
+ "An empty entry has been added in the second tab due to history perusal");
+
+ // 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
+ yield loadTab(TEST_URI);
+ let hud3 = yield openConsole();
+ is(JSON.stringify(hud3.jsterm.history),
+ '["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.
+ hud3.jsterm.setInputValue('"hello from third tab"');
+ hud3.jsterm.execute();
+
+ is(JSON.stringify(hud1.jsterm.history),
+ '["0","1","2","3","4","5","6","7","8","9"]',
+ "First tab history hasn't changed due to command in third tab");
+ is(JSON.stringify(hud2.jsterm.history),
+ '["0","1","2","3","4","5","6","7","8","9",""]',
+ "Second tab history hasn't changed due to command in third tab");
+ is(JSON.stringify(hud3.jsterm.history),
+ '["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.
+ yield loadTab(TEST_URI);
+ let hud4 = yield openConsole();
+ is(JSON.stringify(hud4.jsterm.history),
+ '["1","2","3","4","5","6","7","8","9","\\"hello from third tab\\""]',
+ "Fourth tab has most recent history");
+
+ yield hud4.jsterm.clearHistory();
+ is(JSON.stringify(hud4.jsterm.history), "[]",
+ "Clearing history for a tab works");
+
+ yield loadTab(TEST_URI);
+ let hud5 = yield openConsole();
+ is(JSON.stringify(hud5.jsterm.history), "[]",
+ "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]
+ */
+function* populateInputHistory(hud) {
+ let jsterm = hud.jsterm;
+
+ for (let i = 0; i < INPUT_HISTORY_COUNT; i++) {
+ // Set input value separately from execute so UP arrow accurately navigates
+ // history.
+ jsterm.setInputValue(i);
+ jsterm.execute();
+ }
+}
+
+/**
+ * Check pressing up results in history traversal like:
+ * [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
+ */
+function* testNaviatingHistoryInUI(hud) {
+ let jsterm = hud.jsterm;
+ 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("VK_UP", {});
+ is(jsterm.getInputValue(), i, "Pressing up restores last input");
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_console_iframe_messages.js b/devtools/client/webconsole/test/browser_console_iframe_messages.js
new file mode 100644
index 000000000..9bf3fe2b7
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_iframe_messages.js
@@ -0,0 +1,114 @@
+/* -*- 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/ */
+
+// 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/test-consoleiframes.html";
+
+const expectedMessages = [
+ {
+ text: "main file",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "blah",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR
+ },
+ {
+ text: "iframe 2",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG
+ },
+ {
+ text: "iframe 3",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG
+ }
+];
+
+// "iframe 1" console messages can be coalesced into one if they follow each
+// other in the sequence of messages (depending on timing). If they do not, then
+// they will be displayed in the console output independently, as separate
+// messages. This is why we need to match any of the following two rules.
+const expectedMessagesAny = [
+ {
+ name: "iframe 1 (count: 2)",
+ text: "iframe 1",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ count: 2
+ },
+ {
+ name: "iframe 1 (repeats: 2)",
+ text: "iframe 1",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ repeats: 2
+ },
+];
+
+add_task(function* () {
+ // On e10s, the exception is triggered in child process
+ // and is ignored by test harness
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ ok(hud, "web console opened");
+
+ yield testWebConsole(hud);
+ yield closeConsole();
+ info("web console closed");
+
+ hud = yield HUDService.toggleBrowserConsole();
+ yield testBrowserConsole(hud);
+ yield closeConsole();
+});
+
+function* testWebConsole(hud) {
+ yield waitForMessages({
+ webconsole: hud,
+ messages: expectedMessages,
+ });
+
+ info("first messages matched");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: expectedMessagesAny,
+ matchCondition: "any",
+ });
+}
+
+function* testBrowserConsole(hud) {
+ ok(hud, "browser console opened");
+
+ // TODO: The browser console doesn't show page's console.log statements
+ // in e10s windows. See Bug 1241289.
+ if (Services.appinfo.browserTabsRemoteAutostart) {
+ todo(false, "Bug 1241289");
+ return;
+ }
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: expectedMessages,
+ });
+
+ info("first messages matched");
+ yield waitForMessages({
+ webconsole: hud,
+ messages: expectedMessagesAny,
+ matchCondition: "any",
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_console_keyboard_accessibility.js b/devtools/client/webconsole/test/browser_console_keyboard_accessibility.js
new file mode 100644
index 000000000..c64e45f5d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_keyboard_accessibility.js
@@ -0,0 +1,89 @@
+/* -*- 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/ */
+
+// Check that basic keyboard shortcuts work in the web console.
+
+"use strict";
+
+add_task(function* () {
+ const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+ ok(hud, "Web Console opened");
+
+ info("dump some spew into the console for scrolling");
+ hud.jsterm.execute("(function() { for (var i = 0; i < 100; i++) { " +
+ "console.log('foobarz' + i);" +
+ "}})();");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foobarz99",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ let currentPosition = hud.ui.outputWrapper.scrollTop;
+ let bottom = currentPosition;
+
+ EventUtils.synthesizeKey("VK_PAGE_UP", {});
+ isnot(hud.ui.outputWrapper.scrollTop, currentPosition,
+ "scroll position changed after page up");
+
+ currentPosition = hud.ui.outputWrapper.scrollTop;
+ EventUtils.synthesizeKey("VK_PAGE_DOWN", {});
+ ok(hud.ui.outputWrapper.scrollTop > currentPosition,
+ "scroll position now at bottom");
+
+ EventUtils.synthesizeKey("VK_HOME", {});
+ is(hud.ui.outputWrapper.scrollTop, 0, "scroll position now at top");
+
+ EventUtils.synthesizeKey("VK_END", {});
+
+ let scrollTop = hud.ui.outputWrapper.scrollTop;
+ ok(scrollTop > 0 && Math.abs(scrollTop - bottom) <= 5,
+ "scroll position now at bottom");
+
+ info("try ctrl-l to clear output");
+ executeSoon(() => {
+ let clearShortcut;
+ if (Services.appinfo.OS === "Darwin") {
+ clearShortcut = WCUL10n.getStr("webconsole.clear.keyOSX");
+ } else {
+ clearShortcut = WCUL10n.getStr("webconsole.clear.key");
+ }
+ synthesizeKeyShortcut(clearShortcut);
+ });
+ yield hud.jsterm.once("messages-cleared");
+
+ is(hud.outputNode.textContent.indexOf("foobarz1"), -1, "output cleared");
+ is(hud.jsterm.inputNode.getAttribute("focused"), "true",
+ "jsterm input is focused");
+
+ info("try ctrl-f to focus filter");
+ synthesizeKeyShortcut(WCUL10n.getStr("webconsole.find.key"));
+ ok(!hud.jsterm.inputNode.getAttribute("focused"),
+ "jsterm input is not focused");
+ is(hud.ui.filterBox.getAttribute("focused"), "true",
+ "filter input is focused");
+
+ if (Services.appinfo.OS == "Darwin") {
+ ok(hud.ui.getFilterState("network"), "network category is enabled");
+ EventUtils.synthesizeKey("t", { ctrlKey: true });
+ ok(!hud.ui.getFilterState("network"), "accesskey for Network works");
+ EventUtils.synthesizeKey("t", { ctrlKey: true });
+ ok(hud.ui.getFilterState("network"), "accesskey for Network works (again)");
+ } else {
+ EventUtils.synthesizeKey("N", { altKey: true });
+ let net = hud.ui.document.querySelector("toolbarbutton[category=net]");
+ is(hud.ui.document.activeElement, net,
+ "accesskey for Network category focuses the Net button");
+ }
+});
diff --git a/devtools/client/webconsole/test/browser_console_log_inspectable_object.js b/devtools/client/webconsole/test/browser_console_log_inspectable_object.js
new file mode 100644
index 000000000..f9fd85295
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_log_inspectable_object.js
@@ -0,0 +1,52 @@
+/* -*- 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/ */
+
+// Test that objects given to console.log() are inspectable.
+
+"use strict";
+
+add_task(function* () {
+ yield loadTab("data:text/html;charset=utf8,test for bug 676722 - " +
+ "inspectable objects for window.console");
+
+ let hud = yield openConsole();
+ hud.jsterm.clearOutput(true);
+
+ yield hud.jsterm.execute("myObj = {abba: 'omgBug676722'}");
+ hud.jsterm.execute("console.log('fooBug676722', myObj)");
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "fooBug676722",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ objects: true,
+ }],
+ });
+
+ let msg = [...result.matched][0];
+ ok(msg, "message element");
+
+ let body = msg.querySelector(".message-body");
+ ok(body, "message body");
+
+ let clickable = result.clickableElements[0];
+ ok(clickable, "the console.log() object anchor was found");
+ ok(body.textContent.includes('{ abba: "omgBug676722" }'),
+ "clickable node content is correct");
+
+ executeSoon(() => {
+ EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow);
+ });
+
+ let varView = yield hud.jsterm.once("variablesview-fetched");
+ ok(varView, "object inspector opened on click");
+
+ yield findVariableViewProperties(varView, [{
+ name: "abba",
+ value: "omgBug676722",
+ }], { webconsole: hud });
+});
diff --git a/devtools/client/webconsole/test/browser_console_native_getters.js b/devtools/client/webconsole/test/browser_console_native_getters.js
new file mode 100644
index 000000000..1afb70796
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_native_getters.js
@@ -0,0 +1,101 @@
+/* -*- 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/ */
+
+// Check that native getters and setters for DOM elements work as expected in
+// variables view - bug 870220.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<title>bug870220</title>\n" +
+ "<p>hello world\n<p>native getters!";
+
+requestLongerTimeout(2);
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ let jsterm = hud.jsterm;
+
+ jsterm.execute("document");
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "HTMLDocument \u2192 data:text/html;charset=utf8",
+ category: CATEGORY_OUTPUT,
+ objects: true,
+ }],
+ });
+
+ let clickable = result.clickableElements[0];
+ ok(clickable, "clickable object found");
+
+ executeSoon(() => {
+ EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow);
+ });
+
+ let fetchedVar = yield jsterm.once("variablesview-fetched");
+
+ let variablesView = fetchedVar._variablesView;
+ ok(variablesView, "variables view object");
+
+ let results = yield findVariableViewProperties(fetchedVar, [
+ { name: "title", value: "bug870220" },
+ { name: "bgColor" },
+ ], { webconsole: hud });
+
+ let prop = results[1].matchedProp;
+ ok(prop, "matched the |bgColor| property in the variables view");
+
+ // Check that property value updates work.
+ let updatedVar = yield updateVariablesViewProperty({
+ property: prop,
+ field: "value",
+ string: "'red'",
+ webconsole: hud,
+ });
+
+ info("on fetch after background update");
+
+ jsterm.clearOutput(true);
+ jsterm.execute("document.bgColor");
+
+ [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "red",
+ category: CATEGORY_OUTPUT,
+ }],
+ });
+
+ yield findVariableViewProperties(updatedVar, [
+ { name: "bgColor", value: "red" },
+ ], { webconsole: hud });
+
+ jsterm.execute("$$('p')");
+
+ [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "Array [",
+ category: CATEGORY_OUTPUT,
+ objects: true,
+ }],
+ });
+
+ clickable = result.clickableElements[0];
+ ok(clickable, "clickable object found");
+
+ executeSoon(() => {
+ EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow);
+ });
+
+ fetchedVar = yield jsterm.once("variablesview-fetched");
+
+ yield findVariableViewProperties(fetchedVar, [
+ { name: "0.textContent", value: /hello world/ },
+ { name: "1.textContent", value: /native getters/ },
+ ], { webconsole: hud });
+});
diff --git a/devtools/client/webconsole/test/browser_console_navigation_marker.js b/devtools/client/webconsole/test/browser_console_navigation_marker.js
new file mode 100644
index 000000000..e8ec84caf
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_navigation_marker.js
@@ -0,0 +1,81 @@
+/* -*- 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/ */
+
+// Check that the navigation marker shows on page reload - bug 793996.
+
+"use strict";
+
+const PREF = "devtools.webconsole.persistlog";
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+var hud;
+
+add_task(function* () {
+ Services.prefs.setBoolPref(PREF, true);
+
+ let { browser } = yield loadTab(TEST_URI);
+ hud = yield openConsole();
+
+ yield consoleOpened();
+
+ let loaded = loadBrowser(browser);
+ BrowserReload();
+ yield loaded;
+
+ yield onReload();
+
+ isnot(hud.outputNode.textContent.indexOf("foobarz1"), -1,
+ "foobarz1 is still in the output");
+
+ Services.prefs.clearUserPref(PREF);
+
+ hud = null;
+});
+
+function consoleOpened() {
+ ok(hud, "Web Console opened");
+
+ hud.jsterm.clearOutput();
+
+ ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ content.console.log("foobarz1");
+ });
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foobarz1",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+}
+
+function onReload() {
+ ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ content.console.log("foobarz2");
+ });
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "page reload",
+ text: "test-console.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "foobarz2",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ name: "navigation marker",
+ text: "test-console.html",
+ type: Messages.NavigationMarker,
+ }],
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_console_netlogging.js b/devtools/client/webconsole/test/browser_console_netlogging.js
new file mode 100644
index 000000000..a6f7bec48
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_netlogging.js
@@ -0,0 +1,38 @@
+/* -*- 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 network log messages bring up the network panel.
+
+"use strict";
+
+const TEST_NETWORK_REQUEST_URI =
+ "http://example.com/browser/devtools/client/webconsole/test/" +
+ "test-network-request.html";
+
+add_task(function* () {
+ let finishedRequest = waitForFinishedRequest(({ request }) => {
+ return request.url === TEST_NETWORK_REQUEST_URI;
+ });
+
+ const hud = yield loadPageAndGetHud(TEST_NETWORK_REQUEST_URI,
+ "browserConsole");
+ let request = yield finishedRequest;
+
+ ok(request, "Page load was logged");
+
+ let client = hud.ui.webConsoleClient;
+ let args = [request.actor];
+ const postData = yield getPacket(client, "getRequestPostData", args);
+ const responseContent = yield getPacket(client, "getResponseContent", args);
+
+ is(request.request.url, TEST_NETWORK_REQUEST_URI,
+ "Logged network entry is page load");
+ is(request.request.method, "GET", "Method is correct");
+ ok(!postData.postData.text, "No request body was stored");
+ ok(postData.postDataDiscarded, "Request body was discarded");
+ ok(!responseContent.content.text, "No response body was stored");
+ ok(responseContent.contentDiscarded || request.fromCache,
+ "Response body was discarded or response came from the cache");
+});
diff --git a/devtools/client/webconsole/test/browser_console_nsiconsolemessage.js b/devtools/client/webconsole/test/browser_console_nsiconsolemessage.js
new file mode 100644
index 000000000..fedd0c71c
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_nsiconsolemessage.js
@@ -0,0 +1,85 @@
+/* -*- 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/ */
+
+// Check that nsIConsoleMessages are displayed in the Browser Console.
+// See bug 859756.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<title>bug859756</title>\n" +
+ "<p>hello world\n<p>nsIConsoleMessages ftw!";
+
+function test() {
+ const FILTER_PREF = "devtools.browserconsole.filter.jslog";
+ Services.prefs.setBoolPref(FILTER_PREF, true);
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(FILTER_PREF);
+ });
+
+ Task.spawn(function* () {
+ const {tab} = yield loadTab(TEST_URI);
+
+ // Test for cached nsIConsoleMessages.
+ Services.console.logStringMessage("test1 for bug859756");
+
+ info("open web console");
+ let hud = yield openConsole(tab);
+
+ ok(hud, "web console opened");
+ Services.console.logStringMessage("do-not-show-me");
+
+ ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ content.console.log("foobarz");
+ });
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foobarz",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ let text = hud.outputNode.textContent;
+ is(text.indexOf("do-not-show-me"), -1,
+ "nsIConsoleMessages are not displayed");
+ is(text.indexOf("test1 for bug859756"), -1,
+ "nsIConsoleMessages are not displayed (confirmed)");
+
+ yield closeConsole(tab);
+
+ info("web console closed");
+ hud = yield HUDService.toggleBrowserConsole();
+ ok(hud, "browser console opened");
+
+ Services.console.logStringMessage("test2 for bug859756");
+
+ let results = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "test1 for bug859756",
+ category: CATEGORY_JS,
+ }, {
+ text: "test2 for bug859756",
+ category: CATEGORY_JS,
+ }, {
+ text: "do-not-show-me",
+ category: CATEGORY_JS,
+ }],
+ });
+
+ let msg = [...results[2].matched][0];
+ ok(msg, "message element for do-not-show-me (nsIConsoleMessage)");
+ isnot(msg.textContent.indexOf("do-not-show"), -1,
+ "element content is correct");
+ ok(!msg.classList.contains("filtered-by-type"), "element is not filtered");
+
+ hud.setFilterState("jslog", false);
+
+ ok(msg.classList.contains("filtered-by-type"), "element is filtered");
+ }).then(finishTest);
+}
diff --git a/devtools/client/webconsole/test/browser_console_open_or_focus.js b/devtools/client/webconsole/test/browser_console_open_or_focus.js
new file mode 100644
index 000000000..d537c9aad
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_open_or_focus.js
@@ -0,0 +1,46 @@
+/* -*- 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/ */
+
+// Test that the "browser console" menu item opens or focuses (if already open)
+// the console window instead of toggling it open/close.
+
+"use strict";
+
+var {Tools} = require("devtools/client/definitions");
+
+add_task(function* () {
+ let currWindow, hud, mainWindow;
+
+ mainWindow = Services.wm.getMostRecentWindow(null);
+
+ yield HUDService.openBrowserConsoleOrFocus();
+
+ hud = HUDService.getBrowserConsole();
+
+ console.log("testmessage");
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "testmessage"
+ }],
+ });
+
+ currWindow = Services.wm.getMostRecentWindow(null);
+ is(currWindow.document.documentURI, Tools.webConsole.url,
+ "The Browser Console is open and has focus");
+
+ mainWindow.focus();
+
+ yield HUDService.openBrowserConsoleOrFocus();
+
+ currWindow = Services.wm.getMostRecentWindow(null);
+ is(currWindow.document.documentURI, Tools.webConsole.url,
+ "The Browser Console is open and has focus");
+
+ yield HUDService.toggleBrowserConsole();
+
+ hud = HUDService.getBrowserConsole();
+ ok(!hud, "Browser Console has been closed");
+});
diff --git a/devtools/client/webconsole/test/browser_console_optimized_out_vars.js b/devtools/client/webconsole/test/browser_console_optimized_out_vars.js
new file mode 100644
index 000000000..dc898eb2b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_optimized_out_vars.js
@@ -0,0 +1,91 @@
+/* -*- 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/ */
+
+// Check that inspecting an optimized out variable works when execution is
+// paused.
+
+"use strict";
+
+// Force the old debugger UI since it's directly used (see Bug 1301705)
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+function test() {
+ Task.spawn(function* () {
+ const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-closure-optimized-out.html";
+ let {tab} = yield loadTab(TEST_URI);
+ let hud = yield openConsole(tab);
+ let { toolbox, panel, panelWin } = yield openDebugger();
+
+ let sources = panelWin.DebuggerView.Sources;
+ yield panel.addBreakpoint({ actor: sources.values[0], line: 18 });
+ yield ensureThreadClientState(panel, "resumed");
+
+ let fetchedScopes = panelWin.once(panelWin.EVENTS.FETCHED_SCOPES);
+
+ // Cause the debuggee to pause
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let button = content.document.querySelector("button");
+ button.click();
+ });
+
+ yield fetchedScopes;
+ ok(true, "Scopes were fetched");
+
+ yield toolbox.selectTool("webconsole");
+
+ // This is the meat of the test: evaluate the optimized out variable.
+ hud.jsterm.execute("upvar");
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "optimized out",
+ category: CATEGORY_OUTPUT,
+ }]
+ });
+
+ finishTest();
+ }).then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+}
+
+// Debugger helper functions stolen from devtools/client/debugger/test/head.js.
+
+function ensureThreadClientState(aPanel, aState) {
+ let thread = aPanel.panelWin.gThreadClient;
+ let state = thread.state;
+
+ info("Thread is: '" + state + "'.");
+
+ if (state == aState) {
+ return promise.resolve(null);
+ }
+ return waitForThreadEvents(aPanel, aState);
+}
+
+function waitForThreadEvents(aPanel, aEventName, aEventRepeat = 1) {
+ info("Waiting for thread event: '" + aEventName + "' to fire: " +
+ aEventRepeat + " time(s).");
+
+ let deferred = promise.defer();
+ let thread = aPanel.panelWin.gThreadClient;
+ let count = 0;
+
+ thread.addListener(aEventName, function onEvent(eventName, ...args) {
+ info("Thread event '" + eventName + "' fired: " + (++count) + " time(s).");
+
+ if (count == aEventRepeat) {
+ ok(true, "Enough '" + eventName + "' thread events have been fired.");
+ thread.removeListener(eventName, onEvent);
+ deferred.resolve.apply(deferred, args);
+ }
+ });
+
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_console_private_browsing.js b/devtools/client/webconsole/test/browser_console_private_browsing.js
new file mode 100644
index 000000000..4b3a79329
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_private_browsing.js
@@ -0,0 +1,192 @@
+/* -*- 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/ */
+
+// 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";
+
+function test() {
+ const TEST_URI = "data:text/html;charset=utf8,<p>hello world! bug 874061" +
+ "<button onclick='console.log(\"foobar bug 874061\");" +
+ "fooBazBaz.yummy()'>click</button>";
+ let ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"]
+ .getService(Ci.nsIConsoleAPIStorage);
+ let privateWindow, privateBrowser, privateTab, privateContent;
+ let hud, expectedMessages, nonPrivateMessage;
+
+ // This test is slightly more involved: it opens the web console twice,
+ // a new private window once, and the browser console twice. We can get
+ // a timeout with debug builds on slower machines.
+ requestLongerTimeout(2);
+ start();
+
+ function start() {
+ gBrowser.selectedTab = gBrowser.addTab("data:text/html;charset=utf8," +
+ "<p>hello world! I am not private!");
+ gBrowser.selectedBrowser.addEventListener("load", onLoadTab, true);
+ }
+
+ function onLoadTab() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoadTab, true);
+ info("onLoadTab()");
+
+ // Make sure we have a clean state to start with.
+ Services.console.reset();
+ ConsoleAPIStorage.clearEvents();
+
+ // Add a non-private message to the browser console.
+ ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ content.console.log("bug874061-not-private");
+ });
+
+ nonPrivateMessage = {
+ name: "console message from a non-private window",
+ text: "bug874061-not-private",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ };
+
+ privateWindow = OpenBrowserWindow({ private: true });
+ ok(privateWindow, "new private window");
+ ok(PrivateBrowsingUtils.isWindowPrivate(privateWindow), "window's private");
+
+ whenDelayedStartupFinished(privateWindow, onPrivateWindowReady);
+ }
+
+ function onPrivateWindowReady() {
+ info("private browser window opened");
+ privateBrowser = privateWindow.gBrowser;
+
+ privateTab = privateBrowser.selectedTab = privateBrowser.addTab(TEST_URI);
+ privateBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ info("private tab opened");
+ privateBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ privateContent = privateBrowser.selectedBrowser.contentWindow;
+ ok(PrivateBrowsingUtils.isBrowserPrivate(privateBrowser.selectedBrowser),
+ "tab window is private");
+ openConsole(privateTab).then(consoleOpened);
+ }, true);
+ }
+
+ function addMessages() {
+ let button = privateContent.document.querySelector("button");
+ ok(button, "button in page");
+ EventUtils.synthesizeMouse(button, 2, 2, {}, privateContent);
+ }
+
+ function consoleOpened(injectedHud) {
+ hud = injectedHud;
+ ok(hud, "web console opened");
+
+ addMessages();
+ expectedMessages = [
+ {
+ name: "script error",
+ text: "fooBazBaz is not defined",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ },
+ {
+ name: "console message",
+ text: "foobar bug 874061",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ ];
+
+ // Make sure messages are displayed in the web console as they happen, even
+ // if this is a private tab.
+ waitForMessages({
+ webconsole: hud,
+ messages: expectedMessages,
+ }).then(testCachedMessages);
+ }
+
+ function testCachedMessages() {
+ info("testCachedMessages()");
+ closeConsole(privateTab).then(() => {
+ info("web console closed");
+ openConsole(privateTab).then(consoleReopened);
+ });
+ }
+
+ function consoleReopened(injectedHud) {
+ hud = injectedHud;
+ ok(hud, "web console reopened");
+
+ // Make sure that cached messages are displayed in the web console, even
+ // if this is a private tab.
+ waitForMessages({
+ webconsole: hud,
+ messages: expectedMessages,
+ }).then(testBrowserConsole);
+ }
+
+ function testBrowserConsole() {
+ info("testBrowserConsole()");
+ closeConsole(privateTab).then(() => {
+ info("web console closed");
+ HUDService.toggleBrowserConsole().then(onBrowserConsoleOpen);
+ });
+ }
+
+ // Make sure that the cached messages from private tabs are not displayed in
+ // the browser console.
+ function checkNoPrivateMessages() {
+ let text = hud.outputNode.textContent;
+ is(text.indexOf("fooBazBaz"), -1, "no exception displayed");
+ is(text.indexOf("bug 874061"), -1, "no console message displayed");
+ }
+
+ function onBrowserConsoleOpen(injectedHud) {
+ hud = injectedHud;
+ ok(hud, "browser console opened");
+
+ checkNoPrivateMessages();
+ addMessages();
+ expectedMessages.push(nonPrivateMessage);
+
+ // Make sure that live messages are displayed in the browser console, even
+ // from private tabs.
+ waitForMessages({
+ webconsole: hud,
+ messages: expectedMessages,
+ }).then(testPrivateWindowClose);
+ }
+
+ function testPrivateWindowClose() {
+ info("close the private window and check if private messages are removed");
+ hud.jsterm.once("private-messages-cleared", () => {
+ isnot(hud.outputNode.textContent.indexOf("bug874061-not-private"), -1,
+ "non-private messages are still shown after private window closed");
+ checkNoPrivateMessages();
+
+ info("close the browser console");
+ HUDService.toggleBrowserConsole().then(() => {
+ info("reopen the browser console");
+ executeSoon(() =>
+ HUDService.toggleBrowserConsole().then(onBrowserConsoleReopen));
+ });
+ });
+ privateWindow.BrowserTryToCloseWindow();
+ }
+
+ function onBrowserConsoleReopen(injectedHud) {
+ hud = injectedHud;
+ ok(hud, "browser console reopened");
+
+ // Make sure that the non-private message is still shown after reopen.
+ waitForMessages({
+ webconsole: hud,
+ messages: [nonPrivateMessage],
+ }).then(() => {
+ // Make sure that no private message is displayed after closing the
+ // private window and reopening the Browser Console.
+ checkNoPrivateMessages();
+ executeSoon(finishTest);
+ });
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_console_server_logging.js b/devtools/client/webconsole/test/browser_console_server_logging.js
new file mode 100644
index 000000000..eaef12330
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_server_logging.js
@@ -0,0 +1,74 @@
+/* -*- 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/ */
+
+"use strict";
+
+// Check that server log appears in the console panel - bug 1168872
+add_task(function* () {
+ const TEST_URI = "http://example.com/browser/devtools/client/webconsole/test/test-console-server-logging.sjs";
+
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ // Set logging filter and wait till it's set on the backend
+ hud.setFilterState("serverlog", true);
+ yield updateServerLoggingListener(hud);
+
+ BrowserReloadSkipCache();
+
+ // Note that the test is also checking out the (printf like)
+ // formatters and encoding of UTF8 characters (see the one at the end).
+ let text = "values: string Object { a: 10 } 123 1.12 \u2713";
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: text,
+ category: CATEGORY_SERVER,
+ severity: SEVERITY_LOG,
+ }],
+ });
+ // Clean up filter
+ hud.setFilterState("serverlog", false);
+ yield updateServerLoggingListener(hud);
+});
+
+add_task(function* () {
+ const TEST_URI = "http://example.com/browser/devtools/client/webconsole/test/test-console-server-logging-array.sjs";
+
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ // Set logging filter and wait till it's set on the backend
+ hud.setFilterState("serverlog", true);
+ yield updateServerLoggingListener(hud);
+
+ BrowserReloadSkipCache();
+ // Note that the test is also checking out the (printf like)
+ // formatters and encoding of UTF8 characters (see the one at the end).
+ let text = "Object { best: \"Firefox\", reckless: \"Chrome\", " +
+ "new_ie: \"Safari\", new_new_ie: \"Edge\" }";
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: text,
+ category: CATEGORY_SERVER,
+ severity: SEVERITY_LOG,
+ }],
+ });
+ // Clean up filter
+ hud.setFilterState("serverlog", false);
+ yield updateServerLoggingListener(hud);
+});
+
+function updateServerLoggingListener(hud) {
+ let deferred = promise.defer();
+ hud.ui._updateServerLoggingListener(response => {
+ deferred.resolve(response);
+ });
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_console_variables_view.js b/devtools/client/webconsole/test/browser_console_variables_view.js
new file mode 100644
index 000000000..ecd8071ce
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_variables_view.js
@@ -0,0 +1,204 @@
+/* -*- 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/ */
+
+// Check that variables view works as expected in the web console.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-eval-in-stackframe.html";
+
+var hud, gVariablesView;
+
+registerCleanupFunction(function () {
+ hud = gVariablesView = null;
+});
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ hud = yield openConsole();
+
+ let msg = yield hud.jsterm.execute("(function foo(){})");
+
+ ok(msg, "output message found");
+ ok(msg.textContent.includes("function foo()"),
+ "message text check");
+
+ executeSoon(() => {
+ EventUtils.synthesizeMouse(msg.querySelector("a"), 2, 2, {}, hud.iframeWindow);
+ });
+
+ let varView = yield hud.jsterm.once("variablesview-fetched");
+ ok(varView, "object inspector opened on click");
+
+ yield findVariableViewProperties(varView, [{
+ name: "name",
+ value: "foo",
+ }], { webconsole: hud });
+});
+
+add_task(function* () {
+ let msg = yield hud.jsterm.execute("fooObj");
+
+ ok(msg, "output message found");
+ ok(msg.textContent.includes('{ testProp: "testValue" }'),
+ "message text check");
+
+ let anchor = msg.querySelector("a");
+ ok(anchor, "object link found");
+
+ let fetched = hud.jsterm.once("variablesview-fetched");
+
+ // executeSoon
+ EventUtils.synthesizeMouse(anchor, 2, 2, {}, hud.iframeWindow);
+
+ let view = yield fetched;
+
+ let results = yield onFooObjFetch(view);
+
+ let vView = yield onTestPropFound(results);
+ let results2 = yield onFooObjFetchAfterUpdate(vView);
+
+ let vView2 = yield onUpdatedTestPropFound(results2);
+ let results3 = yield onFooObjFetchAfterPropRename(vView2);
+
+ let vView3 = yield onRenamedTestPropFound(results3);
+ let results4 = yield onPropUpdateError(vView3);
+
+ yield onRenamedTestPropFoundAgain(results4);
+
+ let prop = results4[0].matchedProp;
+ yield testPropDelete(prop);
+});
+
+function onFooObjFetch(aVar) {
+ gVariablesView = aVar._variablesView;
+ ok(gVariablesView, "variables view object");
+
+ return findVariableViewProperties(aVar, [
+ { name: "testProp", value: "testValue" },
+ ], { webconsole: hud });
+}
+
+function onTestPropFound(aResults) {
+ let prop = aResults[0].matchedProp;
+ ok(prop, "matched the |testProp| property in the variables view");
+
+ is("testValue", aResults[0].value,
+ "|fooObj.testProp| value is correct");
+
+ // Check that property value updates work and that jsterm functions can be
+ // used.
+ return updateVariablesViewProperty({
+ property: prop,
+ field: "value",
+ string: "document.title + window.location + $('p')",
+ webconsole: hud
+ });
+}
+
+function onFooObjFetchAfterUpdate(aVar) {
+ info("onFooObjFetchAfterUpdate");
+ let expectedValue = content.document.title + content.location +
+ "[object HTMLParagraphElement]";
+
+ return findVariableViewProperties(aVar, [
+ { name: "testProp", value: expectedValue },
+ ], { webconsole: hud });
+}
+
+function onUpdatedTestPropFound(aResults) {
+ let prop = aResults[0].matchedProp;
+ ok(prop, "matched the updated |testProp| property value");
+
+ is(content.wrappedJSObject.fooObj.testProp, aResults[0].value,
+ "|fooObj.testProp| value has been updated");
+
+ // Check that property name updates work.
+ return updateVariablesViewProperty({
+ property: prop,
+ field: "name",
+ string: "testUpdatedProp",
+ webconsole: hud
+ });
+}
+
+function* onFooObjFetchAfterPropRename(aVar) {
+ info("onFooObjFetchAfterPropRename");
+
+ let expectedValue = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let para = content.wrappedJSObject.document.querySelector("p");
+ return content.document.title + content.location + para;
+ });
+
+ // Check that the new value is in the variables view.
+ return findVariableViewProperties(aVar, [
+ { name: "testUpdatedProp", value: expectedValue },
+ ], { webconsole: hud });
+}
+
+function onRenamedTestPropFound(aResults) {
+ let prop = aResults[0].matchedProp;
+ ok(prop, "matched the renamed |testProp| property");
+
+ ok(!content.wrappedJSObject.fooObj.testProp,
+ "|fooObj.testProp| has been deleted");
+ is(content.wrappedJSObject.fooObj.testUpdatedProp, aResults[0].value,
+ "|fooObj.testUpdatedProp| is correct");
+
+ // Check that property value updates that cause exceptions are reported in
+ // the web console output.
+ return updateVariablesViewProperty({
+ property: prop,
+ field: "value",
+ string: "foobarzFailure()",
+ webconsole: hud
+ });
+}
+
+function* onPropUpdateError(aVar) {
+ info("onPropUpdateError");
+
+ let expectedValue = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let para = content.wrappedJSObject.document.querySelector("p");
+ return content.document.title + content.location + para;
+ });
+
+ // Make sure the property did not change.
+ return findVariableViewProperties(aVar, [
+ { name: "testUpdatedProp", value: expectedValue },
+ ], { webconsole: hud });
+}
+
+function onRenamedTestPropFoundAgain(aResults) {
+ let prop = aResults[0].matchedProp;
+ ok(prop, "matched the renamed |testProp| property again");
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "exception in property update reported in the web console output",
+ text: "foobarzFailure",
+ category: CATEGORY_OUTPUT,
+ severity: SEVERITY_ERROR,
+ }],
+ });
+}
+
+function testPropDelete(aProp) {
+ gVariablesView.window.focus();
+ aProp.focus();
+
+ executeSoon(() => {
+ EventUtils.synthesizeKey("VK_DELETE", {}, gVariablesView.window);
+ });
+
+ return waitForSuccess({
+ name: "property deleted",
+ timeout: 60000,
+ validator: () => !("testUpdatedProp" in content.wrappedJSObject.fooObj)
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_console_variables_view_dom_nodes.js b/devtools/client/webconsole/test/browser_console_variables_view_dom_nodes.js
new file mode 100644
index 000000000..522fe4754
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_variables_view_dom_nodes.js
@@ -0,0 +1,59 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that ensures DOM nodes are rendered correctly in VariablesView.
+
+"use strict";
+
+function test() {
+ const TEST_URI = `
+ data:text/html;charset=utf-8,
+ <html>
+ <head>
+ <title>Test for DOM nodes in variables view</title>
+ </head>
+ <body>
+ <div></div>
+ <div id="testID"></div>
+ <div class="single-class"></div>
+ <div class="multiple-classes another-class"></div>
+ <div class="class-and-id" id="class-and-id"></div>
+ <div class="multiple-classes-and-id another-class"
+ id="multiple-classes-and-id"></div>
+ <div class=" whitespace-start"></div>
+ <div class="whitespace-end "></div>
+ <div class="multiple spaces"></div>
+ </body>
+ </html>
+`;
+
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ const {tab} = yield loadTab(TEST_URI);
+ const hud = yield openConsole(tab);
+ const jsterm = hud.jsterm;
+
+ let deferred = promise.defer();
+ jsterm.once("variablesview-fetched", (_, val) => deferred.resolve(val));
+ jsterm.execute("inspect(document.querySelectorAll('div'))");
+
+ let variableScope = yield deferred.promise;
+ ok(variableScope, "Variables view opened");
+
+ yield findVariableViewProperties(variableScope, [
+ { name: "0", value: "<div>"},
+ { name: "1", value: "<div#testID>"},
+ { name: "2", value: "<div.single-class>"},
+ { name: "3", value: "<div.multiple-classes.another-class>"},
+ { name: "4", value: "<div#class-and-id.class-and-id>"},
+ { name: "5", value: "<div#multiple-classes-and-id." +
+ "multiple-classes-and-id.another-class>"},
+ { name: "6", value: "<div.whitespace-start>"},
+ { name: "7", value: "<div.whitespace-end>"},
+ { name: "8", value: "<div.multiple.spaces>"},
+ ], { webconsole: hud});
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_console_variables_view_dont_sort_non_sortable_classes_properties.js b/devtools/client/webconsole/test/browser_console_variables_view_dont_sort_non_sortable_classes_properties.js
new file mode 100644
index 000000000..ec9ffa7b7
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_variables_view_dont_sort_non_sortable_classes_properties.js
@@ -0,0 +1,135 @@
+/* -*- 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/ */
+
+"use strict";
+
+/* Test case that ensures Array and other list types are not sorted in variables
+ * view.
+ *
+ * The tested types are:
+ * - Array
+ * - Int8Array
+ * - Int16Array
+ * - Int32Array
+ * - Uint8Array
+ * - Uint16Array
+ * - Uint32Array
+ * - Uint8ClampedArray
+ * - Float32Array
+ * - Float64Array
+ * - NodeList
+ */
+
+function test() {
+ const TEST_URI = "data:text/html;charset=utf-8, \
+ <html> \
+ <head> \
+ <title>Test document for bug 977500</title> \
+ </head> \
+ <body> \
+ <div></div> \
+ <div></div> \
+ <div></div> \
+ <div></div> \
+ <div></div> \
+ <div></div> \
+ <div></div> \
+ <div></div> \
+ <div></div> \
+ <div></div> \
+ <div></div> \
+ <div></div> \
+ </body> \
+ </html>";
+
+ let jsterm;
+
+ function* runner() {
+ const typedArrayTypes = ["Int8Array", "Int16Array", "Int32Array",
+ "Uint8Array", "Uint16Array", "Uint32Array",
+ "Uint8ClampedArray", "Float32Array",
+ "Float64Array"];
+
+ const {tab} = yield loadTab(TEST_URI);
+ const hud = yield openConsole(tab);
+ jsterm = hud.jsterm;
+
+ // Create an ArrayBuffer of 80 bytes to test TypedArrays. 80 bytes is
+ // enough to get 10 items in all different TypedArrays.
+ yield jsterm.execute("let buf = new ArrayBuffer(80);");
+
+ // Array
+ yield testNotSorted("Array(0,1,2,3,4,5,6,7,8,9,10)");
+ // NodeList
+ yield testNotSorted("document.querySelectorAll('div')");
+ // Object
+ yield testSorted("Object({'hello':1,1:5,10:2,4:2,'abc':1})");
+
+ // Typed arrays.
+ for (let type of typedArrayTypes) {
+ yield testNotSorted("new " + type + "(buf)");
+ }
+ }
+
+ /**
+ * A helper that ensures the properties are not sorted when an object
+ * specified by aObject is inspected.
+ *
+ * @param string aObject
+ * A string that, once executed, creates and returns the object to
+ * inspect.
+ */
+ function* testNotSorted(aObject) {
+ info("Testing " + aObject);
+ let deferred = promise.defer();
+ jsterm.once("variablesview-fetched", (_, aVar) => deferred.resolve(aVar));
+ jsterm.execute("inspect(" + aObject + ")");
+
+ let variableScope = yield deferred.promise;
+ ok(variableScope, "Variables view opened");
+
+ // If the properties are sorted: keys = ["0", "1", "10",...] <- incorrect
+ // If the properties are not sorted: keys = ["0", "1", "2",...] <- correct
+ let keyIterator = variableScope._store.keys();
+ is(keyIterator.next().value, "0", "First key is 0");
+ is(keyIterator.next().value, "1", "Second key is 1");
+
+ // If the properties are sorted, the next one will be 10.
+ is(keyIterator.next().value, "2", "Third key is 2, not 10");
+ }
+ /**
+ * A helper that ensures the properties are sorted when an object
+ * specified by aObject is inspected.
+ *
+ * @param string aObject
+ * A string that, once executed, creates and returns the object to
+ * inspect.
+ */
+ function* testSorted(aObject) {
+ info("Testing " + aObject);
+ let deferred = promise.defer();
+ jsterm.once("variablesview-fetched", (_, aVar) => deferred.resolve(aVar));
+ jsterm.execute("inspect(" + aObject + ")");
+
+ let variableScope = yield deferred.promise;
+ ok(variableScope, "Variables view opened");
+
+ // If the properties are sorted:
+ // keys = ["1", "4", "10",..., "abc", "hello"] <- correct
+ // If the properties are not sorted:
+ // keys = ["1", "10", "4",...] <- incorrect
+ let keyIterator = variableScope._store.keys();
+ is(keyIterator.next().value, "1", "First key should be 1");
+ is(keyIterator.next().value, "4", "Second key should be 4");
+
+ // If the properties are sorted, the next one will be 10.
+ is(keyIterator.next().value, "10", "Third key is 10");
+ // If sorted next properties should be "abc" then "hello"
+ is(keyIterator.next().value, "abc", "Fourth key is abc");
+ is(keyIterator.next().value, "hello", "Fifth key is hello");
+ }
+
+ Task.spawn(runner).then(finishTest);
+}
diff --git a/devtools/client/webconsole/test/browser_console_variables_view_filter.js b/devtools/client/webconsole/test/browser_console_variables_view_filter.js
new file mode 100644
index 000000000..142410839
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_variables_view_filter.js
@@ -0,0 +1,80 @@
+/* -*- 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/ */
+
+// Check that variables view filter feature works fine in the console.
+
+function props(view, prefix = "") {
+ // First match only the visible one, not hidden by a search
+ let visible = [...view].filter(([id, prop]) => prop._isMatch);
+ // Then flatten the list into a list of strings
+ // being the jsonpath of each attribute being visible in the view
+ return visible.reduce((list, [id, prop]) => {
+ list.push(prefix + id);
+ return list.concat(props(prop, prefix + id + "."));
+ }, []);
+}
+
+function assertAttrs(view, expected, message) {
+ is(props(view).join(","), expected, message);
+}
+
+add_task(function* () {
+ yield loadTab("data:text/html;charset=utf-8,webconsole-filter");
+
+ let hud = yield openConsole();
+
+ let jsterm = hud.jsterm;
+
+ let fetched = jsterm.once("variablesview-fetched");
+
+ yield jsterm.execute("inspect({ foo: { bar : \"baz\" } })");
+
+ let view = yield fetched;
+ let variablesView = view._variablesView;
+ let searchbox = variablesView._searchboxNode;
+
+ assertAttrs(view, "foo,__proto__",
+ "To start with, we just see the top level foo attr");
+
+ fetched = jsterm.once("variablesview-fetched");
+ searchbox.value = "bar";
+ searchbox.doCommand();
+ view = yield fetched;
+
+ assertAttrs(view, "",
+ "If we don't manually expand nested attr, we don't see them");
+
+ fetched = jsterm.once("variablesview-fetched");
+ searchbox.value = "";
+ searchbox.doCommand();
+ view = yield fetched;
+
+ assertAttrs(view, "foo",
+ "If we reset the search, we get back to original state");
+
+ yield [...view][0][1].expand();
+
+ fetched = jsterm.once("variablesview-fetched");
+ searchbox.value = "bar";
+ searchbox.doCommand();
+ view = yield fetched;
+
+ assertAttrs(view, "foo,foo.bar", "Now if we expand, we see the nested attr");
+
+ fetched = jsterm.once("variablesview-fetched");
+ searchbox.value = "baz";
+ searchbox.doCommand();
+ view = yield fetched;
+
+ assertAttrs(view, "foo,foo.bar", "We can also search for attr values");
+
+ fetched = jsterm.once("variablesview-fetched");
+ searchbox.value = "";
+ searchbox.doCommand();
+ view = yield fetched;
+
+ assertAttrs(view, "foo",
+ "If we reset again, we get back to original state again");
+});
diff --git a/devtools/client/webconsole/test/browser_console_variables_view_highlighter.js b/devtools/client/webconsole/test/browser_console_variables_view_highlighter.js
new file mode 100644
index 000000000..c1b2194de
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_variables_view_highlighter.js
@@ -0,0 +1,97 @@
+/* -*- 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/ */
+
+// Check that variables view is linked to the inspector for highlighting and
+// selecting DOM nodes
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-952277-highlight-nodes-in-vview.html";
+
+var gWebConsole, gJSTerm, gVariablesView, gToolbox;
+
+function test() {
+ loadTab(TEST_URI).then(() => {
+ openConsole().then(hud => {
+ consoleOpened(hud);
+ });
+ });
+}
+
+function consoleOpened(hud) {
+ gWebConsole = hud;
+ gJSTerm = hud.jsterm;
+ gToolbox = gDevTools.getToolbox(hud.target);
+ gJSTerm.execute("document.querySelectorAll('p')").then(onQSAexecuted);
+}
+
+function onQSAexecuted(msg) {
+ ok(msg, "output message found");
+ let anchor = msg.querySelector("a");
+ ok(anchor, "object link found");
+
+ gJSTerm.once("variablesview-fetched", onNodeListViewFetched);
+
+ executeSoon(() =>
+ EventUtils.synthesizeMouse(anchor, 2, 2, {}, gWebConsole.iframeWindow)
+ );
+}
+
+function onNodeListViewFetched(event, variable) {
+ gVariablesView = variable._variablesView;
+ ok(gVariablesView, "variables view object");
+
+ // Transform the vview into an array we can filter properties from
+ let props = [...variable].map(([id, prop]) => [id, prop]);
+
+ // These properties are the DOM nodes ones
+ props = props.filter(v => v[0].match(/[0-9]+/));
+
+ function hoverOverDomNodeVariableAndAssertHighlighter(index) {
+ if (props[index]) {
+ let prop = props[index][1];
+
+ gToolbox.once("node-highlight", () => {
+ ok(true, "The highlighter was shown on hover of the DOMNode");
+ gToolbox.highlighterUtils.unhighlight().then(() => {
+ clickOnDomNodeVariableAndAssertInspectorSelected(index);
+ });
+ });
+
+ // Rather than trying to emulate a mouseenter event, let's call the
+ // variable's highlightDomNode and see if it has the desired effect
+ prop.highlightDomNode();
+ } else {
+ finishUp();
+ }
+ }
+
+ function clickOnDomNodeVariableAndAssertInspectorSelected(index) {
+ let prop = props[index][1];
+
+ // Make sure the inspector is initialized so we can listen to its events
+ gToolbox.initInspector().then(() => {
+ // Rather than trying to click on the value here, let's just call the
+ // variable's openNodeInInspector function and see if it has the
+ // desired effect
+ prop.openNodeInInspector().then(() => {
+ is(gToolbox.currentToolId, "inspector",
+ "The toolbox switched over the inspector on DOMNode click");
+ gToolbox.selectTool("webconsole").then(() => {
+ hoverOverDomNodeVariableAndAssertHighlighter(index + 1);
+ });
+ });
+ });
+ }
+
+ hoverOverDomNodeVariableAndAssertHighlighter(0);
+}
+
+function finishUp() {
+ gWebConsole = gJSTerm = gVariablesView = gToolbox = null;
+
+ finishTest();
+}
diff --git a/devtools/client/webconsole/test/browser_console_variables_view_special_names.js b/devtools/client/webconsole/test/browser_console_variables_view_special_names.js
new file mode 100644
index 000000000..fa0dd4b92
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_variables_view_special_names.js
@@ -0,0 +1,38 @@
+/* -*- 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/ */
+
+// Check that variables view handles special names like "<return>"
+// properly for ordinary displays.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<p>test for bug 1084430";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ ok(hud, "web console opened");
+
+ hud.setFilterState("log", false);
+ registerCleanupFunction(() => hud.setFilterState("log", true));
+
+ hud.jsterm.execute("inspect({ '<return>': 47, '<exception>': 91 })");
+
+ let varView = yield hud.jsterm.once("variablesview-fetched");
+ ok(varView, "variables view object");
+
+ let props = yield findVariableViewProperties(varView, [
+ { name: "<return>", value: 47 },
+ { name: "<exception>", value: 91 },
+ ], { webconsole: hud });
+
+ for (let prop of props) {
+ ok(!prop.matchedProp._internalItem, prop.name + " is not marked internal");
+ let target = prop.matchedProp._target;
+ ok(!target.hasAttribute("pseudo-item"),
+ prop.name + " is not a pseudo-item");
+ }
+});
diff --git a/devtools/client/webconsole/test/browser_console_variables_view_while_debugging.js b/devtools/client/webconsole/test/browser_console_variables_view_while_debugging.js
new file mode 100644
index 000000000..e83a8d626
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_variables_view_while_debugging.js
@@ -0,0 +1,109 @@
+/* -*- 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/ */
+
+// Test that makes sure web console eval happens in the user-selected stackframe
+// from the js debugger, when changing the value of a property in the variables
+// view.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-eval-in-stackframe.html";
+
+// Force the old debugger UI since it's directly used (see Bug 1301705)
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ let dbgPanel = yield openDebugger();
+ yield waitForFrameAdded();
+ yield openConsole();
+ yield testVariablesView(hud);
+});
+
+function* waitForFrameAdded() {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = gDevTools.getToolbox(target);
+ let thread = toolbox.threadClient;
+
+ info("Waiting for framesadded");
+ yield new Promise(resolve => {
+ thread.addOneTimeListener("framesadded", resolve);
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.wrappedJSObject.firstCall();
+ });
+ });
+}
+
+function* testVariablesView(hud) {
+ let jsterm = hud.jsterm;
+ let msg = yield jsterm.execute("fooObj");
+ ok(msg, "output message found");
+ ok(msg.textContent.includes('{ testProp2: "testValue2" }'),
+ "message text check");
+
+ let anchor = msg.querySelector("a");
+ ok(anchor, "object link found");
+
+ info("Waiting for variable view to appear");
+ let variable = yield new Promise(resolve => {
+ jsterm.once("variablesview-fetched", (e, variable) => {
+ resolve(variable);
+ });
+ executeSoon(() => EventUtils.synthesizeMouse(anchor, 2, 2, {},
+ hud.iframeWindow));
+ });
+
+ info("Waiting for findVariableViewProperties");
+ let results = yield findVariableViewProperties(variable, [
+ { name: "testProp2", value: "testValue2" },
+ { name: "testProp", value: "testValue", dontMatch: true },
+ ], { webconsole: hud });
+
+ let prop = results[0].matchedProp;
+ ok(prop, "matched the |testProp2| property in the variables view");
+
+ // Check that property value updates work and that jsterm functions can be
+ // used.
+ variable = yield updateVariablesViewProperty({
+ property: prop,
+ field: "value",
+ string: "document.title + foo2 + $('p')",
+ webconsole: hud
+ });
+
+ info("onFooObjFetchAfterUpdate");
+ let expectedValue = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let para = content.wrappedJSObject.document.querySelector("p");
+ return content.document.title + "foo2SecondCall" + para;
+ });
+
+ results = yield findVariableViewProperties(variable, [
+ { name: "testProp2", value: expectedValue },
+ ], { webconsole: hud });
+
+ prop = results[0].matchedProp;
+ ok(prop, "matched the updated |testProp2| property value");
+
+ // Check that testProp2 was updated.
+ yield new Promise(resolve => {
+ executeSoon(() => {
+ jsterm.execute("fooObj.testProp2").then(resolve);
+ });
+ });
+
+ expectedValue = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let para = content.wrappedJSObject.document.querySelector("p");
+ return content.document.title + "foo2SecondCall" + para;
+ });
+
+ isnot(hud.outputNode.textContent.indexOf(expectedValue), -1,
+ "fooObj.testProp2 is correct");
+}
diff --git a/devtools/client/webconsole/test/browser_console_variables_view_while_debugging_and_inspecting.js b/devtools/client/webconsole/test/browser_console_variables_view_while_debugging_and_inspecting.js
new file mode 100644
index 000000000..556e7275d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_variables_view_while_debugging_and_inspecting.js
@@ -0,0 +1,112 @@
+/* -*- 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/ */
+
+// 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 = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-eval-in-stackframe.html";
+
+// Force the old debugger UI since it's directly used (see Bug 1301705)
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ let dbgPanel = yield openDebugger();
+ yield openInspector();
+ yield waitForFrameAdded();
+
+ yield openConsole();
+ yield testVariablesView(hud);
+});
+
+function* waitForFrameAdded() {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = gDevTools.getToolbox(target);
+ let thread = toolbox.threadClient;
+
+ info("Waiting for framesadded");
+ yield new Promise(resolve => {
+ thread.addOneTimeListener("framesadded", resolve);
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.wrappedJSObject.firstCall();
+ });
+ });
+}
+
+function* testVariablesView(hud) {
+ info("testVariablesView");
+ let jsterm = hud.jsterm;
+
+ let msg = yield jsterm.execute("fooObj");
+ ok(msg, "output message found");
+ ok(msg.textContent.includes('{ testProp2: "testValue2" }'),
+ "message text check");
+
+ let anchor = msg.querySelector("a");
+ ok(anchor, "object link found");
+
+ info("Waiting for variable view to appear");
+ let variable = yield new Promise(resolve => {
+ jsterm.once("variablesview-fetched", (e, variable) => {
+ resolve(variable);
+ });
+ executeSoon(() => EventUtils.synthesizeMouse(anchor, 2, 2, {},
+ hud.iframeWindow));
+ });
+
+ info("Waiting for findVariableViewProperties");
+ let results = yield findVariableViewProperties(variable, [
+ { name: "testProp2", value: "testValue2" },
+ { name: "testProp", value: "testValue", dontMatch: true },
+ ], { webconsole: hud });
+
+ let prop = results[0].matchedProp;
+ ok(prop, "matched the |testProp2| property in the variables view");
+
+ // Check that property value updates work and that jsterm functions can be
+ // used.
+ variable = yield updateVariablesViewProperty({
+ property: prop,
+ field: "value",
+ string: "document.title + foo2 + $('p')",
+ webconsole: hud
+ });
+
+ info("onFooObjFetchAfterUpdate");
+ let expectedValue = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let para = content.wrappedJSObject.document.querySelector("p");
+ return content.document.title + "foo2SecondCall" + para;
+ });
+
+ results = yield findVariableViewProperties(variable, [
+ { name: "testProp2", value: expectedValue },
+ ], { webconsole: hud });
+
+ prop = results[0].matchedProp;
+ ok(prop, "matched the updated |testProp2| property value");
+
+ // Check that testProp2 was updated.
+ yield new Promise(resolve => {
+ executeSoon(() => {
+ jsterm.execute("fooObj.testProp2").then(resolve);
+ });
+ });
+
+ expectedValue = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let para = content.wrappedJSObject.document.querySelector("p");
+ return content.document.title + "foo2SecondCall" + para;
+ });
+
+ isnot(hud.outputNode.textContent.indexOf(expectedValue), -1,
+ "fooObj.testProp2 is correct");
+}
diff --git a/devtools/client/webconsole/test/browser_eval_in_debugger_stackframe.js b/devtools/client/webconsole/test/browser_eval_in_debugger_stackframe.js
new file mode 100644
index 000000000..bc923ff44
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_eval_in_debugger_stackframe.js
@@ -0,0 +1,157 @@
+/* -*- 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/ */
+
+// 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/test-eval-in-stackframe.html";
+
+var gWebConsole, gJSTerm, gDebuggerWin, gThread, gDebuggerController;
+var gStackframes;
+
+// Force the old debugger UI since it's directly used (see Bug 1301705)
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+function test() {
+ loadTab(TEST_URI).then(() => {
+ openConsole().then(consoleOpened);
+ });
+}
+
+function consoleOpened(hud) {
+ gWebConsole = hud;
+ gJSTerm = hud.jsterm;
+ gJSTerm.execute("foo").then(onExecuteFoo);
+}
+
+function onExecuteFoo() {
+ isnot(gWebConsole.outputNode.textContent.indexOf("globalFooBug783499"), -1,
+ "|foo| value is correct");
+
+ gJSTerm.clearOutput();
+
+ // Test for Bug 690529 - Web Console and Scratchpad should evaluate
+ // expressions in the scope of the content window, not in a sandbox.
+ executeSoon(() => {
+ gJSTerm.execute("foo2 = 'newFoo'; window.foo2").then(onNewFoo2);
+ });
+}
+
+function onNewFoo2(msg) {
+ is(gWebConsole.outputNode.textContent.indexOf("undefined"), -1,
+ "|undefined| is not displayed after adding |foo2|");
+
+ ok(msg, "output result found");
+
+ isnot(msg.textContent.indexOf("newFoo"), -1,
+ "'newFoo' is displayed after adding |foo2|");
+
+ gJSTerm.clearOutput();
+
+ info("openDebugger");
+ executeSoon(() => openDebugger().then(debuggerOpened));
+}
+
+function debuggerOpened(aResult) {
+ gDebuggerWin = aResult.panelWin;
+ gDebuggerController = gDebuggerWin.DebuggerController;
+ gThread = gDebuggerController.activeThread;
+ gStackframes = gDebuggerController.StackFrames;
+
+ info("openConsole");
+ executeSoon(() =>
+ openConsole().then(() =>
+ gJSTerm.execute("foo + foo2").then(onExecuteFooAndFoo2)
+ )
+ );
+}
+
+function onExecuteFooAndFoo2() {
+ let expected = "globalFooBug783499newFoo";
+ isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1,
+ "|foo + foo2| is displayed after starting the debugger");
+
+ executeSoon(() => {
+ gJSTerm.clearOutput();
+
+ info("openDebugger");
+ openDebugger().then(() => {
+ gThread.addOneTimeListener("framesadded", onFramesAdded);
+
+ info("firstCall()");
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.wrappedJSObject.firstCall();
+ });
+ });
+ });
+}
+
+function onFramesAdded() {
+ info("onFramesAdded, openConsole() now");
+ executeSoon(() =>
+ openConsole().then(() =>
+ gJSTerm.execute("foo + foo2").then(onExecuteFooAndFoo2InSecondCall)
+ )
+ );
+}
+
+function onExecuteFooAndFoo2InSecondCall() {
+ let expected = "globalFooBug783499foo2SecondCall";
+ isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1,
+ "|foo + foo2| from |secondCall()|");
+
+ function runOpenConsole() {
+ openConsole().then(() => {
+ gJSTerm.execute("foo + foo2 + foo3").then(onExecuteFoo23InFirstCall);
+ });
+ }
+
+ executeSoon(() => {
+ gJSTerm.clearOutput();
+
+ info("openDebugger and selectFrame(1)");
+
+ openDebugger().then(() => {
+ gStackframes.selectFrame(1);
+
+ info("openConsole");
+ executeSoon(() => runOpenConsole());
+ });
+ });
+}
+
+function onExecuteFoo23InFirstCall() {
+ let expected = "fooFirstCallnewFoofoo3FirstCall";
+ isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1,
+ "|foo + foo2 + foo3| from |firstCall()|");
+
+ executeSoon(() =>
+ gJSTerm.execute("foo = 'abba'; foo3 = 'bug783499'; foo + foo3").then(
+ onExecuteFooAndFoo3ChangesInFirstCall));
+}
+
+var onExecuteFooAndFoo3ChangesInFirstCall = Task.async(function*() {
+ let expected = "abbabug783499";
+ isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1,
+ "|foo + foo3| updated in |firstCall()|");
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, 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");
+ });
+
+ gWebConsole = gJSTerm = gDebuggerWin = gThread = gDebuggerController =
+ gStackframes = null;
+ executeSoon(finishTest);
+});
diff --git a/devtools/client/webconsole/test/browser_eval_in_debugger_stackframe2.js b/devtools/client/webconsole/test/browser_eval_in_debugger_stackframe2.js
new file mode 100644
index 000000000..bc116d443
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_eval_in_debugger_stackframe2.js
@@ -0,0 +1,71 @@
+/* -*- 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/ */
+
+// 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/test-eval-in-stackframe.html";
+
+// Force the old debugger UI since it's directly used (see Bug 1301705)
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ info("open the web console");
+ let hud = yield openConsole();
+ let {jsterm} = hud;
+
+ info("open the debugger");
+ let {panelWin} = yield openDebugger();
+ let {DebuggerController} = panelWin;
+ let {activeThread} = DebuggerController;
+
+ let firstCall = promise.defer();
+ let frameAdded = promise.defer();
+ executeSoon(() => {
+ info("Executing firstCall");
+ activeThread.addOneTimeListener("framesadded", () => {
+ executeSoon(frameAdded.resolve);
+ });
+ jsterm.execute("firstCall()").then(firstCall.resolve);
+ });
+
+ info("Waiting for a frame to be added");
+ yield frameAdded.promise;
+
+ info("Executing basic command while paused");
+ yield executeAndConfirm(jsterm, "1 + 2", "3");
+
+ info("Executing command using scoped variables while paused");
+ yield executeAndConfirm(jsterm, "foo + foo2",
+ '"globalFooBug783499foo2SecondCall"');
+
+ info("Resuming the thread");
+ activeThread.resume();
+
+ info("Checking the first command, which is the last to resolve since it " +
+ "paused");
+ let node = yield firstCall.promise;
+ is(node.querySelector(".message-body").textContent,
+ "undefined",
+ "firstCall() returned correct value");
+});
+
+function* executeAndConfirm(jsterm, input, output) {
+ info("Executing command `" + input + "`");
+
+ let node = yield jsterm.execute(input);
+
+ is(node.querySelector(".message-body").textContent, output,
+ "Expected result from call to " + input);
+}
diff --git a/devtools/client/webconsole/test/browser_jsterm_inspect.js b/devtools/client/webconsole/test/browser_jsterm_inspect.js
new file mode 100644
index 000000000..aa18cbff6
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_jsterm_inspect.js
@@ -0,0 +1,47 @@
+/* -*- 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/ */
+
+// Check that the inspect() jsterm helper function works.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<p>hello bug 869981";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+ let jsterm = hud.jsterm;
+
+ /* Check that the window object is inspected */
+ jsterm.execute("testProp = 'testValue'");
+
+ let updated = jsterm.once("variablesview-updated");
+ jsterm.execute("inspect(window)");
+ let view = yield updated;
+ ok(view, "variables view object");
+
+ // The single variable view contains a scope with the variable name
+ // and unnamed subitem that contains the properties
+ let variable = view.getScopeAtIndex(0).get(undefined);
+ ok(variable, "variable object");
+
+ yield findVariableViewProperties(variable, [
+ { name: "testProp", value: "testValue" },
+ { name: "document", value: /HTMLDocument \u2192 data:/ },
+ ], { webconsole: hud });
+
+ /* Check that a primitive value can be inspected, too */
+ let updated2 = jsterm.once("variablesview-updated");
+ jsterm.execute("inspect(1)");
+ let view2 = yield updated2;
+ ok(view2, "variables view object");
+
+ // Check the label of the scope - it should contain the value
+ let scope = view.getScopeAtIndex(0);
+ ok(scope, "variable object");
+
+ is(scope.name, "1", "The value of the primitive var is correct");
+});
diff --git a/devtools/client/webconsole/test/browser_longstring_hang.js b/devtools/client/webconsole/test/browser_longstring_hang.js
new file mode 100644
index 000000000..036ad6e88
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_longstring_hang.js
@@ -0,0 +1,57 @@
+/* -*- 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/ */
+
+// Test that very long strings do not hang the browser.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-859170-longstring-hang.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ info("wait for the initial long string");
+
+ let results = yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "find 'foobar', no 'foobaz', in long string output",
+ text: "foobar",
+ noText: "foobaz",
+ category: CATEGORY_WEBDEV,
+ longString: true,
+ },
+ ],
+ });
+
+ let clickable = results[0].longStrings[0];
+ ok(clickable, "long string ellipsis is shown");
+ clickable.scrollIntoView(false);
+
+ EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow);
+
+ info("wait for long string expansion");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "find 'foobaz' after expand, but no 'boom!' at the end",
+ text: "foobaz",
+ noText: "boom!",
+ category: CATEGORY_WEBDEV,
+ longString: false,
+ },
+ {
+ text: "too long to be displayed",
+ longString: false,
+ },
+ ],
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_netmonitor_shows_reqs_in_webconsole.js b/devtools/client/webconsole/test/browser_netmonitor_shows_reqs_in_webconsole.js
new file mode 100644
index 000000000..f6f057549
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_netmonitor_shows_reqs_in_webconsole.js
@@ -0,0 +1,74 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,Test that the netmonitor " +
+ "displays requests that have been recorded in the " +
+ "web console, even if the netmonitor hadn't opened yet.";
+
+const TEST_FILE = "test-network-request.html";
+const TEST_PATH = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/" + TEST_FILE;
+
+const NET_PREF = "devtools.webconsole.filter.networkinfo";
+Services.prefs.setBoolPref(NET_PREF, true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(NET_PREF);
+});
+
+add_task(function* () {
+ let { tab, browser } = yield loadTab(TEST_URI);
+
+ // Test that the request appears in the console.
+ let hud = yield openConsole();
+ info("Web console is open");
+
+ yield loadDocument(browser);
+ info("Document loaded.");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "network message",
+ text: TEST_FILE,
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG
+ }
+ ]
+ });
+
+ // Test that the request appears in the network panel.
+ let target = TargetFactory.forTab(tab);
+ let toolbox = yield gDevTools.showToolbox(target, "netmonitor");
+ info("Network panel is open.");
+
+ testNetmonitor(toolbox);
+});
+
+function loadDocument(browser) {
+ let deferred = promise.defer();
+
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ deferred.resolve();
+ }, true);
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_PATH);
+
+ return deferred.promise;
+}
+
+function testNetmonitor(toolbox) {
+ let monitor = toolbox.getCurrentPanel();
+ let { RequestsMenu } = monitor.panelWin.NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ is(RequestsMenu.itemCount, 1, "Network request appears in the network panel");
+
+ let item = RequestsMenu.getItemAtIndex(0);
+ is(item.attachment.method, "GET", "The attached method is correct.");
+ is(item.attachment.url, TEST_PATH, "The attached url is correct.");
+}
diff --git a/devtools/client/webconsole/test/browser_output_breaks_after_console_dir_uninspectable.js b/devtools/client/webconsole/test/browser_output_breaks_after_console_dir_uninspectable.js
new file mode 100644
index 000000000..38a5b5419
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_output_breaks_after_console_dir_uninspectable.js
@@ -0,0 +1,47 @@
+/* -*- 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/ */
+
+// Make sure that the Web Console output does not break after we try to call
+// console.dir() for objects that are not inspectable.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,test for bug 773466";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ hud.jsterm.clearOutput(true);
+
+ hud.jsterm.execute("console.log('fooBug773466a')");
+ hud.jsterm.execute("myObj = Object.create(null)");
+ hud.jsterm.execute("console.dir(myObj)");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "fooBug773466a",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ name: "console.dir output",
+ consoleDir: "[object Object]",
+ }],
+ });
+
+ content.console.log("fooBug773466b");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "fooBug773466b",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_output_longstring_expand.js b/devtools/client/webconsole/test/browser_output_longstring_expand.js
new file mode 100644
index 000000000..bae8ca128
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_output_longstring_expand.js
@@ -0,0 +1,85 @@
+/* -*- 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/ */
+
+// Test that long strings can be expanded in the console output.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,test for bug 787981 - check " +
+ "that long strings can be expanded in the output.";
+
+add_task(function* () {
+ let { DebuggerServer } = require("devtools/server/main");
+
+ let longString = (new Array(DebuggerServer.LONG_STRING_LENGTH + 4))
+ .join("a") + "foobar";
+ let initialString =
+ longString.substring(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH);
+
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ hud.jsterm.clearOutput(true);
+ hud.jsterm.execute("console.log('bazbaz', '" + longString + "', 'boom')");
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "console.log output",
+ text: ["bazbaz", "boom", initialString],
+ noText: "foobar",
+ longString: true,
+ }],
+ });
+
+ let clickable = result.longStrings[0];
+ ok(clickable, "long string ellipsis is shown");
+
+ clickable.scrollIntoView(false);
+
+ EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow);
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "full string",
+ text: ["bazbaz", "boom", longString],
+ category: CATEGORY_WEBDEV,
+ longString: false,
+ }],
+ });
+
+ hud.jsterm.clearOutput(true);
+ let msg = yield execute(hud, "'" + longString + "'");
+
+ isnot(msg.textContent.indexOf(initialString), -1,
+ "initial string is shown");
+ is(msg.textContent.indexOf(longString), -1,
+ "full string is not shown");
+
+ clickable = msg.querySelector(".longStringEllipsis");
+ ok(clickable, "long string ellipsis is shown");
+
+ clickable.scrollIntoView(false);
+
+ EventUtils.synthesizeMouse(clickable, 3, 4, {}, hud.iframeWindow);
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "full string",
+ text: longString,
+ category: CATEGORY_OUTPUT,
+ longString: false,
+ }],
+ });
+});
+
+function execute(hud, str) {
+ let deferred = promise.defer();
+ hud.jsterm.execute(str, deferred.resolve);
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_repeated_messages_accuracy.js b/devtools/client/webconsole/test/browser_repeated_messages_accuracy.js
new file mode 100644
index 000000000..36b13ce02
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_repeated_messages_accuracy.js
@@ -0,0 +1,178 @@
+/* -*- 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/ */
+
+// Test that makes sure messages are not considered repeated when coming from
+// different lines of code, or from different severities, etc.
+// See bugs 720180 and 800510.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-repeated-messages.html";
+const PREF = "devtools.webconsole.persistlog";
+
+add_task(function* () {
+ Services.prefs.setBoolPref(PREF, true);
+
+ let { browser } = yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ yield consoleOpened(hud);
+
+ let loaded = loadBrowser(browser);
+ BrowserReload();
+ yield loaded;
+
+ yield testCSSRepeats(hud);
+ yield testCSSRepeatsAfterReload(hud);
+ yield testConsoleRepeats(hud);
+ yield testConsoleFalsyValues(hud);
+
+ Services.prefs.clearUserPref(PREF);
+});
+
+function consoleOpened(hud) {
+ // Check that css warnings are not coalesced if they come from different
+ // lines.
+ info("waiting for 2 css warnings");
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "two css warnings",
+ category: CATEGORY_CSS,
+ count: 2,
+ repeats: 1,
+ }],
+ });
+}
+
+function testCSSRepeats(hud) {
+ info("wait for repeats after page reload");
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "two css warnings, repeated twice",
+ category: CATEGORY_CSS,
+ repeats: 2,
+ count: 2,
+ }],
+ });
+}
+
+function testCSSRepeatsAfterReload(hud) {
+ hud.jsterm.clearOutput(true);
+ hud.jsterm.execute("testConsole()");
+
+ info("wait for repeats with the console API");
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "console.log 'foo repeat' repeated twice",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ repeats: 2,
+ },
+ {
+ name: "console.log 'foo repeat' repeated once",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ repeats: 1,
+ },
+ {
+ name: "console.error 'foo repeat' repeated once",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_ERROR,
+ repeats: 1,
+ },
+ ],
+ });
+}
+
+function testConsoleRepeats(hud) {
+ hud.jsterm.clearOutput(true);
+ hud.jsterm.execute("undefined");
+
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.console.log("undefined");
+ });
+
+ info("make sure console API messages are not coalesced with jsterm output");
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "'undefined' jsterm input message",
+ text: "undefined",
+ category: CATEGORY_INPUT,
+ },
+ {
+ name: "'undefined' jsterm output message",
+ text: "undefined",
+ category: CATEGORY_OUTPUT,
+ },
+ {
+ name: "'undefined' console.log message",
+ text: "undefined",
+ category: CATEGORY_WEBDEV,
+ repeats: 1,
+ },
+ ],
+ });
+}
+
+function testConsoleFalsyValues(hud) {
+ hud.jsterm.clearOutput(true);
+ hud.jsterm.execute("testConsoleFalsyValues()");
+
+ info("wait for repeats of falsy values with the console API");
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "console.log 'NaN' repeated once",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ repeats: 1,
+ },
+ {
+ name: "console.log 'undefined' repeated once",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ repeats: 1,
+ },
+ {
+ name: "console.log 'null' repeated once",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ repeats: 1,
+ },
+ {
+ name: "console.log 'NaN' repeated twice",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ repeats: 2,
+ },
+ {
+ name: "console.log 'undefined' repeated twice",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ repeats: 2,
+ },
+ {
+ name: "console.log 'null' repeated twice",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ repeats: 2,
+ },
+ ],
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_result_format_as_string.js b/devtools/client/webconsole/test/browser_result_format_as_string.js
new file mode 100644
index 000000000..0352d0afa
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_result_format_as_string.js
@@ -0,0 +1,40 @@
+/* -*- 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/ */
+
+// Make sure that JS eval result are properly formatted as strings.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-result-format-as-string.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ hud.jsterm.clearOutput(true);
+
+ let msg = yield execute(hud, "document.querySelector('p')");
+
+ is(hud.outputNode.textContent.indexOf("bug772506_content"), -1,
+ "no content element found");
+ ok(!hud.outputNode.querySelector("#foobar"), "no #foobar element found");
+
+ ok(msg, "eval output node found");
+ is(msg.textContent.indexOf("<div>"), -1,
+ "<div> string is not displayed");
+ isnot(msg.textContent.indexOf("<p>"), -1,
+ "<p> string is displayed");
+
+ EventUtils.synthesizeMouseAtCenter(msg, {type: "mousemove"});
+ ok(!gBrowser._bug772506, "no content variable");
+});
+
+function execute(hud, str) {
+ let deferred = promise.defer();
+ hud.jsterm.execute(str, deferred.resolve);
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_warn_user_about_replaced_api.js b/devtools/client/webconsole/test/browser_warn_user_about_replaced_api.js
new file mode 100644
index 000000000..0eeb6eaa3
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_warn_user_about_replaced_api.js
@@ -0,0 +1,86 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_REPLACED_API_URI = "http://example.com/browser/devtools/client/" +
+ "webconsole/test/test-console-replaced-api.html";
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/testscript.js";
+const PREF = "devtools.webconsole.persistlog";
+
+add_task(function* () {
+ Services.prefs.setBoolPref(PREF, true);
+
+ let { browser } = yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ yield testWarningNotPresent(hud);
+
+ let loaded = loadBrowser(browser);
+ BrowserTestUtils.loadURI(browser, TEST_REPLACED_API_URI);
+ yield loaded;
+
+ let hud2 = yield openConsole();
+
+ yield testWarningPresent(hud2);
+
+ Services.prefs.clearUserPref(PREF);
+});
+
+function testWarningNotPresent(hud) {
+ let deferred = promise.defer();
+
+ is(hud.outputNode.textContent.indexOf("logging API"), -1,
+ "no warning displayed");
+
+ // Bug 862024: make sure the warning doesn't show after page reload.
+ info("reload " + TEST_URI);
+ executeSoon(function () {
+ let browser = gBrowser.selectedBrowser;
+ ContentTask.spawn(browser, null, "() => content.location.reload()");
+ });
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "testscript.js",
+ category: CATEGORY_NETWORK,
+ }],
+ }).then(() => executeSoon(() => {
+ is(hud.outputNode.textContent.indexOf("logging API"), -1,
+ "no warning displayed");
+ closeConsole().then(deferred.resolve);
+ }));
+
+ return deferred.promise;
+}
+
+function testWarningPresent(hud) {
+ info("wait for the warning to show");
+ let deferred = promise.defer();
+
+ let warning = {
+ webconsole: hud,
+ messages: [{
+ text: /logging API .+ disabled by a script/,
+ category: CATEGORY_JS,
+ severity: SEVERITY_WARNING,
+ }],
+ };
+
+ waitForMessages(warning).then(() => {
+ hud.jsterm.clearOutput();
+
+ executeSoon(() => {
+ info("reload the test page and wait for the warning to show");
+ waitForMessages(warning).then(deferred.resolve);
+ let browser = gBrowser.selectedBrowser;
+ ContentTask.spawn(browser, null, "() => content.location.reload()");
+ });
+ });
+
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_allow_mixedcontent_securityerrors.js b/devtools/client/webconsole/test/browser_webconsole_allow_mixedcontent_securityerrors.js
new file mode 100644
index 000000000..07f6372d0
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_allow_mixedcontent_securityerrors.js
@@ -0,0 +1,69 @@
+/* -*- 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/ */
+
+// 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/test-mixedcontent-securityerrors.html";
+const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Web/Security/" +
+ "Mixed_content" + DOCS_GA_PARAMS;
+
+add_task(function* () {
+ yield pushPrefEnv();
+
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ let results = yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "Logged mixed active content",
+ text: "Loading mixed (insecure) active content " +
+ "\u201chttp://example.com/\u201d on a secure page",
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_WARNING,
+ objects: true,
+ },
+ {
+ name: "Logged mixed passive content - image",
+ text: "Loading mixed (insecure) display content " +
+ "\u201chttp://example.com/tests/image/test/mochitest/blue.png\u201d " +
+ "on a secure page",
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_WARNING,
+ objects: true,
+ },
+ ],
+ });
+
+ yield testClickOpenNewTab(hud, results);
+});
+
+function pushPrefEnv() {
+ let deferred = promise.defer();
+ let options = {"set":
+ [["security.mixed_content.block_active_content", false],
+ ["security.mixed_content.block_display_content", false]
+ ]};
+ SpecialPowers.pushPrefEnv(options, deferred.resolve);
+ return deferred.promise;
+}
+
+function testClickOpenNewTab(hud, results) {
+ let warningNode = results[0].clickableElements[0];
+ ok(warningNode, "link element");
+ ok(warningNode.classList.contains("learn-more-link"), "link class name");
+ return simulateMessageLinkClick(warningNode, LEARN_MORE_URI);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_assert.js b/devtools/client/webconsole/test/browser_webconsole_assert.js
new file mode 100644
index 000000000..7fc9693f1
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_assert.js
@@ -0,0 +1,56 @@
+/* -*- 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/ */
+
+// Test that console.assert() works as expected (i.e. outputs only on falsy
+// asserts). See bug 760193.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-assert.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+ yield consoleOpened(hud);
+});
+
+function consoleOpened(hud) {
+ hud.jsterm.execute("test()");
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "undefined",
+ category: CATEGORY_OUTPUT,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "start",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "false assert",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_ERROR,
+ },
+ {
+ text: "falsy assert",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_ERROR,
+ },
+ {
+ text: "end",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(() => {
+ let nodes = hud.outputNode.querySelectorAll(".message");
+ is(nodes.length, 6,
+ "only six messages are displayed, no output from the true assert");
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_autocomplete-properties-with-non-alphanumeric-names.js b/devtools/client/webconsole/test/browser_webconsole_autocomplete-properties-with-non-alphanumeric-names.js
new file mode 100644
index 000000000..0ba168078
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_autocomplete-properties-with-non-alphanumeric-names.js
@@ -0,0 +1,47 @@
+/* -*- 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/ */
+
+"use strict";
+
+// Test that properties starting with underscores or dollars can be
+// autocompleted (bug 967468).
+
+add_task(function* () {
+ const TEST_URI = "data:text/html;charset=utf8,test autocompletion with " +
+ "$ or _";
+ yield loadTab(TEST_URI);
+
+ function* autocomplete(term) {
+ let deferred = promise.defer();
+
+ jsterm.setInputValue(term);
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, deferred.resolve);
+
+ yield deferred.promise;
+
+ ok(popup.itemCount > 0,
+ "There's " + popup.itemCount + " suggestions for '" + term + "'");
+ }
+
+ let { jsterm } = yield openConsole();
+ let popup = jsterm.autocompletePopup;
+
+ yield jsterm.execute("var testObject = {$$aaab: '', $$aaac: ''}");
+
+ // Should work with bug 967468.
+ yield autocomplete("Object.__d");
+ yield autocomplete("testObject.$$a");
+
+ // Here's when things go wrong in bug 967468.
+ yield autocomplete("Object.__de");
+ yield autocomplete("testObject.$$aa");
+
+ // Should work with bug 1207868.
+ yield jsterm.execute("let foobar = {a: ''}; const blargh = {a: 1};");
+ yield autocomplete("foobar");
+ yield autocomplete("blargh");
+ yield autocomplete("foobar.a");
+ yield autocomplete("blargh.a");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_autocomplete_accessibility.js b/devtools/client/webconsole/test/browser_webconsole_autocomplete_accessibility.js
new file mode 100644
index 000000000..bcd2e22d0
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_autocomplete_accessibility.js
@@ -0,0 +1,60 @@
+/* -*- 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 the autocomplete input is being blurred and focused when selecting a value.
+// This will help screen-readers notify users of the value that was set in the input.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<p>test code completion";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ let jsterm = hud.jsterm;
+ let input = jsterm.inputNode;
+
+ info("Type 'd' to open the autocomplete popup");
+ yield autocomplete(jsterm, "d");
+
+ // Add listeners for focus and blur events.
+ let wasBlurred = false;
+ input.addEventListener("blur", () => {
+ wasBlurred = true;
+ }, {
+ once: true
+ });
+
+ let wasFocused = false;
+ input.addEventListener("blur", () => {
+ ok(wasBlurred, "jsterm input received a blur event before received back the focus");
+ wasFocused = true;
+ }, {
+ once: true
+ });
+
+ info("Close the autocomplete popup by simulating a TAB key event");
+ let onPopupClosed = jsterm.autocompletePopup.once("popup-closed");
+ EventUtils.synthesizeKey("VK_TAB", {});
+
+ info("Wait for the autocomplete popup to be closed");
+ yield onPopupClosed;
+
+ ok(wasFocused, "jsterm input received a focus event");
+});
+
+function* autocomplete(jsterm, value) {
+ let popup = jsterm.autocompletePopup;
+
+ yield new Promise(resolve => {
+ jsterm.setInputValue(value);
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve);
+ });
+
+ ok(popup.isOpen && popup.itemCount > 0,
+ "Autocomplete popup is open and contains suggestions");
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_autocomplete_and_selfxss.js b/devtools/client/webconsole/test/browser_webconsole_autocomplete_and_selfxss.js
new file mode 100644
index 000000000..d0c6eb673
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_autocomplete_and_selfxss.js
@@ -0,0 +1,130 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>test for bug 642615";
+
+XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1",
+ "nsIClipboardHelper");
+var WebConsoleUtils = require("devtools/client/webconsole/utils").Utils;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ yield consoleOpened(hud);
+});
+
+function consoleOpened(HUD) {
+ let deferred = promise.defer();
+
+ let jsterm = HUD.jsterm;
+ let stringToCopy = "foobazbarBug642615";
+
+ jsterm.clearOutput();
+
+ ok(!jsterm.completeNode.value, "no completeNode.value");
+
+ jsterm.setInputValue("doc");
+
+ let completionValue;
+
+ // wait for key "u"
+ function onCompletionValue() {
+ completionValue = jsterm.completeNode.value;
+
+ // Arguments: expected, setup, success, failure.
+ waitForClipboard(
+ stringToCopy,
+ function () {
+ clipboardHelper.copyString(stringToCopy);
+ },
+ onClipboardCopy,
+ finishTest);
+ }
+
+ function onClipboardCopy() {
+ testSelfXss();
+
+ jsterm.setInputValue("docu");
+ info("wait for completion update after clipboard paste");
+ updateEditUIVisibility();
+ jsterm.once("autocomplete-updated", onClipboardPaste);
+ goDoCommand("cmd_paste");
+ }
+
+ // Self xss prevention tests (bug 994134)
+ function testSelfXss() {
+ 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++) {
+ jsterm.setInputValue(i);
+ jsterm.execute();
+ }
+ is(WebConsoleUtils.usageCount, 4, "Usage count incremented");
+ WebConsoleUtils.usageCount = 0;
+ updateEditUIVisibility();
+
+ let oldVal = jsterm.getInputValue();
+ goDoCommand("cmd_paste");
+ let notificationbox = jsterm.hud.document.getElementById("webconsole-notificationbox");
+ let notification = notificationbox.getNotificationWithValue("selfxss-notification");
+ ok(notification, "Self-xss notification shown");
+ is(oldVal, jsterm.getInputValue(), "Paste blocked by self-xss prevention");
+
+ // Allow pasting
+ jsterm.setInputValue("allow pasting");
+ let evt = document.createEvent("KeyboardEvent");
+ evt.initKeyEvent("keyup", true, true, window,
+ 0, 0, 0, 0,
+ 0, " ".charCodeAt(0));
+ jsterm.inputNode.dispatchEvent(evt);
+ jsterm.setInputValue("");
+ goDoCommand("cmd_paste");
+ isnot("", jsterm.getInputValue(), "Paste works");
+ }
+ function onClipboardPaste() {
+ ok(!jsterm.completeNode.value, "no completion value after paste");
+
+ info("wait for completion update after undo");
+ jsterm.once("autocomplete-updated", onCompletionValueAfterUndo);
+
+ // Get out of the webconsole event loop.
+ executeSoon(() => {
+ goDoCommand("cmd_undo");
+ });
+ }
+
+ function onCompletionValueAfterUndo() {
+ is(jsterm.completeNode.value, completionValue,
+ "same completeNode.value after undo");
+
+ info("wait for completion update after clipboard paste (ctrl-v)");
+ jsterm.once("autocomplete-updated", () => {
+ ok(!jsterm.completeNode.value,
+ "no completion value after paste (ctrl-v)");
+
+ // using executeSoon() to get out of the webconsole event loop.
+ executeSoon(deferred.resolve);
+ });
+
+ // Get out of the webconsole event loop.
+ executeSoon(() => {
+ EventUtils.synthesizeKey("v", {accelKey: true});
+ });
+ }
+
+ info("wait for completion value after typing 'docu'");
+ jsterm.once("autocomplete-updated", onCompletionValue);
+
+ EventUtils.synthesizeKey("u", {});
+
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_autocomplete_crossdomain_iframe.js b/devtools/client/webconsole/test/browser_webconsole_autocomplete_crossdomain_iframe.js
new file mode 100644
index 000000000..b4471bd6b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_autocomplete_crossdomain_iframe.js
@@ -0,0 +1,64 @@
+/* -*- 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/ */
+
+// Test that autocomplete doesn't break when trying to reach into objects from
+// a different domain, bug 989025.
+
+"use strict";
+
+function test() {
+ let hud;
+
+ const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-989025-iframe-parent.html";
+
+ Task.spawn(function* () {
+ const {tab} = yield loadTab(TEST_URI);
+ hud = yield openConsole(tab);
+
+ hud.jsterm.execute("document.title");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "989025 - iframe parent",
+ category: CATEGORY_OUTPUT,
+ }],
+ });
+
+ let autocompleteUpdated = hud.jsterm.once("autocomplete-updated");
+
+ hud.jsterm.setInputValue("window[0].document");
+ executeSoon(() => {
+ EventUtils.synthesizeKey(".", {});
+ });
+
+ yield autocompleteUpdated;
+
+ hud.jsterm.setInputValue("window[0].document.title");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "Permission denied",
+ category: CATEGORY_OUTPUT,
+ severity: SEVERITY_ERROR,
+ }],
+ });
+
+ hud.jsterm.execute("window.location");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "test-bug-989025-iframe-parent.html",
+ category: CATEGORY_OUTPUT,
+ }],
+ });
+
+ yield closeConsole(tab);
+ }).then(finishTest);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_autocomplete_in_debugger_stackframe.js b/devtools/client/webconsole/test/browser_webconsole_autocomplete_in_debugger_stackframe.js
new file mode 100644
index 000000000..60ba5ff0e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_autocomplete_in_debugger_stackframe.js
@@ -0,0 +1,245 @@
+/* -*- 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/ */
+
+// 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/test-autocomplete-in-stackframe.html";
+
+// Force the old debugger UI since it's directly used (see Bug 1301705)
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+var gStackframes;
+registerCleanupFunction(function () {
+ gStackframes = null;
+});
+
+requestLongerTimeout(2);
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ yield testCompletion(hud);
+});
+
+function* testCompletion(hud) {
+ let jsterm = hud.jsterm;
+ let input = jsterm.inputNode;
+ let popup = jsterm.autocompletePopup;
+
+ // Test that document.title gives string methods. Native getters must execute.
+ input.value = "document.title.";
+ input.setSelectionRange(input.value.length, input.value.length);
+ yield new Promise(resolve => {
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve);
+ });
+
+ let newItems = popup.getItems();
+ ok(newItems.length > 0, "'document.title.' gave a list of suggestions");
+ ok(newItems.some(function (item) {
+ return item.label == "substr";
+ }), "autocomplete results do contain substr");
+ ok(newItems.some(function (item) {
+ return item.label == "toLowerCase";
+ }), "autocomplete results do contain toLowerCase");
+ ok(newItems.some(function (item) {
+ return item.label == "strike";
+ }), "autocomplete results do contain strike");
+
+ // Test if 'f' gives 'foo1' but not 'foo2' or 'foo3'
+ input.value = "f";
+ input.setSelectionRange(1, 1);
+ yield new Promise(resolve => {
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve);
+ });
+
+ newItems = popup.getItems();
+ ok(newItems.length > 0, "'f' gave a list of suggestions");
+ ok(!newItems.every(function (item) {
+ return item.label != "foo1";
+ }), "autocomplete results do contain foo1");
+ ok(!newItems.every(function (item) {
+ return item.label != "foo1Obj";
+ }), "autocomplete results do contain foo1Obj");
+ ok(newItems.every(function (item) {
+ return item.label != "foo2";
+ }), "autocomplete results do not contain foo2");
+ ok(newItems.every(function (item) {
+ return item.label != "foo2Obj";
+ }), "autocomplete results do not contain foo2Obj");
+ ok(newItems.every(function (item) {
+ return item.label != "foo3";
+ }), "autocomplete results do not contain foo3");
+ ok(newItems.every(function (item) {
+ return item.label != "foo3Obj";
+ }), "autocomplete results do not contain foo3Obj");
+
+ // Test if 'foo1Obj.' gives 'prop1' and 'prop2'
+ input.value = "foo1Obj.";
+ input.setSelectionRange(8, 8);
+ yield new Promise(resolve => {
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve);
+ });
+
+ newItems = popup.getItems();
+ ok(!newItems.every(function (item) {
+ return item.label != "prop1";
+ }), "autocomplete results do contain prop1");
+ ok(!newItems.every(function (item) {
+ return item.label != "prop2";
+ }), "autocomplete results do contain prop2");
+
+ // Test if 'foo1Obj.prop2.' gives 'prop21'
+ input.value = "foo1Obj.prop2.";
+ input.setSelectionRange(14, 14);
+ yield new Promise(resolve => {
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve);
+ });
+
+ newItems = popup.getItems();
+ ok(!newItems.every(function (item) {
+ return item.label != "prop21";
+ }), "autocomplete results do contain prop21");
+
+ info("Opening Debugger");
+ let dbg = yield openDebugger();
+
+ info("Waiting for pause");
+ yield pauseDebugger(dbg);
+
+ info("Opening Console again");
+ yield openConsole();
+
+ // From this point on the
+ // Test if 'f' gives 'foo3' and 'foo1' but not 'foo2'
+ input.value = "f";
+ input.setSelectionRange(1, 1);
+ yield new Promise(resolve => {
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve);
+ });
+
+ newItems = popup.getItems();
+ ok(newItems.length > 0, "'f' gave a list of suggestions");
+ ok(!newItems.every(function (item) {
+ return item.label != "foo3";
+ }), "autocomplete results do contain foo3");
+ ok(!newItems.every(function (item) {
+ return item.label != "foo3Obj";
+ }), "autocomplete results do contain foo3Obj");
+ ok(!newItems.every(function (item) {
+ return item.label != "foo1";
+ }), "autocomplete results do contain foo1");
+ ok(!newItems.every(function (item) {
+ return item.label != "foo1Obj";
+ }), "autocomplete results do contain foo1Obj");
+ ok(newItems.every(function (item) {
+ return item.label != "foo2";
+ }), "autocomplete results do not contain foo2");
+ ok(newItems.every(function (item) {
+ return item.label != "foo2Obj";
+ }), "autocomplete results do not contain foo2Obj");
+
+ yield openDebugger();
+
+ gStackframes.selectFrame(1);
+
+ info("openConsole");
+ yield openConsole();
+
+ // Test if 'f' gives 'foo2' and 'foo1' but not 'foo3'
+ input.value = "f";
+ input.setSelectionRange(1, 1);
+ yield new Promise(resolve => {
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve);
+ });
+
+ newItems = popup.getItems();
+ ok(newItems.length > 0, "'f' gave a list of suggestions");
+ ok(!newItems.every(function (item) {
+ return item.label != "foo2";
+ }), "autocomplete results do contain foo2");
+ ok(!newItems.every(function (item) {
+ return item.label != "foo2Obj";
+ }), "autocomplete results do contain foo2Obj");
+ ok(!newItems.every(function (item) {
+ return item.label != "foo1";
+ }), "autocomplete results do contain foo1");
+ ok(!newItems.every(function (item) {
+ return item.label != "foo1Obj";
+ }), "autocomplete results do contain foo1Obj");
+ ok(newItems.every(function (item) {
+ return item.label != "foo3";
+ }), "autocomplete results do not contain foo3");
+ ok(newItems.every(function (item) {
+ return item.label != "foo3Obj";
+ }), "autocomplete results do not contain foo3Obj");
+
+ // Test if 'foo2Obj.' gives 'prop1'
+ input.value = "foo2Obj.";
+ input.setSelectionRange(8, 8);
+ yield new Promise(resolve => {
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve);
+ });
+
+ newItems = popup.getItems();
+ ok(!newItems.every(function (item) {
+ return item.label != "prop1";
+ }), "autocomplete results do contain prop1");
+
+ // Test if 'foo2Obj.prop1.' gives 'prop11'
+ input.value = "foo2Obj.prop1.";
+ input.setSelectionRange(14, 14);
+ yield new Promise(resolve => {
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve);
+ });
+
+ newItems = popup.getItems();
+ ok(!newItems.every(function (item) {
+ return item.label != "prop11";
+ }), "autocomplete results do contain prop11");
+
+ // Test if 'foo2Obj.prop1.prop11.' gives suggestions for a string
+ // i.e. 'length'
+ input.value = "foo2Obj.prop1.prop11.";
+ input.setSelectionRange(21, 21);
+ yield new Promise(resolve => {
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve);
+ });
+
+ newItems = popup.getItems();
+ ok(!newItems.every(function (item) {
+ return item.label != "length";
+ }), "autocomplete results do contain length");
+
+ // Test if 'foo1Obj[0].' throws no errors.
+ input.value = "foo2Obj[0].";
+ input.setSelectionRange(11, 11);
+ yield new Promise(resolve => {
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, resolve);
+ });
+
+ newItems = popup.getItems();
+ is(newItems.length, 0, "no items for foo2Obj[0]");
+}
+
+function pauseDebugger(aResult) {
+ let debuggerWin = aResult.panelWin;
+ let debuggerController = debuggerWin.DebuggerController;
+ let thread = debuggerController.activeThread;
+ gStackframes = debuggerController.StackFrames;
+ return new Promise(resolve => {
+ thread.addOneTimeListener("framesadded", resolve);
+
+ info("firstCall()");
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.wrappedJSObject.firstCall();
+ });
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_autocomplete_popup_close_on_tab_switch.js b/devtools/client/webconsole/test/browser_webconsole_autocomplete_popup_close_on_tab_switch.js
new file mode 100644
index 000000000..afa3dd55d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_autocomplete_popup_close_on_tab_switch.js
@@ -0,0 +1,27 @@
+/* -*- 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/ */
+
+// Test that the autocomplete popup closes on switching tabs. See bug 900448.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>bug 900448 - autocomplete " +
+ "popup closes on tab switch";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ let popup = hud.jsterm.autocompletePopup;
+ let popupShown = once(popup, "popup-opened");
+
+ hud.jsterm.setInputValue("sc");
+ EventUtils.synthesizeKey("r", {});
+
+ yield popupShown;
+
+ yield loadTab("data:text/html;charset=utf-8,<p>testing autocomplete closes");
+
+ ok(!popup.isOpen, "Popup closes on tab switch");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_block_mixedcontent_securityerrors.js b/devtools/client/webconsole/test/browser_webconsole_block_mixedcontent_securityerrors.js
new file mode 100644
index 000000000..ff4157a3b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_block_mixedcontent_securityerrors.js
@@ -0,0 +1,110 @@
+/* -*- 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/ */
+
+// 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/test-mixedcontent-securityerrors.html";
+const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Web/Security/" +
+ "Mixed_content" + DOCS_GA_PARAMS;
+
+add_task(function* () {
+ yield pushPrefEnv();
+
+ let { browser } = yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ let results = yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "Logged blocking mixed active content",
+ text: "Blocked loading mixed active content \u201chttp://example.com/\u201d",
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_ERROR,
+ objects: true,
+ },
+ {
+ name: "Logged blocking mixed passive content - image",
+ text: "Blocked loading mixed active content \u201chttp://example.com/\u201d",
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_ERROR,
+ objects: true,
+ },
+ ],
+ });
+
+ yield testClickOpenNewTab(hud, results[0]);
+
+ let results2 = yield mixedContentOverrideTest2(hud, browser);
+
+ yield testClickOpenNewTab(hud, results2[0]);
+});
+
+function pushPrefEnv() {
+ let deferred = promise.defer();
+ let options = {
+ "set": [
+ ["security.mixed_content.block_active_content", true],
+ ["security.mixed_content.block_display_content", true],
+ ["security.mixed_content.use_hsts", false],
+ ["security.mixed_content.send_hsts_priming", false],
+ ]
+ };
+ SpecialPowers.pushPrefEnv(options, deferred.resolve);
+ return deferred.promise;
+}
+
+function mixedContentOverrideTest2(hud, browser) {
+ let deferred = promise.defer();
+ let {gIdentityHandler} = browser.ownerGlobal;
+ ok(gIdentityHandler._identityBox.classList.contains("mixedActiveBlocked"),
+ "Mixed Active Content state appeared on identity box");
+ gIdentityHandler.disableMixedContentProtection();
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "Logged blocking mixed active content",
+ text: "Loading mixed (insecure) active content " +
+ "\u201chttp://example.com/\u201d on a secure page",
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_WARNING,
+ objects: true,
+ },
+ {
+ name: "Logged blocking mixed passive content - image",
+ text: "Loading mixed (insecure) display content" +
+ " \u201chttp://example.com/tests/image/test/mochitest/blue.png\u201d" +
+ " on a secure page",
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_WARNING,
+ objects: true,
+ },
+ ],
+ }).then(msgs => deferred.resolve(msgs), e => console.error(e));
+
+ return deferred.promise;
+}
+
+function testClickOpenNewTab(hud, match) {
+ let warningNode = match.clickableElements[0];
+ ok(warningNode, "link element");
+ ok(warningNode.classList.contains("learn-more-link"), "link class name");
+ return simulateMessageLinkClick(warningNode, LEARN_MORE_URI);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_1006027_message_timestamps_incorrect.js b/devtools/client/webconsole/test/browser_webconsole_bug_1006027_message_timestamps_incorrect.js
new file mode 100644
index 000000000..ee141a72f
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_1006027_message_timestamps_incorrect.js
@@ -0,0 +1,45 @@
+/* -*- 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/ */
+
+"use strict";
+
+function test() {
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ const {tab} = yield loadTab("data:text/html;charset=utf8,<title>Test for " +
+ "Bug 1006027");
+
+ const hud = yield openConsole(tab);
+
+ hud.jsterm.execute("console.log('bug1006027')");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "console.log",
+ text: "bug1006027",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ info("hud.outputNode.textContent:\n" + hud.outputNode.textContent);
+ let timestampNodes = hud.outputNode.querySelectorAll("span.timestamp");
+ let aTimestampMilliseconds = Array.prototype.map.call(timestampNodes,
+ function (value) {
+ // We are parsing timestamps as local time, relative to the begin of
+ // the epoch.
+ // This is not the correct value of the timestamp, but good enough for
+ // comparison.
+ return Date.parse("T" + String.trim(value.textContent));
+ });
+
+ let minTimestamp = Math.min.apply(null, aTimestampMilliseconds);
+ let maxTimestamp = Math.max.apply(null, aTimestampMilliseconds);
+ ok(Math.abs(maxTimestamp - minTimestamp) < 2000,
+ "console.log message timestamp spread < 2000ms confirmed");
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_1010953_cspro.js b/devtools/client/webconsole/test/browser_webconsole_bug_1010953_cspro.js
new file mode 100644
index 000000000..ace13f8d1
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_1010953_cspro.js
@@ -0,0 +1,55 @@
+/* -*- 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/ */
+
+/* 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.
+*/
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,Web Console CSP report only " +
+ "test (bug 1010953)";
+const TEST_VIOLATION = "http://example.com/browser/devtools/client/" +
+ "webconsole/test/test_bug_1010953_cspro.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 http://example.com\u201d).";
+const CSP_REPORT_MSG = "Content Security Policy: The page\u2019s settings " +
+ "observed the loading of a resource at " +
+ "http://some.example.com/test_bug_1010953_cspro.js " +
+ "(\u201cscript-src http://example.com\u201d). A CSP report is " +
+ "being sent.";
+
+add_task(function* () {
+ let { browser } = yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ hud.jsterm.clearOutput();
+
+ let loaded = loadBrowser(browser);
+ BrowserTestUtils.loadURI(browser, TEST_VIOLATION);
+ yield loaded;
+
+ yield waitForSuccess({
+ name: "Confirmed that CSP and CSP-Report-Only log different messages to " +
+ "the console.",
+ validator: function () {
+ console.log(hud.outputNode.textContent);
+ let success = false;
+ success = hud.outputNode.textContent.indexOf(CSP_VIOLATION_MSG) > -1 &&
+ hud.outputNode.textContent.indexOf(CSP_REPORT_MSG) > -1;
+ return success;
+ }
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_1050691_click_function_to_source.js b/devtools/client/webconsole/test/browser_webconsole_bug_1050691_click_function_to_source.js
new file mode 100644
index 000000000..9b220b4a2
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_1050691_click_function_to_source.js
@@ -0,0 +1,60 @@
+/* -*- 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 displays its source in the debugger.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug_1050691_click_function_to_source.html";
+
+// Force the old debugger UI since it's directly used (see Bug 1301705)
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ // Open the Debugger panel.
+ let debuggerPanel = yield openDebugger();
+ // And right after come back to the Console panel.
+ yield openConsole();
+ yield testWithDebuggerOpen(hud, debuggerPanel);
+});
+
+function* testWithDebuggerOpen(hud, debuggerPanel) {
+ let clickable = yield printFunction(hud);
+ let panelWin = debuggerPanel.panelWin;
+ let onEditorLocationSet = panelWin.once(panelWin.EVENTS.EDITOR_LOCATION_SET);
+ synthesizeClick(clickable, hud);
+ yield onEditorLocationSet;
+ ok(isDebuggerCaretPos(debuggerPanel, 7),
+ "Clicking on a function should go to its source in the debugger view");
+}
+
+function synthesizeClick(clickable, hud) {
+ EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow);
+}
+
+var printFunction = Task.async(function* (hud) {
+ hud.jsterm.clearOutput();
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.wrappedJSObject.foo();
+ });
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+ let msg = [...result.matched][0];
+ let clickable = msg.querySelector("a");
+ ok(clickable, "clickable item for object should exist");
+ return clickable;
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_1247459_violation.js b/devtools/client/webconsole/test/browser_webconsole_bug_1247459_violation.js
new file mode 100644
index 000000000..26bac7f57
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_1247459_violation.js
@@ -0,0 +1,40 @@
+/* -*- 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 the Web Console CSP messages for two META policies
+// are correctly displayed.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,Web Console CSP violation test";
+const TEST_VIOLATION = "https://example.com/browser/devtools/client/" +
+ "webconsole/test/test_bug_1247459_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 " +
+ "https://example.com\u201d).";
+
+add_task(function* () {
+ let { browser } = yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ hud.jsterm.clearOutput();
+
+ let loaded = loadBrowser(browser);
+ BrowserTestUtils.loadURI(browser, TEST_VIOLATION);
+ yield loaded;
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "CSP policy URI warning displayed successfully",
+ text: CSP_VIOLATION_MSG,
+ repeats: 2
+ }
+ ]
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_578437_page_reload.js b/devtools/client/webconsole/test/browser_webconsole_bug_578437_page_reload.js
new file mode 100644
index 000000000..fb0182d8b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_578437_page_reload.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 the console object still exists after a page reload.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+var browser;
+
+function test() {
+ loadTab(TEST_URI).then(() => {
+ openConsole().then((tab) => {
+ browser = tab.browser;
+
+ browser.addEventListener("DOMContentLoaded", testPageReload, false);
+ content.location.reload();
+ });
+ });
+ browser.addEventListener("DOMContentLoaded", onLoad, false);
+}
+
+function testPageReload() {
+ browser.removeEventListener("DOMContentLoaded", testPageReload, false);
+
+ let console = browser.contentWindow.wrappedJSObject.console;
+
+ is(typeof console, "object", "window.console is an object, after page reload");
+ is(typeof console.log, "function", "console.log is a function");
+ is(typeof console.info, "function", "console.info is a function");
+ is(typeof console.warn, "function", "console.warn is a function");
+ is(typeof console.error, "function", "console.error is a function");
+ is(typeof console.exception, "function", "console.exception is a function");
+
+ browser = null;
+ finishTest();
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_579412_input_focus.js b/devtools/client/webconsole/test/browser_webconsole_bug_579412_input_focus.js
new file mode 100644
index 000000000..551dbd361
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_579412_input_focus.js
@@ -0,0 +1,20 @@
+/* -*- 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 the input field is focused when the console is opened.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ hud.jsterm.clearOutput();
+
+ let inputNode = hud.jsterm.inputNode;
+ ok(inputNode.getAttribute("focused"), "input node is focused");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_580001_closing_after_completion.js b/devtools/client/webconsole/test/browser_webconsole_bug_580001_closing_after_completion.js
new file mode 100644
index 000000000..4c5fbf9c8
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_580001_closing_after_completion.js
@@ -0,0 +1,47 @@
+/* -*- 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 to ensure that errors don't appear when the console is closed while a
+// completion is being performed.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+add_task(function* () {
+ let { browser } = yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+ yield testClosingAfterCompletion(hud, browser);
+});
+
+function testClosingAfterCompletion(hud, browser) {
+ let deferred = promise.defer();
+
+ let errorWhileClosing = false;
+ function errorListener() {
+ errorWhileClosing = true;
+ }
+
+ browser.addEventListener("error", errorListener, false);
+
+ // Focus the jsterm and perform the keycombo to close the WebConsole.
+ hud.jsterm.focus();
+
+ gDevTools.once("toolbox-destroyed", function () {
+ browser.removeEventListener("error", errorListener, false);
+ is(errorWhileClosing, false, "no error while closing the WebConsole");
+ deferred.resolve();
+ });
+
+ if (Services.appinfo.OS == "Darwin") {
+ EventUtils.synthesizeKey("i", { accelKey: true, altKey: true });
+ } else {
+ EventUtils.synthesizeKey("i", { accelKey: true, shiftKey: true });
+ }
+
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_580030_errors_after_page_reload.js b/devtools/client/webconsole/test/browser_webconsole_bug_580030_errors_after_page_reload.js
new file mode 100644
index 000000000..af00bf913
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_580030_errors_after_page_reload.js
@@ -0,0 +1,50 @@
+/* -*- 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 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/test-error.html";
+
+function test() {
+ Task.spawn(function* () {
+ const {tab} = yield loadTab(TEST_URI);
+ const hud = yield openConsole(tab);
+ info("console opened");
+
+ executeSoon(() => {
+ hud.jsterm.clearOutput();
+ info("wait for reload");
+ content.location.reload();
+ });
+
+ yield hud.target.once("navigate");
+ info("target navigated");
+
+ let button = content.document.querySelector("button");
+ ok(button, "button found");
+
+ // On e10s, the exception is triggered in child process
+ // and is ignored by test harness
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+
+ EventUtils.sendMouseEvent({type: "click"}, button, content);
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "fooBazBaz is not defined",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ }],
+ });
+ }).then(finishTest);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_580454_timestamp_l10n.js b/devtools/client/webconsole/test/browser_webconsole_bug_580454_timestamp_l10n.js
new file mode 100644
index 000000000..6cd03164b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_580454_timestamp_l10n.js
@@ -0,0 +1,26 @@
+/* -*- 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 appropriately-localized timestamps are printed.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ const TEST_TIMESTAMP = 12345678;
+ let date = new Date(TEST_TIMESTAMP);
+ let localizedString = WCUL10n.timestampString(TEST_TIMESTAMP);
+ isnot(localizedString.indexOf(date.getHours()), -1, "the localized " +
+ "timestamp contains the hours");
+ isnot(localizedString.indexOf(date.getMinutes()), -1, "the localized " +
+ "timestamp contains the minutes");
+ isnot(localizedString.indexOf(date.getSeconds()), -1, "the localized " +
+ "timestamp contains the seconds");
+ isnot(localizedString.indexOf(date.getMilliseconds()), -1, "the localized " +
+ "timestamp contains the milliseconds");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_582201_duplicate_errors.js b/devtools/client/webconsole/test/browser_webconsole_bug_582201_duplicate_errors.js
new file mode 100644
index 000000000..5e7b141eb
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_582201_duplicate_errors.js
@@ -0,0 +1,49 @@
+/* -*- 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 exceptions thrown by content don't show up twice in the Web
+// Console.
+
+"use strict";
+
+const INIT_URI = "data:text/html;charset=utf8,hello world";
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-duplicate-error.html";
+
+add_task(function* () {
+ yield loadTab(INIT_URI);
+
+ let hud = yield openConsole();
+
+ // On e10s, the exception is triggered in child process
+ // and is ignored by test harness
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI);
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "fooDuplicateError1",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ },
+ {
+ text: "test-duplicate-error.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ let text = hud.outputNode.textContent;
+ let error1pos = text.indexOf("fooDuplicateError1");
+ ok(error1pos > -1, "found fooDuplicateError1");
+ if (error1pos > -1) {
+ ok(text.indexOf("fooDuplicateError1", error1pos + 1) == -1,
+ "no duplicate for fooDuplicateError1");
+ }
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_583816_No_input_and_Tab_key_pressed.js b/devtools/client/webconsole/test/browser_webconsole_bug_583816_No_input_and_Tab_key_pressed.js
new file mode 100644
index 000000000..7dd271388
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_583816_No_input_and_Tab_key_pressed.js
@@ -0,0 +1,35 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/browser/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+ testCompletion(hud);
+});
+
+function testCompletion(hud) {
+ let jsterm = hud.jsterm;
+ let input = jsterm.inputNode;
+
+ jsterm.setInputValue("");
+ EventUtils.synthesizeKey("VK_TAB", {});
+ is(jsterm.completeNode.value, "<- no result", "<- no result - matched");
+ is(input.value, "", "inputnode is empty - matched");
+ is(input.getAttribute("focused"), "true", "input is still focused");
+
+ // Any thing which is not in property autocompleter
+ jsterm.setInputValue("window.Bug583816");
+ EventUtils.synthesizeKey("VK_TAB", {});
+ is(jsterm.completeNode.value, " <- no result",
+ "completenode content - matched");
+ is(input.value, "window.Bug583816", "inputnode content - matched");
+ is(input.getAttribute("focused"), "true", "input is still focused");
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_585237_line_limit.js b/devtools/client/webconsole/test/browser_webconsole_bug_585237_line_limit.js
new file mode 100644
index 000000000..974557ec0
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_585237_line_limit.js
@@ -0,0 +1,89 @@
+/* -*- 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 the Web Console limits the number of lines displayed according to
+// the user's preferences.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,test for bug 585237";
+
+var outputNode;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ outputNode = hud.outputNode;
+
+ hud.jsterm.clearOutput();
+
+ let prefBranch = Services.prefs.getBranch("devtools.hud.loglimit.");
+ prefBranch.setIntPref("console", 20);
+
+ for (let i = 0; i < 30; i++) {
+ yield ContentTask.spawn(gBrowser.selectedBrowser, i, function (i) {
+ // must change message to prevent repeats
+ content.console.log("foo #" + i);
+ });
+ }
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foo #29",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ is(countMessageNodes(), 20, "there are 20 message nodes in the output " +
+ "when the log limit is set to 20");
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function () {
+ content.console.log("bar bug585237");
+ });
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "bar bug585237",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ is(countMessageNodes(), 20, "there are still 20 message nodes in the " +
+ "output when adding one more");
+
+ prefBranch.setIntPref("console", 30);
+ for (let i = 0; i < 20; i++) {
+ yield ContentTask.spawn(gBrowser.selectedBrowser, i, function (i) {
+ // must change message to prevent repeats
+ content.console.log("boo #" + i);
+ });
+ }
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "boo #19",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ is(countMessageNodes(), 30, "there are 30 message nodes in the output " +
+ "when the log limit is set to 30");
+
+ prefBranch.clearUserPref("console");
+
+ outputNode = null;
+});
+
+function countMessageNodes() {
+ return outputNode.querySelectorAll(".message").length;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_585956_console_trace.js b/devtools/client/webconsole/test/browser_webconsole_bug_585956_console_trace.js
new file mode 100644
index 000000000..c38fd52c1
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_585956_console_trace.js
@@ -0,0 +1,70 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/" +
+ "webconsole/test/test-bug-585956-console-trace.html";
+
+add_task(function* () {
+ let {tab} = yield loadTab("data:text/html;charset=utf8,<p>hello");
+ let hud = yield openConsole(tab);
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI);
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "console.trace output",
+ consoleTrace: {
+ file: "test-bug-585956-console-trace.html",
+ fn: "window.foobar585956c",
+ },
+ }],
+ });
+
+ let node = [...result.matched][0];
+ ok(node, "found trace log node");
+
+ let obj = node._messageObject;
+ ok(obj, "console.trace message object");
+
+ // The expected stack trace object.
+ let stacktrace = [
+ {
+ columnNumber: 3,
+ filename: TEST_URI,
+ functionName: "window.foobar585956c",
+ language: 2,
+ lineNumber: 9
+ },
+ {
+ columnNumber: 10,
+ filename: TEST_URI,
+ functionName: "foobar585956b",
+ language: 2,
+ lineNumber: 14
+ },
+ {
+ columnNumber: 10,
+ filename: TEST_URI,
+ functionName: "foobar585956a",
+ language: 2,
+ lineNumber: 18
+ },
+ {
+ columnNumber: 1,
+ filename: TEST_URI,
+ functionName: "",
+ language: 2,
+ lineNumber: 21
+ }
+ ];
+
+ ok(obj._stacktrace, "found stacktrace object");
+ is(obj._stacktrace.toSource(), stacktrace.toSource(),
+ "stacktrace is correct");
+ isnot(node.textContent.indexOf("bug-585956"), -1, "found file name");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js b/devtools/client/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js
new file mode 100644
index 000000000..0021a8cc1
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js
@@ -0,0 +1,367 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>bug 585991 - autocomplete " +
+ "popup keyboard usage test";
+
+// We should turn off auto-multiline editing during these tests
+const PREF_AUTO_MULTILINE = "devtools.webconsole.autoMultiline";
+var HUD, popup, jsterm, inputNode, completeNode;
+
+add_task(function* () {
+ Services.prefs.setBoolPref(PREF_AUTO_MULTILINE, false);
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ yield consoleOpened(hud);
+ yield popupHideAfterTab();
+ yield testReturnKey();
+ yield dontShowArrayNumbers();
+ yield testReturnWithNoSelection();
+ yield popupHideAfterReturnWithNoSelection();
+ yield testCompletionInText();
+ yield popupHideAfterCompletionInText();
+
+ HUD = popup = jsterm = inputNode = completeNode = null;
+ Services.prefs.setBoolPref(PREF_AUTO_MULTILINE, true);
+});
+
+var consoleOpened = Task.async(function* (hud) {
+ let deferred = promise.defer();
+ HUD = hud;
+ info("web console opened");
+
+ jsterm = HUD.jsterm;
+
+ yield jsterm.execute("window.foobarBug585991={" +
+ "'item0': 'value0'," +
+ "'item1': 'value1'," +
+ "'item2': 'value2'," +
+ "'item3': 'value3'" +
+ "}");
+ yield jsterm.execute("window.testBug873250a = 'hello world';"
+ + "window.testBug873250b = 'hello world 2';");
+ popup = jsterm.autocompletePopup;
+ completeNode = jsterm.completeNode;
+ inputNode = jsterm.inputNode;
+
+ ok(!popup.isOpen, "popup is not open");
+
+ popup.once("popup-opened", () => {
+ ok(popup.isOpen, "popup is open");
+
+ // 4 values, and the following properties:
+ // __defineGetter__ __defineSetter__ __lookupGetter__ __lookupSetter__
+ // __proto__ hasOwnProperty isPrototypeOf propertyIsEnumerable
+ // toLocaleString toString toSource unwatch valueOf watch constructor.
+ is(popup.itemCount, 19, "popup.itemCount is correct");
+
+ let sameItems = popup.getItems().reverse().map(function (e) {
+ return e.label;
+ });
+
+ ok(sameItems.every(function (prop, index) {
+ return [
+ "__defineGetter__",
+ "__defineSetter__",
+ "__lookupGetter__",
+ "__lookupSetter__",
+ "__proto__",
+ "constructor",
+ "hasOwnProperty",
+ "isPrototypeOf",
+ "item0",
+ "item1",
+ "item2",
+ "item3",
+ "propertyIsEnumerable",
+ "toLocaleString",
+ "toSource",
+ "toString",
+ "unwatch",
+ "valueOf",
+ "watch",
+ ][index] === prop;
+ }), "getItems returns the items we expect");
+
+ is(popup.selectedIndex, 18,
+ "Index of the first item from bottom is selected.");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ let prefix = jsterm.getInputValue().replace(/[\S]/g, " ");
+
+ is(popup.selectedIndex, 0, "index 0 is selected");
+ is(popup.selectedItem.label, "watch", "watch is selected");
+ is(completeNode.value, prefix + "watch",
+ "completeNode.value holds watch");
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ is(popup.selectedIndex, 1, "index 1 is selected");
+ is(popup.selectedItem.label, "valueOf", "valueOf is selected");
+ is(completeNode.value, prefix + "valueOf",
+ "completeNode.value holds valueOf");
+
+ EventUtils.synthesizeKey("VK_UP", {});
+
+ is(popup.selectedIndex, 0, "index 0 is selected");
+ is(popup.selectedItem.label, "watch", "watch is selected");
+ is(completeNode.value, prefix + "watch",
+ "completeNode.value holds watch");
+
+ let currentSelectionIndex = popup.selectedIndex;
+
+ EventUtils.synthesizeKey("VK_PAGE_DOWN", {});
+
+ ok(popup.selectedIndex > currentSelectionIndex,
+ "Index is greater after PGDN");
+
+ currentSelectionIndex = popup.selectedIndex;
+ EventUtils.synthesizeKey("VK_PAGE_UP", {});
+
+ ok(popup.selectedIndex < currentSelectionIndex,
+ "Index is less after Page UP");
+
+ EventUtils.synthesizeKey("VK_END", {});
+ is(popup.selectedIndex, 18, "index is last after End");
+
+ EventUtils.synthesizeKey("VK_HOME", {});
+ is(popup.selectedIndex, 0, "index is first after Home");
+
+ info("press Tab and wait for popup to hide");
+ popup.once("popup-closed", () => {
+ deferred.resolve();
+ });
+ EventUtils.synthesizeKey("VK_TAB", {});
+ });
+
+ jsterm.setInputValue("window.foobarBug585991");
+ EventUtils.synthesizeKey(".", {});
+
+ return deferred.promise;
+});
+
+function popupHideAfterTab() {
+ let deferred = promise.defer();
+
+ // At this point the completion suggestion should be accepted.
+ ok(!popup.isOpen, "popup is not open");
+
+ is(jsterm.getInputValue(), "window.foobarBug585991.watch",
+ "completion was successful after VK_TAB");
+
+ ok(!completeNode.value, "completeNode is empty");
+
+ popup.once("popup-opened", function onShown() {
+ ok(popup.isOpen, "popup is open");
+
+ is(popup.itemCount, 19, "popup.itemCount is correct");
+
+ is(popup.selectedIndex, 18, "First index from bottom is selected");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ let prefix = jsterm.getInputValue().replace(/[\S]/g, " ");
+
+ is(popup.selectedIndex, 0, "index 0 is selected");
+ is(popup.selectedItem.label, "watch", "watch is selected");
+ is(completeNode.value, prefix + "watch",
+ "completeNode.value holds watch");
+
+ popup.once("popup-closed", function onHidden() {
+ ok(!popup.isOpen, "popup is not open after VK_ESCAPE");
+
+ is(jsterm.getInputValue(), "window.foobarBug585991.",
+ "completion was cancelled");
+
+ ok(!completeNode.value, "completeNode is empty");
+
+ deferred.resolve();
+ }, false);
+
+ info("press Escape to close the popup");
+ executeSoon(function () {
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ });
+ }, false);
+
+ info("wait for completion: window.foobarBug585991.");
+ executeSoon(function () {
+ jsterm.setInputValue("window.foobarBug585991");
+ EventUtils.synthesizeKey(".", {});
+ });
+
+ return deferred.promise;
+}
+
+function testReturnKey() {
+ let deferred = promise.defer();
+
+ popup.once("popup-opened", function onShown() {
+ ok(popup.isOpen, "popup is open");
+
+ is(popup.itemCount, 19, "popup.itemCount is correct");
+
+ is(popup.selectedIndex, 18, "First index from bottom is selected");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ let prefix = jsterm.getInputValue().replace(/[\S]/g, " ");
+
+ is(popup.selectedIndex, 0, "index 0 is selected");
+ is(popup.selectedItem.label, "watch", "watch is selected");
+ is(completeNode.value, prefix + "watch",
+ "completeNode.value holds watch");
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ is(popup.selectedIndex, 1, "index 1 is selected");
+ is(popup.selectedItem.label, "valueOf", "valueOf is selected");
+ is(completeNode.value, prefix + "valueOf",
+ "completeNode.value holds valueOf");
+
+ popup.once("popup-closed", function onHidden() {
+ ok(!popup.isOpen, "popup is not open after VK_RETURN");
+
+ is(jsterm.getInputValue(), "window.foobarBug585991.valueOf",
+ "completion was successful after VK_RETURN");
+
+ ok(!completeNode.value, "completeNode is empty");
+
+ deferred.resolve();
+ }, false);
+
+ info("press Return to accept suggestion. wait for popup to hide");
+
+ executeSoon(() => EventUtils.synthesizeKey("VK_RETURN", {}));
+ }, false);
+
+ info("wait for completion suggestions: window.foobarBug585991.");
+
+ executeSoon(function () {
+ jsterm.setInputValue("window.foobarBug58599");
+ EventUtils.synthesizeKey("1", {});
+ EventUtils.synthesizeKey(".", {});
+ });
+
+ return deferred.promise;
+}
+
+function* dontShowArrayNumbers() {
+ let deferred = promise.defer();
+
+ info("dontShowArrayNumbers");
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.wrappedJSObject.foobarBug585991 = ["Sherlock Holmes"];
+ });
+
+ jsterm = HUD.jsterm;
+ popup = jsterm.autocompletePopup;
+
+ popup.once("popup-opened", function onShown() {
+ let sameItems = popup.getItems().map(function (e) {
+ return e.label;
+ });
+ ok(!sameItems.some(function (prop) {
+ prop === "0";
+ }), "Completing on an array doesn't show numbers.");
+
+ popup.once("popup-closed", function popupHidden() {
+ deferred.resolve();
+ }, false);
+
+ info("wait for popup to hide");
+ executeSoon(() => EventUtils.synthesizeKey("VK_ESCAPE", {}));
+ }, false);
+
+ info("wait for popup to show");
+ executeSoon(() => {
+ jsterm.setInputValue("window.foobarBug585991");
+ EventUtils.synthesizeKey(".", {});
+ });
+
+ return deferred.promise;
+}
+
+function testReturnWithNoSelection() {
+ let deferred = promise.defer();
+
+ info("test pressing return with open popup, but no selection, see bug 873250");
+
+ popup.once("popup-opened", function onShown() {
+ 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");
+ popup.once("popup-closed", function popupHidden() {
+ deferred.resolve();
+ });
+ executeSoon(() => EventUtils.synthesizeKey("VK_RETURN", {}));
+ });
+
+ executeSoon(() => {
+ info("wait for popup to show");
+ jsterm.setInputValue("window.testBu");
+ EventUtils.synthesizeKey("g", {});
+ });
+
+ return deferred.promise;
+}
+
+function popupHideAfterReturnWithNoSelection() {
+ ok(!popup.isOpen, "popup is not open after VK_RETURN");
+
+ is(jsterm.getInputValue(), "", "inputNode is empty after VK_RETURN");
+ is(completeNode.value, "", "completeNode is empty");
+ is(jsterm.history[jsterm.history.length - 1], "window.testBug",
+ "jsterm history is correct");
+
+ return promise.resolve();
+}
+
+function testCompletionInText() {
+ info("test that completion works inside text, see bug 812618");
+
+ let deferred = promise.defer();
+
+ popup.once("popup-opened", function onShown() {
+ ok(popup.isOpen, "popup is open");
+ is(popup.itemCount, 2, "popup.itemCount is correct");
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(popup.selectedIndex, 0, "popup.selectedIndex is correct");
+ ok(!completeNode.value, "completeNode.value is empty");
+
+ let items = popup.getItems().reverse().map(e => e.label);
+ let sameItems = items.every((prop, index) =>
+ ["testBug873250a", "testBug873250b"][index] === prop);
+ ok(sameItems, "getItems returns the items we expect");
+
+ info("press Tab and wait for popup to hide");
+ popup.once("popup-closed", function popupHidden() {
+ deferred.resolve();
+ });
+ EventUtils.synthesizeKey("VK_TAB", {});
+ });
+
+ jsterm.setInputValue("dump(window.testBu)");
+ inputNode.selectionStart = inputNode.selectionEnd = 18;
+ EventUtils.synthesizeKey("g", {});
+ return deferred.promise;
+}
+
+function popupHideAfterCompletionInText() {
+ // At this point the completion suggestion should be accepted.
+ ok(!popup.isOpen, "popup is not open");
+ is(jsterm.getInputValue(), "dump(window.testBug873250b)",
+ "completion was successful after VK_TAB");
+ is(inputNode.selectionStart, 26, "cursor location is correct");
+ is(inputNode.selectionStart, inputNode.selectionEnd,
+ "cursor location (confirmed)");
+ ok(!completeNode.value, "completeNode is empty");
+
+ return promise.resolve();
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_585991_autocomplete_popup.js b/devtools/client/webconsole/test/browser_webconsole_bug_585991_autocomplete_popup.js
new file mode 100644
index 000000000..df1a42edf
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_585991_autocomplete_popup.js
@@ -0,0 +1,123 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>bug 585991 - autocomplete " +
+ "popup test";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ yield consoleOpened(hud);
+});
+
+function consoleOpened(HUD) {
+ let deferred = promise.defer();
+
+ let items = [
+ {label: "item0", value: "value0"},
+ {label: "item1", value: "value1"},
+ {label: "item2", value: "value2"},
+ ];
+
+ let popup = HUD.jsterm.autocompletePopup;
+ let input = HUD.jsterm.inputNode;
+
+ ok(!popup.isOpen, "popup is not open");
+ ok(!input.hasAttribute("aria-activedescendant"), "no aria-activedescendant");
+
+ popup.once("popup-opened", () => {
+ ok(popup.isOpen, "popup is open");
+
+ is(popup.itemCount, 0, "no items");
+ ok(!input.hasAttribute("aria-activedescendant"), "no aria-activedescendant");
+
+ popup.setItems(items);
+
+ is(popup.itemCount, items.length, "items added");
+
+ let sameItems = popup.getItems();
+ is(sameItems.every(function (item, index) {
+ return item === items[index];
+ }), true, "getItems returns back the same items");
+
+ is(popup.selectedIndex, 2, "Index of the first item from bottom is selected.");
+ is(popup.selectedItem, items[2], "First item from bottom is selected");
+ checkActiveDescendant(popup, input);
+
+ popup.selectedIndex = 1;
+
+ is(popup.selectedIndex, 1, "index 1 is selected");
+ is(popup.selectedItem, items[1], "item1 is selected");
+ checkActiveDescendant(popup, input);
+
+ popup.selectedItem = items[2];
+
+ is(popup.selectedIndex, 2, "index 2 is selected");
+ is(popup.selectedItem, items[2], "item2 is selected");
+ checkActiveDescendant(popup, input);
+
+ is(popup.selectPreviousItem(), items[1], "selectPreviousItem() works");
+
+ is(popup.selectedIndex, 1, "index 1 is selected");
+ is(popup.selectedItem, items[1], "item1 is selected");
+ checkActiveDescendant(popup, input);
+
+ is(popup.selectNextItem(), items[2], "selectNextItem() works");
+
+ is(popup.selectedIndex, 2, "index 2 is selected");
+ is(popup.selectedItem, items[2], "item2 is selected");
+ checkActiveDescendant(popup, input);
+
+ ok(popup.selectNextItem(), "selectNextItem() works");
+
+ is(popup.selectedIndex, 0, "index 0 is selected");
+ is(popup.selectedItem, items[0], "item0 is selected");
+ checkActiveDescendant(popup, input);
+
+ items.push({label: "label3", value: "value3"});
+ popup.appendItem(items[3]);
+
+ is(popup.itemCount, items.length, "item3 appended");
+
+ popup.selectedIndex = 3;
+ is(popup.selectedItem, items[3], "item3 is selected");
+ checkActiveDescendant(popup, input);
+
+ popup.removeItem(items[2]);
+
+ is(popup.selectedIndex, 2, "index2 is selected");
+ is(popup.selectedItem, items[3], "item3 is still selected");
+ checkActiveDescendant(popup, input);
+ is(popup.itemCount, items.length - 1, "item2 removed");
+
+ popup.clearItems();
+ is(popup.itemCount, 0, "items cleared");
+ ok(!input.hasAttribute("aria-activedescendant"), "no aria-activedescendant");
+
+ popup.once("popup-closed", () => {
+ deferred.resolve();
+ });
+ popup.hidePopup();
+ });
+
+ popup.openPopup(input);
+
+ return deferred.promise;
+}
+
+function checkActiveDescendant(popup, input) {
+ let activeElement = input.ownerDocument.activeElement;
+ let descendantId = activeElement.getAttribute("aria-activedescendant");
+ let popupItem = popup._tooltip.panel.querySelector("#" + descendantId);
+ let cloneItem = input.ownerDocument.querySelector("#" + descendantId);
+
+ ok(popupItem, "Active descendant is found in the popup list");
+ ok(cloneItem, "Active descendant is found in the list clone");
+ is(popupItem.innerHTML, cloneItem.innerHTML,
+ "Cloned item has the same HTML as the original element");
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_586388_select_all.js b/devtools/client/webconsole/test/browser_webconsole_bug_586388_select_all.js
new file mode 100644
index 000000000..cda31191e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_586388_select_all.js
@@ -0,0 +1,84 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "http://example.com/";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+ yield testSelectionWhenMovingBetweenBoxes(hud);
+ performTestsAfterOutput(hud);
+});
+
+var testSelectionWhenMovingBetweenBoxes = Task.async(function* (hud) {
+ let jsterm = hud.jsterm;
+
+ // Fill the console with some output.
+ jsterm.clearOutput();
+ yield jsterm.execute("1 + 2");
+ yield jsterm.execute("3 + 4");
+ yield jsterm.execute("5 + 6");
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "3",
+ category: CATEGORY_OUTPUT,
+ },
+ {
+ text: "7",
+ category: CATEGORY_OUTPUT,
+ },
+ {
+ text: "11",
+ category: CATEGORY_OUTPUT,
+ }],
+ });
+});
+
+function performTestsAfterOutput(hud) {
+ let outputNode = hud.outputNode;
+
+ ok(outputNode.childNodes.length >= 3, "the output node has children after " +
+ "executing some JavaScript");
+
+ // Test that the global Firefox "Select All" functionality (e.g. Edit >
+ // Select All) works properly in the Web Console.
+ let commandController = hud.ui._commandController;
+ ok(commandController != null, "the window has a command controller object");
+
+ commandController.selectAll();
+
+ let selectedCount = hud.ui.output.getSelectedMessages().length;
+ is(selectedCount, outputNode.childNodes.length,
+ "all console messages are selected after performing a regular browser " +
+ "select-all operation");
+
+ hud.iframeWindow.getSelection().removeAllRanges();
+
+ // Test the context menu "Select All" (which has a different code path) works
+ // properly as well.
+ let contextMenuId = hud.ui.outputWrapper.getAttribute("context");
+ let contextMenu = hud.ui.document.getElementById(contextMenuId);
+ ok(contextMenu != null, "the output node has a context menu");
+
+ let selectAllItem = contextMenu.querySelector("*[command='cmd_selectAll']");
+ ok(selectAllItem != null,
+ "the context menu on the output node has a \"Select All\" item");
+
+ outputNode.focus();
+
+ selectAllItem.doCommand();
+
+ selectedCount = hud.ui.output.getSelectedMessages().length;
+ is(selectedCount, outputNode.childNodes.length,
+ "all console messages are selected after performing a select-all " +
+ "operation from the context menu");
+
+ hud.iframeWindow.getSelection().removeAllRanges();
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_587617_output_copy.js b/devtools/client/webconsole/test/browser_webconsole_bug_587617_output_copy.js
new file mode 100644
index 000000000..208baf3d6
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_587617_output_copy.js
@@ -0,0 +1,106 @@
+/* -*- 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/ */
+/* globals goUpdateCommand goDoCommand */
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+var HUD, outputNode;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+ yield consoleOpened(hud);
+ yield testContextMenuCopy();
+
+ HUD = outputNode = null;
+});
+
+function consoleOpened(hud) {
+ HUD = hud;
+
+ let deferred = promise.defer();
+
+ // See bugs 574036, 586386 and 587617.
+ outputNode = HUD.outputNode;
+
+ HUD.jsterm.clearOutput();
+
+ let controller = top.document.commandDispatcher
+ .getControllerForCommand("cmd_copy");
+ is(controller.isCommandEnabled("cmd_copy"), false, "cmd_copy is disabled");
+
+ ContentTask.spawn(gBrowser.selectedBrowser, null,
+ "() => content.console.log('Hello world! bug587617')");
+
+ waitForMessages({
+ webconsole: HUD,
+ messages: [{
+ text: "bug587617",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(([result]) => {
+ let msg = [...result.matched][0];
+ HUD.ui.output.selectMessage(msg);
+
+ outputNode.focus();
+
+ goUpdateCommand("cmd_copy");
+ controller = top.document.commandDispatcher
+ .getControllerForCommand("cmd_copy");
+ is(controller.isCommandEnabled("cmd_copy"), true, "cmd_copy is enabled");
+
+ // Remove new lines and whitespace since getSelection() includes
+ // a new line between message and line number, but the clipboard doesn't
+ // @see bug 1119503
+ let selection = (HUD.iframeWindow.getSelection() + "")
+ .replace(/\r?\n|\r| /g, "");
+ isnot(selection.indexOf("bug587617"), -1,
+ "selection text includes 'bug587617'");
+
+ waitForClipboard((str) => {
+ // Strip out spaces for comparison ease
+ return selection.trim() == str.trim().replace(/ /g, "");
+ }, () => {
+ goDoCommand("cmd_copy");
+ }, deferred.resolve, deferred.resolve);
+ });
+ return deferred.promise;
+}
+
+// Test that the context menu "Copy" (which has a different code path) works
+// properly as well.
+function testContextMenuCopy() {
+ let deferred = promise.defer();
+
+ let contextMenuId = HUD.ui.outputWrapper.getAttribute("context");
+ let contextMenu = HUD.ui.document.getElementById(contextMenuId);
+ ok(contextMenu, "the output node has a context menu");
+
+ let copyItem = contextMenu.querySelector("*[command='cmd_copy']");
+ ok(copyItem, "the context menu on the output node has a \"Copy\" item");
+
+ // Remove new lines and whitespace since getSelection() includes
+ // a new line between message and line number, but the clipboard doesn't
+ // @see bug 1119503
+ let selection = (HUD.iframeWindow.getSelection() + "")
+ .replace(/\r?\n|\r| /g, "");
+
+ copyItem.doCommand();
+
+ waitForClipboard((str) => {
+ // Strip out spaces for comparison ease
+ return selection.trim() == str.trim().replace(/ /g, "");
+ }, () => {
+ goDoCommand("cmd_copy");
+ }, deferred.resolve, deferred.resolve);
+ HUD = outputNode = null;
+
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_588342_document_focus.js b/devtools/client/webconsole/test/browser_webconsole_bug_588342_document_focus.js
new file mode 100644
index 000000000..ff926fc13
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_588342_document_focus.js
@@ -0,0 +1,36 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 588342";
+
+add_task(function* () {
+ let { browser } = yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ yield checkConsoleFocus(hud);
+
+ let isFocused = yield ContentTask.spawn(browser, { }, function* () {
+ var fm = Components.classes["@mozilla.org/focus-manager;1"].
+ getService(Components.interfaces.nsIFocusManager);
+ return fm.focusedWindow == content;
+ });
+
+ ok(isFocused, "content document has focus");
+});
+
+function* checkConsoleFocus(hud) {
+ let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
+
+ yield new Promise(resolve => {
+ waitForFocus(resolve);
+ });
+
+ is(hud.jsterm.inputNode.getAttribute("focused"), "true",
+ "jsterm input is focused on web console open");
+ is(fm.focusedWindow, hud.iframeWindow, "hud window is focused");
+ yield closeConsole(null);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_588730_text_node_insertion.js b/devtools/client/webconsole/test/browser_webconsole_bug_588730_text_node_insertion.js
new file mode 100644
index 000000000..94a0ad77e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_588730_text_node_insertion.js
@@ -0,0 +1,53 @@
+/* -*- 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 adding text to one of the output labels doesn't cause errors.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ yield testTextNodeInsertion(hud);
+});
+
+// Test for bug 588730: Adding a text node to an existing label element causes
+// warnings
+function testTextNodeInsertion(hud) {
+ let deferred = promise.defer();
+ let outputNode = hud.outputNode;
+
+ let label = document.createElementNS(
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "label");
+ outputNode.appendChild(label);
+
+ let error = false;
+ let listener = {
+ observe: function (aMessage) {
+ let messageText = aMessage.message;
+ if (messageText.indexOf("JavaScript Warning") !== -1) {
+ error = true;
+ }
+ }
+ };
+
+ Services.console.registerListener(listener);
+
+ // This shouldn't fail.
+ label.appendChild(document.createTextNode("foo"));
+
+ executeSoon(function () {
+ Services.console.unregisterListener(listener);
+ ok(!error, "no error when adding text nodes as children of labels");
+
+ return deferred.resolve();
+ });
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_588967_input_expansion.js b/devtools/client/webconsole/test/browser_webconsole_bug_588967_input_expansion.js
new file mode 100644
index 000000000..c590495c4
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_588967_input_expansion.js
@@ -0,0 +1,44 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ testInputExpansion(hud);
+});
+
+function testInputExpansion(hud) {
+ let input = hud.jsterm.inputNode;
+
+ input.focus();
+
+ is(input.getAttribute("multiline"), "true", "multiline is enabled");
+
+ let ordinaryHeight = input.clientHeight;
+
+ // Tests if the inputNode expands.
+ input.value = "hello\nworld\n";
+ let length = input.value.length;
+ input.selectionEnd = length;
+ input.selectionStart = length;
+ // Performs an "d". This will trigger/test for the input event that should
+ // change the height of the inputNode.
+ EventUtils.synthesizeKey("d", {});
+ ok(input.clientHeight > ordinaryHeight, "the input expanded");
+
+ // Test if the inputNode shrinks again.
+ input.value = "";
+ EventUtils.synthesizeKey("d", {});
+ is(input.clientHeight, ordinaryHeight, "the input's height is normal again");
+
+ input = length = null;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_589162_css_filter.js b/devtools/client/webconsole/test/browser_webconsole_bug_589162_css_filter.js
new file mode 100644
index 000000000..509c875f8
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_589162_css_filter.js
@@ -0,0 +1,39 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,<div style='font-size:3em;" +
+ "foobarCssParser:baz'>test CSS parser filter</div>";
+
+/**
+ * Unit test for bug 589162:
+ * CSS filtering on the console does not work
+ */
+add_task(function* () {
+ let {tab} = yield loadTab(TEST_URI);
+ let hud = yield openConsole(tab);
+
+ // CSS warnings are disabled by default.
+ hud.setFilterState("cssparser", true);
+ hud.jsterm.clearOutput();
+
+ BrowserReload();
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foobarCssParser",
+ category: CATEGORY_CSS,
+ severity: SEVERITY_WARNING,
+ }],
+ });
+
+ hud.setFilterState("cssparser", false);
+
+ let msg = "the unknown CSS property warning is not displayed, " +
+ "after filtering";
+ testLogEntry(hud.outputNode, "foobarCssParser", msg, true, true);
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_592442_closing_brackets.js b/devtools/client/webconsole/test/browser_webconsole_bug_592442_closing_brackets.js
new file mode 100644
index 000000000..adbf13086
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_592442_closing_brackets.js
@@ -0,0 +1,29 @@
+/* -*- 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, when the user types an extraneous closing bracket, no error
+// appears.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,test for bug 592442";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ hud.jsterm.clearOutput();
+ let jsterm = hud.jsterm;
+
+ jsterm.setInputValue("document.getElementById)");
+
+ let error = false;
+ try {
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY);
+ } catch (ex) {
+ error = true;
+ }
+
+ ok(!error, "no error was thrown when an extraneous bracket was inserted");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_593003_iframe_wrong_hud.js b/devtools/client/webconsole/test/browser_webconsole_bug_593003_iframe_wrong_hud.js
new file mode 100644
index 000000000..9f429a3d1
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_593003_iframe_wrong_hud.js
@@ -0,0 +1,68 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-593003-iframe-wrong-hud.html";
+
+const TEST_IFRAME_URI = "http://example.com/browser/devtools/client/" +
+ "webconsole/test/test-bug-593003-iframe-wrong-" +
+ "hud-iframe.html";
+
+const TEST_DUMMY_URI = "http://example.com/browser/devtools/client/" +
+ "webconsole/test/test-console.html";
+
+add_task(function* () {
+
+ let tab1 = (yield loadTab(TEST_URI)).tab;
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.console.log("FOO");
+ });
+ yield openConsole();
+
+ let tab2 = (yield loadTab(TEST_DUMMY_URI)).tab;
+ yield openConsole(gBrowser.selectedTab);
+
+ info("Reloading tab 1");
+ yield reloadTab(tab1);
+
+ info("Checking for messages");
+ yield checkMessages(tab1, tab2);
+
+ info("Cleaning up");
+ yield closeConsole(tab1);
+ yield closeConsole(tab2);
+});
+
+function* reloadTab(tab) {
+ let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ tab.linkedBrowser.reload();
+ yield loaded;
+}
+
+function* checkMessages(tab1, tab2) {
+ let hud1 = yield openConsole(tab1);
+ let outputNode1 = hud1.outputNode;
+
+ info("Waiting for messages");
+ yield waitForMessages({
+ webconsole: hud1,
+ messages: [{
+ text: TEST_IFRAME_URI,
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ }]
+ });
+
+ let hud2 = yield openConsole(tab2);
+ let outputNode2 = hud2.outputNode;
+
+ isnot(outputNode1, outputNode2,
+ "the two HUD outputNodes must be different");
+
+ let msg = "Didn't find the iframe network request in tab2";
+ testLogEntry(outputNode2, TEST_IFRAME_URI, msg, true, true);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_594497_history_arrow_keys.js b/devtools/client/webconsole/test/browser_webconsole_bug_594497_history_arrow_keys.js
new file mode 100644
index 000000000..514f875c0
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_594497_history_arrow_keys.js
@@ -0,0 +1,155 @@
+/* -*- 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/ */
+
+"use strict";
+
+var jsterm, inputNode, values;
+
+var TEST_URI = "data:text/html;charset=utf-8,Web Console test for " +
+ "bug 594497 and bug 619598";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ setup(hud);
+ performTests();
+
+ jsterm = inputNode = values = null;
+});
+
+function setup(HUD) {
+ jsterm = HUD.jsterm;
+ inputNode = jsterm.inputNode;
+
+ jsterm.focus();
+
+ ok(!jsterm.getInputValue(), "jsterm.getInputValue() is empty");
+
+ values = ["document", "window", "document.body"];
+ values.push(values.join(";\n"), "document.location");
+
+ // Execute each of the values;
+ for (let i = 0; i < values.length; i++) {
+ jsterm.setInputValue(values[i]);
+ jsterm.execute();
+ }
+}
+
+function performTests() {
+ EventUtils.synthesizeKey("VK_UP", {});
+
+
+ is(jsterm.getInputValue(), values[4],
+ "VK_UP: jsterm.getInputValue() #4 is correct");
+
+ ok(inputNode.selectionStart == values[4].length &&
+ inputNode.selectionStart == inputNode.selectionEnd,
+ "caret location is correct");
+
+ EventUtils.synthesizeKey("VK_UP", {});
+
+ is(jsterm.getInputValue(), values[3],
+ "VK_UP: jsterm.getInputValue() #3 is correct");
+
+ ok(inputNode.selectionStart == values[3].length &&
+ inputNode.selectionStart == inputNode.selectionEnd,
+ "caret location is correct");
+
+ inputNode.setSelectionRange(values[3].length - 2, values[3].length - 2);
+
+ EventUtils.synthesizeKey("VK_UP", {});
+ EventUtils.synthesizeKey("VK_UP", {});
+
+ is(jsterm.getInputValue(), values[3],
+ "VK_UP two times: jsterm.getInputValue() #3 is correct");
+
+ ok(inputNode.selectionStart == jsterm.getInputValue().indexOf("\n") &&
+ inputNode.selectionStart == inputNode.selectionEnd,
+ "caret location is correct");
+
+ EventUtils.synthesizeKey("VK_UP", {});
+
+ is(jsterm.getInputValue(), values[3],
+ "VK_UP again: jsterm.getInputValue() #3 is correct");
+
+ ok(inputNode.selectionStart == 0 &&
+ inputNode.selectionStart == inputNode.selectionEnd,
+ "caret location is correct");
+
+ EventUtils.synthesizeKey("VK_UP", {});
+
+ is(jsterm.getInputValue(), values[2],
+ "VK_UP: jsterm.getInputValue() #2 is correct");
+
+ EventUtils.synthesizeKey("VK_UP", {});
+
+ is(jsterm.getInputValue(), values[1],
+ "VK_UP: jsterm.getInputValue() #1 is correct");
+
+ EventUtils.synthesizeKey("VK_UP", {});
+
+ is(jsterm.getInputValue(), values[0],
+ "VK_UP: jsterm.getInputValue() #0 is correct");
+
+ ok(inputNode.selectionStart == values[0].length &&
+ inputNode.selectionStart == inputNode.selectionEnd,
+ "caret location is correct");
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ is(jsterm.getInputValue(), values[1],
+ "VK_DOWN: jsterm.getInputValue() #1 is correct");
+
+ ok(inputNode.selectionStart == values[1].length &&
+ inputNode.selectionStart == inputNode.selectionEnd,
+ "caret location is correct");
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ is(jsterm.getInputValue(), values[2],
+ "VK_DOWN: jsterm.getInputValue() #2 is correct");
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ is(jsterm.getInputValue(), values[3],
+ "VK_DOWN: jsterm.getInputValue() #3 is correct");
+
+ ok(inputNode.selectionStart == values[3].length &&
+ inputNode.selectionStart == inputNode.selectionEnd,
+ "caret location is correct");
+
+ inputNode.setSelectionRange(2, 2);
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ is(jsterm.getInputValue(), values[3],
+ "VK_DOWN two times: jsterm.getInputValue() #3 is correct");
+
+ ok(inputNode.selectionStart > jsterm.getInputValue().lastIndexOf("\n") &&
+ inputNode.selectionStart == inputNode.selectionEnd,
+ "caret location is correct");
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ is(jsterm.getInputValue(), values[3],
+ "VK_DOWN again: jsterm.getInputValue() #3 is correct");
+
+ ok(inputNode.selectionStart == values[3].length &&
+ inputNode.selectionStart == inputNode.selectionEnd,
+ "caret location is correct");
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ is(jsterm.getInputValue(), values[4],
+ "VK_DOWN: jsterm.getInputValue() #4 is correct");
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ ok(!jsterm.getInputValue(),
+ "VK_DOWN: jsterm.getInputValue() is empty");
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_595223_file_uri.js b/devtools/client/webconsole/test/browser_webconsole_bug_595223_file_uri.js
new file mode 100644
index 000000000..d57d724ca
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_595223_file_uri.js
@@ -0,0 +1,64 @@
+/* -*- 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/ */
+
+"use strict";
+
+const PREF = "devtools.webconsole.persistlog";
+const TEST_FILE = "test-network.html";
+const TEST_URI = "data:text/html;charset=utf8,<p>test file URI";
+
+var hud;
+
+add_task(function* () {
+ Services.prefs.setBoolPref(PREF, true);
+
+ let jar = getJar(getRootDirectory(gTestPath));
+ let dir = jar ?
+ extractJarToTmp(jar) :
+ getChromeDir(getResolvedURI(gTestPath));
+
+ dir.append(TEST_FILE);
+ let uri = Services.io.newFileURI(dir);
+
+ let { browser } = yield loadTab(TEST_URI);
+
+ hud = yield openConsole();
+ hud.jsterm.clearOutput();
+
+ let loaded = loadBrowser(browser);
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, uri.spec);
+ yield loaded;
+
+ yield testMessages();
+
+ Services.prefs.clearUserPref(PREF);
+ hud = null;
+});
+
+function testMessages() {
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "running network console logging tests",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "test-network.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "test-image.png",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "testscript.js",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ }],
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js b/devtools/client/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js
new file mode 100644
index 000000000..1951cb366
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js
@@ -0,0 +1,100 @@
+/* -*- 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 the Web Console doesn't leak when multiple tabs and windows are
+// opened and then closed.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 595350";
+
+var win1 = window, win2;
+var openTabs = [];
+var loadedTabCount = 0;
+
+function test() {
+ requestLongerTimeout(3);
+
+ // Add two tabs in the main window.
+ addTabs(win1);
+
+ // Open a new window.
+ win2 = OpenBrowserWindow();
+ win2.addEventListener("load", onWindowLoad, true);
+}
+
+function onWindowLoad(aEvent) {
+ win2.removeEventListener(aEvent.type, onWindowLoad, true);
+
+ // Add two tabs in the new window.
+ addTabs(win2);
+}
+
+function addTabs(aWindow) {
+ for (let i = 0; i < 2; i++) {
+ let tab = aWindow.gBrowser.addTab(TEST_URI);
+ openTabs.push(tab);
+
+ tab.linkedBrowser.addEventListener("load", function onLoad(aEvent) {
+ tab.linkedBrowser.removeEventListener(aEvent.type, onLoad, true);
+
+ loadedTabCount++;
+ info("tabs loaded: " + loadedTabCount);
+ if (loadedTabCount >= 4) {
+ executeSoon(openConsoles);
+ }
+ }, true);
+ }
+}
+
+function openConsoles() {
+ function open(i) {
+ let tab = openTabs[i];
+ openConsole(tab).then(function (hud) {
+ ok(hud, "HUD is open for tab " + i);
+ let window = hud.target.tab.linkedBrowser.contentWindow;
+ window.console.log("message for tab " + i);
+
+ if (i >= openTabs.length - 1) {
+ // Use executeSoon() to allow the promise to resolve.
+ executeSoon(closeConsoles);
+ }
+ else {
+ executeSoon(() => open(i + 1));
+ }
+ });
+ }
+
+ // open the Web Console for each of the four tabs and log a message.
+ open(0);
+}
+
+function closeConsoles() {
+ let consolesClosed = 0;
+
+ function onWebConsoleClose(aSubject, aTopic) {
+ if (aTopic == "web-console-destroyed") {
+ consolesClosed++;
+ info("consoles destroyed: " + consolesClosed);
+ if (consolesClosed == 4) {
+ // Use executeSoon() to allow all the observers to execute.
+ executeSoon(finishTest);
+ }
+ }
+ }
+
+ Services.obs.addObserver(onWebConsoleClose, "web-console-destroyed", false);
+
+ registerCleanupFunction(() => {
+ Services.obs.removeObserver(onWebConsoleClose, "web-console-destroyed");
+ });
+
+ win2.close();
+
+ win1.gBrowser.removeTab(openTabs[0]);
+ win1.gBrowser.removeTab(openTabs[1]);
+
+ openTabs = win1 = win2 = null;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_595934_message_categories.js b/devtools/client/webconsole/test/browser_webconsole_bug_595934_message_categories.js
new file mode 100644
index 000000000..855cfbb88
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_595934_message_categories.js
@@ -0,0 +1,211 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for " +
+ "bug 595934 - message categories coverage.";
+const TESTS_PATH = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/";
+const TESTS = [
+ {
+ // #0
+ file: "test-bug-595934-css-loader.html",
+ category: "CSS Loader",
+ matchString: "text/css",
+ },
+ {
+ // #1
+ file: "test-bug-595934-imagemap.html",
+ category: "Layout: ImageMap",
+ matchString: "shape=\"rect\"",
+ },
+ {
+ // #2
+ file: "test-bug-595934-html.html",
+ category: "HTML",
+ matchString: "multipart/form-data",
+ onload: function () {
+ let form = content.document.querySelector("form");
+ form.submit();
+ },
+ },
+ {
+ // #3
+ file: "test-bug-595934-workers.html",
+ category: "Web Worker",
+ matchString: "fooBarWorker",
+ },
+ {
+ // #4
+ file: "test-bug-595934-malformedxml.xhtml",
+ category: "malformed-xml",
+ matchString: "no root element found",
+ },
+ {
+ // #5
+ file: "test-bug-595934-svg.xhtml",
+ category: "SVG",
+ matchString: "fooBarSVG",
+ },
+ {
+ // #6
+ file: "test-bug-595934-css-parser.html",
+ category: "CSS Parser",
+ matchString: "foobarCssParser",
+ },
+ {
+ // #7
+ file: "test-bug-595934-malformedxml-external.html",
+ category: "malformed-xml",
+ matchString: "</html>",
+ },
+ {
+ // #8
+ file: "test-bug-595934-empty-getelementbyid.html",
+ category: "DOM",
+ matchString: "getElementById",
+ },
+ {
+ // #9
+ file: "test-bug-595934-canvas-css.html",
+ category: "CSS Parser",
+ matchString: "foobarCanvasCssParser",
+ },
+ {
+ // #10
+ file: "test-bug-595934-image.html",
+ category: "Image",
+ matchString: "corrupt",
+ },
+];
+
+var pos = -1;
+
+var foundCategory = false;
+var foundText = false;
+var pageLoaded = false;
+var pageError = false;
+var output = null;
+var jsterm = null;
+var hud = null;
+var testEnded = false;
+
+var TestObserver = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+ observe: function testObserve(subject) {
+ if (testEnded || !(subject instanceof Ci.nsIScriptError)) {
+ return;
+ }
+
+ let expectedCategory = TESTS[pos].category;
+
+ info("test #" + pos + " console observer got " + subject.category +
+ ", is expecting " + expectedCategory);
+
+ if (subject.category == expectedCategory) {
+ foundCategory = true;
+ startNextTest();
+ } else {
+ info("unexpected message was: " + subject.sourceName + ":" +
+ subject.lineNumber + "; " + subject.errorMessage);
+ }
+ }
+};
+
+function consoleOpened(hudConsole) {
+ hud = hudConsole;
+ output = hud.outputNode;
+ jsterm = hud.jsterm;
+
+ Services.console.registerListener(TestObserver);
+
+ registerCleanupFunction(testEnd);
+
+ testNext();
+}
+
+function testNext() {
+ jsterm.clearOutput();
+ foundCategory = false;
+ foundText = false;
+ pageLoaded = false;
+ pageError = false;
+
+ pos++;
+ info("testNext: #" + pos);
+ if (pos < TESTS.length) {
+ test = TESTS[pos];
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "message for test #" + pos + ": '" + test.matchString + "'",
+ text: test.matchString,
+ }],
+ }).then(() => {
+ foundText = true;
+ startNextTest();
+ });
+
+ let testLocation = TESTS_PATH + test.file;
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
+ if (content.location.href != testLocation) {
+ return;
+ }
+ gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
+
+ pageLoaded = true;
+ test.onload && test.onload(evt);
+
+ if (test.expectError) {
+ content.addEventListener("error", function _onError() {
+ content.removeEventListener("error", _onError);
+ pageError = true;
+ startNextTest();
+ });
+ // On e10s, the exception is triggered in child process
+ // and is ignored by test harness
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+ } else {
+ pageError = true;
+ }
+
+ startNextTest();
+ }, true);
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, testLocation);
+ } else {
+ testEnded = true;
+ finishTest();
+ }
+}
+
+function testEnd() {
+ if (!testEnded) {
+ info("foundCategory " + foundCategory + " foundText " + foundText +
+ " pageLoaded " + pageLoaded + " pageError " + pageError);
+ }
+
+ Services.console.unregisterListener(TestObserver);
+ hud = TestObserver = output = jsterm = null;
+}
+
+function startNextTest() {
+ if (!testEnded && foundCategory && foundText && pageLoaded && pageError) {
+ testNext();
+ }
+}
+
+function test() {
+ requestLongerTimeout(2);
+
+ loadTab(TEST_URI).then(() => {
+ openConsole().then(consoleOpened);
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js b/devtools/client/webconsole/test/browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js
new file mode 100644
index 000000000..e14c3a069
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js
@@ -0,0 +1,97 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+var tab1, tab2, win1, win2;
+var noErrors = true;
+
+function tab1Loaded() {
+ win2 = OpenBrowserWindow();
+ whenDelayedStartupFinished(win2, win2Loaded);
+}
+
+function win2Loaded() {
+ tab2 = win2.gBrowser.addTab(TEST_URI);
+ win2.gBrowser.selectedTab = tab2;
+ tab2.linkedBrowser.addEventListener("load", tab2Loaded, true);
+}
+
+function tab2Loaded(aEvent) {
+ tab2.linkedBrowser.removeEventListener(aEvent.type, tab2Loaded, true);
+
+ let consolesOpened = 0;
+ function onWebConsoleOpen() {
+ consolesOpened++;
+ if (consolesOpened == 2) {
+ executeSoon(closeConsoles);
+ }
+ }
+
+ function openConsoles() {
+ try {
+ let target1 = TargetFactory.forTab(tab1);
+ gDevTools.showToolbox(target1, "webconsole").then(onWebConsoleOpen);
+ } catch (ex) {
+ ok(false, "gDevTools.showToolbox(target1) exception: " + ex);
+ noErrors = false;
+ }
+
+ try {
+ let target2 = TargetFactory.forTab(tab2);
+ gDevTools.showToolbox(target2, "webconsole").then(onWebConsoleOpen);
+ } catch (ex) {
+ ok(false, "gDevTools.showToolbox(target2) exception: " + ex);
+ noErrors = false;
+ }
+ }
+
+ function closeConsoles() {
+ try {
+ let target1 = TargetFactory.forTab(tab1);
+ gDevTools.closeToolbox(target1).then(function () {
+ try {
+ let target2 = TargetFactory.forTab(tab2);
+ gDevTools.closeToolbox(target2).then(testEnd);
+ } catch (ex) {
+ ok(false, "gDevTools.closeToolbox(target2) exception: " + ex);
+ noErrors = false;
+ }
+ });
+ } catch (ex) {
+ ok(false, "gDevTools.closeToolbox(target1) exception: " + ex);
+ noErrors = false;
+ }
+ }
+
+ function testEnd() {
+ ok(noErrors, "there were no errors");
+
+ win1.gBrowser.removeTab(tab1);
+
+ Array.forEach(win2.gBrowser.tabs, function (aTab) {
+ win2.gBrowser.removeTab(aTab);
+ });
+
+ executeSoon(function () {
+ win2.close();
+ tab1 = tab2 = win1 = win2 = null;
+ finishTest();
+ });
+ }
+
+ openConsoles();
+}
+
+function test() {
+ loadTab(TEST_URI).then(() => {
+ tab1 = gBrowser.selectedTab;
+ win1 = window;
+ tab1Loaded();
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_597136_external_script_errors.js b/devtools/client/webconsole/test/browser_webconsole_bug_597136_external_script_errors.js
new file mode 100644
index 000000000..336700ada
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_597136_external_script_errors.js
@@ -0,0 +1,33 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/" +
+ "webconsole/test/test-bug-597136-external-script-" +
+ "errors.html";
+
+function test() {
+ Task.spawn(function* () {
+ const {tab} = yield loadTab(TEST_URI);
+ const hud = yield openConsole(tab);
+
+ // On e10s, the exception is triggered in child process
+ // and is ignored by test harness
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+ BrowserTestUtils.synthesizeMouseAtCenter("button", {}, gBrowser.selectedBrowser);
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "bogus is not defined",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ }],
+ });
+ }).then(finishTest);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_597136_network_requests_from_chrome.js b/devtools/client/webconsole/test/browser_webconsole_bug_597136_network_requests_from_chrome.js
new file mode 100644
index 000000000..473f02ccc
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_597136_network_requests_from_chrome.js
@@ -0,0 +1,52 @@
+/* -*- 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 network requests from chrome don't cause the Web Console to
+// throw exceptions.
+
+"use strict";
+
+const TEST_URI = "http://example.com/";
+
+var good = true;
+var listener = {
+ QueryInterface: XPCOMUtils.generateQI([ Ci.nsIObserver ]),
+ observe: function (subject) {
+ if (subject instanceof Ci.nsIScriptError &&
+ subject.category === "XPConnect JavaScript" &&
+ subject.sourceName.includes("webconsole")) {
+ good = false;
+ }
+ }
+};
+
+var xhr;
+
+function test() {
+ Services.console.registerListener(listener);
+
+ // trigger a lazy-load of the HUD Service
+ HUDService;
+
+ xhr = new XMLHttpRequest();
+ xhr.addEventListener("load", xhrComplete, false);
+ xhr.open("GET", TEST_URI, true);
+ xhr.send(null);
+}
+
+function xhrComplete() {
+ xhr.removeEventListener("load", xhrComplete, false);
+ window.setTimeout(checkForException, 0);
+}
+
+function checkForException() {
+ ok(good, "no exception was thrown when sending a network request from a " +
+ "chrome window");
+
+ Services.console.unregisterListener(listener);
+ listener = xhr = null;
+
+ finishTest();
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_597460_filter_scroll.js b/devtools/client/webconsole/test/browser_webconsole_bug_597460_filter_scroll.js
new file mode 100644
index 000000000..2de4c9f21
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_597460_filter_scroll.js
@@ -0,0 +1,80 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-network.html";
+const PREF = "devtools.webconsole.persistlog";
+
+add_task(function* () {
+ Services.prefs.setBoolPref(PREF, true);
+
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ let results = yield consoleOpened(hud);
+
+ testScroll(results, hud);
+
+ Services.prefs.clearUserPref(PREF);
+});
+
+function consoleOpened(hud) {
+ let deferred = promise.defer();
+
+ for (let i = 0; i < 200; i++) {
+ content.console.log("test message " + i);
+ }
+
+ hud.setFilterState("network", false);
+ hud.setFilterState("networkinfo", false);
+
+ hud.ui.filterBox.value = "test message";
+ hud.ui.adjustVisibilityOnSearchStringChange();
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "console messages displayed",
+ text: "test message 199",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(() => {
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "test-network.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(deferred.resolve);
+
+ content.location.reload();
+ });
+
+ return deferred.promise;
+}
+
+function testScroll([result], hud) {
+ let scrollNode = hud.ui.outputWrapper;
+ let msgNode = [...result.matched][0];
+ ok(msgNode.classList.contains("filtered-by-type"),
+ "network message is filtered by type");
+ ok(msgNode.classList.contains("filtered-by-string"),
+ "network message is filtered by string");
+
+ ok(scrollNode.scrollTop > 0, "scroll location is not at the top");
+
+ // Make sure the Web Console output is scrolled as near as possible to the
+ // bottom.
+ let nodeHeight = msgNode.clientHeight;
+ ok(scrollNode.scrollTop >= scrollNode.scrollHeight - scrollNode.clientHeight -
+ nodeHeight * 2, "scroll location is correct");
+
+ hud.setFilterState("network", true);
+ hud.setFilterState("networkinfo", true);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_597756_reopen_closed_tab.js b/devtools/client/webconsole/test/browser_webconsole_bug_597756_reopen_closed_tab.js
new file mode 100644
index 000000000..5a8280eed
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_597756_reopen_closed_tab.js
@@ -0,0 +1,70 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-597756-reopen-closed-tab.html";
+
+var HUD;
+
+add_task(function* () {
+ // On e10s, the exception is triggered in child process
+ // and is ignored by test harness
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+
+ let { browser } = yield loadTab(TEST_URI);
+ HUD = yield openConsole();
+
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+
+ yield reload(browser);
+
+ yield testMessages();
+
+ yield closeConsole();
+
+ // Close and reopen
+ gBrowser.removeCurrentTab();
+
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+
+ let tab = yield loadTab(TEST_URI);
+ HUD = yield openConsole();
+
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+
+ yield reload(tab.browser);
+
+ yield testMessages();
+
+ HUD = null;
+});
+
+function reload(browser) {
+ let loaded = loadBrowser(browser);
+ browser.reload();
+ return loaded;
+}
+
+function testMessages() {
+ return waitForMessages({
+ webconsole: HUD,
+ messages: [{
+ name: "error message displayed",
+ text: "fooBug597756_error",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ }],
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_599725_response_headers.js b/devtools/client/webconsole/test/browser_webconsole_bug_599725_response_headers.js
new file mode 100644
index 000000000..4849793cb
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_599725_response_headers.js
@@ -0,0 +1,67 @@
+/* -*- 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/ */
+
+"use strict";
+
+const INIT_URI = "data:text/plain;charset=utf8,hello world";
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-599725-response-headers.sjs";
+
+function performTest(request, hud) {
+ let deferred = promise.defer();
+
+ let headers = null;
+
+ function readHeader(name) {
+ for (let header of headers) {
+ if (header.name == name) {
+ return header.value;
+ }
+ }
+ return null;
+ }
+
+ hud.ui.proxy.webConsoleClient.getResponseHeaders(request.actor,
+ function (response) {
+ headers = response.headers;
+ ok(headers, "we have the response headers for reload");
+
+ let contentType = readHeader("Content-Type");
+ let contentLength = readHeader("Content-Length");
+
+ ok(!contentType, "we do not have the Content-Type header");
+ isnot(contentLength, 60, "Content-Length != 60");
+
+ executeSoon(deferred.resolve);
+ });
+
+ return deferred.promise;
+}
+
+let waitForRequest = Task.async(function*(hud) {
+ let request = yield waitForFinishedRequest(req=> {
+ return req.response.status === "304";
+ });
+
+ yield performTest(request, hud);
+});
+
+add_task(function* () {
+ let { browser } = yield loadTab(INIT_URI);
+
+ let hud = yield openConsole();
+
+ let gotLastRequest = waitForRequest(hud);
+
+ let loaded = loadBrowser(browser);
+ BrowserTestUtils.loadURI(browser, TEST_URI);
+ yield loaded;
+
+ let reloaded = loadBrowser(browser);
+ ContentTask.spawn(browser, null, "() => content.location.reload()");
+ yield reloaded;
+
+ yield gotLastRequest;
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_600183_charset.js b/devtools/client/webconsole/test/browser_webconsole_bug_600183_charset.js
new file mode 100644
index 000000000..153863824
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_600183_charset.js
@@ -0,0 +1,59 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const INIT_URI = "data:text/html;charset=utf-8,Web Console - bug 600183 test";
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-600183-charset.html";
+
+function performTest(lastFinishedRequest, console) {
+ let deferred = promise.defer();
+
+ ok(lastFinishedRequest, "charset test page was loaded and logged");
+ HUDService.lastFinishedRequest.callback = null;
+
+ executeSoon(() => {
+ console.webConsoleClient.getResponseContent(lastFinishedRequest.actor,
+ (response) => {
+ ok(!response.contentDiscarded, "response body was not discarded");
+
+ let body = response.content.text;
+ ok(body, "we have the response body");
+
+ // 的问候!
+ let chars = "\u7684\u95ee\u5019!";
+ isnot(body.indexOf("<p>" + chars + "</p>"), -1,
+ "found the chinese simplified string");
+
+ HUDService.lastFinishedRequest.callback = null;
+ executeSoon(deferred.resolve);
+ });
+ });
+
+ return deferred.promise;
+}
+
+function waitForRequest() {
+ let deferred = promise.defer();
+ HUDService.lastFinishedRequest.callback = (req, console) => {
+ performTest(req, console).then(deferred.resolve);
+ };
+ return deferred.promise;
+}
+
+add_task(function* () {
+ let { browser } = yield loadTab(INIT_URI);
+
+ yield openConsole();
+
+ let gotLastRequest = waitForRequest();
+
+ let loaded = loadBrowser(browser);
+ BrowserTestUtils.loadURI(browser, TEST_URI);
+ yield loaded;
+
+ yield gotLastRequest;
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_601177_log_levels.js b/devtools/client/webconsole/test/browser_webconsole_bug_601177_log_levels.js
new file mode 100644
index 000000000..9dd81c9fd
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_601177_log_levels.js
@@ -0,0 +1,76 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for " +
+ "bug 601177: log levels";
+const TEST_URI2 = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-601177-log-levels.html";
+
+add_task(function* () {
+ Services.prefs.setBoolPref("javascript.options.strict", true);
+
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ // On e10s, the exception is triggered in child process
+ // and is ignored by test harness
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+
+ yield testLogLevels(hud);
+
+ Services.prefs.clearUserPref("javascript.options.strict");
+});
+
+function testLogLevels(hud) {
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI2);
+
+ info("waiting for messages");
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ text: "test-bug-601177-log-levels.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "test-bug-601177-log-levels.js",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "test-image.png",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "foobar-known-to-fail.png",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_ERROR,
+ },
+ {
+ text: "foobarBug601177exception",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ },
+ {
+ text: "undefinedPropertyBug601177",
+ category: CATEGORY_JS,
+ severity: SEVERITY_WARNING,
+ },
+ {
+ text: "foobarBug601177strictError",
+ category: CATEGORY_JS,
+ severity: SEVERITY_WARNING,
+ },
+ ],
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_601352_scroll.js b/devtools/client/webconsole/test/browser_webconsole_bug_601352_scroll.js
new file mode 100644
index 000000000..89bd83a7a
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_601352_scroll.js
@@ -0,0 +1,84 @@
+/* -*- 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/ */
+
+// Test that the console output scrolls to JS eval results when there are many
+// messages displayed. See bug 601352.
+
+"use strict";
+
+add_task(function* () {
+ let {tab} = yield loadTab("data:text/html;charset=utf-8,Web Console test " +
+ "for bug 601352");
+ let hud = yield openConsole(tab);
+ hud.jsterm.clearOutput();
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let longMessage = "";
+ for (let i = 0; i < 50; i++) {
+ longMessage += "LongNonwrappingMessage";
+ }
+
+ for (let i = 0; i < 50; i++) {
+ content.console.log("test1 message " + i);
+ }
+
+ content.console.log(longMessage);
+
+ for (let i = 0; i < 50; i++) {
+ content.console.log("test2 message " + i);
+ }
+ });
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "test1 message 0",
+ }, {
+ text: "test1 message 49",
+ }, {
+ text: "LongNonwrappingMessage",
+ }, {
+ text: "test2 message 0",
+ }, {
+ text: "test2 message 49",
+ }],
+ });
+
+ let node = yield hud.jsterm.execute("1+1");
+
+ let scrollNode = hud.ui.outputWrapper;
+ let rectNode = node.getBoundingClientRect();
+ let rectOutput = scrollNode.getBoundingClientRect();
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {
+ rectNode,
+ rectOutput,
+ scrollHeight: scrollNode.scrollHeight,
+ scrollTop: scrollNode.scrollTop,
+ clientHeight: scrollNode.clientHeight,
+ }, function* (args) {
+ console.debug("rectNode", args.rectNode, "rectOutput", args.rectOutput);
+ console.log("scrollNode scrollHeight", args.scrollHeight,
+ "scrollTop", args.scrollTop, "clientHeight",
+ args.clientHeight);
+ });
+
+ isnot(scrollNode.scrollTop, 0, "scroll location is not at the top");
+
+ // The bounding client rect .top/left coordinates are relative to the
+ // console iframe.
+
+ // Visible scroll viewport.
+ let height = rectOutput.height;
+
+ // Top and bottom coordinates of the last message node, relative to the
+ // outputNode.
+ let top = rectNode.top - rectOutput.top;
+ let bottom = top + rectNode.height;
+ info("node top " + top + " node bottom " + bottom + " node clientHeight " +
+ node.clientHeight);
+
+ ok(top >= 0 && bottom <= height, "last message is visible");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_601667_filter_buttons.js b/devtools/client/webconsole/test/browser_webconsole_bug_601667_filter_buttons.js
new file mode 100644
index 000000000..6dae0a7b7
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_601667_filter_buttons.js
@@ -0,0 +1,267 @@
+/* -*- 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 the filter button UI logic works correctly.
+
+"use strict";
+
+const TEST_URI = "http://example.com/";
+const FILTER_PREF_DOMAIN = "devtools.webconsole.filter.";
+
+var hud, hudId, hudBox;
+var prefs = {};
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ hud = yield openConsole();
+ hudId = hud.hudId;
+ hudBox = hud.ui.rootElement;
+
+ savePrefs();
+
+ testFilterButtons();
+
+ restorePrefs();
+
+ hud = hudId = hudBox = null;
+});
+
+function savePrefs() {
+ let branch = Services.prefs.getBranch(FILTER_PREF_DOMAIN);
+ let children = branch.getChildList("");
+ for (let child of children) {
+ prefs[child] = branch.getBoolPref(child);
+ }
+}
+
+function restorePrefs() {
+ let branch = Services.prefs.getBranch(FILTER_PREF_DOMAIN);
+ for (let p in prefs) {
+ branch.setBoolPref(p, prefs[p]);
+ }
+}
+
+function testFilterButtons() {
+ testMenuFilterButton("net");
+ testMenuFilterButton("css");
+ testMenuFilterButton("js");
+ testMenuFilterButton("logging");
+ testMenuFilterButton("security");
+ testMenuFilterButton("server");
+
+ testIsolateFilterButton("net");
+ testIsolateFilterButton("css");
+ testIsolateFilterButton("js");
+ testIsolateFilterButton("logging");
+ testIsolateFilterButton("security");
+ testIsolateFilterButton("server");
+}
+
+function testMenuFilterButton(category) {
+ let selector = ".webconsole-filter-button[category=\"" + category + "\"]";
+ let button = hudBox.querySelector(selector);
+ ok(button, "we have the \"" + category + "\" button");
+
+ let firstMenuItem = button.querySelector("menuitem");
+ ok(firstMenuItem, "we have the first menu item for the \"" + category +
+ "\" button");
+
+ // Turn all the filters off, if they were on.
+ let menuItem = firstMenuItem;
+ while (menuItem != null) {
+ if (menuItem.hasAttribute("prefKey") && isChecked(menuItem)) {
+ chooseMenuItem(menuItem);
+ }
+ menuItem = menuItem.nextSibling;
+ }
+
+ // Turn all the filters on; make sure the button gets checked.
+ menuItem = firstMenuItem;
+ let prefKey;
+ while (menuItem) {
+ if (menuItem.hasAttribute("prefKey")) {
+ prefKey = menuItem.getAttribute("prefKey");
+ chooseMenuItem(menuItem);
+ ok(isChecked(menuItem), "menu item " + prefKey + " for category " +
+ category + " is checked after clicking it");
+ ok(hud.ui.filterPrefs[prefKey], prefKey + " messages are " +
+ "on after clicking the appropriate menu item");
+ }
+ menuItem = menuItem.nextSibling;
+ }
+ ok(isChecked(button), "the button for category " + category + " is " +
+ "checked after turning on all its menu items");
+
+ // Turn one filter off; make sure the button is still checked.
+ prefKey = firstMenuItem.getAttribute("prefKey");
+ chooseMenuItem(firstMenuItem);
+ ok(!isChecked(firstMenuItem), "the first menu item for category " +
+ category + " is no longer checked after clicking it");
+ ok(!hud.ui.filterPrefs[prefKey], prefKey + " messages are " +
+ "turned off after clicking the appropriate menu item");
+ ok(isChecked(button), "the button for category " + category + " is still " +
+ "checked after turning off its first menu item");
+
+ // Turn all the filters off by clicking the main part of the button.
+ let subbutton = getMainButton(button);
+ ok(subbutton, "we have the subbutton for category " + category);
+
+ clickButton(subbutton);
+ ok(!isChecked(button), "the button for category " + category + " is " +
+ "no longer checked after clicking its main part");
+
+ menuItem = firstMenuItem;
+ while (menuItem) {
+ prefKey = menuItem.getAttribute("prefKey");
+ if (prefKey) {
+ ok(!isChecked(menuItem), "menu item " + prefKey + " for category " +
+ category + " is no longer checked after clicking the button");
+ ok(!hud.ui.filterPrefs[prefKey], prefKey + " messages are " +
+ "off after clicking the button");
+ }
+ menuItem = menuItem.nextSibling;
+ }
+
+ // Turn all the filters on by clicking the main part of the button.
+ clickButton(subbutton);
+
+ ok(isChecked(button), "the button for category " + category + " is " +
+ "checked after clicking its main part");
+
+ menuItem = firstMenuItem;
+ while (menuItem) {
+ if (menuItem.hasAttribute("prefKey")) {
+ prefKey = menuItem.getAttribute("prefKey");
+ // The CSS/Log menu item should not be checked. See bug 971798.
+ if (category == "css" && prefKey == "csslog") {
+ ok(!isChecked(menuItem), "menu item " + prefKey + " for category " +
+ category + " should not be checked after clicking the button");
+ ok(!hud.ui.filterPrefs[prefKey], prefKey + " messages are " +
+ "off after clicking the button");
+ } else {
+ ok(isChecked(menuItem), "menu item " + prefKey + " for category " +
+ category + " is checked after clicking the button");
+ ok(hud.ui.filterPrefs[prefKey], prefKey + " messages are " +
+ "on after clicking the button");
+ }
+ }
+ menuItem = menuItem.nextSibling;
+ }
+
+ // Uncheck the main button by unchecking all the filters
+ menuItem = firstMenuItem;
+ while (menuItem) {
+ // The csslog menu item is already unchecked at this point.
+ // Make sure it is not selected. See bug 971798.
+ prefKey = menuItem.getAttribute("prefKey");
+ if (prefKey && prefKey != "csslog") {
+ chooseMenuItem(menuItem);
+ }
+ menuItem = menuItem.nextSibling;
+ }
+
+ ok(!isChecked(button), "the button for category " + category + " is " +
+ "unchecked after unchecking all its filters");
+
+ // Turn all the filters on again by clicking the button.
+ clickButton(subbutton);
+}
+
+function testIsolateFilterButton(category) {
+ let selector = ".webconsole-filter-button[category=\"" + category + "\"]";
+ let targetButton = hudBox.querySelector(selector);
+ ok(targetButton, "we have the \"" + category + "\" button");
+
+ // Get the main part of the filter button.
+ let subbutton = getMainButton(targetButton);
+ ok(subbutton, "we have the subbutton for category " + category);
+
+ // Turn on all the filters by alt clicking the main part of the button.
+ altClickButton(subbutton);
+ ok(isChecked(targetButton), "the button for category " + category +
+ " is checked after isolating for filter");
+
+ // Check if all the filters for the target button are on.
+ let menuItems = targetButton.querySelectorAll("menuitem");
+ Array.forEach(menuItems, (item) => {
+ let prefKey = item.getAttribute("prefKey");
+ // The CSS/Log filter should not be checked. See bug 971798.
+ if (category == "css" && prefKey == "csslog") {
+ ok(!isChecked(item), "menu item " + prefKey + " for category " +
+ category + " should not be checked after isolating for " + category);
+ ok(!hud.ui.filterPrefs[prefKey], prefKey + " messages should be " +
+ "turned off after isolating for " + category);
+ } else if (prefKey) {
+ ok(isChecked(item), "menu item " + prefKey + " for category " +
+ category + " is checked after isolating for " + category);
+ ok(hud.ui.filterPrefs[prefKey], prefKey + " messages are " +
+ "turned on after isolating for " + category);
+ }
+ });
+
+ // Ensure all other filter buttons are toggled off and their
+ // associated filters are turned off
+ let buttons = hudBox.querySelectorAll(".webconsole-filter-button[category]");
+ Array.forEach(buttons, (filterButton) => {
+ if (filterButton !== targetButton) {
+ let categoryBtn = filterButton.getAttribute("category");
+ ok(!isChecked(filterButton), "the button for category " +
+ categoryBtn + " is unchecked after isolating for " + category);
+
+ menuItems = filterButton.querySelectorAll("menuitem");
+ Array.forEach(menuItems, (item) => {
+ let prefKey = item.getAttribute("prefKey");
+ if (prefKey) {
+ ok(!isChecked(item), "menu item " + prefKey + " for category " +
+ category + " is unchecked after isolating for " + category);
+ ok(!hud.ui.filterPrefs[prefKey], prefKey + " messages are " +
+ "turned off after isolating for " + category);
+ }
+ });
+
+ // Turn all the filters on again by clicking the button.
+ let mainButton = getMainButton(filterButton);
+ clickButton(mainButton);
+ }
+ });
+}
+
+/**
+ * Return the main part of the target filter button.
+ */
+function getMainButton(targetButton) {
+ let anonymousNodes = hud.ui.document.getAnonymousNodes(targetButton);
+ let subbutton;
+
+ for (let i = 0; i < anonymousNodes.length; i++) {
+ let node = anonymousNodes[i];
+ if (node.classList.contains("toolbarbutton-menubutton-button")) {
+ subbutton = node;
+ break;
+ }
+ }
+
+ return subbutton;
+}
+
+function clickButton(node) {
+ EventUtils.sendMouseEvent({ type: "click" }, node);
+}
+
+function altClickButton(node) {
+ EventUtils.sendMouseEvent({ type: "click", altKey: true }, node);
+}
+
+function chooseMenuItem(node) {
+ let event = document.createEvent("XULCommandEvent");
+ event.initCommandEvent("command", true, true, window, 0, false, false, false,
+ false, null);
+ node.dispatchEvent(event);
+}
+
+function isChecked(node) {
+ return node.getAttribute("checked") === "true";
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_603750_websocket.js b/devtools/client/webconsole/test/browser_webconsole_bug_603750_websocket.js
new file mode 100644
index 000000000..f14530d06
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_603750_websocket.js
@@ -0,0 +1,37 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-603750-websocket.html";
+const TEST_URI2 = "data:text/html;charset=utf-8,Web Console test for " +
+ "bug 603750: Web Socket errors";
+
+add_task(function* () {
+ yield loadTab(TEST_URI2);
+
+ let hud = yield openConsole();
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI);
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ text: "ws://0.0.0.0:81",
+ source: { url: "test-bug-603750-websocket.js" },
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ },
+ {
+ text: "ws://0.0.0.0:82",
+ source: { url: "test-bug-603750-websocket.js" },
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ },
+ ]
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_611795.js b/devtools/client/webconsole/test/browser_webconsole_bug_611795.js
new file mode 100644
index 000000000..1fa4d717e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_611795.js
@@ -0,0 +1,67 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = 'data:text/html;charset=utf-8,<div style="-moz-opacity:0;">' +
+ 'test repeated css warnings</div><p style="-moz-opacity:0">' +
+ "hi</p>";
+var hud;
+
+/**
+ * Unit test for bug 611795:
+ * Repeated CSS messages get collapsed into one.
+ */
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ hud = yield openConsole();
+ hud.jsterm.clearOutput(true);
+
+ BrowserReload();
+ yield loadBrowser(gBrowser.selectedBrowser);
+
+ yield onContentLoaded();
+ yield testConsoleLogRepeats();
+
+ hud = null;
+});
+
+function onContentLoaded() {
+ let cssWarning = "Unknown property \u2018-moz-opacity\u2019. Declaration dropped.";
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: cssWarning,
+ category: CATEGORY_CSS,
+ severity: SEVERITY_WARNING,
+ repeats: 2,
+ }],
+ });
+}
+
+function testConsoleLogRepeats() {
+ let jsterm = hud.jsterm;
+
+ jsterm.clearOutput();
+
+ jsterm.setInputValue("for (let i = 0; i < 10; ++i) console.log('this is a " +
+ "line of reasonably long text that I will use to " +
+ "verify that the repeated text node is of an " +
+ "appropriate size.');");
+ jsterm.execute();
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "this is a line of reasonably long text",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ repeats: 10,
+ }],
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_613013_console_api_iframe.js b/devtools/client/webconsole/test/browser_webconsole_bug_613013_console_api_iframe.js
new file mode 100644
index 000000000..5d0067958
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_613013_console_api_iframe.js
@@ -0,0 +1,26 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-613013-console-api-iframe.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ BrowserReload();
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foobarBug613013",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_613280_jsterm_copy.js b/devtools/client/webconsole/test/browser_webconsole_bug_613280_jsterm_copy.js
new file mode 100644
index 000000000..95752021d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_613280_jsterm_copy.js
@@ -0,0 +1,64 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 613280";
+
+function test() {
+ loadTab(TEST_URI).then(() => {
+ openConsole().then((HUD) => {
+ ContentTask.spawn(gBrowser.selectedBrowser, null, function*(){
+ content.console.log("foobarBazBug613280");
+ });
+ waitForMessages({
+ webconsole: HUD,
+ messages: [{
+ text: "foobarBazBug613280",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(performTest.bind(null, HUD));
+ });
+ });
+}
+
+function performTest(HUD, [result]) {
+ let msg = [...result.matched][0];
+ let input = HUD.jsterm.inputNode;
+
+ let clipboardSetup = function () {
+ goDoCommand("cmd_copy");
+ };
+
+ let clipboardCopyDone = function () {
+ finishTest();
+ };
+
+ let controller = top.document.commandDispatcher
+ .getControllerForCommand("cmd_copy");
+ is(controller.isCommandEnabled("cmd_copy"), false, "cmd_copy is disabled");
+
+ HUD.ui.output.selectMessage(msg);
+ HUD.outputNode.focus();
+
+ goUpdateCommand("cmd_copy");
+
+ controller = top.document.commandDispatcher
+ .getControllerForCommand("cmd_copy");
+ is(controller.isCommandEnabled("cmd_copy"), true, "cmd_copy is enabled");
+
+ // Remove new lines and whitespace since getSelection() includes
+ // a new line between message and line number, but the clipboard doesn't
+ // @see bug 1119503
+ let selectionText = (HUD.iframeWindow.getSelection() + "")
+ .replace(/\r?\n|\r| /g, "");
+ isnot(selectionText.indexOf("foobarBazBug613280"), -1,
+ "selection text includes 'foobarBazBug613280'");
+
+ waitForClipboard((str) => {
+ return selectionText.trim() === str.trim().replace(/ /g, "");
+ }, clipboardSetup, clipboardCopyDone, clipboardCopyDone);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_613642_maintain_scroll.js b/devtools/client/webconsole/test/browser_webconsole_bug_613642_maintain_scroll.js
new file mode 100644
index 000000000..e24ce28e2
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_613642_maintain_scroll.js
@@ -0,0 +1,119 @@
+/* -*- 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/ */
+
+"use strict";
+
+var TEST_URI = "data:text/html;charset=utf-8,Web Console test for " +
+ "bug 613642: remember scroll location";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ hud.jsterm.clearOutput();
+ let outputNode = hud.outputNode;
+ let scrollBox = hud.ui.outputWrapper;
+
+ for (let i = 0; i < 150; i++) {
+ ContentTask.spawn(gBrowser.selectedBrowser, i, function* (num) {
+ content.console.log("test message " + num);
+ });
+ }
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "test message 149",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ ok(scrollBox.scrollTop > 0, "scroll location is not at the top");
+
+ // scroll to the first node
+ outputNode.focus();
+
+ let scrolled = promise.defer();
+
+ scrollBox.onscroll = () => {
+ info("onscroll top " + scrollBox.scrollTop);
+ if (scrollBox.scrollTop != 0) {
+ // Wait for scroll to 0.
+ return;
+ }
+ scrollBox.onscroll = null;
+ is(scrollBox.scrollTop, 0, "scroll location updated (moved to top)");
+ scrolled.resolve();
+ };
+ EventUtils.synthesizeKey("VK_HOME", {}, hud.iframeWindow);
+
+ yield scrolled.promise;
+
+ // add a message and make sure scroll doesn't change
+ ContentTask.spawn(gBrowser.selectedBrowser, null,
+ "() => content.console.log('test message 150')");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "test message 150",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ scrolled = promise.defer();
+ scrollBox.onscroll = () => {
+ if (scrollBox.scrollTop != 0) {
+ // Wait for scroll to stabilize at the top.
+ return;
+ }
+ scrollBox.onscroll = null;
+ is(scrollBox.scrollTop, 0, "scroll location is still at the top");
+ scrolled.resolve();
+ };
+
+ // Make sure that scroll stabilizes at the top. executeSoon() is needed for
+ // the yield to work.
+ executeSoon(scrollBox.onscroll);
+
+ yield scrolled.promise;
+
+ // scroll back to the bottom
+ outputNode.lastChild.focus();
+
+ scrolled = promise.defer();
+ scrollBox.onscroll = () => {
+ if (scrollBox.scrollTop == 0) {
+ // Wait for scroll to bottom.
+ return;
+ }
+ scrollBox.onscroll = null;
+ isnot(scrollBox.scrollTop, 0, "scroll location updated (moved to bottom)");
+ scrolled.resolve();
+ };
+ EventUtils.synthesizeKey("VK_END", {});
+ yield scrolled.promise;
+
+ let oldScrollTop = scrollBox.scrollTop;
+
+ ContentTask.spawn(gBrowser.selectedBrowser, null,
+ "() => content.console.log('test message 151')");
+
+ scrolled = promise.defer();
+ scrollBox.onscroll = () => {
+ if (scrollBox.scrollTop == oldScrollTop) {
+ // Wait for scroll to change.
+ return;
+ }
+ scrollBox.onscroll = null;
+ isnot(scrollBox.scrollTop, oldScrollTop,
+ "scroll location updated (moved to bottom again)");
+ scrolled.resolve();
+ };
+ yield scrolled.promise;
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_613642_prune_scroll.js b/devtools/client/webconsole/test/browser_webconsole_bug_613642_prune_scroll.js
new file mode 100644
index 000000000..c53fe1683
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_613642_prune_scroll.js
@@ -0,0 +1,82 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for " +
+ "bug 613642: maintain scroll with pruning of old messages";
+
+var hud;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ hud = yield openConsole();
+
+ hud.jsterm.clearOutput();
+
+ let outputNode = hud.outputNode;
+
+ Services.prefs.setIntPref("devtools.hud.loglimit.console", 140);
+ let scrollBoxElement = hud.ui.outputWrapper;
+
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ for (let i = 0; i < 150; i++) {
+ content.console.log("test message " + i);
+ }
+ });
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "test message 149",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ let oldScrollTop = scrollBoxElement.scrollTop;
+ isnot(oldScrollTop, 0, "scroll location is not at the top");
+
+ let firstNode = outputNode.firstChild;
+ ok(firstNode, "found the first message");
+
+ let msgNode = outputNode.children[80];
+ ok(msgNode, "found the 80th message");
+
+ // scroll to the middle message node
+ msgNode.scrollIntoView(false);
+
+ isnot(scrollBoxElement.scrollTop, oldScrollTop,
+ "scroll location updated (scrolled to message)");
+
+ oldScrollTop = scrollBoxElement.scrollTop;
+
+ // add a message
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.console.log("hello world");
+ });
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "hello world",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ // Scroll location needs to change, because one message is also removed, and
+ // we need to scroll a bit towards the top, to keep the current view in sync.
+ isnot(scrollBoxElement.scrollTop, oldScrollTop,
+ "scroll location updated (added a message)");
+
+ isnot(outputNode.firstChild, firstNode,
+ "first message removed");
+
+ Services.prefs.clearUserPref("devtools.hud.loglimit.console");
+
+ hud = null;
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_614793_jsterm_scroll.js b/devtools/client/webconsole/test/browser_webconsole_bug_614793_jsterm_scroll.js
new file mode 100644
index 000000000..ae61023a9
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_614793_jsterm_scroll.js
@@ -0,0 +1,54 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for " +
+ "bug 614793: jsterm result scroll";
+
+requestLongerTimeout(2);
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ yield testScrollPosition(hud);
+});
+
+function* testScrollPosition(hud) {
+ hud.jsterm.clearOutput();
+
+ let scrollNode = hud.ui.outputWrapper;
+
+ for (let i = 0; i < 150; i++) {
+ yield ContentTask.spawn(gBrowser.selectedBrowser, i, function* (i) {
+ content.console.log("test message " + i);
+ });
+ }
+
+ let oldScrollTop = -1;
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "test message 149",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ oldScrollTop = scrollNode.scrollTop;
+ isnot(oldScrollTop, 0, "scroll location is not at the top");
+
+ let msg = yield hud.jsterm.execute("'hello world'");
+
+ isnot(scrollNode.scrollTop, oldScrollTop, "scroll location updated");
+
+ oldScrollTop = scrollNode.scrollTop;
+
+ msg.scrollIntoView(false);
+
+ is(scrollNode.scrollTop, oldScrollTop, "scroll location is the same");
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_618078_network_exceptions.js b/devtools/client/webconsole/test/browser_webconsole_bug_618078_network_exceptions.js
new file mode 100644
index 000000000..439793b22
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_618078_network_exceptions.js
@@ -0,0 +1,36 @@
+/* -*- 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 we report JS exceptions in event handlers coming from
+// network requests, like onreadystate for XHR. See bug 618078.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 618078";
+const TEST_URI2 = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-618078-network-exceptions.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ // On e10s, the exception is triggered in child process
+ // and is ignored by test harness
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI2);
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "bug618078exception",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ }],
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_621644_jsterm_dollar.js b/devtools/client/webconsole/test/browser_webconsole_bug_621644_jsterm_dollar.js
new file mode 100644
index 000000000..6f4248c51
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_621644_jsterm_dollar.js
@@ -0,0 +1,47 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-621644-jsterm-dollar.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ yield test$(hud);
+ yield test$$(hud);
+});
+
+function* test$(HUD) {
+ let deferred = promise.defer();
+
+ HUD.jsterm.clearOutput();
+
+ HUD.jsterm.execute("$(document.body)", (msg) => {
+ ok(msg.textContent.indexOf("<p>") > -1,
+ "jsterm output is correct for $()");
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
+
+function test$$(HUD) {
+ let deferred = promise.defer();
+
+ HUD.jsterm.clearOutput();
+
+ HUD.jsterm.setInputValue();
+ HUD.jsterm.execute("$$(document)", (msg) => {
+ ok(msg.textContent.indexOf("621644") > -1,
+ "jsterm output is correct for $$()");
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_622303_persistent_filters.js b/devtools/client/webconsole/test/browser_webconsole_bug_622303_persistent_filters.js
new file mode 100644
index 000000000..f4b5dca96
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_622303_persistent_filters.js
@@ -0,0 +1,149 @@
+/* -*- 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/ */
+
+"use strict";
+
+const prefs = {
+ "net": [
+ "network",
+ "netwarn",
+ "netxhr",
+ "networkinfo"
+ ],
+ "css": [
+ "csserror",
+ "cssparser",
+ "csslog"
+ ],
+ "js": [
+ "exception",
+ "jswarn",
+ "jslog",
+ ],
+ "logging": [
+ "error",
+ "warn",
+ "info",
+ "log",
+ "serviceworkers",
+ "sharedworkers",
+ "windowlessworkers"
+ ]
+};
+
+add_task(function* () {
+ // Set all prefs to true
+ for (let category in prefs) {
+ prefs[category].forEach(function (pref) {
+ Services.prefs.setBoolPref("devtools.webconsole.filter." + pref, true);
+ });
+ }
+
+ yield loadTab("about:blank");
+
+ let hud = yield openConsole();
+
+ let hud2 = yield onConsoleOpen(hud);
+ let hud3 = yield onConsoleReopen1(hud2);
+ yield onConsoleReopen2(hud3);
+
+ // Clear prefs
+ for (let category in prefs) {
+ prefs[category].forEach(function (pref) {
+ Services.prefs.clearUserPref("devtools.webconsole.filter." + pref);
+ });
+ }
+});
+
+function onConsoleOpen(hud) {
+ let deferred = promise.defer();
+
+ let hudBox = hud.ui.rootElement;
+
+ // Check if the filters menuitems exists and are checked
+ for (let category in prefs) {
+ let button = hudBox.querySelector(".webconsole-filter-button[category=\""
+ + category + "\"]");
+ ok(isChecked(button), "main button for " + category +
+ " category is checked");
+
+ prefs[category].forEach(function (pref) {
+ let menuitem = hudBox.querySelector("menuitem[prefKey=" + pref + "]");
+ ok(isChecked(menuitem), "menuitem for " + pref + " is checked");
+ });
+ }
+
+ // Set all prefs to false
+ for (let category in prefs) {
+ prefs[category].forEach(function (pref) {
+ hud.setFilterState(pref, false);
+ });
+ }
+
+ // Re-init the console
+ closeConsole().then(() => {
+ openConsole().then(deferred.resolve);
+ });
+
+ return deferred.promise;
+}
+
+function onConsoleReopen1(hud) {
+ info("testing after reopening once");
+ let deferred = promise.defer();
+
+ let hudBox = hud.ui.rootElement;
+
+ // Check if the filter button and menuitems are unchecked
+ for (let category in prefs) {
+ let button = hudBox.querySelector(".webconsole-filter-button[category=\""
+ + category + "\"]");
+ ok(isUnchecked(button), "main button for " + category +
+ " category is not checked");
+
+ prefs[category].forEach(function (pref) {
+ let menuitem = hudBox.querySelector("menuitem[prefKey=" + pref + "]");
+ ok(isUnchecked(menuitem), "menuitem for " + pref + " is not checked");
+ });
+ }
+
+ // Set first pref in each category to true
+ for (let category in prefs) {
+ hud.setFilterState(prefs[category][0], true);
+ }
+
+ // Re-init the console
+ closeConsole().then(() => {
+ openConsole().then(deferred.resolve);
+ });
+
+ return deferred.promise;
+}
+
+function onConsoleReopen2(hud) {
+ info("testing after reopening again");
+
+ let hudBox = hud.ui.rootElement;
+
+ // Check the main category button is checked and first menuitem is checked
+ for (let category in prefs) {
+ let button = hudBox.querySelector(".webconsole-filter-button[category=\"" +
+ category + "\"]");
+ ok(isChecked(button), category +
+ " button is checked when first pref is true");
+
+ let pref = prefs[category][0];
+ let menuitem = hudBox.querySelector("menuitem[prefKey=" + pref + "]");
+ ok(isChecked(menuitem), "first " + category + " menuitem is checked");
+ }
+}
+
+function isChecked(aNode) {
+ return aNode.getAttribute("checked") === "true";
+}
+
+function isUnchecked(aNode) {
+ return aNode.getAttribute("checked") === "false";
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_623749_ctrl_a_select_all_winnt.js b/devtools/client/webconsole/test/browser_webconsole_bug_623749_ctrl_a_select_all_winnt.js
new file mode 100644
index 000000000..3de10774d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_623749_ctrl_a_select_all_winnt.js
@@ -0,0 +1,32 @@
+/* -*- 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/ */
+
+// Test for https://bugzilla.mozilla.org/show_bug.cgi?id=623749
+// Map Control + A to Select All, In the web console input, on Windows
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Test console for bug 623749";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ let jsterm = hud.jsterm;
+ jsterm.setInputValue("Ignore These Four Words");
+ let inputNode = jsterm.inputNode;
+
+ // Test select all with Control + A.
+ EventUtils.synthesizeKey("a", { ctrlKey: true });
+ let inputLength = inputNode.selectionEnd - inputNode.selectionStart;
+ is(inputLength, jsterm.getInputValue().length, "Select all of input");
+
+ // Test do nothing on Control + E.
+ jsterm.setInputValue("Ignore These Four Words");
+ inputNode.selectionStart = 0;
+ EventUtils.synthesizeKey("e", { ctrlKey: true });
+ is(inputNode.selectionStart, 0, "Control + E does not move to end of input");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js b/devtools/client/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js
new file mode 100644
index 000000000..509749953
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js
@@ -0,0 +1,120 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>Web Console test for " +
+ "bug 630733";
+const TEST_URI2 = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-630733-response-redirect-headers.sjs";
+
+var lastFinishedRequests = {};
+var webConsoleClient;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ yield consoleOpened(hud);
+ yield getHeaders();
+ yield getContent();
+
+ performTest();
+});
+
+function consoleOpened(hud) {
+ let deferred = promise.defer();
+
+ webConsoleClient = hud.ui.webConsoleClient;
+ HUDService.lastFinishedRequest.callback = (aHttpRequest) => {
+ let status = aHttpRequest.response.status;
+ lastFinishedRequests[status] = aHttpRequest;
+ if ("301" in lastFinishedRequests &&
+ "404" in lastFinishedRequests) {
+ deferred.resolve();
+ }
+ };
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI2);
+
+ return deferred.promise;
+}
+
+function getHeaders() {
+ let deferred = promise.defer();
+
+ HUDService.lastFinishedRequest.callback = null;
+
+ ok("301" in lastFinishedRequests, "request 1: 301 Moved Permanently");
+ ok("404" in lastFinishedRequests, "request 2: 404 Not found");
+
+ webConsoleClient.getResponseHeaders(lastFinishedRequests["301"].actor,
+ function (response) {
+ lastFinishedRequests["301"].response.headers = response.headers;
+
+ webConsoleClient.getResponseHeaders(lastFinishedRequests["404"].actor,
+ function (resp) {
+ lastFinishedRequests["404"].response.headers = resp.headers;
+ executeSoon(deferred.resolve);
+ });
+ });
+ return deferred.promise;
+}
+
+function getContent() {
+ let deferred = promise.defer();
+
+ webConsoleClient.getResponseContent(lastFinishedRequests["301"].actor,
+ function (response) {
+ lastFinishedRequests["301"].response.content = response.content;
+ lastFinishedRequests["301"].discardResponseBody = response.contentDiscarded;
+
+ webConsoleClient.getResponseContent(lastFinishedRequests["404"].actor,
+ function (resp) {
+ lastFinishedRequests["404"].response.content = resp.content;
+ lastFinishedRequests["404"].discardResponseBody =
+ resp.contentDiscarded;
+
+ webConsoleClient = null;
+ executeSoon(deferred.resolve);
+ });
+ });
+ return deferred.promise;
+}
+
+function performTest() {
+ function readHeader(name) {
+ for (let header of headers) {
+ if (header.name == name) {
+ return header.value;
+ }
+ }
+ return null;
+ }
+
+ let headers = lastFinishedRequests["301"].response.headers;
+ is(readHeader("Content-Type"), "text/html",
+ "we do have the Content-Type header");
+ is(readHeader("Content-Length"), 71, "Content-Length is correct");
+ is(readHeader("Location"), "/redirect-from-bug-630733",
+ "Content-Length is correct");
+ is(readHeader("x-foobar-bug630733"), "bazbaz",
+ "X-Foobar-bug630733 is correct");
+
+ let body = lastFinishedRequests["301"].response.content;
+ ok(!body.text, "body discarded for request 1");
+ ok(lastFinishedRequests["301"].discardResponseBody,
+ "body discarded for request 1 (confirmed)");
+
+ headers = lastFinishedRequests["404"].response.headers;
+ ok(!readHeader("Location"), "no Location header");
+ ok(!readHeader("x-foobar-bug630733"), "no X-Foobar-bug630733 header");
+
+ body = lastFinishedRequests["404"].response.content.text;
+ isnot(body.indexOf("404"), -1,
+ "body is correct for request 2");
+
+ lastFinishedRequests = webConsoleClient = null;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js b/devtools/client/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js
new file mode 100644
index 000000000..45d1f7102
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js
@@ -0,0 +1,47 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-632275-getters.html";
+
+var getterValue = null;
+
+function test() {
+ loadTab(TEST_URI).then(() => {
+ openConsole().then(consoleOpened);
+ });
+}
+
+function consoleOpened(hud) {
+ let doc = content.wrappedJSObject.document;
+ getterValue = doc.foobar._val;
+ hud.jsterm.execute("console.dir(document)");
+
+ let onOpen = onViewOpened.bind(null, hud);
+ hud.jsterm.once("variablesview-fetched", onOpen);
+}
+
+function onViewOpened(hud, event, view) {
+ let doc = content.wrappedJSObject.document;
+
+ findVariableViewProperties(view, [
+ { name: /^(width|height)$/, dontMatch: 1 },
+ { name: "foobar._val", value: getterValue },
+ { name: "foobar.val", isGetter: true },
+ ], { webconsole: hud }).then(function () {
+ is(doc.foobar._val, getterValue, "getter did not execute");
+ is(doc.foobar.val, getterValue + 1, "getter executed");
+ is(doc.foobar._val, getterValue + 1, "getter executed (recheck)");
+
+ let textContent = hud.outputNode.textContent;
+ is(textContent.indexOf("document.body.client"), -1,
+ "no document.width/height warning displayed");
+
+ getterValue = null;
+ finishTest();
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js b/devtools/client/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js
new file mode 100644
index 000000000..c5e672444
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js
@@ -0,0 +1,84 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-632347-iterators-generators.html";
+
+function test() {
+ requestLongerTimeout(6);
+
+ loadTab(TEST_URI).then(() => {
+ openConsole().then(consoleOpened);
+ });
+}
+
+function consoleOpened(HUD) {
+ let {JSPropertyProvider} = require("devtools/shared/webconsole/js-property-provider");
+
+ let tmp = Cu.import("resource://gre/modules/jsdebugger.jsm", {});
+ tmp.addDebuggerToGlobal(tmp);
+ let dbg = new tmp.Debugger();
+
+ let jsterm = HUD.jsterm;
+ let win = content.wrappedJSObject;
+ let dbgWindow = dbg.addDebuggee(content);
+ let container = win._container;
+
+ // Make sure autocomplete does not walk through iterators and generators.
+ let result = container.gen1.next();
+ let completion = JSPropertyProvider(dbgWindow, null, "_container.gen1.");
+ isnot(completion.matches.length, 0, "Got matches for gen1");
+
+ is(result + 1, container.gen1.next(), "gen1.next() did not execute");
+
+ result = container.gen2.next().value;
+
+ completion = JSPropertyProvider(dbgWindow, null, "_container.gen2.");
+ isnot(completion.matches.length, 0, "Got matches for gen2");
+
+ is((result / 2 + 1) * 2, container.gen2.next().value,
+ "gen2.next() did not execute");
+
+ result = container.iter1.next();
+ is(result[0], "foo", "iter1.next() [0] is correct");
+ is(result[1], "bar", "iter1.next() [1] is correct");
+
+ completion = JSPropertyProvider(dbgWindow, null, "_container.iter1.");
+ isnot(completion.matches.length, 0, "Got matches for iter1");
+
+ result = container.iter1.next();
+ is(result[0], "baz", "iter1.next() [0] is correct");
+ is(result[1], "baaz", "iter1.next() [1] is correct");
+
+ let dbgContent = dbg.makeGlobalObjectReference(content);
+ completion = JSPropertyProvider(dbgContent, null, "_container.iter2.");
+ isnot(completion.matches.length, 0, "Got matches for iter2");
+
+ completion = JSPropertyProvider(dbgWindow, null, "window._container.");
+ ok(completion, "matches available for window._container");
+ ok(completion.matches.length, "matches available for window (length)");
+
+ dbg.removeDebuggee(content);
+ jsterm.clearOutput();
+
+ jsterm.execute("window._container", (msg) => {
+ jsterm.once("variablesview-fetched", testVariablesView.bind(null, HUD));
+ let anchor = msg.querySelector(".message-body a");
+ EventUtils.synthesizeMouse(anchor, 2, 2, {}, HUD.iframeWindow);
+ });
+}
+
+function testVariablesView(aWebconsole, aEvent, aView) {
+ findVariableViewProperties(aView, [
+ { name: "gen1", isGenerator: true },
+ { name: "gen2", isGenerator: true },
+ { name: "iter1", isIterator: true },
+ { name: "iter2", isIterator: true },
+ ], { webconsole: aWebconsole }).then(function () {
+ executeSoon(finishTest);
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_632817.js b/devtools/client/webconsole/test/browser_webconsole_bug_632817.js
new file mode 100644
index 000000000..561e3b112
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_632817.js
@@ -0,0 +1,217 @@
+/* -*- 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 Web console messages can be filtered for NET events.
+
+"use strict";
+
+const TEST_NETWORK_REQUEST_URI =
+ "https://example.com/browser/devtools/client/webconsole/test/" +
+ "test-network-request.html";
+
+const TEST_IMG = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-image.png";
+
+const TEST_DATA_JSON_CONTENT =
+ '{ id: "test JSON data", myArray: [ "foo", "bar", "baz", "biff" ] }';
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console network logging " +
+ "tests";
+
+const PAGE_REQUEST_PREDICATE =
+ ({ request }) => request.url.endsWith("test-network-request.html");
+
+const TEST_DATA_REQUEST_PREDICATE =
+ ({ request }) => request.url.endsWith("test-data.json");
+
+const XHR_WARN_REQUEST_PREDICATE =
+ ({ request }) => request.url.endsWith("sjs_cors-test-server.sjs");
+
+let hud;
+
+add_task(function*() {
+ const PREF = "devtools.webconsole.persistlog";
+ const NET_PREF = "devtools.webconsole.filter.networkinfo";
+ const NETXHR_PREF = "devtools.webconsole.filter.netxhr";
+ const MIXED_AC_PREF = "security.mixed_content.block_active_content";
+ let original = Services.prefs.getBoolPref(NET_PREF);
+ let originalXhr = Services.prefs.getBoolPref(NETXHR_PREF);
+ let originalMixedActive = Services.prefs.getBoolPref(MIXED_AC_PREF);
+ Services.prefs.setBoolPref(NET_PREF, true);
+ Services.prefs.setBoolPref(NETXHR_PREF, true);
+ Services.prefs.setBoolPref(MIXED_AC_PREF, false);
+ Services.prefs.setBoolPref(PREF, true);
+ registerCleanupFunction(() => {
+ Services.prefs.setBoolPref(NET_PREF, original);
+ Services.prefs.setBoolPref(NETXHR_PREF, originalXhr);
+ Services.prefs.setBoolPref(MIXED_AC_PREF, originalMixedActive);
+ Services.prefs.clearUserPref(PREF);
+ hud = null;
+ });
+
+ yield loadTab(TEST_URI);
+ hud = yield openConsole();
+
+ yield testPageLoad();
+ yield testXhrGet();
+ yield testXhrWarn();
+ yield testXhrPost();
+ yield testFormSubmission();
+ yield testLiveFilteringOnSearchStrings();
+});
+
+function testPageLoad() {
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_NETWORK_REQUEST_URI);
+ let lastRequest = yield waitForFinishedRequest(PAGE_REQUEST_PREDICATE);
+
+ // Check if page load was logged correctly.
+ ok(lastRequest, "Page load was logged");
+ is(lastRequest.request.url, TEST_NETWORK_REQUEST_URI,
+ "Logged network entry is page load");
+ is(lastRequest.request.method, "GET", "Method is correct");
+}
+
+function testXhrGet() {
+ // Start the XMLHttpRequest() GET test.
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function*() {
+ content.wrappedJSObject.testXhrGet();
+ });
+
+ let lastRequest = yield waitForFinishedRequest(TEST_DATA_REQUEST_PREDICATE);
+
+ ok(lastRequest, "testXhrGet() was logged");
+ is(lastRequest.request.method, "GET", "Method is correct");
+ ok(lastRequest.isXHR, "It's an XHR request");
+}
+
+function testXhrWarn() {
+ // Start the XMLHttpRequest() warn test.
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function*() {
+ content.wrappedJSObject.testXhrWarn();
+ });
+
+ let lastRequest = yield waitForFinishedRequest(XHR_WARN_REQUEST_PREDICATE);
+ if (lastRequest.request.method == "HEAD") {
+ // in non-e10s, we get the HEAD request that priming sends, so make sure
+ // a priming request should be sent, and then get the actual request
+ is(Services.prefs.getBoolPref("security.mixed_content.send_hsts_priming"),
+ true, "Found HSTS Priming Request");
+ lastRequest = yield waitForFinishedRequest(XHR_WARN_REQUEST_PREDICATE);
+ }
+
+ ok(lastRequest, "testXhrWarn() was logged");
+ is(lastRequest.request.method, "GET", "Method is correct");
+ ok(lastRequest.isXHR, "It's an XHR request");
+ is(lastRequest.securityInfo, "insecure", "It's an insecure request");
+}
+
+function testXhrPost() {
+ // Start the XMLHttpRequest() POST test.
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function*() {
+ content.wrappedJSObject.testXhrPost();
+ });
+
+ let lastRequest = yield waitForFinishedRequest(TEST_DATA_REQUEST_PREDICATE);
+
+ ok(lastRequest, "testXhrPost() was logged");
+ is(lastRequest.request.method, "POST", "Method is correct");
+ ok(lastRequest.isXHR, "It's an XHR request");
+}
+
+function testFormSubmission() {
+ // Start the form submission test. As the form is submitted, the page is
+ // loaded again. Bind to the load event to catch when this is done.
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function*() {
+ let form = content.document.querySelector("form");
+ ok(form, "we have the HTML form");
+ form.submit();
+ });
+
+ // The form POSTs to the page URL but over https (page over http).
+ let lastRequest = yield waitForFinishedRequest(PAGE_REQUEST_PREDICATE);
+
+ ok(lastRequest, "testFormSubmission() was logged");
+ is(lastRequest.request.method, "POST", "Method is correct");
+
+ // There should be 3 network requests pointing to the HTML file.
+ waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ text: "test-network-request.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ count: 3,
+ },
+ {
+ text: "test-data.json",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_INFO,
+ isXhr: true,
+ count: 2,
+ },
+ {
+ text: "http://example.com/",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_WARNING,
+ isXhr: true,
+ count: 1,
+ },
+ ],
+ });
+}
+
+function testLiveFilteringOnSearchStrings() {
+ setStringFilter("http");
+ isnot(countMessageNodes(), 0, "the log nodes are not hidden when the " +
+ "search string is set to \"http\"");
+
+ setStringFilter("HTTP");
+ isnot(countMessageNodes(), 0, "the log nodes are not hidden when the " +
+ "search string is set to \"HTTP\"");
+
+ setStringFilter("hxxp");
+ is(countMessageNodes(), 0, "the log nodes are hidden when the search " +
+ "string is set to \"hxxp\"");
+
+ setStringFilter("ht tp");
+ isnot(countMessageNodes(), 0, "the log nodes are not hidden when the " +
+ "search string is set to \"ht tp\"");
+
+ setStringFilter("");
+ isnot(countMessageNodes(), 0, "the log nodes are not hidden when the " +
+ "search string is removed");
+
+ setStringFilter("json");
+ is(countMessageNodes(), 2, "the log nodes show only the nodes with \"json\"");
+
+ setStringFilter("'foo'");
+ is(countMessageNodes(), 0, "the log nodes are hidden when searching for " +
+ "the string 'foo'");
+
+ setStringFilter("foo\"bar'baz\"boo'");
+ is(countMessageNodes(), 0, "the log nodes are hidden when searching for " +
+ "the string \"foo\"bar'baz\"boo'\"");
+}
+
+function countMessageNodes() {
+ let messageNodes = hud.outputNode.querySelectorAll(".message");
+ let displayedMessageNodes = 0;
+ let view = hud.iframeWindow;
+ for (let i = 0; i < messageNodes.length; i++) {
+ let computedStyle = view.getComputedStyle(messageNodes[i], null);
+ if (computedStyle.display !== "none") {
+ displayedMessageNodes++;
+ }
+ }
+
+ return displayedMessageNodes;
+}
+
+function setStringFilter(value) {
+ hud.ui.filterBox.value = value;
+ hud.ui.adjustVisibilityOnSearchStringChange();
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_642108_pruneTest.js b/devtools/client/webconsole/test/browser_webconsole_bug_642108_pruneTest.js
new file mode 100644
index 000000000..caaa73628
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_642108_pruneTest.js
@@ -0,0 +1,81 @@
+/* -*- 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 the Web Console limits the number of lines displayed according to
+// the user's preferences.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>test for bug 642108.";
+const LOG_LIMIT = 20;
+
+function test() {
+ let hud;
+
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ let {tab} = yield loadTab(TEST_URI);
+
+ Services.prefs.setIntPref("devtools.hud.loglimit.cssparser", LOG_LIMIT);
+ Services.prefs.setBoolPref("devtools.webconsole.filter.cssparser", true);
+
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("devtools.hud.loglimit.cssparser");
+ Services.prefs.clearUserPref("devtools.webconsole.filter.cssparser");
+ });
+
+ hud = yield openConsole(tab);
+
+ for (let i = 0; i < 5; i++) {
+ logCSSMessage("css log x");
+ }
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "css log x",
+ category: CATEGORY_CSS,
+ severity: SEVERITY_WARNING,
+ repeats: 5,
+ }],
+ });
+
+ for (let i = 0; i < LOG_LIMIT + 5; i++) {
+ logCSSMessage("css log " + i);
+ }
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "css log 5",
+ category: CATEGORY_CSS,
+ severity: SEVERITY_WARNING,
+ },
+ {
+ // LOG_LIMIT + 5
+ text: "css log 24",
+ category: CATEGORY_CSS,
+ severity: SEVERITY_WARNING,
+ }],
+ });
+
+ is(hud.ui.outputNode.querySelectorAll(".message").length, LOG_LIMIT,
+ "number of messages");
+
+ is(Object.keys(hud.ui._repeatNodes).length, LOG_LIMIT,
+ "repeated nodes pruned from repeatNodes");
+
+ let msg = [...result.matched][0];
+ let repeats = msg.querySelector(".message-repeats");
+ is(repeats.getAttribute("value"), 1,
+ "repeated nodes pruned from repeatNodes (confirmed)");
+ }
+
+ function logCSSMessage(msg) {
+ let node = hud.ui.createMessageNode(CATEGORY_CSS, SEVERITY_WARNING, msg);
+ hud.ui.outputMessage(CATEGORY_CSS, node);
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_644419_log_limits.js b/devtools/client/webconsole/test/browser_webconsole_bug_644419_log_limits.js
new file mode 100644
index 000000000..93063e436
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_644419_log_limits.js
@@ -0,0 +1,235 @@
+/* -*- 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 the Web Console limits the number of lines displayed according to
+// the limit set for each category.
+
+"use strict";
+
+const INIT_URI = "data:text/html;charset=utf-8,Web Console test for " +
+ "bug 644419: Console should " +
+ "have user-settable log limits for each message category";
+
+const TEST_URI = "http://example.com/browser/devtools/client/" +
+ "webconsole/test/test-bug-644419-log-limits.html";
+
+var hud, outputNode;
+
+add_task(function* () {
+ let { browser } = yield loadTab(INIT_URI);
+
+ hud = yield openConsole();
+
+ hud.jsterm.clearOutput();
+ outputNode = hud.outputNode;
+
+ let loaded = loadBrowser(browser);
+
+ // On e10s, the exception is triggered in child process
+ // and is ignored by test harness
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI);
+ yield loaded;
+
+ yield testWebDevLimits();
+ yield testWebDevLimits2();
+ yield testJsLimits();
+ yield testJsLimits2();
+
+ yield testNetLimits();
+ yield loadImage();
+ yield testCssLimits();
+ yield testCssLimits2();
+
+ hud = outputNode = null;
+});
+
+function testWebDevLimits() {
+ Services.prefs.setIntPref("devtools.hud.loglimit.console", 10);
+
+ // Find the sentinel entry.
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "bar is not defined",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ }],
+ });
+}
+
+function testWebDevLimits2() {
+ // Fill the log with Web Developer errors.
+ for (let i = 0; i < 11; i++) {
+ content.console.log("test message " + i);
+ }
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "test message 10",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(() => {
+ testLogEntry(outputNode, "test message 0", "first message is pruned",
+ false, true);
+ findLogEntry("test message 1");
+ // Check if the sentinel entry is still there.
+ findLogEntry("bar is not defined");
+
+ Services.prefs.clearUserPref("devtools.hud.loglimit.console");
+ });
+}
+
+function testJsLimits() {
+ Services.prefs.setIntPref("devtools.hud.loglimit.exception", 10);
+
+ hud.jsterm.clearOutput();
+ content.console.log("testing JS limits");
+
+ // Find the sentinel entry.
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "testing JS limits",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+}
+
+function testJsLimits2() {
+ // Fill the log with JS errors.
+ let head = content.document.getElementsByTagName("head")[0];
+ for (let i = 0; i < 11; i++) {
+ let script = content.document.createElement("script");
+ script.text = "fubar" + i + ".bogus(6);";
+
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+ head.insertBefore(script, head.firstChild);
+ }
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "fubar10 is not defined",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ }],
+ }).then(() => {
+ testLogEntry(outputNode, "fubar0 is not defined", "first message is pruned",
+ false, true);
+ findLogEntry("fubar1 is not defined");
+ // Check if the sentinel entry is still there.
+ findLogEntry("testing JS limits");
+
+ Services.prefs.clearUserPref("devtools.hud.loglimit.exception");
+ });
+}
+
+var gCounter, gImage;
+
+function testNetLimits() {
+ Services.prefs.setIntPref("devtools.hud.loglimit.network", 10);
+
+ hud.jsterm.clearOutput();
+ content.console.log("testing Net limits");
+
+ // Find the sentinel entry.
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "testing Net limits",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(() => {
+ // Fill the log with network messages.
+ gCounter = 0;
+ });
+}
+
+function loadImage() {
+ if (gCounter < 11) {
+ let body = content.document.getElementsByTagName("body")[0];
+ gImage && gImage.removeEventListener("load", loadImage, true);
+ gImage = content.document.createElement("img");
+ gImage.src = "test-image.png?_fubar=" + gCounter;
+ body.insertBefore(gImage, body.firstChild);
+ gImage.addEventListener("load", loadImage, true);
+ gCounter++;
+ return true;
+ }
+
+ is(gCounter, 11, "loaded 11 files");
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "test-image.png",
+ url: "test-image.png?_fubar=10",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(() => {
+ let msgs = outputNode.querySelectorAll(".message[category=network]");
+ is(msgs.length, 10, "number of network messages");
+ isnot(msgs[0].url.indexOf("fubar=1"), -1, "first network message");
+ isnot(msgs[1].url.indexOf("fubar=2"), -1, "second network message");
+ findLogEntry("testing Net limits");
+
+ Services.prefs.clearUserPref("devtools.hud.loglimit.network");
+ });
+}
+
+function testCssLimits() {
+ Services.prefs.setIntPref("devtools.hud.loglimit.cssparser", 10);
+
+ hud.jsterm.clearOutput();
+ content.console.log("testing CSS limits");
+
+ // Find the sentinel entry.
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "testing CSS limits",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+}
+
+function testCssLimits2() {
+ // Fill the log with CSS errors.
+ let body = content.document.getElementsByTagName("body")[0];
+ for (let i = 0; i < 11; i++) {
+ let div = content.document.createElement("div");
+ div.setAttribute("style", "-moz-foobar" + i + ": 42;");
+ body.insertBefore(div, body.firstChild);
+ }
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "-moz-foobar10",
+ category: CATEGORY_CSS,
+ severity: SEVERITY_WARNING,
+ }],
+ }).then(() => {
+ testLogEntry(outputNode, "Unknown property \u2018-moz-foobar0\u2019",
+ "first message is pruned", false, true);
+ findLogEntry("Unknown property \u2018-moz-foobar1\u2019");
+ // Check if the sentinel entry is still there.
+ findLogEntry("testing CSS limits");
+
+ Services.prefs.clearUserPref("devtools.hud.loglimit.cssparser");
+ });
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_646025_console_file_location.js b/devtools/client/webconsole/test/browser_webconsole_bug_646025_console_file_location.js
new file mode 100644
index 000000000..81573e56f
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_646025_console_file_location.js
@@ -0,0 +1,57 @@
+/* -*- 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 console logging methods display the method location along with
+// the output in the console.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console file location " +
+ "display test";
+const TEST_URI2 = "http://example.com/browser/devtools/client/" +
+ "webconsole/test/" +
+ "test-bug-646025-console-file-location.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI2);
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "message for level log",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ source: { url: "test-file-location.js", line: 8 },
+ },
+ {
+ text: "message for level info",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_INFO,
+ source: { url: "test-file-location.js", line: 9 },
+ },
+ {
+ text: "message for level warn",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_WARNING,
+ source: { url: "test-file-location.js", line: 10 },
+ },
+ {
+ text: "message for level error",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_ERROR,
+ source: { url: "test-file-location.js", line: 11 },
+ },
+ {
+ text: "message for level debug",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ source: { url: "test-file-location.js", line: 12 },
+ }],
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js b/devtools/client/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js
new file mode 100644
index 000000000..233643d51
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js
@@ -0,0 +1,102 @@
+/* -*- 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 document.body autocompletes in the web console.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console autocompletion " +
+ "bug in document.body";
+
+var gHUD;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ gHUD = yield openConsole();
+
+ yield consoleOpened();
+ yield autocompletePopupHidden();
+ let view = yield testPropertyPanel();
+ yield onVariablesViewReady(view);
+
+ gHUD = null;
+});
+
+function consoleOpened() {
+ let deferred = promise.defer();
+
+ let jsterm = gHUD.jsterm;
+ let popup = jsterm.autocompletePopup;
+
+ ok(!popup.isOpen, "popup is not open");
+
+ popup.once("popup-opened", () => {
+ ok(popup.isOpen, "popup is open");
+
+ is(popup.itemCount, jsterm._autocompleteCache.length,
+ "popup.itemCount is correct");
+ isnot(jsterm._autocompleteCache.indexOf("addEventListener"), -1,
+ "addEventListener is in the list of suggestions");
+ isnot(jsterm._autocompleteCache.indexOf("bgColor"), -1,
+ "bgColor is in the list of suggestions");
+ isnot(jsterm._autocompleteCache.indexOf("ATTRIBUTE_NODE"), -1,
+ "ATTRIBUTE_NODE is in the list of suggestions");
+
+ popup.once("popup-closed", () => {
+ deferred.resolve();
+ });
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ });
+
+ jsterm.setInputValue("document.body");
+ EventUtils.synthesizeKey(".", {});
+
+ return deferred.promise;
+}
+
+function autocompletePopupHidden() {
+ let deferred = promise.defer();
+
+ let jsterm = gHUD.jsterm;
+ let popup = jsterm.autocompletePopup;
+ let completeNode = jsterm.completeNode;
+
+ ok(!popup.isOpen, "popup is not open");
+
+ jsterm.once("autocomplete-updated", function () {
+ is(completeNode.value, testStr + "dy", "autocomplete shows document.body");
+ deferred.resolve();
+ });
+
+ let inputStr = "document.b";
+ jsterm.setInputValue(inputStr);
+ EventUtils.synthesizeKey("o", {});
+ let testStr = inputStr.replace(/./g, " ") + " ";
+
+ return deferred.promise;
+}
+
+function testPropertyPanel() {
+ let deferred = promise.defer();
+
+ let jsterm = gHUD.jsterm;
+ jsterm.clearOutput();
+ jsterm.execute("document", (msg) => {
+ jsterm.once("variablesview-fetched", (evt, view) => {
+ deferred.resolve(view);
+ });
+ let anchor = msg.querySelector(".message-body a");
+ EventUtils.synthesizeMouse(anchor, 2, 2, {}, gHUD.iframeWindow);
+ });
+
+ return deferred.promise;
+}
+
+function onVariablesViewReady(view) {
+ return findVariableViewProperties(view, [
+ { name: "body", value: "<body>" },
+ ], { webconsole: gHUD });
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js b/devtools/client/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js
new file mode 100644
index 000000000..217d481e2
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js
@@ -0,0 +1,109 @@
+/* -*- 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 the $0 console helper works as intended.
+
+"use strict";
+
+var inspector, h1, outputNode;
+
+function createDocument() {
+ let doc = content.document;
+ let div = doc.createElement("div");
+ h1 = doc.createElement("h1");
+ let p1 = doc.createElement("p");
+ let p2 = doc.createElement("p");
+ let div2 = doc.createElement("div");
+ let p3 = doc.createElement("p");
+ doc.title = "Inspector Tree Selection Test";
+ h1.textContent = "Inspector Tree Selection Test";
+ p1.textContent = "This is some example text";
+ p2.textContent = "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.";
+ p3.textContent = "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.";
+ div.appendChild(h1);
+ div.appendChild(p1);
+ div.appendChild(p2);
+ div2.appendChild(p3);
+ doc.body.appendChild(div);
+ doc.body.appendChild(div2);
+ setupHighlighterTests();
+}
+
+function setupHighlighterTests() {
+ ok(h1, "we have the header node");
+ openInspector().then(runSelectionTests);
+}
+
+var runSelectionTests = Task.async(function* (aInspector) {
+ inspector = aInspector;
+
+ let onPickerStarted = inspector.toolbox.once("picker-started");
+ inspector.toolbox.highlighterUtils.startPicker();
+ yield onPickerStarted;
+
+ info("Picker mode started, now clicking on H1 to select that node");
+ h1.scrollIntoView();
+ let onPickerStopped = inspector.toolbox.once("picker-stopped");
+ let onInspectorUpdated = inspector.once("inspector-updated");
+ EventUtils.synthesizeMouseAtCenter(h1, {}, content);
+ yield onPickerStopped;
+ yield onInspectorUpdated;
+
+ info("Picker mode stopped, H1 selected, now switching to the console");
+ let hud = yield openConsole(gBrowser.selectedTab);
+
+ performWebConsoleTests(hud);
+});
+
+function performWebConsoleTests(hud) {
+ let jsterm = hud.jsterm;
+ outputNode = hud.outputNode;
+
+ jsterm.clearOutput();
+ jsterm.execute("$0", onNodeOutput);
+
+ function onNodeOutput(node) {
+ isnot(node.textContent.indexOf("<h1>"), -1, "correct output for $0");
+
+ jsterm.clearOutput();
+ jsterm.execute("$0.textContent = 'bug653531'", onNodeUpdate);
+ }
+
+ function onNodeUpdate(node) {
+ isnot(node.textContent.indexOf("bug653531"), -1,
+ "correct output for $0.textContent");
+ is(inspector.selection.node.textContent, "bug653531",
+ "node successfully updated");
+
+ inspector = h1 = outputNode = null;
+ gBrowser.removeCurrentTab();
+ finishTest();
+ }
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ waitForFocus(createDocument, content);
+ }, true);
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser,
+ "data:text/html;charset=utf-8,test for highlighter helper in web console");
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_658368_time_methods.js b/devtools/client/webconsole/test/browser_webconsole_bug_658368_time_methods.js
new file mode 100644
index 000000000..2c6c933fc
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_658368_time_methods.js
@@ -0,0 +1,67 @@
+/* -*- 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 the Console API implements the time() and timeEnd() methods.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-658368-time-methods.html";
+
+const TEST_URI2 = "data:text/html;charset=utf-8,<script>" +
+ "console.timeEnd('bTimer');</script>";
+
+const TEST_URI3 = "data:text/html;charset=utf-8,<script>" +
+ "console.time('bTimer');</script>";
+
+const TEST_URI4 = "data:text/html;charset=utf-8," +
+ "<script>console.timeEnd('bTimer');</script>";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud1 = yield openConsole();
+
+ yield waitForMessages({
+ webconsole: hud1,
+ messages: [{
+ name: "aTimer started",
+ consoleTime: "aTimer",
+ }, {
+ name: "aTimer end",
+ consoleTimeEnd: "aTimer",
+ }],
+ });
+
+ // The next test makes sure that timers with the same name but in separate
+ // tabs, do not contain the same value.
+ let { browser } = yield loadTab(TEST_URI2);
+ let hud2 = yield openConsole();
+
+ testLogEntry(hud2.outputNode, "bTimer: timer started",
+ "bTimer was not started", false, true);
+
+ // The next test makes sure that timers with the same name but in separate
+ // pages, do not contain the same value.
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI3);
+
+ yield waitForMessages({
+ webconsole: hud2,
+ messages: [{
+ name: "bTimer started",
+ consoleTime: "bTimer",
+ }],
+ });
+
+ hud2.jsterm.clearOutput();
+
+ // Now the following console.timeEnd() call shouldn't display anything,
+ // if the timers in different pages are not related.
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI4);
+ yield loadBrowser(browser);
+
+ testLogEntry(hud2.outputNode, "bTimer: timer started",
+ "bTimer was not started", false, true);
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_659907_console_dir.js b/devtools/client/webconsole/test/browser_webconsole_bug_659907_console_dir.js
new file mode 100644
index 000000000..03741a249
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_659907_console_dir.js
@@ -0,0 +1,36 @@
+/* -*- 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 console.dir works as intended.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for " +
+ "bug 659907: Expand console object with a dir method";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ hud.jsterm.clearOutput();
+
+ hud.jsterm.execute("console.dir(document)");
+
+ let varView = yield hud.jsterm.once("variablesview-fetched");
+
+ yield findVariableViewProperties(varView, [
+ {
+ name: "__proto__.__proto__.querySelectorAll",
+ value: "querySelectorAll()"
+ },
+ {
+ name: "location",
+ value: /Location \u2192 data:Web/
+ },
+ {
+ name: "__proto__.write",
+ value: "write()"
+ },
+ ], { webconsole: hud });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_660806_history_nav.js b/devtools/client/webconsole/test/browser_webconsole_bug_660806_history_nav.js
new file mode 100644
index 000000000..5906d62d6
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_660806_history_nav.js
@@ -0,0 +1,54 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>bug 660806 - history " +
+ "navigation must not show the autocomplete popup";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ yield consoleOpened(hud);
+});
+
+function consoleOpened(HUD) {
+ let deferred = promise.defer();
+
+ let jsterm = HUD.jsterm;
+ let popup = jsterm.autocompletePopup;
+ let onShown = function () {
+ ok(false, "popup shown");
+ };
+
+ jsterm.execute(`window.foobarBug660806 = {
+ 'location': 'value0',
+ 'locationbar': 'value1'
+ }`);
+
+ popup.on("popup-opened", onShown);
+
+ ok(!popup.isOpen, "popup is not open");
+
+ ok(!jsterm.lastInputValue, "no lastInputValue");
+ jsterm.setInputValue("window.foobarBug660806.location");
+ is(jsterm.lastInputValue, "window.foobarBug660806.location",
+ "lastInputValue is correct");
+
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ EventUtils.synthesizeKey("VK_UP", {});
+
+ is(jsterm.lastInputValue, "window.foobarBug660806.location",
+ "lastInputValue is correct, again");
+
+ executeSoon(function () {
+ ok(!popup.isOpen, "popup is not open");
+ popup.off("popup-opened", onShown);
+ executeSoon(deferred.resolve);
+ });
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_664131_console_group.js b/devtools/client/webconsole/test/browser_webconsole_bug_664131_console_group.js
new file mode 100644
index 000000000..fd510240e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_664131_console_group.js
@@ -0,0 +1,79 @@
+/* -*- 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 console.group/groupEnd works as intended.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for " +
+ "bug 664131: Expand console object with group methods";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+ let jsterm = hud.jsterm;
+
+ hud.jsterm.clearOutput();
+
+ yield jsterm.execute("console.group('bug664131a')");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "bug664131a",
+ consoleGroup: 1,
+ }],
+ });
+
+ yield jsterm.execute("console.log('bug664131a-inside')");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "bug664131a-inside",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ groupDepth: 1,
+ }],
+ });
+
+ yield jsterm.execute('console.groupEnd("bug664131a")');
+ yield jsterm.execute('console.log("bug664131-outside")');
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "bug664131-outside",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ groupDepth: 0,
+ }],
+ });
+
+ yield jsterm.execute('console.groupCollapsed("bug664131b")');
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "bug664131b",
+ consoleGroup: 1,
+ }],
+ });
+
+ // Test that clearing the console removes the indentation.
+ hud.jsterm.clearOutput();
+ yield jsterm.execute('console.log("bug664131-cleared")');
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "bug664131-cleared",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ groupDepth: 0,
+ }],
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_686937_autocomplete_JSTerm_helpers.js b/devtools/client/webconsole/test/browser_webconsole_bug_686937_autocomplete_JSTerm_helpers.js
new file mode 100644
index 000000000..2b1588ef9
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_686937_autocomplete_JSTerm_helpers.js
@@ -0,0 +1,75 @@
+/* -*- 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 the autocompletion results contain the names of JSTerm helpers.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<p>test JSTerm Helpers " +
+ "autocomplete";
+
+var jsterm;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ jsterm = hud.jsterm;
+ let input = jsterm.inputNode;
+ let popup = jsterm.autocompletePopup;
+
+ // Test if 'i' gives 'inspect'
+ input.value = "i";
+ input.setSelectionRange(1, 1);
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ let newItems = popup.getItems().map(function (e) {
+ return e.label;
+ });
+ ok(newItems.indexOf("inspect") > -1,
+ "autocomplete results contain helper 'inspect'");
+
+ // Test if 'window.' does not give 'inspect'.
+ input.value = "window.";
+ input.setSelectionRange(7, 7);
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ newItems = popup.getItems().map(function (e) {
+ return e.label;
+ });
+ is(newItems.indexOf("inspect"), -1,
+ "autocomplete results do not contain helper 'inspect'");
+
+ // Test if 'dump(i' gives 'inspect'
+ input.value = "dump(i";
+ input.setSelectionRange(6, 6);
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ newItems = popup.getItems().map(function (e) {
+ return e.label;
+ });
+ ok(newItems.indexOf("inspect") > -1,
+ "autocomplete results contain helper 'inspect'");
+
+ // Test if 'window.dump(i' gives 'inspect'
+ input.value = "window.dump(i";
+ input.setSelectionRange(13, 13);
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ newItems = popup.getItems().map(function (e) {
+ return e.label;
+ });
+ ok(newItems.indexOf("inspect") > -1,
+ "autocomplete results contain helper 'inspect'");
+
+ jsterm = null;
+});
+
+function complete(type) {
+ let updated = jsterm.once("autocomplete-updated");
+ jsterm.complete(type);
+ return updated;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_704295.js b/devtools/client/webconsole/test/browser_webconsole_bug_704295.js
new file mode 100644
index 000000000..df21232cf
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_704295.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 for bug 704295
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ testCompletion(hud);
+});
+
+function testCompletion(hud) {
+ let jsterm = hud.jsterm;
+ let input = jsterm.inputNode;
+
+ // Test typing 'var d = 5;' and press RETURN
+ jsterm.setInputValue("var d = ");
+ EventUtils.synthesizeKey("5", {});
+ EventUtils.synthesizeKey(";", {});
+ is(input.value, "var d = 5;", "var d = 5;");
+ is(jsterm.completeNode.value, "", "no completion");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ is(jsterm.completeNode.value, "", "clear completion on execute()");
+
+ // Test typing 'var a = d' and press RETURN
+ jsterm.setInputValue("var a = ");
+ EventUtils.synthesizeKey("d", {});
+ is(input.value, "var a = d", "var a = d");
+ is(jsterm.completeNode.value, "", "no completion");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ is(jsterm.completeNode.value, "", "clear completion on execute()");
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_734061_No_input_change_and_Tab_key_pressed.js b/devtools/client/webconsole/test/browser_webconsole_bug_734061_No_input_change_and_Tab_key_pressed.js
new file mode 100644
index 000000000..af9e172c8
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_734061_No_input_change_and_Tab_key_pressed.js
@@ -0,0 +1,35 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/browser/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ let jsterm = hud.jsterm;
+ let input = jsterm.inputNode;
+
+ is(input.getAttribute("focused"), "true", "input has focus");
+ EventUtils.synthesizeKey("VK_TAB", {});
+ is(input.getAttribute("focused"), "", "focus moved away");
+
+ // Test user changed something
+ input.focus();
+ EventUtils.synthesizeKey("A", {});
+ EventUtils.synthesizeKey("VK_TAB", {});
+ is(input.getAttribute("focused"), "true", "input is still focused");
+
+ // Test non empty input but not changed since last focus
+ input.blur();
+ input.focus();
+ EventUtils.synthesizeKey("VK_RIGHT", {});
+ EventUtils.synthesizeKey("VK_TAB", {});
+ is(input.getAttribute("focused"), "", "input moved away");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_737873_mixedcontent.js b/devtools/client/webconsole/test/browser_webconsole_bug_737873_mixedcontent.js
new file mode 100644
index 000000000..4665af42a
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_737873_mixedcontent.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 the Web Console Mixed Content messages are displayed
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,Web Console mixed content test";
+const TEST_HTTPS_URI = "https://example.com/browser/devtools/client/" +
+ "webconsole/test/test-bug-737873-mixedcontent.html";
+const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Web/Security/" +
+ "Mixed_content";
+
+registerCleanupFunction(function*() {
+ Services.prefs.clearUserPref("security.mixed_content.block_display_content");
+ Services.prefs.clearUserPref("security.mixed_content.block_active_content");
+});
+
+add_task(function* () {
+ Services.prefs.setBoolPref("security.mixed_content.block_display_content",
+ false);
+ Services.prefs.setBoolPref("security.mixed_content.block_active_content",
+ false);
+
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ yield testMixedContent(hud);
+});
+
+var testMixedContent = Task.async(function* (hud) {
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_HTTPS_URI);
+
+ let results = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "example.com",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_WARNING,
+ }],
+ });
+
+ let msg = [...results[0].matched][0];
+ ok(msg, "page load logged");
+ ok(msg.classList.contains("mixed-content"), ".mixed-content element");
+
+ let link = msg.querySelector(".learn-more-link");
+ ok(link, "mixed content link element");
+ is(link.textContent, "[Mixed Content]", "link text is accurate");
+
+ yield simulateMessageLinkClick(link, LEARN_MORE_URI);
+
+ ok(!msg.classList.contains("filtered-by-type"), "message is not filtered");
+
+ hud.setFilterState("netwarn", false);
+
+ ok(msg.classList.contains("filtered-by-type"), "message is filtered");
+
+ hud.setFilterState("netwarn", true);
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_752559_ineffective_iframe_sandbox_warning.js b/devtools/client/webconsole/test/browser_webconsole_bug_752559_ineffective_iframe_sandbox_warning.js
new file mode 100644
index 000000000..85b99a79a
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_752559_ineffective_iframe_sandbox_warning.js
@@ -0,0 +1,83 @@
+/* -*- 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 warnings about ineffective iframe sandboxing are logged to the
+// web console when necessary (and not otherwise).
+
+"use strict";
+
+requestLongerTimeout(2);
+
+const TEST_URI_WARNING = "http://example.com/browser/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning0.html";
+const TEST_URI_NOWARNING = [
+ "http://example.com/browser/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning1.html",
+ "http://example.com/browser/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning2.html",
+ "http://example.com/browser/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning3.html",
+ "http://example.com/browser/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning4.html",
+ "http://example.com/browser/devtools/client/webconsole/test/test-bug-752559-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(function* () {
+ yield testYesWarning();
+
+ for (let id = 0; id < TEST_URI_NOWARNING.length; id++) {
+ yield testNoWarning(id);
+ }
+});
+
+function* testYesWarning() {
+ yield loadTab(TEST_URI_WARNING);
+ let hud = yield openConsole();
+
+ ContentTask.spawn(gBrowser.selectedBrowser, SENTINEL_MSG, function* (msg) {
+ content.console.log(msg);
+ });
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "Ineffective iframe sandboxing warning displayed successfully",
+ text: INEFFECTIVE_IFRAME_SANDBOXING_MSG,
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_WARNING
+ },
+ {
+ text: SENTINEL_MSG,
+ severity: SEVERITY_LOG
+ }
+ ]
+ });
+
+ let msgs = hud.outputNode.querySelectorAll(".message[category=security]");
+ is(msgs.length, 1, "one security message");
+}
+
+function* testNoWarning(id) {
+ yield loadTab(TEST_URI_NOWARNING[id]);
+ let hud = yield openConsole();
+
+ ContentTask.spawn(gBrowser.selectedBrowser, SENTINEL_MSG, function* (msg) {
+ content.console.log(msg);
+ });
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ text: SENTINEL_MSG,
+ severity: SEVERITY_LOG
+ }
+ ]
+ });
+
+ let msgs = hud.outputNode.querySelectorAll(".message[category=security]");
+ is(msgs.length, 0, "no security messages (case " + id + ")");
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_about_blank_web_console_warning.js b/devtools/client/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_about_blank_web_console_warning.js
new file mode 100644
index 000000000..49df4d1fc
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_about_blank_web_console_warning.js
@@ -0,0 +1,32 @@
+/* -*- 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 errors about insecure passwords are logged to the web console.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-762593-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(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "Insecure password error displayed successfully",
+ text: INSECURE_PASSWORD_MSG,
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_WARNING
+ },
+ ],
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_web_console_warning.js b/devtools/client/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_web_console_warning.js
new file mode 100644
index 000000000..00a620fc8
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_web_console_warning.js
@@ -0,0 +1,62 @@
+/* -*- 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 errors about insecure passwords are logged to the web console.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-762593-insecure-passwords-web-" +
+ "console-warning.html";
+const INSECURE_PASSWORD_MSG = "Password fields present on an insecure " +
+ "(http://) page. This is a security risk that allows user " +
+ "login credentials to be stolen.";
+const INSECURE_FORM_ACTION_MSG = "Password fields present in a form with an " +
+ "insecure (http://) form action. This is a security risk " +
+ "that allows user login credentials to be stolen.";
+const INSECURE_IFRAME_MSG = "Password fields present on an insecure " +
+ "(http://) iframe. This is a security risk that allows " +
+ "user login credentials to be stolen.";
+const INSECURE_PASSWORDS_URI = "https://developer.mozilla.org/docs/Web/" +
+ "Security/Insecure_passwords" + DOCS_GA_PARAMS;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ let result = yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "Insecure password error displayed successfully",
+ text: INSECURE_PASSWORD_MSG,
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_WARNING
+ },
+ {
+ name: "Insecure iframe error displayed successfully",
+ text: INSECURE_IFRAME_MSG,
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_WARNING
+ },
+ {
+ name: "Insecure form action error displayed successfully",
+ text: INSECURE_FORM_ACTION_MSG,
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_WARNING
+ },
+ ],
+ });
+
+ yield testClickOpenNewTab(hud, result);
+});
+
+function testClickOpenNewTab(hud, [result]) {
+ let msg = [...result.matched][0];
+ let warningNode = msg.querySelector(".learn-more-link");
+ ok(warningNode, "learn more link");
+ return simulateMessageLinkClick(warningNode, INSECURE_PASSWORDS_URI);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_764572_output_open_url.js b/devtools/client/webconsole/test/browser_webconsole_bug_764572_output_open_url.js
new file mode 100644
index 000000000..731e79d8b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_764572_output_open_url.js
@@ -0,0 +1,142 @@
+/* -*- 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/ */
+
+// This is a test for the Open URL context menu item
+// that is shown for network requests
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+const COMMAND_NAME = "consoleCmd_openURL";
+const CONTEXT_MENU_ID = "#menu_openURL";
+
+var HUD = null, outputNode = null, contextMenu = null;
+
+add_task(function* () {
+ Services.prefs.setBoolPref("devtools.webconsole.filter.networkinfo", true);
+
+ yield loadTab(TEST_URI);
+ HUD = yield openConsole();
+
+ let results = yield consoleOpened();
+ yield onConsoleMessage(results);
+
+ let results2 = yield testOnNetActivity();
+ let msg = yield onNetworkMessage(results2);
+
+ yield testOnNetActivityContextMenu(msg);
+
+ Services.prefs.clearUserPref("devtools.webconsole.filter.networkinfo");
+
+ HUD = null;
+ outputNode = null;
+ contextMenu = null;
+});
+
+function consoleOpened() {
+ outputNode = HUD.outputNode;
+ contextMenu = HUD.iframeWindow.document.getElementById("output-contextmenu");
+
+ HUD.jsterm.clearOutput();
+
+ content.console.log("bug 764572");
+
+ return waitForMessages({
+ webconsole: HUD,
+ messages: [{
+ text: "bug 764572",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+}
+
+function onConsoleMessage(results) {
+ outputNode.focus();
+ outputNode.selectedItem = [...results[0].matched][0];
+
+ // Check if the command is disabled non-network messages.
+ goUpdateCommand(COMMAND_NAME);
+ let controller = top.document.commandDispatcher
+ .getControllerForCommand(COMMAND_NAME);
+
+ let isDisabled = !controller || !controller.isCommandEnabled(COMMAND_NAME);
+ ok(isDisabled, COMMAND_NAME + " should be disabled.");
+
+ return waitForContextMenu(contextMenu, outputNode.selectedItem, () => {
+ let isHidden = contextMenu.querySelector(CONTEXT_MENU_ID).hidden;
+ ok(isHidden, CONTEXT_MENU_ID + " should be hidden.");
+ });
+}
+
+function testOnNetActivity() {
+ HUD.jsterm.clearOutput();
+
+ // Reload the url to show net activity in console.
+ content.location.reload();
+
+ return waitForMessages({
+ webconsole: HUD,
+ messages: [{
+ text: "test-console.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ }],
+ });
+}
+
+function onNetworkMessage(results) {
+ let deferred = promise.defer();
+
+ outputNode.focus();
+ let msg = [...results[0].matched][0];
+ ok(msg, "network message");
+ HUD.ui.output.selectMessage(msg);
+
+ let currentTab = gBrowser.selectedTab;
+ let newTab = null;
+
+ gBrowser.tabContainer.addEventListener("TabOpen", function onOpen(evt) {
+ gBrowser.tabContainer.removeEventListener("TabOpen", onOpen, true);
+ newTab = evt.target;
+ newTab.linkedBrowser.addEventListener("load", onTabLoaded, true);
+ }, true);
+
+ function onTabLoaded() {
+ newTab.linkedBrowser.removeEventListener("load", onTabLoaded, true);
+ gBrowser.removeTab(newTab);
+ gBrowser.selectedTab = currentTab;
+ executeSoon(deferred.resolve.bind(null, msg));
+ }
+
+ // Check if the command is enabled for a network message.
+ goUpdateCommand(COMMAND_NAME);
+ let controller = top.document.commandDispatcher
+ .getControllerForCommand(COMMAND_NAME);
+ ok(controller.isCommandEnabled(COMMAND_NAME),
+ COMMAND_NAME + " should be enabled.");
+
+ // Try to open the URL.
+ goDoCommand(COMMAND_NAME);
+
+ return deferred.promise;
+}
+
+function testOnNetActivityContextMenu(msg) {
+ let deferred = promise.defer();
+
+ outputNode.focus();
+ HUD.ui.output.selectMessage(msg);
+
+ info("net activity context menu");
+
+ waitForContextMenu(contextMenu, msg, () => {
+ let isShown = !contextMenu.querySelector(CONTEXT_MENU_ID).hidden;
+ ok(isShown, CONTEXT_MENU_ID + " should be shown.");
+ }).then(deferred.resolve);
+
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_766001_JS_Console_in_Debugger.js b/devtools/client/webconsole/test/browser_webconsole_bug_766001_JS_Console_in_Debugger.js
new file mode 100644
index 000000000..3686fba89
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_766001_JS_Console_in_Debugger.js
@@ -0,0 +1,88 @@
+/* -*- 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/ */
+
+// Test that message source links for js errors and console API calls open in
+// the jsdebugger when clicked.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/test" +
+ "/test-bug-766001-js-console-links.html";
+
+// Force the new debugger UI, in case this gets uplifted with the old
+// debugger still turned on
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", true);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+function test() {
+ let hud;
+
+ requestLongerTimeout(2);
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ // On e10s, the exception is triggered in child process
+ // and is ignored by test harness
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+
+ let {tab} = yield loadTab(TEST_URI);
+ hud = yield openConsole(tab);
+
+ let [exceptionRule, consoleRule] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "document.bar",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ },
+ {
+ text: "Blah Blah",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ let exceptionMsg = [...exceptionRule.matched][0];
+ let consoleMsg = [...consoleRule.matched][0];
+ let nodes = [exceptionMsg.querySelector(".message-location > .frame-link"),
+ consoleMsg.querySelector(".message-location > .frame-link")];
+ ok(nodes[0], ".location node for the exception message");
+ ok(nodes[1], ".location node for the console message");
+
+ for (let i = 0; i < nodes.length; i++) {
+ yield checkClickOnNode(i, nodes[i]);
+ yield gDevTools.showToolbox(hud.target, "webconsole");
+ }
+
+ // check again the first node.
+ yield checkClickOnNode(0, nodes[0]);
+ }
+
+ function* checkClickOnNode(index, node) {
+ info("checking click on node index " + index);
+
+ let url = node.getAttribute("data-url");
+ ok(url, "source url found for index " + index);
+
+ let line = node.getAttribute("data-line");
+ ok(line, "found source line for index " + index);
+
+ executeSoon(() => {
+ EventUtils.sendMouseEvent({ type: "click" }, node.querySelector(".frame-link-filename"));
+ });
+
+ yield hud.ui.once("source-in-debugger-opened");
+
+ let toolbox = yield gDevTools.getToolbox(hud.target);
+ let dbg = toolbox.getPanel("jsdebugger");
+ is(dbg._selectors().getSelectedSource(dbg._getState()).get("url"),
+ url,
+ "expected source url");
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_770099_violation.js b/devtools/client/webconsole/test/browser_webconsole_bug_770099_violation.js
new file mode 100644
index 000000000..3a7134202
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_770099_violation.js
@@ -0,0 +1,35 @@
+/* -*- 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 the Web Console CSP messages are displayed
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,Web Console CSP violation test";
+const TEST_VIOLATION = "https://example.com/browser/devtools/client/" +
+ "webconsole/test/test_bug_770099_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 (\u201cdefault-src " +
+ "https://example.com\u201d).";
+
+add_task(function* () {
+ let { browser } = yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ hud.jsterm.clearOutput();
+
+ let loaded = loadBrowser(browser);
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_VIOLATION);
+ yield loaded;
+
+ yield waitForSuccess({
+ name: "CSP policy URI warning displayed successfully",
+ validator: function () {
+ return hud.outputNode.textContent.indexOf(CSP_VIOLATION_MSG) > -1;
+ }
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js b/devtools/client/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js
new file mode 100644
index 000000000..f2efd7922
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js
@@ -0,0 +1,140 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/test" +
+ "/test-bug-782653-css-errors.html";
+
+var nodes, hud, StyleEditorUI;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ hud = yield openConsole();
+
+ let styleEditor = yield testViewSource();
+ yield onStyleEditorReady(styleEditor);
+
+ nodes = hud = StyleEditorUI = null;
+});
+
+function testViewSource() {
+ let deferred = promise.defer();
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "\u2018font-weight\u2019",
+ category: CATEGORY_CSS,
+ severity: SEVERITY_WARNING,
+ },
+ {
+ text: "\u2018color\u2019",
+ category: CATEGORY_CSS,
+ severity: SEVERITY_WARNING,
+ }],
+ }).then(([error1Rule, error2Rule]) => {
+ let error1Msg = [...error1Rule.matched][0];
+ let error2Msg = [...error2Rule.matched][0];
+ nodes = [error1Msg.querySelector(".message-location .frame-link"),
+ error2Msg.querySelector(".message-location .frame-link")];
+ ok(nodes[0], ".frame-link node for the first error");
+ ok(nodes[1], ".frame-link node for the second error");
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = gDevTools.getToolbox(target);
+ toolbox.once("styleeditor-selected", (event, panel) => {
+ StyleEditorUI = panel.UI;
+ deferred.resolve(panel);
+ });
+
+ EventUtils.sendMouseEvent({ type: "click" }, nodes[0].querySelector(".frame-link-filename"));
+ });
+
+ return deferred.promise;
+}
+
+function onStyleEditorReady(panel) {
+ let deferred = promise.defer();
+
+ let win = panel.panelWindow;
+ ok(win, "Style Editor Window is defined");
+ ok(StyleEditorUI, "Style Editor UI is defined");
+
+ function fireEvent(toolbox, href, line) {
+ toolbox.once("styleeditor-selected", function (evt) {
+ info(evt + " event fired");
+
+ checkStyleEditorForSheetAndLine(href, line - 1).then(deferred.resolve);
+ });
+
+ EventUtils.sendMouseEvent({ type: "click" }, nodes[1].querySelector(".frame-link-filename"));
+ }
+
+ waitForFocus(function () {
+ info("style editor window focused");
+
+ let href = nodes[0].getAttribute("data-url");
+ let line = nodes[0].getAttribute("data-line");
+ ok(line, "found source line");
+
+ checkStyleEditorForSheetAndLine(href, line - 1).then(function () {
+ info("first check done");
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = gDevTools.getToolbox(target);
+
+ href = nodes[1].getAttribute("data-url");
+ line = nodes[1].getAttribute("data-line");
+ ok(line, "found source line");
+
+ toolbox.selectTool("webconsole").then(function () {
+ info("webconsole selected");
+ fireEvent(toolbox, href, line);
+ });
+ });
+ }, win);
+
+ return deferred.promise;
+}
+
+function checkStyleEditorForSheetAndLine(href, line) {
+ let foundEditor = null;
+ for (let editor of StyleEditorUI.editors) {
+ if (editor.styleSheet.href == href) {
+ foundEditor = editor;
+ break;
+ }
+ }
+
+ ok(foundEditor, "found style editor for " + href);
+ return performLineCheck(foundEditor, line);
+}
+
+function performLineCheck(editor, line) {
+ let deferred = promise.defer();
+
+ function checkForCorrectState() {
+ is(editor.sourceEditor.getCursor().line, line,
+ "correct line is selected");
+ is(StyleEditorUI.selectedStyleSheetIndex, editor.styleSheet.styleSheetIndex,
+ "correct stylesheet is selected in the editor");
+
+ executeSoon(deferred.resolve);
+ }
+
+ info("wait for source editor to load");
+
+ // Get out of the styleeditor-selected event loop.
+ executeSoon(() => {
+ editor.getSourceEditor().then(() => {
+ // Get out of the editor's source-editor-load event loop.
+ executeSoon(checkForCorrectState);
+ });
+ });
+
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_804845_ctrl_key_nav.js b/devtools/client/webconsole/test/browser_webconsole_bug_804845_ctrl_key_nav.js
new file mode 100644
index 000000000..b040e6314
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_804845_ctrl_key_nav.js
@@ -0,0 +1,227 @@
+/* -*- 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/ */
+
+// Test navigation of webconsole contents via ctrl-a, ctrl-e, ctrl-p, ctrl-n
+// see https://bugzilla.mozilla.org/show_bug.cgi?id=804845
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for " +
+ "bug 804845 and bug 619598";
+
+var jsterm, inputNode;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ doTests(hud);
+
+ jsterm = inputNode = null;
+});
+
+function doTests(HUD) {
+ jsterm = HUD.jsterm;
+ inputNode = jsterm.inputNode;
+ ok(!jsterm.getInputValue(), "jsterm.getInputValue() is empty");
+ is(jsterm.inputNode.selectionStart, 0);
+ is(jsterm.inputNode.selectionEnd, 0);
+
+ testSingleLineInputNavNoHistory();
+ testMultiLineInputNavNoHistory();
+ testNavWithHistory();
+}
+
+function testSingleLineInputNavNoHistory() {
+ // Single char input
+ EventUtils.synthesizeKey("1", {});
+ is(inputNode.selectionStart, 1, "caret location after single char input");
+
+ // nav to start/end with ctrl-a and ctrl-e;
+ EventUtils.synthesizeKey("a", { ctrlKey: true });
+ is(inputNode.selectionStart, 0,
+ "caret location after single char input and ctrl-a");
+
+ EventUtils.synthesizeKey("e", { ctrlKey: true });
+ is(inputNode.selectionStart, 1,
+ "caret location after single char input and ctrl-e");
+
+ // Second char input
+ EventUtils.synthesizeKey("2", {});
+ // nav to start/end with up/down keys; verify behaviour using ctrl-p/ctrl-n
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(inputNode.selectionStart, 0,
+ "caret location after two char input and VK_UP");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(inputNode.selectionStart, 2,
+ "caret location after two char input and VK_DOWN");
+
+ EventUtils.synthesizeKey("a", { ctrlKey: true });
+ is(inputNode.selectionStart, 0,
+ "move caret to beginning of 2 char input with ctrl-a");
+ EventUtils.synthesizeKey("a", { ctrlKey: true });
+ is(inputNode.selectionStart, 0,
+ "no change of caret location on repeat ctrl-a");
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ is(inputNode.selectionStart, 0,
+ "no change of caret location on ctrl-p from beginning of line");
+
+ EventUtils.synthesizeKey("e", { ctrlKey: true });
+ is(inputNode.selectionStart, 2,
+ "move caret to end of 2 char input with ctrl-e");
+ EventUtils.synthesizeKey("e", { ctrlKey: true });
+ is(inputNode.selectionStart, 2,
+ "no change of caret location on repeat ctrl-e");
+ EventUtils.synthesizeKey("n", { ctrlKey: true });
+ is(inputNode.selectionStart, 2,
+ "no change of caret location on ctrl-n from end of line");
+
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ is(inputNode.selectionStart, 0, "ctrl-p moves to start of line");
+
+ EventUtils.synthesizeKey("n", { ctrlKey: true });
+ is(inputNode.selectionStart, 2, "ctrl-n moves to end of line");
+}
+
+function testMultiLineInputNavNoHistory() {
+ let lineValues = ["one", "2", "something longer", "", "", "three!"];
+ jsterm.setInputValue("");
+ // simulate shift-return
+ for (let i = 0; i < lineValues.length; i++) {
+ jsterm.setInputValue(jsterm.getInputValue() + lineValues[i]);
+ EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true });
+ }
+ let inputValue = jsterm.getInputValue();
+ is(inputNode.selectionStart, inputNode.selectionEnd);
+ is(inputNode.selectionStart, inputValue.length,
+ "caret at end of multiline input");
+
+ // possibility newline is represented by one ('\r', '\n') or two
+ // ('\r\n') chars
+ let newlineString = inputValue.match(/(\r\n?|\n\r?)$/)[0];
+
+ // Ok, test navigating within the multi-line string!
+ EventUtils.synthesizeKey("VK_UP", {});
+ let expectedStringAfterCarat = lineValues[5] + newlineString;
+ is(jsterm.getInputValue().slice(inputNode.selectionStart), expectedStringAfterCarat,
+ "up arrow from end of multiline");
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(jsterm.getInputValue().slice(inputNode.selectionStart), "",
+ "down arrow from within multiline");
+
+ // navigate up through input lines
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ is(jsterm.getInputValue().slice(inputNode.selectionStart), expectedStringAfterCarat,
+ "ctrl-p from end of multiline");
+
+ for (let i = 4; i >= 0; i--) {
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ expectedStringAfterCarat = lineValues[i] + newlineString +
+ expectedStringAfterCarat;
+ is(jsterm.getInputValue().slice(inputNode.selectionStart),
+ expectedStringAfterCarat, "ctrl-p from within line " + i +
+ " of multiline input");
+ }
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ is(inputNode.selectionStart, 0, "reached start of input");
+ is(jsterm.getInputValue(), inputValue,
+ "no change to multiline input on ctrl-p from beginning of multiline");
+
+ // navigate to end of first line
+ EventUtils.synthesizeKey("e", { ctrlKey: true });
+ let caretPos = inputNode.selectionStart;
+ let expectedStringBeforeCarat = lineValues[0];
+ is(jsterm.getInputValue().slice(0, caretPos), expectedStringBeforeCarat,
+ "ctrl-e into multiline input");
+ EventUtils.synthesizeKey("e", { ctrlKey: true });
+ is(inputNode.selectionStart, caretPos,
+ "repeat ctrl-e doesn't change caret position in multiline input");
+
+ // navigate down one line; ctrl-a to the beginning; ctrl-e to end
+ for (let i = 1; i < lineValues.length; i++) {
+ EventUtils.synthesizeKey("n", { ctrlKey: true });
+ EventUtils.synthesizeKey("a", { ctrlKey: true });
+ caretPos = inputNode.selectionStart;
+ expectedStringBeforeCarat += newlineString;
+ is(jsterm.getInputValue().slice(0, caretPos), expectedStringBeforeCarat,
+ "ctrl-a to beginning of line " + (i + 1) + " in multiline input");
+
+ EventUtils.synthesizeKey("e", { ctrlKey: true });
+ caretPos = inputNode.selectionStart;
+ expectedStringBeforeCarat += lineValues[i];
+ is(jsterm.getInputValue().slice(0, caretPos), expectedStringBeforeCarat,
+ "ctrl-e to end of line " + (i + 1) + "in multiline input");
+ }
+}
+
+function testNavWithHistory() {
+ // NOTE: Tests does NOT currently define behaviour for ctrl-p/ctrl-n with
+ // caret placed _within_ single line input
+ let values = ['"single line input"',
+ '"a longer single-line input to check caret repositioning"',
+ ['"multi-line"', '"input"', '"here!"'].join("\n"),
+ ];
+ // submit to history
+ for (let i = 0; i < values.length; i++) {
+ jsterm.setInputValue(values[i]);
+ jsterm.execute();
+ }
+ is(inputNode.selectionStart, 0, "caret location at start of empty line");
+
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ is(inputNode.selectionStart, values[values.length - 1].length,
+ "caret location correct at end of last history input");
+
+ // Navigate backwards history with ctrl-p
+ for (let i = values.length - 1; i > 0; i--) {
+ let match = values[i].match(/(\n)/g);
+ if (match) {
+ // multi-line inputs won't update from history unless caret at beginning
+ EventUtils.synthesizeKey("a", { ctrlKey: true });
+ for (let j = 0; j < match.length; j++) {
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ }
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ } else {
+ // single-line inputs will update from history from end of line
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ }
+ is(jsterm.getInputValue(), values[i - 1],
+ "ctrl-p updates inputNode from backwards history values[" + i - 1 + "]");
+ }
+ let inputValue = jsterm.getInputValue();
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ is(inputNode.selectionStart, 0,
+ "ctrl-p at beginning of history moves caret location to beginning " +
+ "of line");
+ is(jsterm.getInputValue(), inputValue,
+ "no change to input value on ctrl-p from beginning of line");
+
+ // Navigate forwards history with ctrl-n
+ for (let i = 1; i < values.length; i++) {
+ EventUtils.synthesizeKey("n", { ctrlKey: true });
+ is(jsterm.getInputValue(), values[i],
+ "ctrl-n updates inputNode from forwards history values[" + i + "]");
+ is(inputNode.selectionStart, values[i].length,
+ "caret location correct at end of history input for values[" + i + "]");
+ }
+ EventUtils.synthesizeKey("n", { ctrlKey: true });
+ ok(!jsterm.getInputValue(), "ctrl-n at end of history updates to empty input");
+
+ // Simulate editing multi-line
+ inputValue = "one\nlinebreak";
+ jsterm.setInputValue(inputValue);
+
+ // Attempt nav within input
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ is(jsterm.getInputValue(), inputValue,
+ "ctrl-p from end of multi-line does not trigger history");
+
+ EventUtils.synthesizeKey("a", { ctrlKey: true });
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ is(jsterm.getInputValue(), values[values.length - 1],
+ "ctrl-p from start of multi-line triggers history");
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_817834_add_edited_input_to_history.js b/devtools/client/webconsole/test/browser_webconsole_bug_817834_add_edited_input_to_history.js
new file mode 100644
index 000000000..ccbcb3bf3
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_817834_add_edited_input_to_history.js
@@ -0,0 +1,57 @@
+/* -*- 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/ */
+
+// Test that user input that is not submitted in the command line input is not
+// lost after navigating in history.
+// See https://bugzilla.mozilla.org/show_bug.cgi?id=817834
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 817834";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ testEditedInputHistory(hud);
+});
+
+function testEditedInputHistory(HUD) {
+ let jsterm = HUD.jsterm;
+ let inputNode = jsterm.inputNode;
+ ok(!jsterm.getInputValue(), "jsterm.getInputValue() is empty");
+ is(inputNode.selectionStart, 0);
+ is(inputNode.selectionEnd, 0);
+
+ jsterm.setInputValue('"first item"');
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(jsterm.getInputValue(), '"first item"', "null test history up");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(jsterm.getInputValue(), '"first item"', "null test history down");
+
+ jsterm.execute();
+ is(jsterm.getInputValue(), "", "cleared input line after submit");
+
+ jsterm.setInputValue('"editing input 1"');
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(jsterm.getInputValue(), '"first item"', "test history up");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(jsterm.getInputValue(), '"editing input 1"',
+ "test history down restores in-progress input");
+
+ jsterm.setInputValue('"second item"');
+ jsterm.execute();
+ jsterm.setInputValue('"editing input 2"');
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(jsterm.getInputValue(), '"second item"', "test history up");
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(jsterm.getInputValue(), '"first item"', "test history up");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(jsterm.getInputValue(), '"second item"', "test history down");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(jsterm.getInputValue(), '"editing input 2"',
+ "test history down restores new in-progress input again");
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_837351_securityerrors.js b/devtools/client/webconsole/test/browser_webconsole_bug_837351_securityerrors.js
new file mode 100644
index 000000000..0524e1c4c
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_837351_securityerrors.js
@@ -0,0 +1,42 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "https://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-837351-security-errors.html";
+
+add_task(function* () {
+ yield pushPrefEnv();
+
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ let button = hud.ui.rootElement.querySelector(".webconsole-filter-button[category=\"security\"]");
+ ok(button, "Found security button in the web console");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "Logged blocking mixed active content",
+ text: "Blocked loading mixed active content \u201chttp://example.com/\u201d",
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_ERROR
+ },
+ ],
+ });
+});
+
+function pushPrefEnv() {
+ let deferred = promise.defer();
+ let options = {
+ set: [["security.mixed_content.block_active_content", true]]
+ };
+ SpecialPowers.pushPrefEnv(options, deferred.resolve);
+ return deferred.promise;
+}
+
diff --git a/devtools/client/webconsole/test/browser_webconsole_bug_922212_console_dirxml.js b/devtools/client/webconsole/test/browser_webconsole_bug_922212_console_dirxml.js
new file mode 100644
index 000000000..8062ffeec
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_922212_console_dirxml.js
@@ -0,0 +1,48 @@
+/* -*- 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 console.dirxml works as intended.
+
+"use strict";
+
+const TEST_URI = `data:text/html;charset=utf-8,Web Console test for bug 922212:
+ Add console.dirxml`;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ hud.jsterm.clearOutput();
+
+ // Should work like console.log(window)
+ hud.jsterm.execute("console.dirxml(window)");
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "console.dirxml(window) output:",
+ text: /Window \u2192/,
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ hud.jsterm.clearOutput();
+
+ hud.jsterm.execute("console.dirxml(document.body)");
+
+ // Should work like console.log(document.body);
+ [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "console.dirxml(document.body) output:",
+ text: "<body>",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+ let msg = [...result.matched][0];
+ yield checkLinkToInspector(true, msg);
+});
+
diff --git a/devtools/client/webconsole/test/browser_webconsole_cached_autocomplete.js b/devtools/client/webconsole/test/browser_webconsole_cached_autocomplete.js
new file mode 100644
index 000000000..fd5c4d29a
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_cached_autocomplete.js
@@ -0,0 +1,114 @@
+/* -*- 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 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,<p>test cached autocompletion " +
+ "results";
+
+var jsterm;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ jsterm = hud.jsterm;
+ let input = jsterm.inputNode;
+ let popup = jsterm.autocompletePopup;
+
+ // Test if 'doc' gives 'document'
+ input.value = "doc";
+ input.setSelectionRange(3, 3);
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ is(input.value, "doc", "'docu' completion (input.value)");
+ is(jsterm.completeNode.value, " ument", "'docu' completion (completeNode)");
+
+ // Test typing 'window.'.
+ input.value = "window.";
+ input.setSelectionRange(7, 7);
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ ok(popup.getItems().length > 0, "'window.' gave a list of suggestions");
+
+ yield jsterm.execute("window.docfoobar = true");
+
+ // Test typing 'window.doc'.
+ input.value = "window.doc";
+ input.setSelectionRange(10, 10);
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ let newItems = popup.getItems();
+ ok(newItems.every(function (item) {
+ return item.label != "docfoobar";
+ }), "autocomplete cached results do not contain docfoobar. list has not " +
+ "been updated");
+
+ // Test that backspace does not cause a request to the server
+ input.value = "window.do";
+ input.setSelectionRange(9, 9);
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ newItems = popup.getItems();
+ ok(newItems.every(function (item) {
+ return item.label != "docfoobar";
+ }), "autocomplete cached results do not contain docfoobar. list has not " +
+ "been updated");
+
+ yield jsterm.execute("delete window.docfoobar");
+
+ // Test if 'window.getC' gives 'getComputedStyle'
+ input.value = "window.";
+ input.setSelectionRange(7, 7);
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ input.value = "window.getC";
+ input.setSelectionRange(11, 11);
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ newItems = popup.getItems();
+ ok(!newItems.every(function (item) {
+ return item.label != "getComputedStyle";
+ }), "autocomplete results do contain getComputedStyle");
+
+ // Test if 'dump(d' gives non-zero results
+ input.value = "dump(d";
+ input.setSelectionRange(6, 6);
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ ok(popup.getItems().length > 0, "'dump(d' gives non-zero results");
+
+ // Test that 'dump(window.)' works.
+ input.value = "dump(window.)";
+ input.setSelectionRange(12, 12);
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ yield jsterm.execute("window.docfoobar = true");
+
+ // Make sure 'dump(window.doc)' does not contain 'docfoobar'.
+ input.value = "dump(window.doc)";
+ input.setSelectionRange(15, 15);
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ newItems = popup.getItems();
+ ok(newItems.every(function (item) {
+ return item.label != "docfoobar";
+ }), "autocomplete cached results do not contain docfoobar. list has not " +
+ "been updated");
+
+ yield jsterm.execute("delete window.docfoobar");
+
+ jsterm = null;
+});
+
+function complete(type) {
+ let updated = jsterm.once("autocomplete-updated");
+ jsterm.complete(type);
+ return updated;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_cd_iframe.js b/devtools/client/webconsole/test/browser_webconsole_cd_iframe.js
new file mode 100644
index 000000000..480c60940
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_cd_iframe.js
@@ -0,0 +1,115 @@
+/* -*- 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/ */
+
+// Test that the cd() jsterm helper function works as expected. See bug 609872.
+
+"use strict";
+
+function test() {
+ let hud;
+
+ const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug-609872-cd-iframe-parent.html";
+
+ const parentMessages = [{
+ name: "document.title in parent iframe",
+ text: "bug 609872 - iframe parent",
+ category: CATEGORY_OUTPUT,
+ }, {
+ name: "paragraph content",
+ text: "p: test for bug 609872 - iframe parent",
+ category: CATEGORY_OUTPUT,
+ }, {
+ name: "object content",
+ text: "obj: parent!",
+ category: CATEGORY_OUTPUT,
+ }];
+
+ const childMessages = [{
+ name: "document.title in child iframe",
+ text: "bug 609872 - iframe child",
+ category: CATEGORY_OUTPUT,
+ }, {
+ name: "paragraph content",
+ text: "p: test for bug 609872 - iframe child",
+ category: CATEGORY_OUTPUT,
+ }, {
+ name: "object content",
+ text: "obj: child!",
+ category: CATEGORY_OUTPUT,
+ }];
+
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ const {tab} = yield loadTab(TEST_URI);
+ hud = yield openConsole(tab);
+
+ yield executeWindowTest();
+
+ yield waitForMessages({ webconsole: hud, messages: parentMessages });
+
+ info("cd() into the iframe using a selector");
+ hud.jsterm.clearOutput();
+ yield hud.jsterm.execute("cd('iframe')");
+ yield executeWindowTest();
+
+ yield waitForMessages({ webconsole: hud, messages: childMessages });
+
+ info("cd() out of the iframe, reset to default window");
+ hud.jsterm.clearOutput();
+ yield hud.jsterm.execute("cd()");
+ yield executeWindowTest();
+
+ yield waitForMessages({ webconsole: hud, messages: parentMessages });
+
+ info("call cd() with unexpected arguments");
+ hud.jsterm.clearOutput();
+ yield hud.jsterm.execute("cd(document)");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "Cannot cd()",
+ category: CATEGORY_OUTPUT,
+ severity: SEVERITY_ERROR,
+ }],
+ });
+
+ hud.jsterm.clearOutput();
+ yield hud.jsterm.execute("cd('p')");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "Cannot cd()",
+ category: CATEGORY_OUTPUT,
+ severity: SEVERITY_ERROR,
+ }],
+ });
+
+ info("cd() into the iframe using an iframe DOM element");
+ hud.jsterm.clearOutput();
+ yield hud.jsterm.execute("cd($('iframe'))");
+ yield executeWindowTest();
+
+ yield waitForMessages({ webconsole: hud, messages: childMessages });
+
+ info("cd(window.parent)");
+ hud.jsterm.clearOutput();
+ yield hud.jsterm.execute("cd(window.parent)");
+ yield executeWindowTest();
+
+ yield waitForMessages({ webconsole: hud, messages: parentMessages });
+
+ yield closeConsole(tab);
+ }
+
+ function* executeWindowTest() {
+ yield hud.jsterm.execute("document.title");
+ yield hud.jsterm.execute("'p: ' + document.querySelector('p').textContent");
+ yield hud.jsterm.execute("'obj: ' + window.foobarBug609872");
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_certificate_messages.js b/devtools/client/webconsole/test/browser_webconsole_certificate_messages.js
new file mode 100644
index 000000000..ca08d1a0f
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_certificate_messages.js
@@ -0,0 +1,81 @@
+/* -*- 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 the Web Console shows weak crypto warnings (SHA-1 Certificate)
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,Web Console weak crypto " +
+ "warnings test";
+const TEST_URI_PATH = "/browser/devtools/client/webconsole/test/" +
+ "test-certificate-messages.html";
+
+var gWebconsoleTests = [
+ {url: "https://sha1ee.example.com" + TEST_URI_PATH,
+ name: "SHA1 warning displayed successfully",
+ warning: ["SHA-1"], nowarning: ["SSL 3.0", "RC4"]},
+ {url: "https://sha256ee.example.com" + TEST_URI_PATH,
+ name: "SSL warnings appropriately not present",
+ warning: [], nowarning: ["SHA-1", "SSL 3.0", "RC4"]},
+];
+const TRIGGER_MSG = "If you haven't seen ssl warnings yet, you won't";
+
+var gHud = undefined, gContentBrowser;
+var gCurrentTest;
+
+function test() {
+ registerCleanupFunction(function () {
+ gHud = gContentBrowser = null;
+ });
+
+ loadTab(TEST_URI).then(({browser}) => {
+ gContentBrowser = browser;
+ openConsole().then(runTestLoop);
+ });
+}
+
+function runTestLoop(theHud) {
+ gCurrentTest = gWebconsoleTests.shift();
+ if (!gCurrentTest) {
+ finishTest();
+ return;
+ }
+ if (!gHud) {
+ gHud = theHud;
+ }
+ gHud.jsterm.clearOutput();
+ gContentBrowser.addEventListener("load", onLoad, true);
+ if (gCurrentTest.pref) {
+ SpecialPowers.pushPrefEnv({"set": gCurrentTest.pref},
+ function () {
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, gCurrentTest.url);
+ });
+ } else {
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, gCurrentTest.url);
+ }
+}
+
+function onLoad() {
+ gContentBrowser.removeEventListener("load", onLoad, true);
+
+ waitForSuccess({
+ name: gCurrentTest.name,
+ validator: function () {
+ if (gHud.outputNode.textContent.indexOf(TRIGGER_MSG) >= 0) {
+ for (let warning of gCurrentTest.warning) {
+ if (gHud.outputNode.textContent.indexOf(warning) < 0) {
+ return false;
+ }
+ }
+ for (let nowarning of gCurrentTest.nowarning) {
+ if (gHud.outputNode.textContent.indexOf(nowarning) >= 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+ }).then(runTestLoop);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_chrome.js b/devtools/client/webconsole/test/browser_webconsole_chrome.js
new file mode 100644
index 000000000..2513d1df5
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_chrome.js
@@ -0,0 +1,38 @@
+/* -*- 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 code completion works properly in chrome tabs, like about:credits.
+
+"use strict";
+
+function test() {
+ Task.spawn(function* () {
+ const {tab} = yield loadTab("about:config");
+ ok(tab, "tab loaded");
+
+ const hud = yield openConsole(tab);
+ ok(hud, "we have a console");
+ ok(hud.iframeWindow, "we have the console UI window");
+
+ let jsterm = hud.jsterm;
+ ok(jsterm, "we have a jsterm");
+
+ let input = jsterm.inputNode;
+ ok(hud.outputNode, "we have an output node");
+
+ // Test typing 'docu'.
+ input.value = "docu";
+ input.setSelectionRange(4, 4);
+
+ let deferred = promise.defer();
+
+ jsterm.complete(jsterm.COMPLETE_HINT_ONLY, function () {
+ is(jsterm.completeNode.value, " ment", "'docu' completion");
+ deferred.resolve(null);
+ });
+
+ yield deferred.promise;
+ }).then(finishTest);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_clear_method.js b/devtools/client/webconsole/test/browser_webconsole_clear_method.js
new file mode 100644
index 000000000..a4702980e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_clear_method.js
@@ -0,0 +1,131 @@
+/* -*- 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/ */
+
+// Check that calls to console.clear from a script delete the messages
+// previously logged.
+
+"use strict";
+
+add_task(function* () {
+ const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-clear.html";
+
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ ok(hud, "Web Console opened");
+
+ info("Check the console.clear() done on page load has been processed.");
+ yield waitForLog("Console was cleared", hud);
+ ok(hud.outputNode.textContent.includes("Console was cleared"),
+ "console.clear() message is displayed");
+ ok(!hud.outputNode.textContent.includes("log1"), "log1 not displayed");
+ ok(!hud.outputNode.textContent.includes("log2"), "log2 not displayed");
+
+ info("Logging two messages log3, log4");
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.wrappedJSObject.console.log("log3");
+ content.wrappedJSObject.console.log("log4");
+ });
+
+ yield waitForLog("log3", hud);
+ yield waitForLog("log4", hud);
+
+ ok(hud.outputNode.textContent.includes("Console was cleared"),
+ "console.clear() message is still displayed");
+ ok(hud.outputNode.textContent.includes("log3"), "log3 is displayed");
+ ok(hud.outputNode.textContent.includes("log4"), "log4 is displayed");
+
+ info("Open the variables view sidebar for 'objFromPage'");
+ yield openSidebar("objFromPage", { a: 1 }, hud);
+ let sidebarClosed = hud.jsterm.once("sidebar-closed");
+
+ info("Call console.clear from the page");
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.wrappedJSObject.console.clear();
+ });
+
+ // Cannot wait for "Console was cleared" here because such a message is
+ // already present and would yield immediately.
+ info("Wait for variables view sidebar to be closed after console.clear()");
+ yield sidebarClosed;
+
+ ok(!hud.outputNode.textContent.includes("log3"), "log3 not displayed");
+ ok(!hud.outputNode.textContent.includes("log4"), "log4 not displayed");
+ ok(hud.outputNode.textContent.includes("Console was cleared"),
+ "console.clear() message is still displayed");
+ is(hud.outputNode.textContent.split("Console was cleared").length, 2,
+ "console.clear() message is only displayed once");
+
+ info("Logging one messages log5");
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.wrappedJSObject.console.log("log5");
+ });
+ yield waitForLog("log5", hud);
+
+ info("Close and reopen the webconsole.");
+ yield closeConsole(gBrowser.selectedTab);
+ hud = yield openConsole();
+ yield waitForLog("Console was cleared", hud);
+
+ ok(hud.outputNode.textContent.includes("Console was cleared"),
+ "console.clear() message is still displayed");
+ ok(!hud.outputNode.textContent.includes("log1"), "log1 not displayed");
+ ok(!hud.outputNode.textContent.includes("log2"), "log1 not displayed");
+ ok(!hud.outputNode.textContent.includes("log3"), "log3 not displayed");
+ ok(!hud.outputNode.textContent.includes("log4"), "log4 not displayed");
+ ok(hud.outputNode.textContent.includes("log5"), "log5 still displayed");
+});
+
+/**
+ * Wait for a single message to be logged in the provided webconsole instance
+ * with the category CATEGORY_WEBDEV and the SEVERITY_LOG severity.
+ *
+ * @param {String} message
+ * The expected messaged.
+ * @param {WebConsole} webconsole
+ * WebConsole instance in which the message should be logged.
+ */
+function* waitForLog(message, webconsole, options) {
+ yield waitForMessages({
+ webconsole: webconsole,
+ messages: [{
+ text: message,
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+}
+
+/**
+ * Open the variables view sidebar for the object with the provided name objName
+ * and wait for the expected object is displayed in the variables view.
+ *
+ * @param {String} objName
+ * The name of the object to open in the sidebar.
+ * @param {Object} expectedObj
+ * The properties that should be displayed in the variables view.
+ * @param {WebConsole} webconsole
+ * WebConsole instance in which the message should be logged.
+ *
+ */
+function* openSidebar(objName, expectedObj, webconsole) {
+ let msg = yield webconsole.jsterm.execute(objName);
+ ok(msg, "output message found");
+
+ let anchor = msg.querySelector("a");
+ let body = msg.querySelector(".message-body");
+ ok(anchor, "object anchor");
+ ok(body, "message body");
+
+ yield EventUtils.synthesizeMouse(anchor, 2, 2, {}, webconsole.iframeWindow);
+
+ let vviewVar = yield webconsole.jsterm.once("variablesview-fetched");
+ let vview = vviewVar._variablesView;
+ ok(vview, "variables view object exists");
+
+ yield findVariableViewProperties(vviewVar, [
+ expectedObj,
+ ], { webconsole: webconsole });
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_clickable_urls.js b/devtools/client/webconsole/test/browser_webconsole_clickable_urls.js
new file mode 100644
index 000000000..57d81fd05
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_clickable_urls.js
@@ -0,0 +1,103 @@
+/* -*- 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/ */
+
+// When strings containing URLs are entered into the webconsole, check
+// its output and ensure that the output can be clicked to open those URLs.
+
+"use strict";
+
+const inputTests = [
+
+ // 0: URL opens page when clicked.
+ {
+ input: "'http://example.com'",
+ output: "http://example.com",
+ expectedTab: "http://example.com/",
+ },
+
+ // 1: URL opens page using https when clicked.
+ {
+ input: "'https://example.com'",
+ output: "https://example.com",
+ expectedTab: "https://example.com/",
+ },
+
+ // 2: URL with port opens page when clicked.
+ {
+ input: "'https://example.com:443'",
+ output: "https://example.com:443",
+ expectedTab: "https://example.com/",
+ },
+
+ // 3: URL containing non-empty path opens page when clicked.
+ {
+ input: "'http://example.com/foo'",
+ output: "http://example.com/foo",
+ expectedTab: "http://example.com/foo",
+ },
+
+ // 4: URL opens page when clicked, even when surrounded by non-URL tokens.
+ {
+ input: "'foo http://example.com bar'",
+ output: "foo http://example.com bar",
+ expectedTab: "http://example.com/",
+ },
+
+ // 5: URL opens page when clicked, and whitespace is be preserved.
+ {
+ input: "'foo\\nhttp://example.com\\nbar'",
+ output: "foo\nhttp://example.com\nbar",
+ expectedTab: "http://example.com/",
+ },
+
+ // 6: URL opens page when clicked when multiple links are present.
+ {
+ input: "'http://example.com http://example.com'",
+ output: "http://example.com http://example.com",
+ expectedTab: "http://example.com/",
+ },
+
+ // 7: URL without scheme does not open page when clicked.
+ {
+ input: "'example.com'",
+ output: "example.com",
+ },
+
+ // 8: URL with invalid scheme does not open page when clicked.
+ {
+ input: "'foo://example.com'",
+ output: "foo://example.com",
+ },
+
+ // 9: Shortened URL in an array
+ {
+ input: "['http://example.com/abcdefghijabcdefghij some other text']",
+ output: "Array [ \"http://example.com/abcdefghijabcdef\u2026\" ]",
+ printOutput: "http://example.com/abcdefghijabcdefghij some other text",
+ expectedTab: "http://example.com/abcdefghijabcdefghij",
+ getClickableNode: (msg) => msg.querySelectorAll("a")[1],
+ },
+
+ // 10: Shortened URL in an object
+ {
+ input: "{test: 'http://example.com/abcdefghijabcdefghij some other text'}",
+ output: "Object { test: \"http://example.com/abcdefghijabcdef\u2026\" }",
+ printOutput: "[object Object]",
+ evalOutput: "http://example.com/abcdefghijabcdefghij some other text",
+ noClick: true,
+ consoleLogClick: true,
+ expectedTab: "http://example.com/abcdefghijabcdefghij",
+ getClickableNode: (msg) => msg.querySelectorAll("a")[1],
+ },
+
+];
+
+const url = "data:text/html;charset=utf8,Bug 1005909 - Clickable URLS";
+
+add_task(function* () {
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ let hud = yield openConsole();
+ yield checkOutputForInputs(hud, inputTests);
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_closure_inspection.js b/devtools/client/webconsole/test/browser_webconsole_closure_inspection.js
new file mode 100644
index 000000000..6a29d61aa
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_closure_inspection.js
@@ -0,0 +1,100 @@
+/* -*- 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/ */
+
+// Check that inspecting a closure in the variables view sidebar works when
+// execution is paused.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-closures.html";
+
+var gWebConsole, gJSTerm, gVariablesView;
+
+// Force the old debugger UI since it's directly used (see Bug 1301705)
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+function test() {
+ registerCleanupFunction(() => {
+ gWebConsole = gJSTerm = gVariablesView = null;
+ });
+
+ function fetchScopes(hud, toolbox, panelWin, deferred) {
+ panelWin.once(panelWin.EVENTS.FETCHED_SCOPES, () => {
+ ok(true, "Scopes were fetched");
+ toolbox.selectTool("webconsole").then(() => consoleOpened(hud));
+ deferred.resolve();
+ });
+ }
+
+ loadTab(TEST_URI).then(() => {
+ openConsole().then((hud) => {
+ openDebugger().then(({ toolbox, panelWin }) => {
+ let deferred = promise.defer();
+ fetchScopes(hud, toolbox, panelWin, deferred);
+
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let button = content.document.querySelector("button");
+ ok(button, "button element found");
+ button.click();
+ });
+
+ return deferred.promise;
+ });
+ });
+ });
+}
+
+function consoleOpened(hud) {
+ gWebConsole = hud;
+ gJSTerm = hud.jsterm;
+ gJSTerm.execute("window.george.getName");
+
+ waitForMessages({
+ webconsole: gWebConsole,
+ messages: [{
+ text: "function _pfactory/<.getName()",
+ category: CATEGORY_OUTPUT,
+ objects: true,
+ }],
+ }).then(onExecuteGetName);
+}
+
+function onExecuteGetName(results) {
+ let clickable = results[0].clickableElements[0];
+ ok(clickable, "clickable object found");
+
+ gJSTerm.once("variablesview-fetched", onGetNameFetch);
+ let contextMenu =
+ gWebConsole.iframeWindow.document.getElementById("output-contextmenu");
+ waitForContextMenu(contextMenu, clickable, () => {
+ let openInVarView = contextMenu.querySelector("#menu_openInVarView");
+ ok(openInVarView.disabled === false,
+ "the \"Open In Variables View\" context menu item should be clickable");
+ // EventUtils.synthesizeMouseAtCenter seems to fail here in Mac OSX
+ openInVarView.click();
+ });
+}
+
+function onGetNameFetch(evt, view) {
+ gVariablesView = view._variablesView;
+ ok(gVariablesView, "variables view object");
+
+ findVariableViewProperties(view, [
+ { name: /_pfactory/, value: "" },
+ ], { webconsole: gWebConsole }).then(onExpandClosure);
+}
+
+function onExpandClosure(results) {
+ let prop = results[0].matchedProp;
+ ok(prop, "matched the name property in the variables view");
+
+ gVariablesView.window.focus();
+ gJSTerm.once("sidebar-closed", finishTest);
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, gVariablesView.window);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_column_numbers.js b/devtools/client/webconsole/test/browser_webconsole_column_numbers.js
new file mode 100644
index 000000000..8407e34d5
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_column_numbers.js
@@ -0,0 +1,46 @@
+/* -*- 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/ */
+
+ // Check if console provides the right column number alongside line number
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-column.html";
+
+var hud;
+
+function test() {
+ loadTab(TEST_URI).then(() => {
+ openConsole().then(consoleOpened);
+ });
+}
+
+function consoleOpened(aHud) {
+ hud = aHud;
+
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "Error Message",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_ERROR
+ }]
+ }).then(testLocationColumn);
+}
+
+function testLocationColumn() {
+ let messages = hud.outputNode.children;
+ let expected = ["10:7", "10:39", "11:9", "12:11", "13:9", "14:7"];
+
+ for (let i = 0, len = messages.length; i < len; i++) {
+ let msg = messages[i].textContent;
+
+ is(msg.includes(expected[i]), true, "Found expected line:column of " +
+ expected[i]);
+ }
+
+ finishTest();
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_completion.js b/devtools/client/webconsole/test/browser_webconsole_completion.js
new file mode 100644
index 000000000..ee0c6809e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_completion.js
@@ -0,0 +1,106 @@
+/* -*- 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 code completion works properly.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<p>test code completion";
+
+var jsterm;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ jsterm = hud.jsterm;
+ let input = jsterm.inputNode;
+
+ // Test typing 'docu'.
+ input.value = "docu";
+ input.setSelectionRange(4, 4);
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ is(input.value, "docu", "'docu' completion (input.value)");
+ is(jsterm.completeNode.value, " ment", "'docu' completion (completeNode)");
+
+ // Test typing 'docu' and press tab.
+ input.value = "docu";
+ input.setSelectionRange(4, 4);
+ yield complete(jsterm.COMPLETE_FORWARD);
+
+ is(input.value, "document", "'docu' tab completion");
+ is(input.selectionStart, 8, "start selection is alright");
+ is(input.selectionEnd, 8, "end selection is alright");
+ is(jsterm.completeNode.value.replace(/ /g, ""), "", "'docu' completed");
+
+ // Test typing 'window.Ob' and press tab. Just 'window.O' is
+ // ambiguous: could be window.Object, window.Option, etc.
+ input.value = "window.Ob";
+ input.setSelectionRange(9, 9);
+ yield complete(jsterm.COMPLETE_FORWARD);
+
+ is(input.value, "window.Object", "'window.Ob' tab completion");
+
+ // Test typing 'document.getElem'.
+ input.value = "document.getElem";
+ input.setSelectionRange(16, 16);
+ yield complete(jsterm.COMPLETE_FORWARD);
+
+ is(input.value, "document.getElem", "'document.getElem' completion");
+ is(jsterm.completeNode.value, " entsByTagNameNS",
+ "'document.getElem' completion");
+
+ // Test pressing tab another time.
+ yield jsterm.complete(jsterm.COMPLETE_FORWARD);
+
+ is(input.value, "document.getElem", "'document.getElem' completion");
+ is(jsterm.completeNode.value, " entsByTagName",
+ "'document.getElem' another tab completion");
+
+ // Test pressing shift_tab.
+ complete(jsterm.COMPLETE_BACKWARD);
+
+ is(input.value, "document.getElem", "'document.getElem' untab completion");
+ is(jsterm.completeNode.value, " entsByTagNameNS",
+ "'document.getElem' completion");
+
+ jsterm.clearOutput();
+
+ input.value = "docu";
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ is(jsterm.completeNode.value, " ment", "'docu' completion");
+ yield jsterm.execute();
+ is(jsterm.completeNode.value, "", "clear completion on execute()");
+
+ // Test multi-line completion works
+ input.value = "console.log('one');\nconsol";
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ is(jsterm.completeNode.value, " \n e",
+ "multi-line completion");
+
+ // Test non-object autocompletion.
+ input.value = "Object.name.sl";
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ is(jsterm.completeNode.value, " ice", "non-object completion");
+
+ // Test string literal autocompletion.
+ input.value = "'Asimov'.sl";
+ yield complete(jsterm.COMPLETE_HINT_ONLY);
+
+ is(jsterm.completeNode.value, " ice", "string literal completion");
+
+ jsterm = null;
+});
+
+function complete(type) {
+ let updated = jsterm.once("autocomplete-updated");
+ jsterm.complete(type);
+ return updated;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_console_api_stackframe.js b/devtools/client/webconsole/test/browser_webconsole_console_api_stackframe.js
new file mode 100644
index 000000000..f8f02aa15
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_console_api_stackframe.js
@@ -0,0 +1,85 @@
+/* -*- 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/ */
+
+"use strict";
+
+// Test that the console API messages for console.error()/exception()/assert()
+// include a stackframe. See bug 920116.
+
+function test() {
+ let hud;
+
+ const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-api-stackframe.html";
+ const TEST_FILE = TEST_URI.substr(TEST_URI.lastIndexOf("/"));
+
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ const {tab} = yield loadTab(TEST_URI);
+ hud = yield openConsole(tab);
+
+ const stack = [{
+ file: TEST_FILE,
+ fn: "thirdCall",
+ // 21,22,23
+ line: /\b2[123]\b/,
+ }, {
+ file: TEST_FILE,
+ fn: "secondCall",
+ line: 16,
+ }, {
+ file: TEST_FILE,
+ fn: "firstCall",
+ line: 12,
+ }];
+
+ let results = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foo-log",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ collapsible: false,
+ }, {
+ text: "foo-error",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_ERROR,
+ collapsible: true,
+ stacktrace: stack,
+ }, {
+ text: "foo-exception",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_ERROR,
+ collapsible: true,
+ stacktrace: stack,
+ }, {
+ text: "foo-assert",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_ERROR,
+ collapsible: true,
+ stacktrace: stack,
+ }],
+ });
+
+ let elem = [...results[1].matched][0];
+ ok(elem, "message element");
+
+ let msg = elem._messageObject;
+ ok(msg, "message object");
+
+ ok(msg.collapsed, "message is collapsed");
+
+ msg.toggleDetails();
+
+ ok(!msg.collapsed, "message is not collapsed");
+
+ msg.toggleDetails();
+
+ ok(msg.collapsed, "message is collapsed");
+
+ yield closeConsole(tab);
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_console_custom_styles.js b/devtools/client/webconsole/test/browser_webconsole_console_custom_styles.js
new file mode 100644
index 000000000..310d4fc8b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_console_custom_styles.js
@@ -0,0 +1,81 @@
+/* -*- 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/ */
+
+"use strict";
+
+// Test that the '%c' modifier works with the console API. See bug 823097.
+
+function test() {
+ let hud;
+
+ const TEST_URI = "data:text/html;charset=utf8,<p>test for " +
+ "console.log('%ccustom styles', 'color:red')";
+
+ const checks = [{
+ // check the basics work
+ style: "color:red;font-size:1.3em",
+ props: { color: true, fontSize: true },
+ sameStyleExpected: true,
+ }, {
+ // check that the url() is not allowed
+ style: "color:blue;background-image:url('http://example.com/test')",
+ props: { color: true, fontSize: false, background: false,
+ backgroundImage: false },
+ sameStyleExpected: false,
+ }, {
+ // check that some properties are not allowed
+ style: "color:pink;position:absolute;top:10px",
+ props: { color: true, position: false, top: false },
+ sameStyleExpected: false,
+ }];
+
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ const {tab} = yield loadTab(TEST_URI);
+ hud = yield openConsole(tab);
+
+ for (let check of checks) {
+ yield checkStyle(check);
+ }
+
+ yield closeConsole(tab);
+ }
+
+ function* checkStyle(check) {
+ hud.jsterm.clearOutput();
+
+ info("checkStyle " + check.style);
+ hud.jsterm.execute("console.log('%cfoobar', \"" + check.style + "\")");
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foobar",
+ category: CATEGORY_WEBDEV,
+ }],
+ });
+
+ let msg = [...result.matched][0];
+ ok(msg, "message element");
+
+ let span = msg.querySelector(".message-body span[style]");
+ ok(span, "span element");
+
+ info("span textContent is: " + span.textContent);
+ isnot(span.textContent.indexOf("foobar"), -1, "span textContent check");
+
+ let outputStyle = span.getAttribute("style").replace(/\s+|;+$/g, "");
+ if (check.sameStyleExpected) {
+ is(outputStyle, check.style, "span style is correct");
+ } else {
+ isnot(outputStyle, check.style, "span style is not the same");
+ }
+
+ for (let prop of Object.keys(check.props)) {
+ is(!!span.style[prop], check.props[prop], "property check for " + prop);
+ }
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_console_extras.js b/devtools/client/webconsole/test/browser_webconsole_console_extras.js
new file mode 100644
index 000000000..078e33119
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_console_extras.js
@@ -0,0 +1,43 @@
+/* -*- 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/ */
+
+// Test that window.console functions that are not implemented yet do not
+// output anything in the web console and they do not throw any exceptions.
+// See bug 614350.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-extras.html";
+
+function test() {
+ loadTab(TEST_URI).then(() => {
+ openConsole().then(consoleOpened);
+ });
+}
+
+function consoleOpened(hud) {
+ waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "start",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "end",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ }).then(() => {
+ let nodes = hud.outputNode.querySelectorAll(".message");
+ is(nodes.length, 2, "only two messages are displayed");
+ finishTest();
+ });
+
+ let button = content.document.querySelector("button");
+ ok(button, "we have the button");
+ EventUtils.sendMouseEvent({ type: "click" }, button, content);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_console_logging_api.js b/devtools/client/webconsole/test/browser_webconsole_console_logging_api.js
new file mode 100644
index 000000000..317337543
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_console_logging_api.js
@@ -0,0 +1,102 @@
+/* -*- 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 the basic console.log()-style APIs and filtering work.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ hud.jsterm.clearOutput();
+
+ let outputNode = hud.outputNode;
+
+ let methods = ["log", "info", "warn", "error", "exception", "debug"];
+ for (let method of methods) {
+ yield testMethod(method, hud, outputNode);
+ }
+});
+
+function* testMethod(method, hud, outputNode) {
+ let console = content.console;
+
+ hud.jsterm.clearOutput();
+
+ console[method]("foo-bar-baz");
+ console[method]("baar-baz");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foo-bar-baz",
+ }, {
+ text: "baar-baz",
+ }],
+ });
+
+ setStringFilter("foo", hud);
+
+ is(outputNode.querySelectorAll(".filtered-by-string").length, 1,
+ "1 hidden " + method + " node via string filtering");
+
+ hud.jsterm.clearOutput();
+
+ // now toggle the current method off - make sure no visible message
+ // TODO: move all filtering tests into a separate test file: see bug 608135
+
+ console[method]("foo-bar-baz");
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foo-bar-baz",
+ }],
+ });
+
+ setStringFilter("", hud);
+ let filter;
+ switch (method) {
+ case "debug":
+ filter = "log";
+ break;
+ case "exception":
+ filter = "error";
+ break;
+ default:
+ filter = method;
+ break;
+ }
+
+ hud.setFilterState(filter, false);
+
+ is(outputNode.querySelectorAll(".filtered-by-type").length, 1,
+ "1 message hidden for " + method + " (logging turned off)");
+
+ hud.setFilterState(filter, true);
+
+ is(outputNode.querySelectorAll(".message:not(.filtered-by-type)").length, 1,
+ "1 message shown for " + method + " (logging turned on)");
+
+ hud.jsterm.clearOutput();
+
+ // test for multiple arguments.
+ console[method]("foo", "bar");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foo bar",
+ category: CATEGORY_WEBDEV,
+ }],
+ });
+}
+
+function setStringFilter(value, hud) {
+ hud.ui.filterBox.value = value;
+ hud.ui.adjustVisibilityOnSearchStringChange();
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_console_logging_workers_api.js b/devtools/client/webconsole/test/browser_webconsole_console_logging_workers_api.js
new file mode 100644
index 000000000..9575721c3
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_console_logging_workers_api.js
@@ -0,0 +1,39 @@
+/* -*- 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 the basic console.log()-style APIs and filtering work for
+// sharedWorkers
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-workers.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foo-bar-shared-worker"
+ }],
+ });
+
+ hud.setFilterState("sharedworkers", false);
+
+ is(hud.outputNode.querySelectorAll(".filtered-by-type").length, 1,
+ "1 message hidden for sharedworkers (logging turned off)");
+
+ hud.setFilterState("sharedworkers", true);
+
+ is(hud.outputNode.querySelectorAll(".filtered-by-type").length, 0,
+ "1 message shown for sharedworkers (logging turned on)");
+
+ hud.setFilterState("sharedworkers", false);
+
+ hud.jsterm.clearOutput(true);
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_console_trace_async.js b/devtools/client/webconsole/test/browser_webconsole_console_trace_async.js
new file mode 100644
index 000000000..10c3ff7a5
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_console_trace_async.js
@@ -0,0 +1,75 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/" +
+ "webconsole/test/test-console-trace-async.html";
+
+add_task(function* runTest() {
+ // Async stacks aren't on by default in all builds
+ yield new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["javascript.options.asyncstack", true]
+ ]}, resolve);
+ });
+
+ let {tab} = yield loadTab("data:text/html;charset=utf8,<p>hello");
+ let hud = yield openConsole(tab);
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI);
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "console.trace output",
+ consoleTrace: {
+ file: "test-console-trace-async.html",
+ fn: "inner",
+ },
+ }],
+ });
+
+ let node = [...result.matched][0];
+ ok(node, "found trace log node");
+ ok(node.textContent.includes("console.trace()"),
+ "trace log node includes console.trace()");
+ ok(node.textContent.includes("promise callback"),
+ "trace log node includes promise callback");
+ ok(node.textContent.includes("setTimeout handler"),
+ "trace log node includes setTimeout handler");
+
+ // The expected stack trace object.
+ let stacktrace = [
+ {
+ columnNumber: 3,
+ filename: TEST_URI,
+ functionName: "inner",
+ language: 2,
+ lineNumber: 9
+ },
+ {
+ asyncCause: "promise callback",
+ columnNumber: 3,
+ filename: TEST_URI,
+ functionName: "time1",
+ language: 2,
+ lineNumber: 13,
+ },
+ {
+ asyncCause: "setTimeout handler",
+ columnNumber: 1,
+ filename: TEST_URI,
+ functionName: "",
+ language: 2,
+ lineNumber: 18,
+ }
+ ];
+
+ let obj = node._messageObject;
+ ok(obj, "console.trace message object");
+ ok(obj._stacktrace, "found stacktrace object");
+ is(obj._stacktrace.toSource(), stacktrace.toSource(),
+ "stacktrace is correct");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_console_trace_duplicates.js b/devtools/client/webconsole/test/browser_webconsole_console_trace_duplicates.js
new file mode 100644
index 000000000..e1c6f966e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_console_trace_duplicates.js
@@ -0,0 +1,50 @@
+/* -*- 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/ */
+
+"use strict";
+
+function test() {
+ const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug_939783_console_trace_duplicates.html";
+
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ const {tab} = yield loadTab("data:text/html;charset=utf8,<p>hello");
+ const hud = yield openConsole(tab);
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI);
+
+ // NB: Now that stack frames include a column number multiple invocations
+ // on the same line are considered unique. ie:
+ // |foo(); foo();|
+ // will generate two distinct trace entries.
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "console.trace output for foo1()",
+ text: "foo1",
+ consoleTrace: {
+ file: "test-bug_939783_console_trace_duplicates.html",
+ fn: "foo3",
+ },
+ }, {
+ name: "console.trace output for foo1()",
+ text: "foo1",
+ consoleTrace: {
+ file: "test-bug_939783_console_trace_duplicates.html",
+ fn: "foo3",
+ },
+ }, {
+ name: "console.trace output for foo1b()",
+ text: "foo1b",
+ consoleTrace: {
+ file: "test-bug_939783_console_trace_duplicates.html",
+ fn: "foo3",
+ },
+ }],
+ });
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_context_menu_open_in_var_view.js b/devtools/client/webconsole/test/browser_webconsole_context_menu_open_in_var_view.js
new file mode 100644
index 000000000..8451ec762
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_context_menu_open_in_var_view.js
@@ -0,0 +1,51 @@
+/* -*- 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 the "Open in Variables View" context menu item is enabled
+// only for objects.
+
+"use strict";
+
+const TEST_URI = `data:text/html,<script>
+ console.log("foo");
+ console.log("foo", window);
+</script>`;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ count: 2,
+ text: /foo/
+ }],
+ });
+
+ let [msgWithText, msgWithObj] = [...result.matched];
+ ok(msgWithText && msgWithObj, "Two messages should have appeared");
+
+ let contextMenu = hud.iframeWindow.document
+ .getElementById("output-contextmenu");
+ let openInVarViewItem = contextMenu.querySelector("#menu_openInVarView");
+ let obj = msgWithObj.querySelector(".cm-variable");
+ let text = msgWithText.querySelector(".console-string");
+
+ yield waitForContextMenu(contextMenu, obj, () => {
+ ok(openInVarViewItem.disabled === false, "The \"Open In Variables View\" " +
+ "context menu item should be available for objects");
+ }, () => {
+ ok(openInVarViewItem.disabled === true, "The \"Open In Variables View\" " +
+ "context menu item should be disabled on popup hiding");
+ });
+
+ yield waitForContextMenu(contextMenu, text, () => {
+ ok(openInVarViewItem.disabled === true, "The \"Open In Variables View\" " +
+ "context menu item should be disabled for texts");
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_context_menu_store_as_global.js b/devtools/client/webconsole/test/browser_webconsole_context_menu_store_as_global.js
new file mode 100644
index 000000000..4508101ee
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_context_menu_store_as_global.js
@@ -0,0 +1,66 @@
+/* -*- 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 the "Store as global variable" context menu item feature.
+// It should be work, and be enabled only for objects
+
+"use strict";
+
+const TEST_URI = `data:text/html,<script>
+ window.bar = { baz: 1 };
+ console.log("foo");
+ console.log("foo", window.bar);
+</script>`;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ count: 2,
+ text: /foo/
+ }],
+ });
+
+ let [msgWithText, msgWithObj] = [...result.matched];
+ ok(msgWithText && msgWithObj, "Two messages should have appeared");
+
+ let contextMenu = hud.iframeWindow.document
+ .getElementById("output-contextmenu");
+ let storeAsGlobalItem = contextMenu.querySelector("#menu_storeAsGlobal");
+ let obj = msgWithObj.querySelector(".cm-variable");
+ let text = msgWithText.querySelector(".console-string");
+ let onceInputSet = hud.jsterm.once("set-input-value");
+
+ info("Waiting for context menu on the object");
+ yield waitForContextMenu(contextMenu, obj, () => {
+ ok(storeAsGlobalItem.disabled === false, "The \"Store as global\" " +
+ "context menu item should be available for objects");
+ storeAsGlobalItem.click();
+ }, () => {
+ ok(storeAsGlobalItem.disabled === true, "The \"Store as global\" " +
+ "context menu item should be disabled on popup hiding");
+ });
+
+ info("Waiting for context menu on the text node");
+ yield waitForContextMenu(contextMenu, text, () => {
+ ok(storeAsGlobalItem.disabled === true, "The \"Store as global\" " +
+ "context menu item should be disabled for texts");
+ });
+
+ info("Waiting for input to be set");
+ yield onceInputSet;
+
+ is(hud.jsterm.getInputValue(), "temp0", "Input was set");
+ let executedResult = yield hud.jsterm.execute();
+
+ ok(executedResult.textContent.includes("{ baz: 1 }"),
+ "Correct variable assigned into console");
+
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_count.js b/devtools/client/webconsole/test/browser_webconsole_count.js
new file mode 100644
index 000000000..abb31a08d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_count.js
@@ -0,0 +1,77 @@
+/* -*- 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/ */
+
+// Test that console.count() counts as expected. See bug 922208.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-count.html";
+
+function test() {
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ const {tab} = yield loadTab(TEST_URI);
+ const hud = yield openConsole(tab);
+
+ BrowserTestUtils.synthesizeMouseAtCenter("#local", {}, gBrowser.selectedBrowser);
+ let messages = [];
+ [
+ "start",
+ "<no label>: 2",
+ "console.count() testcounter: 1",
+ "console.count() testcounter: 2",
+ "console.count() testcounter: 3",
+ "console.count() testcounter: 4",
+ "end"
+ ].forEach(function (msg) {
+ messages.push({
+ text: msg,
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG
+ });
+ });
+ messages.push({
+ name: "Three local counts with no label and count=1",
+ text: "<no label>: 1",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ count: 3
+ });
+ yield waitForMessages({
+ webconsole: hud,
+ messages: messages
+ });
+
+ hud.jsterm.clearOutput();
+
+ BrowserTestUtils.synthesizeMouseAtCenter("#external", {}, gBrowser.selectedBrowser);
+ messages = [];
+ [
+ "start",
+ "console.count() testcounter: 5",
+ "console.count() testcounter: 6",
+ "end"
+ ].forEach(function (msg) {
+ messages.push({
+ text: msg,
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG
+ });
+ });
+ messages.push({
+ name: "Two external counts with no label and count=1",
+ text: "<no label>: 1",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ count: 2
+ });
+ yield waitForMessages({
+ webconsole: hud,
+ messages: messages
+ });
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_dont_navigate_on_doubleclick.js b/devtools/client/webconsole/test/browser_webconsole_dont_navigate_on_doubleclick.js
new file mode 100644
index 000000000..61ac68208
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_dont_navigate_on_doubleclick.js
@@ -0,0 +1,56 @@
+/* -*- 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 if a link in console is double clicked, the console frame doesn't
+// navigate to that destination (bug 975707).
+
+"use strict";
+
+function test() {
+ let originalNetPref = Services.prefs
+ .getBoolPref("devtools.webconsole.filter.networkinfo");
+ registerCleanupFunction(() => {
+ Services.prefs.setBoolPref("devtools.webconsole.filter.networkinfo",
+ originalNetPref);
+ });
+ Services.prefs.setBoolPref("devtools.webconsole.filter.networkinfo", true);
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ const TEST_PAGE_URI = "http://example.com/browser/devtools/client/" +
+ "webconsole/test/test-console.html" + "?_uniq=" +
+ Date.now();
+
+ const {tab} = yield loadTab("data:text/html;charset=utf8,<p>hello</p>");
+ const hud = yield openConsole(tab);
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_PAGE_URI);
+
+ let messages = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "Network request message",
+ url: TEST_PAGE_URI,
+ category: CATEGORY_NETWORK
+ }]
+ });
+
+ let networkEventMessage = messages[0].matched.values().next().value;
+ let urlNode = networkEventMessage.querySelector(".url");
+
+ let deferred = promise.defer();
+ urlNode.addEventListener("click", function onClick(event) {
+ urlNode.removeEventListener("click", onClick);
+ ok(event.defaultPrevented, "The default action was prevented.");
+
+ deferred.resolve();
+ });
+
+ EventUtils.synthesizeMouseAtCenter(urlNode, {clickCount: 2},
+ hud.iframeWindow);
+
+ yield deferred.promise;
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_exception_stackframe.js b/devtools/client/webconsole/test/browser_webconsole_exception_stackframe.js
new file mode 100644
index 000000000..5cedfbad5
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_exception_stackframe.js
@@ -0,0 +1,104 @@
+/* -*- 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/ */
+
+"use strict";
+
+// Test that the console receive exceptions include a stackframe.
+// See bug 1184172.
+
+// On e10s, the exception is triggered in child process
+// and is ignored by test harness
+if (!Services.appinfo.browserTabsRemoteAutostart) {
+ SimpleTest.ignoreAllUncaughtExceptions();
+}
+
+function test() {
+ let hud;
+
+ const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-exception-stackframe.html";
+ const TEST_FILE = TEST_URI.substr(TEST_URI.lastIndexOf("/"));
+
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ const {tab} = yield loadTab(TEST_URI);
+ hud = yield openConsole(tab);
+
+ const stack = [{
+ file: TEST_FILE,
+ fn: "thirdCall",
+ line: 21,
+ }, {
+ file: TEST_FILE,
+ fn: "secondCall",
+ line: 17,
+ }, {
+ file: TEST_FILE,
+ fn: "firstCall",
+ line: 12,
+ }];
+
+ let results = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "nonExistingMethodCall is not defined",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ collapsible: true,
+ stacktrace: stack,
+ }, {
+ text: "SyntaxError: 'buggy;selector' is not a valid selector",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ collapsible: true,
+ stacktrace: [{
+ file: TEST_FILE,
+ fn: "domAPI",
+ line: 25,
+ }, {
+ file: TEST_FILE,
+ fn: "onLoadDomAPI",
+ line: 33,
+ }
+ ]
+ }, {
+ text: "DOMException",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ collapsible: true,
+ stacktrace: [{
+ file: TEST_FILE,
+ fn: "domException",
+ line: 29,
+ }, {
+ file: TEST_FILE,
+ fn: "onLoadDomException",
+ line: 36,
+ },
+
+ ]
+ }],
+ });
+
+ let elem = [...results[0].matched][0];
+ ok(elem, "message element");
+
+ let msg = elem._messageObject;
+ ok(msg, "message object");
+
+ ok(msg.collapsed, "message is collapsed");
+
+ msg.toggleDetails();
+
+ ok(!msg.collapsed, "message is not collapsed");
+
+ msg.toggleDetails();
+
+ ok(msg.collapsed, "message is collapsed");
+
+ yield closeConsole(tab);
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_execution_scope.js b/devtools/client/webconsole/test/browser_webconsole_execution_scope.js
new file mode 100644
index 000000000..78865c9b2
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_execution_scope.js
@@ -0,0 +1,37 @@
+/* -*- 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 commands run by the user are executed in content space.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ hud.jsterm.clearOutput();
+ hud.jsterm.execute("window.location.href;");
+
+ let [input, output] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "window.location.href;",
+ category: CATEGORY_INPUT,
+ },
+ {
+ text: TEST_URI,
+ category: CATEGORY_OUTPUT,
+ }],
+ });
+
+ let inputNode = [...input.matched][0];
+ let outputNode = [...output.matched][0];
+ is(inputNode.getAttribute("category"), "input",
+ "input node category is correct");
+ is(outputNode.getAttribute("category"), "output",
+ "output node category is correct");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_expandable_timestamps.js b/devtools/client/webconsole/test/browser_webconsole_expandable_timestamps.js
new file mode 100644
index 000000000..192387e8a
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_expandable_timestamps.js
@@ -0,0 +1,57 @@
+/* -*- 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/ */
+
+// 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 TEST_URI = "data:text/html;charset=utf-8,Web Console test for " +
+ "bug 722267 - preference for toggling timestamps in messages";
+const PREF_MESSAGE_TIMESTAMP = "devtools.webconsole.timestampMessages";
+var hud;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ hud = yield openConsole();
+ let panel = yield consoleOpened();
+
+ yield onOptionsPanelSelected(panel);
+ onPrefChanged();
+
+ Services.prefs.clearUserPref(PREF_MESSAGE_TIMESTAMP);
+ hud = null;
+});
+
+function consoleOpened() {
+ info("console opened");
+ let prefValue = Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP);
+ ok(!prefValue, "messages have no timestamp by default (pref check)");
+ ok(hud.outputNode.classList.contains("hideTimestamps"),
+ "messages have no timestamp (class name check)");
+
+ let toolbox = gDevTools.getToolbox(hud.target);
+ return toolbox.selectTool("options");
+}
+
+function onOptionsPanelSelected(panel) {
+ info("options panel opened");
+
+ let prefChanged = gDevTools.once("pref-changed", onPrefChanged);
+
+ let checkbox = panel.panelDoc.getElementById("webconsole-timestamp-messages");
+ checkbox.click();
+
+ return prefChanged;
+}
+
+function onPrefChanged() {
+ info("pref changed");
+ let prefValue = Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP);
+ ok(prefValue, "messages have timestamps (pref check)");
+ ok(!hud.outputNode.classList.contains("hideTimestamps"),
+ "messages have timestamps (class name check)");
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_filter_buttons_contextmenu.js b/devtools/client/webconsole/test/browser_webconsole_filter_buttons_contextmenu.js
new file mode 100644
index 000000000..e210bd81a
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_filter_buttons_contextmenu.js
@@ -0,0 +1,95 @@
+/* -*- 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 the filter button context menu logic works correctly.
+
+"use strict";
+
+const TEST_URI = "http://example.com/";
+
+function test() {
+ loadTab(TEST_URI).then(() => {
+ openConsole().then(testFilterButtons);
+ });
+}
+
+function testFilterButtons(aHud) {
+ let hudBox = aHud.ui.rootElement;
+
+ testRightClick("net", hudBox, aHud)
+ .then(() => testRightClick("css", hudBox, aHud))
+ .then(() => testRightClick("js", hudBox, aHud))
+ .then(() => testRightClick("logging", hudBox, aHud))
+ .then(() => testRightClick("security", hudBox, aHud))
+ .then(finishTest);
+}
+
+function testRightClick(aCategory, hudBox, aHud) {
+ let deferred = promise.defer();
+ let selector = ".webconsole-filter-button[category=\"" + aCategory + "\"]";
+ let button = hudBox.querySelector(selector);
+ let mainButton = getMainButton(button, aHud);
+ let origCheckedState = button.getAttribute("aria-pressed");
+ let contextMenu = aHud.iframeWindow.document.getElementById(aCategory + "-contextmenu");
+
+ function verifyContextMenuIsClosed() {
+ info("verify the context menu is closed");
+ is(button.getAttribute("open"), false, "The context menu for the \"" +
+ aCategory + "\" button is closed");
+ }
+
+ function verifyOriginalCheckedState() {
+ info("verify the button has the original checked state");
+ is(button.getAttribute("aria-pressed"), origCheckedState,
+ "The button state should not have changed");
+ }
+
+ function verifyNewCheckedState() {
+ info("verify the button's checked state has changed");
+ isnot(button.getAttribute("aria-pressed"), origCheckedState,
+ "The button state should have changed");
+ }
+
+ function leftClickToClose() {
+ info("left click the button to close the contextMenu");
+ EventUtils.sendMouseEvent({type: "click"}, button);
+ executeSoon(() => {
+ verifyContextMenuIsClosed();
+ verifyOriginalCheckedState();
+ leftClickToChangeCheckedState();
+ });
+ }
+
+ function leftClickToChangeCheckedState() {
+ info("left click the mainbutton to change checked state");
+ EventUtils.sendMouseEvent({type: "click"}, mainButton);
+ executeSoon(() => {
+ verifyContextMenuIsClosed();
+ verifyNewCheckedState();
+ deferred.resolve();
+ });
+ }
+
+ verifyContextMenuIsClosed();
+ info("right click the button to open the context menu");
+ waitForContextMenu(contextMenu, mainButton, verifyOriginalCheckedState,
+ leftClickToClose);
+ return deferred.promise;
+}
+
+function getMainButton(aTargetButton, aHud) {
+ let anonymousNodes = aHud.ui.document.getAnonymousNodes(aTargetButton);
+ let subbutton;
+
+ for (let i = 0; i < anonymousNodes.length; i++) {
+ let node = anonymousNodes[i];
+ if (node.classList.contains("toolbarbutton-menubutton-button")) {
+ subbutton = node;
+ break;
+ }
+ }
+
+ return subbutton;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_for_of.js b/devtools/client/webconsole/test/browser_webconsole_for_of.js
new file mode 100644
index 000000000..83d3aaa3d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_for_of.js
@@ -0,0 +1,32 @@
+/* -*- 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/ */
+
+// A for-of loop in Web Console code can loop over a content NodeList.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-for-of.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+ yield testForOf(hud);
+});
+
+function testForOf(hud) {
+ let deferred = promise.defer();
+
+ let jsterm = hud.jsterm;
+ jsterm.execute("{ let _nodes = []; for (let x of document.body.childNodes) { if (x.nodeType === 1) { _nodes.push(x.tagName); } } _nodes.join(' '); }",
+ (node) => {
+ ok(/H1 DIV H2 P/.test(node.textContent),
+ "for-of loop should find all top-level nodes");
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_history.js b/devtools/client/webconsole/test/browser_webconsole_history.js
new file mode 100644
index 000000000..5ae709a4b
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_history.js
@@ -0,0 +1,62 @@
+/* -*- 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 the console history feature accessed via the up and down arrow keys.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+// Constants used for defining the direction of JSTerm input history navigation.
+const HISTORY_BACK = -1;
+const HISTORY_FORWARD = 1;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ hud.jsterm.clearOutput();
+
+ let jsterm = hud.jsterm;
+ let input = jsterm.inputNode;
+
+ let executeList = ["document", "window", "window.location"];
+
+ for (let item of executeList) {
+ input.value = item;
+ yield jsterm.execute();
+ }
+
+ for (let x = executeList.length - 1; x != -1; x--) {
+ jsterm.historyPeruse(HISTORY_BACK);
+ is(input.value, executeList[x], "check history previous idx:" + x);
+ }
+
+ jsterm.historyPeruse(HISTORY_BACK);
+ is(input.value, executeList[0], "test that item is still index 0");
+
+ jsterm.historyPeruse(HISTORY_BACK);
+ is(input.value, executeList[0], "test that item is still still index 0");
+
+ for (let i = 1; i < executeList.length; i++) {
+ jsterm.historyPeruse(HISTORY_FORWARD);
+ is(input.value, executeList[i], "check history next idx:" + i);
+ }
+
+ jsterm.historyPeruse(HISTORY_FORWARD);
+ is(input.value, "", "check input is empty again");
+
+ // Simulate pressing Arrow_Down a few times and then if Arrow_Up shows
+ // the previous item from history again.
+ jsterm.historyPeruse(HISTORY_FORWARD);
+ jsterm.historyPeruse(HISTORY_FORWARD);
+ jsterm.historyPeruse(HISTORY_FORWARD);
+
+ is(input.value, "", "check input is still empty");
+
+ let idxLast = executeList.length - 1;
+ jsterm.historyPeruse(HISTORY_BACK);
+ is(input.value, executeList[idxLast], "check history next idx:" + idxLast);
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_hpkp_invalid-headers.js b/devtools/client/webconsole/test/browser_webconsole_hpkp_invalid-headers.js
new file mode 100644
index 000000000..3ee33669d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_hpkp_invalid-headers.js
@@ -0,0 +1,126 @@
+/* -*- 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 errors about invalid HPKP security headers are logged to the web
+// console.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console HPKP invalid " +
+ "header test";
+const SJS_URL = "https://example.com/browser/devtools/client/webconsole/" +
+ "test/test_hpkp-invalid-headers.sjs";
+const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Web/Security/" +
+ "Public_Key_Pinning" + DOCS_GA_PARAMS;
+const NON_BUILTIN_ROOT_PREF = "security.cert_pinning.process_headers_from_" +
+ "non_builtin_roots";
+
+add_task(function* () {
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(NON_BUILTIN_ROOT_PREF);
+ });
+
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ yield* checkForMessage({
+ url: SJS_URL + "?badSyntax",
+ name: "Could not parse header error displayed successfully",
+ text: "Public-Key-Pins: The site specified a header that could not be " +
+ "parsed successfully."
+ }, hud);
+
+ yield* checkForMessage({
+ url: SJS_URL + "?noMaxAge",
+ name: "No max-age error displayed successfully",
+ text: "Public-Key-Pins: The site specified a header that did not include " +
+ "a \u2018max-age\u2019 directive."
+ }, hud);
+
+ yield* checkForMessage({
+ url: SJS_URL + "?invalidIncludeSubDomains",
+ name: "Invalid includeSubDomains error displayed successfully",
+ text: "Public-Key-Pins: The site specified a header that included an " +
+ "invalid \u2018includeSubDomains\u2019 directive."
+ }, hud);
+
+ yield* checkForMessage({
+ url: SJS_URL + "?invalidMaxAge",
+ name: "Invalid max-age error displayed successfully",
+ text: "Public-Key-Pins: The site specified a header that included an " +
+ "invalid \u2018max-age\u2019 directive."
+ }, hud);
+
+ yield* checkForMessage({
+ url: SJS_URL + "?multipleIncludeSubDomains",
+ name: "Multiple includeSubDomains error displayed successfully",
+ text: "Public-Key-Pins: The site specified a header that included " +
+ "multiple \u2018includeSubDomains\u2019 directives."
+ }, hud);
+
+ yield* checkForMessage({
+ url: SJS_URL + "?multipleMaxAge",
+ name: "Multiple max-age error displayed successfully",
+ text: "Public-Key-Pins: The site specified a header that included " +
+ "multiple \u2018max-age\u2019 directives."
+ }, hud);
+
+ yield* checkForMessage({
+ url: SJS_URL + "?multipleReportURIs",
+ name: "Multiple report-uri error displayed successfully",
+ text: "Public-Key-Pins: The site specified a header that included " +
+ "multiple \u2018report-uri\u2019 directives."
+ }, hud);
+
+ // The root used for mochitests is not built-in, so set the relevant pref to
+ // true to have the PKP implementation return more specific errors.
+ Services.prefs.setBoolPref(NON_BUILTIN_ROOT_PREF, true);
+
+ yield* checkForMessage({
+ url: SJS_URL + "?pinsetDoesNotMatch",
+ name: "Non-matching pinset error displayed successfully",
+ text: "Public-Key-Pins: The site specified a header that did not include " +
+ "a matching pin."
+ }, hud);
+
+ Services.prefs.setBoolPref(NON_BUILTIN_ROOT_PREF, false);
+
+ yield* checkForMessage({
+ url: SJS_URL + "?pinsetDoesNotMatch",
+ name: "Non-built-in root error displayed successfully",
+ text: "Public-Key-Pins: The certificate used by the site was not issued " +
+ "by a certificate in the default root certificate store. To " +
+ "prevent accidental breakage, the specified header was ignored."
+ }, hud);
+});
+
+function* checkForMessage(curTest, hud) {
+ hud.jsterm.clearOutput();
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, curTest.url);
+
+ let results = yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: curTest.name,
+ text: curTest.text,
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_WARNING,
+ objects: true,
+ },
+ ],
+ });
+
+ yield testClickOpenNewTab(hud, results);
+}
+
+function testClickOpenNewTab(hud, results) {
+ let warningNode = results[0].clickableElements[0];
+ ok(warningNode, "link element");
+ ok(warningNode.classList.contains("learn-more-link"), "link class name");
+ return simulateMessageLinkClick(warningNode, LEARN_MORE_URI);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_hsts_invalid-headers.js b/devtools/client/webconsole/test/browser_webconsole_hsts_invalid-headers.js
new file mode 100644
index 000000000..19cedefdb
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_hsts_invalid-headers.js
@@ -0,0 +1,92 @@
+/* -*- 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 errors about invalid HSTS security headers are logged
+// to the web console.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console HSTS invalid " +
+ "header test";
+const SJS_URL = "https://example.com/browser/devtools/client/webconsole/" +
+ "test/test_hsts-invalid-headers.sjs";
+const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Web/Security/" +
+ "HTTP_strict_transport_security" + DOCS_GA_PARAMS;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ yield* checkForMessage({
+ 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);
+
+ yield* checkForMessage({
+ 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);
+
+ yield* checkForMessage({
+ 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);
+
+ yield* checkForMessage({
+ 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);
+
+ yield* checkForMessage({
+ 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);
+
+ yield* checkForMessage({
+ 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);
+});
+
+function* checkForMessage(curTest, hud) {
+ hud.jsterm.clearOutput();
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, curTest.url);
+
+ let results = yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: curTest.name,
+ text: curTest.text,
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_WARNING,
+ objects: true,
+ },
+ ],
+ });
+
+ yield testClickOpenNewTab(hud, results);
+}
+
+function testClickOpenNewTab(hud, results) {
+ let warningNode = results[0].clickableElements[0];
+ ok(warningNode, "link element");
+ ok(warningNode.classList.contains("learn-more-link"), "link class name");
+ return simulateMessageLinkClick(warningNode, LEARN_MORE_URI);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_input_field_focus_on_panel_select.js b/devtools/client/webconsole/test/browser_webconsole_input_field_focus_on_panel_select.js
new file mode 100644
index 000000000..2d7fda7f5
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_input_field_focus_on_panel_select.js
@@ -0,0 +1,34 @@
+/* -*- 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/ */
+
+// Test that the JS input field is focused when the user switches back to the
+// web console from other tools, see bug 891581.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<p>hello";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ hud.jsterm.clearOutput();
+
+ is(hud.jsterm.inputNode.hasAttribute("focused"), true,
+ "inputNode should be focused");
+
+ hud.ui.filterBox.focus();
+
+ is(hud.ui.filterBox.hasAttribute("focused"), true,
+ "filterBox should be focused");
+
+ is(hud.jsterm.inputNode.hasAttribute("focused"), false,
+ "inputNode shouldn't be focused");
+
+ yield openInspector();
+ hud = yield openConsole();
+
+ is(hud.jsterm.inputNode.hasAttribute("focused"), true,
+ "inputNode should be focused");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_inspect-parsed-documents.js b/devtools/client/webconsole/test/browser_webconsole_inspect-parsed-documents.js
new file mode 100644
index 000000000..f79ba386f
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_inspect-parsed-documents.js
@@ -0,0 +1,35 @@
+/* -*- 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/ */
+
+// Test that dynamically created (HTML|XML|SVG)Documents can be inspected by
+// clicking on the object in console (bug 1035198).
+
+"use strict";
+
+const TEST_CASES = [
+ {
+ input: '(new DOMParser()).parseFromString("<a />", "text/html")',
+ output: "HTMLDocument",
+ inspectable: true,
+ },
+ {
+ input: '(new DOMParser()).parseFromString("<a />", "application/xml")',
+ output: "XMLDocument",
+ inspectable: true,
+ },
+ {
+ input: '(new DOMParser()).parseFromString("<svg></svg>", "image/svg+xml")',
+ output: "XMLDocument",
+ inspectable: true,
+ },
+];
+
+const TEST_URI = "data:text/html;charset=utf8," +
+ "browser_webconsole_inspect-parsed-documents.js";
+add_task(function* () {
+ let {tab} = yield loadTab(TEST_URI);
+ let hud = yield openConsole(tab);
+ yield checkOutputForInputs(hud, TEST_CASES);
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_js_input_expansion.js b/devtools/client/webconsole/test/browser_webconsole_js_input_expansion.js
new file mode 100644
index 000000000..7d45059fc
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_js_input_expansion.js
@@ -0,0 +1,55 @@
+/* -*- 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 the input box expands as the user types long lines.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ hud.jsterm.clearOutput();
+
+ let input = hud.jsterm.inputNode;
+ input.focus();
+
+ is(input.getAttribute("multiline"), "true", "multiline is enabled");
+ // Tests if the inputNode expands.
+ input.value = "hello\nworld\n";
+ let length = input.value.length;
+ input.selectionEnd = length;
+ input.selectionStart = length;
+ function getHeight() {
+ return input.clientHeight;
+ }
+ let initialHeight = getHeight();
+ // Performs an "d". This will trigger/test for the input event that should
+ // change the "row" attribute of the inputNode.
+ EventUtils.synthesizeKey("d", {});
+ let newHeight = getHeight();
+ ok(initialHeight < newHeight, "Height changed: " + newHeight);
+
+ // Add some more rows. Tests for the 8 row limit.
+ input.value = "row1\nrow2\nrow3\nrow4\nrow5\nrow6\nrow7\nrow8\nrow9\nrow10\n";
+ length = input.value.length;
+ input.selectionEnd = length;
+ input.selectionStart = length;
+ EventUtils.synthesizeKey("d", {});
+ let newerHeight = getHeight();
+
+ ok(newerHeight > newHeight, "height changed: " + newerHeight);
+
+ // Test if the inputNode shrinks again.
+ input.value = "";
+ EventUtils.synthesizeKey("d", {});
+ let height = getHeight();
+ info("height: " + height);
+ info("initialHeight: " + initialHeight);
+ let finalHeightDifference = Math.abs(initialHeight - height);
+ ok(finalHeightDifference <= 1, "height shrank to original size within 1px");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_jsterm.js b/devtools/client/webconsole/test/browser_webconsole_jsterm.js
new file mode 100644
index 000000000..221c96fa6
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_jsterm.js
@@ -0,0 +1,195 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+var jsterm;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ jsterm = hud.jsterm;
+ yield testJSTerm(hud);
+ jsterm = null;
+});
+
+function checkResult(msg, desc) {
+ let def = promise.defer();
+ waitForMessages({
+ webconsole: jsterm.hud.owner,
+ messages: [{
+ name: desc,
+ category: CATEGORY_OUTPUT,
+ }],
+ }).then(([result]) => {
+ let node = [...result.matched][0].querySelector(".message-body");
+ if (typeof msg == "string") {
+ is(node.textContent.trim(), msg,
+ "correct message shown for " + desc);
+ } else if (typeof msg == "function") {
+ ok(msg(node), "correct message shown for " + desc);
+ }
+
+ def.resolve();
+ });
+ return def.promise;
+}
+
+function* testJSTerm(hud) {
+ const HELP_URL = "https://developer.mozilla.org/docs/Tools/" +
+ "Web_Console/Helpers";
+
+ jsterm.clearOutput();
+ yield jsterm.execute("$('#header').getAttribute('id')");
+ yield checkResult('"header"', "$() worked");
+
+ jsterm.clearOutput();
+ yield jsterm.execute("$$('h1').length");
+ yield checkResult("1", "$$() worked");
+
+ jsterm.clearOutput();
+ yield jsterm.execute("$x('.//*', document.body)[0] == $$('h1')[0]");
+ yield checkResult("true", "$x() worked");
+
+ // no jsterm.clearOutput() here as we clear the output using the clear() fn.
+ yield jsterm.execute("clear()");
+
+ yield waitForSuccess({
+ name: "clear() worked",
+ validator: function () {
+ return jsterm.outputNode.childNodes.length == 0;
+ }
+ });
+
+ jsterm.clearOutput();
+ yield jsterm.execute("keys({b:1})[0] == 'b'");
+ yield checkResult("true", "keys() worked", 1);
+
+ jsterm.clearOutput();
+ yield jsterm.execute("values({b:1})[0] == 1");
+ yield checkResult("true", "values() worked", 1);
+
+ jsterm.clearOutput();
+
+ let openedLinks = 0;
+ let oldOpenLink = hud.openLink;
+ hud.openLink = (url) => {
+ if (url == HELP_URL) {
+ openedLinks++;
+ }
+ };
+
+ yield jsterm.execute("help()");
+ yield jsterm.execute("help");
+ yield jsterm.execute("?");
+
+ let output = jsterm.outputNode.querySelector(".message[category='output']");
+ ok(!output, "no output for help() calls");
+ is(openedLinks, 3, "correct number of pages opened by the help calls");
+ hud.openLink = oldOpenLink;
+
+ jsterm.clearOutput();
+ yield jsterm.execute("pprint({b:2, a:1})");
+ yield checkResult("\" b: 2\n a: 1\"", "pprint()");
+
+ // check instanceof correctness, bug 599940
+ jsterm.clearOutput();
+ yield jsterm.execute("[] instanceof Array");
+ yield checkResult("true", "[] instanceof Array == true");
+
+ jsterm.clearOutput();
+ yield jsterm.execute("({}) instanceof Object");
+ yield checkResult("true", "({}) instanceof Object == true");
+
+ // check for occurrences of Object XRayWrapper, bug 604430
+ jsterm.clearOutput();
+ yield jsterm.execute("document");
+ yield checkResult(function (node) {
+ return node.textContent.search(/\[object xraywrapper/i) == -1;
+ }, "document - no XrayWrapper");
+
+ // check that pprint(window) and keys(window) don't throw, bug 608358
+ jsterm.clearOutput();
+ yield jsterm.execute("pprint(window)");
+ yield checkResult(null, "pprint(window)");
+
+ jsterm.clearOutput();
+ yield jsterm.execute("keys(window)");
+ yield checkResult(null, "keys(window)");
+
+ // bug 614561
+ jsterm.clearOutput();
+ yield jsterm.execute("pprint('hi')");
+ yield checkResult("\" 0: \"h\"\n 1: \"i\"\"", "pprint('hi')");
+
+ // check that pprint(function) shows function source, bug 618344
+ jsterm.clearOutput();
+ yield jsterm.execute("pprint(function() { var someCanaryValue = 42; })");
+ yield checkResult(function (node) {
+ return node.textContent.indexOf("someCanaryValue") > -1;
+ }, "pprint(function) shows source");
+
+ // check that an evaluated null produces "null", bug 650780
+ jsterm.clearOutput();
+ yield jsterm.execute("null");
+ yield checkResult("null", "null is null");
+
+ jsterm.clearOutput();
+ yield jsterm.execute("undefined");
+ yield checkResult("undefined", "undefined is printed");
+
+ // check that thrown strings produce error messages,
+ // and the message text matches that of a stringified error object
+ // bug 1099071
+ jsterm.clearOutput();
+ yield jsterm.execute("throw '';");
+ yield checkResult((node) => {
+ return node.closest(".message").getAttribute("severity") === "error" &&
+ node.textContent === new Error("").toString();
+ }, "thrown empty string generates error message");
+
+ jsterm.clearOutput();
+ yield jsterm.execute("throw 'tomatoes';");
+ yield checkResult((node) => {
+ return node.closest(".message").getAttribute("severity") === "error" &&
+ node.textContent === new Error("tomatoes").toString();
+ }, "thrown non-empty string generates error message");
+
+ jsterm.clearOutput();
+ yield jsterm.execute("throw { foo: 'bar' };");
+ yield checkResult((node) => {
+ return node.closest(".message").getAttribute("severity") === "error" &&
+ node.textContent === Object.prototype.toString();
+ }, "thrown object generates error message");
+
+ // check that errors with entires in errordocs.js display links
+ // alongside their messages.
+ const ErrorDocs = require("devtools/server/actors/errordocs");
+
+ const ErrorDocStatements = {
+ "JSMSG_BAD_RADIX": "(42).toString(0);",
+ "JSMSG_BAD_ARRAY_LENGTH": "([]).length = -1",
+ "JSMSG_NEGATIVE_REPETITION_COUNT": "'abc'.repeat(-1);",
+ "JSMSG_BAD_FORMAL": "var f = Function('x y', 'return x + y;');",
+ "JSMSG_PRECISION_RANGE": "77.1234.toExponential(-1);",
+ };
+
+ for (let errorMessageName of Object.keys(ErrorDocStatements)) {
+ let title = ErrorDocs.GetURL({ errorMessageName }).split("?")[0];
+
+ jsterm.clearOutput();
+ yield jsterm.execute(ErrorDocStatements[errorMessageName]);
+ yield checkResult((node) => {
+ return node.parentNode.getElementsByTagName("a")[0].title == title;
+ }, `error links to ${title}`);
+ }
+
+ // Ensure that dom errors, with error numbers outside of the range
+ // of valid js.msg errors, don't cause crashes (bug 1270721).
+ yield jsterm.execute("new Request('',{redirect:'foo'})");
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_live_filtering_of_message_types.js b/devtools/client/webconsole/test/browser_webconsole_live_filtering_of_message_types.js
new file mode 100644
index 000000000..1dbfa80d9
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_live_filtering_of_message_types.js
@@ -0,0 +1,56 @@
+/* -*- 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 the message type filter checkboxes work.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ hud.jsterm.clearOutput();
+
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ for (let i = 0; i < 50; i++) {
+ content.console.log("foobarz #" + i);
+ }
+ });
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "foobarz #49",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ is(hud.outputNode.children.length, 50, "number of messages");
+
+ hud.setFilterState("log", false);
+ is(countMessageNodes(hud), 0, "the log nodes are hidden when the " +
+ "corresponding filter is switched off");
+
+ hud.setFilterState("log", true);
+ is(countMessageNodes(hud), 50, "the log nodes reappear when the " +
+ "corresponding filter is switched on");
+});
+
+function countMessageNodes(hud) {
+ let messageNodes = hud.outputNode.querySelectorAll(".message");
+ let displayedMessageNodes = 0;
+ let view = hud.iframeWindow;
+ for (let i = 0; i < messageNodes.length; i++) {
+ let computedStyle = view.getComputedStyle(messageNodes[i], null);
+ if (computedStyle.display !== "none") {
+ displayedMessageNodes++;
+ }
+ }
+
+ return displayedMessageNodes;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_live_filtering_on_search_strings.js b/devtools/client/webconsole/test/browser_webconsole_live_filtering_on_search_strings.js
new file mode 100644
index 000000000..d41d5cf2e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_live_filtering_on_search_strings.js
@@ -0,0 +1,96 @@
+/* -*- 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 the text filter box works.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ hud.jsterm.clearOutput();
+
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ for (let i = 0; i < 50; i++) {
+ content.console.log("http://www.example.com/ " + i);
+ }
+ });
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "http://www.example.com/ 49",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ is(hud.outputNode.children.length, 50, "number of messages");
+
+ setStringFilter(hud, "http");
+ isnot(countMessageNodes(hud), 0, "the log nodes are not hidden when the " +
+ "search string is set to \"http\"");
+
+ setStringFilter(hud, "hxxp");
+ is(countMessageNodes(hud), 0, "the log nodes are hidden when the search " +
+ "string is set to \"hxxp\"");
+
+ setStringFilter(hud, "ht tp");
+ isnot(countMessageNodes(hud), 0, "the log nodes are not hidden when the " +
+ "search string is set to \"ht tp\"");
+
+ setStringFilter(hud, " zzzz zzzz ");
+ is(countMessageNodes(hud), 0, "the log nodes are hidden when the search " +
+ "string is set to \" zzzz zzzz \"");
+
+ setStringFilter(hud, "");
+ isnot(countMessageNodes(hud), 0, "the log nodes are not hidden when the " +
+ "search string is removed");
+
+ setStringFilter(hud, "\u9f2c");
+ is(countMessageNodes(hud), 0, "the log nodes are hidden when searching " +
+ "for weasels");
+
+ setStringFilter(hud, "\u0007");
+ is(countMessageNodes(hud), 0, "the log nodes are hidden when searching for " +
+ "the bell character");
+
+ setStringFilter(hud, '"foo"');
+ is(countMessageNodes(hud), 0, "the log nodes are hidden when searching for " +
+ 'the string "foo"');
+
+ setStringFilter(hud, "'foo'");
+ is(countMessageNodes(hud), 0, "the log nodes are hidden when searching for " +
+ "the string 'foo'");
+
+ setStringFilter(hud, "foo\"bar'baz\"boo'");
+ is(countMessageNodes(hud), 0, "the log nodes are hidden when searching for " +
+ "the string \"foo\"bar'baz\"boo'\"");
+});
+
+function countMessageNodes(hud) {
+ let outputNode = hud.outputNode;
+
+ let messageNodes = outputNode.querySelectorAll(".message");
+ let displayedMessageNodes = 0;
+ let view = hud.iframeWindow;
+ for (let i = 0; i < messageNodes.length; i++) {
+ let computedStyle = view.getComputedStyle(messageNodes[i], null);
+ if (computedStyle.display !== "none") {
+ displayedMessageNodes++;
+ }
+ }
+
+ return displayedMessageNodes;
+}
+
+function setStringFilter(hud, value) {
+ hud.ui.filterBox.value = value;
+ hud.ui.adjustVisibilityOnSearchStringChange();
+}
+
diff --git a/devtools/client/webconsole/test/browser_webconsole_log_file_filter.js b/devtools/client/webconsole/test/browser_webconsole_log_file_filter.js
new file mode 100644
index 000000000..d5059485f
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_log_file_filter.js
@@ -0,0 +1,83 @@
+/* -*- 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 the text filter box works to filter based on filenames
+// where the logs were generated.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-bug_923281_console_log_filter.html";
+
+var hud;
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ hud = yield openConsole();
+ yield consoleOpened();
+
+ testLiveFilteringOnSearchStrings();
+
+ hud = null;
+});
+
+function consoleOpened() {
+ let console = content.console;
+ console.log("sentinel log");
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "sentinel log",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG
+ }],
+ });
+}
+
+function testLiveFilteringOnSearchStrings() {
+ is(hud.outputNode.children.length, 4, "number of messages");
+
+ setStringFilter("random");
+ is(countMessageNodes(), 1, "the log nodes not containing string " +
+ "\"random\" are hidden");
+
+ setStringFilter("test2.js");
+ is(countMessageNodes(), 2, "show only log nodes containing string " +
+ "\"test2.js\" or log nodes created from files with filename " +
+ "containing \"test2.js\" as substring.");
+
+ setStringFilter("test1");
+ is(countMessageNodes(), 2, "show only log nodes containing string " +
+ "\"test1\" or log nodes created from files with filename " +
+ "containing \"test1\" as substring.");
+
+ setStringFilter("");
+ is(countMessageNodes(), 4, "show all log nodes on setting filter string " +
+ "as \"\".");
+}
+
+function countMessageNodes() {
+ let outputNode = hud.outputNode;
+
+ let messageNodes = outputNode.querySelectorAll(".message");
+ content.console.log(messageNodes.length);
+ let displayedMessageNodes = 0;
+ let view = hud.iframeWindow;
+ for (let i = 0; i < messageNodes.length; i++) {
+ let computedStyle = view.getComputedStyle(messageNodes[i], null);
+ if (computedStyle.display !== "none") {
+ displayedMessageNodes++;
+ }
+ }
+
+ return displayedMessageNodes;
+}
+
+function setStringFilter(value) {
+ hud.ui.filterBox.value = value;
+ hud.ui.adjustVisibilityOnSearchStringChange();
+}
+
diff --git a/devtools/client/webconsole/test/browser_webconsole_message_node_id.js b/devtools/client/webconsole/test/browser_webconsole_message_node_id.js
new file mode 100644
index 000000000..bec657740
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_message_node_id.js
@@ -0,0 +1,28 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ hud.jsterm.execute("console.log('a log message')");
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "a log message",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ let msg = [...result.matched][0];
+ ok(msg.getAttribute("id"), "log message has an ID");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_multiline_input.js b/devtools/client/webconsole/test/browser_webconsole_multiline_input.js
new file mode 100644
index 000000000..7285c2127
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_multiline_input.js
@@ -0,0 +1,70 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of 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/test-console.html";
+
+let 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 },
+];
+let 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(function* () {
+ let { tab, browser } = yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ let inputNode = hud.jsterm.inputNode;
+
+ for (let test of SHOULD_ENTER_MULTILINE) {
+ hud.jsterm.setInputValue(test.input);
+ EventUtils.synthesizeKey("VK_RETURN", { shiftKey: test.shiftKey });
+ let inputValue = hud.jsterm.getInputValue();
+ is(inputNode.selectionStart, inputNode.selectionEnd,
+ "selection is collapsed");
+ is(inputNode.selectionStart, inputValue.length,
+ "caret at end of multiline input");
+ let inputWithNewline = test.input + "\n";
+ is(inputValue, inputWithNewline, "Input value is correct");
+ }
+
+ for (let test of SHOULD_EXECUTE) {
+ hud.jsterm.setInputValue(test.input);
+ EventUtils.synthesizeKey("VK_RETURN", { shiftKey: test.shiftKey });
+ let inputValue = hud.jsterm.getInputValue();
+ is(inputNode.selectionStart, 0, "selection starts/ends at 0");
+ is(inputNode.selectionEnd, 0, "selection starts/ends at 0");
+ is(inputValue, "", "Input value is cleared");
+ }
+
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_netlogging.js b/devtools/client/webconsole/test/browser_webconsole_netlogging.js
new file mode 100644
index 000000000..63730c9b4
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_netlogging.js
@@ -0,0 +1,139 @@
+/* -*- 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 response logging for different request types.
+
+"use strict";
+
+// This test runs very slowly on linux32 debug - bug 1269977
+requestLongerTimeout(2);
+
+const TEST_NETWORK_REQUEST_URI =
+ "http://example.com/browser/devtools/client/webconsole/test/" +
+ "test-network-request.html";
+
+const TEST_DATA_JSON_CONTENT =
+ '{ id: "test JSON data", myArray: [ "foo", "bar", "baz", "biff" ] }';
+
+const PAGE_REQUEST_PREDICATE =
+ ({ request }) => request.url.endsWith("test-network-request.html");
+
+const TEST_DATA_REQUEST_PREDICATE =
+ ({ request }) => request.url.endsWith("test-data.json");
+
+add_task(function* testPageLoad() {
+ // Enable logging in the UI. Not needed to pass test but makes it easier
+ // to debug interactively.
+ yield new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({"set":
+ [["devtools.webconsole.filter.networkinfo", true]
+ ]}, resolve);
+ });
+
+ let finishedRequest = waitForFinishedRequest(PAGE_REQUEST_PREDICATE);
+ let hud = yield loadPageAndGetHud(TEST_NETWORK_REQUEST_URI);
+ let request = yield finishedRequest;
+
+ ok(request, "Page load was logged");
+
+ let client = hud.ui.webConsoleClient;
+ let args = [request.actor];
+ const postData = yield getPacket(client, "getRequestPostData", args);
+ const responseContent = yield getPacket(client, "getResponseContent", args);
+
+ is(request.request.url, TEST_NETWORK_REQUEST_URI,
+ "Logged network entry is page load");
+ is(request.request.method, "GET", "Method is correct");
+ ok(!postData.postData.text, "No request body was stored");
+ ok(!postData.postDataDiscarded,
+ "Request body was not discarded");
+ is(responseContent.content.text.indexOf("<!DOCTYPE HTML>"), 0,
+ "Response body's beginning is okay");
+
+ yield closeTabAndToolbox();
+});
+
+add_task(function* testXhrGet() {
+ let hud = yield loadPageAndGetHud(TEST_NETWORK_REQUEST_URI);
+
+ let finishedRequest = waitForFinishedRequest(TEST_DATA_REQUEST_PREDICATE);
+ content.wrappedJSObject.testXhrGet();
+ let request = yield finishedRequest;
+
+ ok(request, "testXhrGet() was logged");
+
+ let client = hud.ui.webConsoleClient;
+ let args = [request.actor];
+ const postData = yield getPacket(client, "getRequestPostData", args);
+ const responseContent = yield getPacket(client, "getResponseContent", args);
+
+ is(request.request.method, "GET", "Method is correct");
+ ok(!postData.postData.text, "No request body was sent");
+ ok(!postData.postDataDiscarded,
+ "Request body was not discarded");
+ is(responseContent.content.text, TEST_DATA_JSON_CONTENT,
+ "Response is correct");
+
+ yield closeTabAndToolbox();
+});
+
+add_task(function* testXhrPost() {
+ let hud = yield loadPageAndGetHud(TEST_NETWORK_REQUEST_URI);
+
+ let finishedRequest = waitForFinishedRequest(TEST_DATA_REQUEST_PREDICATE);
+ content.wrappedJSObject.testXhrPost();
+ let request = yield finishedRequest;
+
+ ok(request, "testXhrPost() was logged");
+
+ let client = hud.ui.webConsoleClient;
+ let args = [request.actor];
+ const postData = yield getPacket(client, "getRequestPostData", args);
+ const responseContent = yield getPacket(client, "getResponseContent", args);
+
+ is(request.request.method, "POST", "Method is correct");
+ is(postData.postData.text, "Hello world!", "Request body was logged");
+ is(responseContent.content.text, TEST_DATA_JSON_CONTENT,
+ "Response is correct");
+
+ yield closeTabAndToolbox();
+});
+
+add_task(function* testFormSubmission() {
+ let pageLoadRequestFinished = waitForFinishedRequest(PAGE_REQUEST_PREDICATE);
+ let hud = yield loadPageAndGetHud(TEST_NETWORK_REQUEST_URI);
+
+ info("Waiting for the page load to be finished.");
+ yield pageLoadRequestFinished;
+
+ // The form POSTs to the page URL but over https (page over http).
+ let finishedRequest = waitForFinishedRequest(PAGE_REQUEST_PREDICATE);
+ ContentTask.spawn(gBrowser.selectedBrowser, { }, `function()
+ {
+ let form = content.document.querySelector("form");
+ form.submit();
+ }`);
+ let request = yield finishedRequest;
+
+ ok(request, "testFormSubmission() was logged");
+
+ let client = hud.ui.webConsoleClient;
+ let args = [request.actor];
+ const postData = yield getPacket(client, "getRequestPostData", args);
+ const responseContent = yield getPacket(client, "getResponseContent", args);
+
+ is(request.request.method, "POST", "Method is correct");
+ isnot(postData.postData.text
+ .indexOf("Content-Type: application/x-www-form-urlencoded"), -1,
+ "Content-Type is correct");
+ isnot(postData.postData.text
+ .indexOf("Content-Length: 20"), -1, "Content-length is correct");
+ isnot(postData.postData.text
+ .indexOf("name=foo+bar&age=144"), -1, "Form data is correct");
+ is(responseContent.content.text.indexOf("<!DOCTYPE HTML>"), 0,
+ "Response body's beginning is okay");
+
+ yield closeTabAndToolbox();
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_netlogging_basic.js b/devtools/client/webconsole/test/browser_webconsole_netlogging_basic.js
new file mode 100644
index 000000000..c6fa12401
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_netlogging_basic.js
@@ -0,0 +1,44 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the page's resources are displayed in the console as they're
+// loaded
+
+"use strict";
+
+const TEST_NETWORK_URI = "http://example.com/browser/devtools/client/" +
+ "webconsole/test/test-network.html" + "?_date=" +
+ Date.now();
+
+add_task(function* () {
+ yield loadTab("data:text/html;charset=utf-8,Web Console basic network " +
+ "logging test");
+ let hud = yield openConsole();
+
+ yield BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_NETWORK_URI);
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "running network console",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "test-network.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "testscript.js",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "test-image.png",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ }],
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_netlogging_panel.js b/devtools/client/webconsole/test/browser_webconsole_netlogging_panel.js
new file mode 100644
index 000000000..b44b49453
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_netlogging_panel.js
@@ -0,0 +1,30 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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.
+
+"use strict";
+
+const TEST_NETWORK_REQUEST_URI =
+ "http://example.com/browser/devtools/client/webconsole/test/" +
+ "test-network-request.html";
+
+add_task(function* () {
+ let finishedRequest = waitForFinishedRequest(({ request }) => {
+ return request.url.endsWith("test-network-request.html");
+ });
+
+ const hud = yield loadPageAndGetHud(TEST_NETWORK_REQUEST_URI);
+ let request = yield finishedRequest;
+
+ yield hud.ui.openNetworkPanel(request.actor);
+ let toolbox = gDevTools.getToolbox(hud.target);
+ is(toolbox.currentToolId, "netmonitor", "Network panel was opened");
+ let panel = toolbox.getCurrentPanel();
+ let selected = panel.panelWin.NetMonitorView.RequestsMenu.selectedItem;
+ is(selected.attachment.method, request.request.method,
+ "The correct request is selected");
+ is(selected.attachment.url, request.request.url,
+ "The correct request is definitely selected");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_netlogging_reset_filter.js b/devtools/client/webconsole/test/browser_webconsole_netlogging_reset_filter.js
new file mode 100644
index 000000000..265bc7c00
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_netlogging_reset_filter.js
@@ -0,0 +1,95 @@
+/* -*- 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 network log messages bring up the network panel and select the
+// right request even if it was previously filtered off.
+
+"use strict";
+
+const TEST_FILE_URI =
+ "http://example.com/browser/devtools/client/webconsole/test/" +
+ "test-network.html";
+const TEST_URI = "data:text/html;charset=utf8,<p>test file URI";
+
+var hud;
+
+add_task(function* () {
+ let Actions = require("devtools/client/netmonitor/actions/index");
+
+ let requests = [];
+ let { browser } = yield loadTab(TEST_URI);
+
+ yield pushPrefEnv();
+ hud = yield openConsole();
+ hud.jsterm.clearOutput();
+
+ HUDService.lastFinishedRequest.callback = request => requests.push(request);
+
+ let loaded = loadBrowser(browser);
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_FILE_URI);
+ yield loaded;
+
+ yield testMessages();
+ let htmlRequest = requests.find(e => e.request.url.endsWith("html"));
+ ok(htmlRequest, "htmlRequest was a html");
+
+ yield hud.ui.openNetworkPanel(htmlRequest.actor);
+ let toolbox = gDevTools.getToolbox(hud.target);
+ is(toolbox.currentToolId, "netmonitor", "Network panel was opened");
+
+ let panel = toolbox.getCurrentPanel();
+ let selected = panel.panelWin.NetMonitorView.RequestsMenu.selectedItem;
+ is(selected.attachment.method, htmlRequest.request.method,
+ "The correct request is selected");
+ is(selected.attachment.url, htmlRequest.request.url,
+ "The correct request is definitely selected");
+
+ // Filter out the HTML request.
+ panel.panelWin.gStore.dispatch(Actions.toggleFilterType("js"));
+
+ yield toolbox.selectTool("webconsole");
+ is(toolbox.currentToolId, "webconsole", "Web console was selected");
+ yield hud.ui.openNetworkPanel(htmlRequest.actor);
+
+ panel.panelWin.NetMonitorView.RequestsMenu.selectedItem;
+ is(selected.attachment.method, htmlRequest.request.method,
+ "The correct request is selected");
+ is(selected.attachment.url, htmlRequest.request.url,
+ "The correct request is definitely selected");
+
+ // All tests are done. Shutdown.
+ HUDService.lastFinishedRequest.callback = null;
+ htmlRequest = browser = requests = hud = null;
+});
+
+function testMessages() {
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "running network console logging tests",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "test-network.html",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "testscript.js",
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ }],
+ });
+}
+
+function pushPrefEnv() {
+ let deferred = promise.defer();
+ let options = {
+ set: [["devtools.webconsole.filter.networkinfo", true]]
+ };
+ SpecialPowers.pushPrefEnv(options, deferred.resolve);
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_notifications.js b/devtools/client/webconsole/test/browser_webconsole_notifications.js
new file mode 100644
index 000000000..4bda9192f
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_notifications.js
@@ -0,0 +1,77 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>Web Console test for " +
+ "notifications";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let consoleOpened = promise.defer();
+ let gotEvents = waitForEvents(consoleOpened.promise);
+ yield openConsole().then(() => {
+ consoleOpened.resolve();
+ });
+
+ yield gotEvents;
+});
+
+function waitForEvents(onConsoleOpened) {
+ let deferred = promise.defer();
+
+ function webConsoleCreated(id) {
+ Services.obs.removeObserver(observer, "web-console-created");
+ ok(HUDService.getHudReferenceById(id), "We have a hud reference");
+ content.wrappedJSObject.console.log("adding a log message");
+ }
+
+ function webConsoleDestroyed(id) {
+ Services.obs.removeObserver(observer, "web-console-destroyed");
+ ok(!HUDService.getHudReferenceById(id), "We do not have a hud reference");
+ executeSoon(deferred.resolve);
+ }
+
+ function webConsoleMessage(id, nodeID) {
+ Services.obs.removeObserver(observer, "web-console-message-created");
+ ok(id, "we have a console ID");
+ is(typeof nodeID, "string", "message node id is a string");
+ onConsoleOpened.then(closeConsole);
+ }
+
+ let observer = {
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+ observe: function observe(subject, topic, data) {
+ subject = subject.QueryInterface(Ci.nsISupportsString);
+
+ switch (topic) {
+ case "web-console-created":
+ webConsoleCreated(subject.data);
+ break;
+ case "web-console-destroyed":
+ webConsoleDestroyed(subject.data);
+ break;
+ case "web-console-message-created":
+ webConsoleMessage(subject, data);
+ break;
+ default:
+ break;
+ }
+ },
+
+ init: function init() {
+ Services.obs.addObserver(this, "web-console-created", false);
+ Services.obs.addObserver(this, "web-console-destroyed", false);
+ Services.obs.addObserver(this, "web-console-message-created", false);
+ }
+ };
+
+ observer.init();
+
+ return deferred.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_open-links-without-callback.js b/devtools/client/webconsole/test/browser_webconsole_open-links-without-callback.js
new file mode 100644
index 000000000..ae11305de
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_open-links-without-callback.js
@@ -0,0 +1,54 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that if a link without an onclick callback is clicked the link is
+// opened in a new tab and no exception occurs (bug 999236).
+
+"use strict";
+
+function test() {
+ function* runner() {
+ const TEST_EVAL_STRING = "document";
+ const TEST_PAGE_URI = "http://example.com/browser/devtools/client/" +
+ "webconsole/test/test-console.html";
+ const {tab} = yield loadTab(TEST_PAGE_URI);
+ const hud = yield openConsole(tab);
+
+ hud.jsterm.execute(TEST_EVAL_STRING);
+
+ const EXPECTED_OUTPUT = new RegExp("HTMLDocument \.+");
+
+ let messages = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "JS eval output",
+ text: EXPECTED_OUTPUT,
+ category: CATEGORY_OUTPUT,
+ }],
+ });
+
+ let messageNode = messages[0].matched.values().next().value;
+
+ // The correct anchor is second in the message node; the first anchor has
+ // class .cm-variable. Ignore the first one by not matching anchors that
+ // have the class .cm-variable.
+ let urlNode = messageNode.querySelector("a:not(.cm-variable)");
+
+ let linkOpened = false;
+ let oldOpenUILinkIn = window.openUILinkIn;
+ window.openUILinkIn = function (aLink) {
+ if (aLink == TEST_PAGE_URI) {
+ linkOpened = true;
+ }
+ };
+
+ EventUtils.synthesizeMouseAtCenter(urlNode, {}, hud.iframeWindow);
+
+ ok(linkOpened, "Clicking the URL opens the desired page");
+ window.openUILinkIn = oldOpenUILinkIn;
+ }
+
+ Task.spawn(runner).then(finishTest);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_01.js b/devtools/client/webconsole/test/browser_webconsole_output_01.js
new file mode 100644
index 000000000..c75577ea7
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_01.js
@@ -0,0 +1,122 @@
+/* -*- 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/ */
+
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+
+"use strict";
+
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("null");
+
+// Test the webconsole output for various types of objects.
+
+const TEST_URI = "data:text/html;charset=utf8,test for console output - 01";
+
+var {DebuggerServer} = require("devtools/server/main");
+
+var longString = (new Array(DebuggerServer.LONG_STRING_LENGTH + 4)).join("a");
+var initialString = longString.substring(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH);
+
+var inputTests = [
+ // 0
+ {
+ input: "'hello \\nfrom \\rthe \\\"string world!'",
+ output: "\"hello \nfrom \rthe \"string world!\"",
+ consoleOutput: "hello \nfrom \rthe \"string world!",
+ },
+
+ // 1
+ {
+ // unicode test
+ input: "'\xFA\u1E47\u0129\xE7\xF6d\xEA \u021B\u0115\u0219\u0165'",
+ output: "\"\xFA\u1E47\u0129\xE7\xF6d\xEA \u021B\u0115\u0219\u0165\"",
+ consoleOutput: "\xFA\u1E47\u0129\xE7\xF6d\xEA \u021B\u0115\u0219\u0165",
+ },
+
+ // 2
+ {
+ input: "'" + longString + "'",
+ output: '"' + initialString + "\"[\u2026]",
+ consoleOutput: initialString + "[\u2026]",
+ printOutput: initialString,
+ },
+
+ // 3
+ {
+ input: "''",
+ output: '""',
+ consoleOutput: "",
+ printOutput: '""',
+ },
+
+ // 4
+ {
+ input: "0",
+ output: "0",
+ },
+
+ // 5
+ {
+ input: "'0'",
+ output: '"0"',
+ consoleOutput: "0",
+ },
+
+ // 6
+ {
+ input: "42",
+ output: "42",
+ },
+
+ // 7
+ {
+ input: "'42'",
+ output: '"42"',
+ consoleOutput: "42",
+ },
+
+ // 8
+ {
+ input: "/foobar/",
+ output: "/foobar/",
+ inspectable: true,
+ },
+
+ // 9
+ {
+ input: "Symbol()",
+ output: "Symbol()"
+ },
+
+ // 10
+ {
+ input: "Symbol('foo')",
+ output: "Symbol(foo)"
+ },
+
+ // 11
+ {
+ input: "Symbol.iterator",
+ output: "Symbol(Symbol.iterator)"
+ },
+];
+
+longString = initialString = null;
+
+function test() {
+ requestLongerTimeout(2);
+
+ Task.spawn(function* () {
+ let {tab} = yield loadTab(TEST_URI);
+ let hud = yield openConsole(tab);
+ return checkOutputForInputs(hud, inputTests);
+ }).then(finishUp);
+}
+
+function finishUp() {
+ longString = initialString = inputTests = null;
+ finishTest();
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_02.js b/devtools/client/webconsole/test/browser_webconsole_output_02.js
new file mode 100644
index 000000000..8018669a9
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_02.js
@@ -0,0 +1,183 @@
+/* -*- 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/ */
+
+// Test the webconsole output for various types of objects.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-output-02.html";
+
+var inputTests = [
+ // 0 - native named function
+ {
+ input: "document.getElementById",
+ output: "function getElementById()",
+ printOutput: "function getElementById() {\n [native code]\n}",
+ inspectable: true,
+ variablesViewLabel: "getElementById()",
+ },
+
+ // 1 - anonymous function
+ {
+ input: "(function() { return 42; })",
+ output: "function ()",
+ printOutput: "function () { return 42; }",
+ suppressClick: true
+ },
+
+ // 2 - named function
+ {
+ input: "window.testfn1",
+ output: "function testfn1()",
+ printOutput: "function testfn1() { return 42; }",
+ suppressClick: true
+ },
+
+ // 3 - anonymous function, but spidermonkey gives us an inferred name.
+ {
+ input: "testobj1.testfn2",
+ output: "function testobj1.testfn2()",
+ printOutput: "function () { return 42; }",
+ suppressClick: true
+ },
+
+ // 4 - named function with custom display name
+ {
+ input: "window.testfn3",
+ output: "function testfn3DisplayName()",
+ printOutput: "function testfn3() { return 42; }",
+ suppressClick: true
+ },
+
+ // 5 - basic array
+ {
+ input: "window.array1",
+ output: 'Array [ 1, 2, 3, "a", "b", "c", "4", "5" ]',
+ printOutput: "1,2,3,a,b,c,4,5",
+ inspectable: true,
+ variablesViewLabel: "Array[8]",
+ },
+
+ // 6 - array with objects
+ {
+ input: "window.array2",
+ output: 'Array [ "a", HTMLDocument \u2192 test-console-output-02.html, ' +
+ "<body>, DOMStringMap[0], DOMTokenList[0] ]",
+ printOutput: '"a,[object HTMLDocument],[object HTMLBodyElement],' +
+ '[object DOMStringMap],"',
+ inspectable: true,
+ variablesViewLabel: "Array[5]",
+ },
+
+ // 7 - array with more than 10 elements
+ {
+ input: "window.array3",
+ output: "Array [ 1, Window \u2192 test-console-output-02.html, null, " +
+ '"a", "b", undefined, false, "", -Infinity, ' +
+ "testfn3DisplayName(), 3 more\u2026 ]",
+ printOutput: '"1,[object Window],,a,b,,false,,-Infinity,' +
+ 'function testfn3() { return 42; },[object Object],foo,bar"',
+ inspectable: true,
+ variablesViewLabel: "Array[13]",
+ },
+
+ // 8 - array with holes and a cyclic reference
+ {
+ input: "window.array4",
+ output: 'Array [ <5 empty slots>, "test", Array[7] ]',
+ printOutput: '",,,,,test,"',
+ inspectable: true,
+ variablesViewLabel: "Array[7]",
+ },
+
+ // 9
+ {
+ input: "window.typedarray1",
+ output: "Int32Array [ 1, 287, 8651, 40983, 8754 ]",
+ printOutput: "1,287,8651,40983,8754",
+ inspectable: true,
+ variablesViewLabel: "Int32Array[5]",
+ },
+
+ // 10 - Set with cyclic reference
+ {
+ input: "window.set1",
+ output: 'Set [ 1, 2, null, Array[13], "a", "b", undefined, <head>, ' +
+ "Set[9] ]",
+ printOutput: "[object Set]",
+ inspectable: true,
+ variablesViewLabel: "Set[9]",
+ },
+
+ // 11 - Object with cyclic reference and a getter
+ {
+ input: "window.testobj2",
+ output: 'Object { a: "b", c: "d", e: 1, f: "2", foo: Object, ' +
+ "bar: Object, getterTest: Getter }",
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object",
+ },
+
+ // 12 - Object with more than 10 properties
+ {
+ input: "window.testobj3",
+ output: 'Object { a: "b", c: "d", e: 1, f: "2", g: true, h: null, ' +
+ 'i: undefined, j: "", k: StyleSheetList[0], l: NodeList[5], ' +
+ "2 more\u2026 }",
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object",
+ },
+
+ // 13 - Object with a non-enumerable property that we do not show
+ {
+ input: "window.testobj4",
+ output: 'Object { a: "b", c: "d", 1 more\u2026 }',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object",
+ },
+
+ // 14 - Map with cyclic references
+ {
+ input: "window.map1",
+ output: 'Map { a: "b", HTMLCollection[2]: Object, Map[3]: Set[9] }',
+ printOutput: "[object Map]",
+ inspectable: true,
+ variablesViewLabel: "Map[3]",
+ },
+
+ // 15 - WeakSet
+ {
+ input: "window.weakset",
+ // Need a regexp because the order may vary.
+ output: new RegExp("WeakSet \\[ (String, <head>|<head>, String) \\]"),
+ printOutput: "[object WeakSet]",
+ inspectable: true,
+ variablesViewLabel: "WeakSet[2]",
+ },
+
+ // 16 - WeakMap
+ {
+ input: "window.weakmap",
+ // Need a regexp because the order may vary.
+ output: new RegExp("WeakMap { (String: 23, HTMLCollection\\[2\\]: Object|HTMLCollection\\[2\\]: Object, String: 23) }"),
+ printOutput: "[object WeakMap]",
+ inspectable: true,
+ variablesViewLabel: "WeakMap[2]",
+ },
+];
+
+function test() {
+ requestLongerTimeout(2);
+ Task.spawn(function* () {
+ const {tab} = yield loadTab(TEST_URI);
+ const hud = yield openConsole(tab);
+ yield checkOutputForInputs(hud, inputTests);
+ inputTests = null;
+ }).then(finishTest);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_03.js b/devtools/client/webconsole/test/browser_webconsole_output_03.js
new file mode 100644
index 000000000..bd77c2a4d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_03.js
@@ -0,0 +1,168 @@
+/* -*- 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/ */
+
+// Test the webconsole output for various types of objects.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-output-03.html";
+
+var inputTests = [
+
+ // 0
+ {
+ input: "document",
+ output: "HTMLDocument \u2192 " + TEST_URI,
+ printOutput: "[object HTMLDocument]",
+ inspectable: true,
+ noClick: true,
+ },
+
+ // 1
+ {
+ input: "window",
+ output: "Window \u2192 " + TEST_URI,
+ printOutput: "[object Window",
+ inspectable: true,
+ noClick: true,
+ },
+
+ // 2
+ {
+ input: "document.body",
+ output: "<body>",
+ printOutput: "[object HTMLBodyElement]",
+ inspectable: true,
+ noClick: true,
+ },
+
+ // 3
+ {
+ input: "document.body.dataset",
+ output: "DOMStringMap { }",
+ printOutput: "[object DOMStringMap]",
+ inspectable: true,
+ variablesViewLabel: "DOMStringMap[0]",
+ },
+
+ // 4
+ {
+ input: "document.body.classList",
+ output: "DOMTokenList [ ]",
+ printOutput: '""',
+ inspectable: true,
+ variablesViewLabel: "DOMTokenList[0]",
+ },
+
+ // 5
+ {
+ input: "window.location.href",
+ output: '"' + TEST_URI + '"',
+ noClick: true,
+ },
+
+ // 6
+ {
+ input: "window.location",
+ output: "Location \u2192 " + TEST_URI,
+ printOutput: TEST_URI,
+ inspectable: true,
+ variablesViewLabel: "Location \u2192 test-console-output-03.html",
+ },
+
+ // 7
+ {
+ input: "document.body.attributes",
+ output: "NamedNodeMap [ ]",
+ printOutput: "[object NamedNodeMap]",
+ inspectable: true,
+ variablesViewLabel: "NamedNodeMap[0]",
+ },
+
+ // 8
+ {
+ input: "document.styleSheets",
+ output: "StyleSheetList [ ]",
+ printOutput: "[object StyleSheetList",
+ inspectable: true,
+ variablesViewLabel: "StyleSheetList[0]",
+ },
+
+ // 9
+ {
+ input: "testBodyClassName()",
+ output: '<body class="test1 tezt2">',
+ printOutput: "[object HTMLBodyElement]",
+ inspectable: true,
+ noClick: true,
+ },
+
+ // 10
+ {
+ input: "testBodyID()",
+ output: '<body class="test1 tezt2" id="foobarid">',
+ printOutput: "[object HTMLBodyElement]",
+ inspectable: true,
+ noClick: true,
+ },
+
+ // 11
+ {
+ input: "document.body.classList",
+ output: 'DOMTokenList [ "test1", "tezt2" ]',
+ printOutput: '"test1 tezt2"',
+ inspectable: true,
+ variablesViewLabel: "DOMTokenList[2]",
+ },
+
+ // 12
+ {
+ input: "testBodyDataset()",
+ output: '<body class="test1 tezt2" id="foobarid"' +
+ ' data-preview="zuzu&quot;&lt;a&gt;foo">',
+ printOutput: "[object HTMLBodyElement]",
+ inspectable: true,
+ noClick: true,
+ },
+
+ // 13
+ {
+ input: "document.body.dataset",
+ output: 'DOMStringMap { preview: "zuzu"<a>foo" }',
+ printOutput: "[object DOMStringMap]",
+ inspectable: true,
+ variablesViewLabel: "DOMStringMap[1]",
+ },
+
+ // 14
+ {
+ input: "document.body.attributes",
+ output: 'NamedNodeMap [ class="test1 tezt2", id="foobarid", ' +
+ 'data-preview="zuzu&quot;&lt;a&gt;foo" ]',
+ printOutput: "[object NamedNodeMap]",
+ inspectable: true,
+ variablesViewLabel: "NamedNodeMap[3]",
+ },
+
+ // 15
+ {
+ input: "document.body.attributes[0]",
+ output: 'class="test1 tezt2"',
+ printOutput: "[object Attr]",
+ inspectable: true,
+ variablesViewLabel: 'class="test1 tezt2"',
+ },
+];
+
+function test() {
+ requestLongerTimeout(2);
+ Task.spawn(function* () {
+ const {tab} = yield loadTab(TEST_URI);
+ const hud = yield openConsole(tab);
+ yield checkOutputForInputs(hud, inputTests);
+ inputTests = null;
+ }).then(finishTest);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_04.js b/devtools/client/webconsole/test/browser_webconsole_output_04.js
new file mode 100644
index 000000000..d829594a7
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_04.js
@@ -0,0 +1,129 @@
+/* -*- 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/ */
+
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+
+"use strict";
+
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("null");
+
+// Test the webconsole output for various types of objects.
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-output-04.html";
+
+var inputTests = [
+ // 0
+ {
+ input: "testTextNode()",
+ output: '#text "hello world!"',
+ printOutput: "[object Text]",
+ inspectable: true,
+ noClick: true,
+ },
+
+ // 1
+ {
+ input: "testCommentNode()",
+ output: /<!--\s+- Any copyright /,
+ printOutput: "[object Comment]",
+ inspectable: true,
+ noClick: true,
+ },
+
+ // 2
+ {
+ input: "testDocumentFragment()",
+ output: "DocumentFragment [ <div#foo1.bar>, <div#foo3> ]",
+ printOutput: "[object DocumentFragment]",
+ inspectable: true,
+ variablesViewLabel: "DocumentFragment[2]",
+ },
+
+ // 3
+ {
+ input: "testError()",
+ output: "TypeError: window.foobar is not a function\n" +
+ "Stack trace:\n" +
+ "testError@" + TEST_URI + ":44",
+ printOutput: '"TypeError: window.foobar is not a function"',
+ inspectable: true,
+ variablesViewLabel: "TypeError",
+ },
+
+ // 4
+ {
+ input: "testDOMException()",
+ output: `DOMException [SyntaxError: "'foo;()bar!' is not a valid selector"`,
+ printOutput: `"SyntaxError: 'foo;()bar!' is not a valid selector"`,
+ inspectable: true,
+ variablesViewLabel: "SyntaxError",
+ },
+
+ // 5
+ {
+ input: "testCSSStyleDeclaration()",
+ output: 'CSS2Properties { color: "green", font-size: "2em" }',
+ printOutput: "[object CSS2Properties]",
+ inspectable: true,
+ noClick: true,
+ },
+
+ // 6
+ {
+ input: "testStyleSheetList()",
+ output: "StyleSheetList [ CSSStyleSheet ]",
+ printOutput: "[object StyleSheetList",
+ inspectable: true,
+ variablesViewLabel: "StyleSheetList[1]",
+ },
+
+ // 7
+ {
+ input: "document.styleSheets[0]",
+ output: "CSSStyleSheet",
+ printOutput: "[object CSSStyleSheet]",
+ inspectable: true,
+ },
+
+ // 8
+ {
+ input: "document.styleSheets[0].cssRules",
+ output: "CSSRuleList [ CSSStyleRule, CSSMediaRule ]",
+ printOutput: "[object CSSRuleList",
+ inspectable: true,
+ variablesViewLabel: "CSSRuleList[2]",
+ },
+
+ // 9
+ {
+ input: "document.styleSheets[0].cssRules[0]",
+ output: 'CSSStyleRule "p, div"',
+ printOutput: "[object CSSStyleRule",
+ inspectable: true,
+ variablesViewLabel: "CSSStyleRule",
+ },
+
+ // 10
+ {
+ input: "document.styleSheets[0].cssRules[1]",
+ output: 'CSSMediaRule "print"',
+ printOutput: "[object CSSMediaRule",
+ inspectable: true,
+ variablesViewLabel: "CSSMediaRule",
+ },
+];
+
+function test() {
+ requestLongerTimeout(2);
+ Task.spawn(function* () {
+ const {tab} = yield loadTab(TEST_URI);
+ const hud = yield openConsole(tab);
+ yield checkOutputForInputs(hud, inputTests);
+ inputTests = null;
+ }).then(finishTest);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_05.js b/devtools/client/webconsole/test/browser_webconsole_output_05.js
new file mode 100644
index 000000000..53bfd768c
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_05.js
@@ -0,0 +1,177 @@
+/* -*- 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/ */
+
+// Test the webconsole output for various types of objects.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,test for console output - 05";
+const {ELLIPSIS} = require("devtools/shared/l10n");
+
+// March, 1960: The first implementation of Lisp. From Wikipedia:
+//
+// > Lisp was first implemented by Steve Russell on an IBM 704 computer. Russell
+// > had read McCarthy's paper, and realized (to McCarthy's surprise) that the
+// > Lisp eval function could be implemented in machine code. The result was a
+// > working Lisp interpreter which could be used to run Lisp programs, or more
+// > properly, 'evaluate Lisp expressions.'
+var testDate = -310435200000;
+
+var inputTests = [
+ // 0
+ {
+ input: "/foo?b*\\s\"ar/igym",
+ output: "/foo?b*\\s\"ar/gimy",
+ printOutput: "/foo?b*\\s\"ar/gimy",
+ inspectable: true,
+ },
+
+ // 1
+ {
+ input: "null",
+ output: "null",
+ },
+
+ // 2
+ {
+ input: "undefined",
+ output: "undefined",
+ },
+
+ // 3
+ {
+ input: "true",
+ output: "true",
+ },
+
+ // 4
+ {
+ input: "new Boolean(false)",
+ output: "Boolean { false }",
+ printOutput: "false",
+ inspectable: true,
+ variablesViewLabel: "Boolean { false }"
+ },
+
+ // 5
+ {
+ input: "new Date(" + testDate + ")",
+ output: "Date " + (new Date(testDate)).toISOString(),
+ printOutput: (new Date(testDate)).toString(),
+ inspectable: true,
+ },
+
+ // 6
+ {
+ input: "new Date('test')",
+ output: "Invalid Date",
+ printOutput: "Invalid Date",
+ inspectable: true,
+ variablesViewLabel: "Invalid Date",
+ },
+
+ // 7
+ {
+ input: "Date.prototype",
+ output: /Object \{.*\}/,
+ printOutput: "Invalid Date",
+ inspectable: true,
+ variablesViewLabel: "Object",
+ },
+
+ // 8
+ {
+ input: "new Number(43)",
+ output: "Number { 43 }",
+ printOutput: "43",
+ inspectable: true,
+ variablesViewLabel: "Number { 43 }"
+ },
+
+ // 9
+ {
+ input: "new String('hello')",
+ output: /String { "hello", 6 more.* }/,
+ printOutput: "hello",
+ inspectable: true,
+ variablesViewLabel: "String"
+ },
+
+ // 10
+ {
+ input: "(function () { var s = new String('hello'); s.whatever = 23; " +
+ " return s;})()",
+ output: /String { "hello", whatever: 23, 6 more.* }/,
+ printOutput: "hello",
+ inspectable: true,
+ variablesViewLabel: "String"
+ },
+
+ // 11
+ {
+ input: "(function () { var s = new String('hello'); s[8] = 'x'; " +
+ " return s;})()",
+ output: /String { "hello", 8: "x", 6 more.* }/,
+ printOutput: "hello",
+ inspectable: true,
+ variablesViewLabel: "String"
+ },
+
+ // 12
+ {
+ // XXX: Can't test fulfilled and rejected promises, because promises get
+ // settled on the next tick of the event loop.
+ input: "new Promise(function () {})",
+ output: 'Promise { <state>: "pending" }',
+ printOutput: "[object Promise]",
+ inspectable: true,
+ variablesViewLabel: "Promise"
+ },
+
+ // 13
+ {
+ input: "(function () { var p = new Promise(function () {}); " +
+ "p.foo = 1; return p; }())",
+ output: 'Promise { <state>: "pending", foo: 1 }',
+ printOutput: "[object Promise]",
+ inspectable: true,
+ variablesViewLabel: "Promise"
+ },
+
+ // 14
+ {
+ input: "new Object({1: 'this\\nis\\nsupposed\\nto\\nbe\\na\\nvery" +
+ "\\nlong\\nstring\\n,shown\\non\\na\\nsingle\\nline', " +
+ "2: 'a shorter string', 3: 100})",
+ output: '[ <1 empty slot>, "this is supposed to be a very long ' + ELLIPSIS +
+ '", "a shorter string", 100 ]',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object[4]"
+ },
+
+ // 15
+ {
+ input: "new Proxy({a:1},[1,2,3])",
+ output: 'Proxy { <target>: Object, <handler>: Array[3] }',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Proxy"
+ }
+];
+
+function test() {
+ requestLongerTimeout(2);
+ Task.spawn(function* () {
+ let {tab} = yield loadTab(TEST_URI);
+ let hud = yield openConsole(tab);
+ return checkOutputForInputs(hud, inputTests);
+ }).then(finishUp);
+}
+
+function finishUp() {
+ inputTests = testDate = null;
+ finishTest();
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_06.js b/devtools/client/webconsole/test/browser_webconsole_output_06.js
new file mode 100644
index 000000000..ad69b3908
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_06.js
@@ -0,0 +1,283 @@
+/* -*- 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/ */
+
+"use strict";
+
+// Test the webconsole output for various arrays.
+
+const TEST_URI = "data:text/html;charset=utf8,test for console output - 06";
+const {ELLIPSIS} = require("devtools/shared/l10n");
+
+const testStrIn = "SHOW\\nALL\\nOF\\nTHIS\\nON\\nA\\nSINGLE" +
+ "\\nLINE ONLY. ESCAPE ALL NEWLINE";
+const testStrOut = "SHOW ALL OF THIS ON A SINGLE LINE O" + ELLIPSIS;
+
+var inputTests = [
+ // 1 - array with empty slots only
+ {
+ input: "Array(5)",
+ output: "Array [ <5 empty slots> ]",
+ printOutput: ",,,,",
+ inspectable: true,
+ variablesViewLabel: "Array[5]",
+ },
+ // 2 - array with one empty slot at the beginning
+ {
+ input: "[,1,2,3]",
+ output: "Array [ <1 empty slot>, 1, 2, 3 ]",
+ printOutput: ",1,2,3",
+ inspectable: true,
+ variablesViewLabel: "Array[4]",
+ },
+ // 3 - array with multiple consecutive empty slots at the beginning
+ {
+ input: "[,,,3,4,5]",
+ output: "Array [ <3 empty slots>, 3, 4, 5 ]",
+ printOutput: ",,,3,4,5",
+ inspectable: true,
+ variablesViewLabel: "Array[6]",
+ },
+ // 4 - array with one empty slot at the middle
+ {
+ input: "[0,1,,3,4,5]",
+ output: "Array [ 0, 1, <1 empty slot>, 3, 4, 5 ]",
+ printOutput: "0,1,,3,4,5",
+ inspectable: true,
+ variablesViewLabel: "Array[6]",
+ },
+ // 5 - array with multiple successive empty slots at the middle
+ {
+ input: "[0,1,,,,5]",
+ output: "Array [ 0, 1, <3 empty slots>, 5 ]",
+ printOutput: "0,1,,,,5",
+ inspectable: true,
+ variablesViewLabel: "Array[6]",
+ },
+ // 6 - array with multiple non successive single empty slots
+ {
+ input: "[0,,2,,4,5]",
+ output: "Array [ 0, <1 empty slot>, 2, <1 empty slot>, 4, 5 ]",
+ printOutput: "0,,2,,4,5",
+ inspectable: true,
+ variablesViewLabel: "Array[6]",
+ },
+ // 7 - array with multiple multi-slot holes
+ {
+ input: "[0,,,3,,,,7,8]",
+ output: "Array [ 0, <2 empty slots>, 3, <3 empty slots>, 7, 8 ]",
+ printOutput: "0,,,3,,,,7,8",
+ inspectable: true,
+ variablesViewLabel: "Array[9]",
+ },
+ // 8 - array with a single slot hole at the end
+ {
+ input: "[0,1,2,3,4,,]",
+ output: "Array [ 0, 1, 2, 3, 4, <1 empty slot> ]",
+ printOutput: "0,1,2,3,4,",
+ inspectable: true,
+ variablesViewLabel: "Array[6]",
+ },
+ // 9 - array with multiple consecutive empty slots at the end
+ {
+ input: "[0,1,2,,,,]",
+ output: "Array [ 0, 1, 2, <3 empty slots> ]",
+ printOutput: "0,1,2,,,",
+ inspectable: true,
+ variablesViewLabel: "Array[6]",
+ },
+
+ // 10 - array with members explicitly set to null
+ {
+ input: "[0,null,null,3,4,5]",
+ output: "Array [ 0, null, null, 3, 4, 5 ]",
+ printOutput: "0,,,3,4,5",
+ inspectable: true,
+ variablesViewLabel: "Array[6]"
+ },
+
+ // 11 - array with members explicitly set to undefined
+ {
+ input: "[0,undefined,undefined,3,4,5]",
+ output: "Array [ 0, undefined, undefined, 3, 4, 5 ]",
+ printOutput: "0,,,3,4,5",
+ inspectable: true,
+ variablesViewLabel: "Array[6]"
+ },
+
+ // 12 - array with long strings as elements
+ {
+ input: '["' + testStrIn + '", "' + testStrIn + '", "' + testStrIn + '"]',
+ output: 'Array [ "' + testStrOut + '", "' + testStrOut + '", "' +
+ testStrOut + '" ]',
+ inspectable: true,
+ printOutput: "SHOW\nALL\nOF\nTHIS\nON\nA\nSINGLE\nLINE ONLY. ESCAPE " +
+ "ALL NEWLINE,SHOW\nALL\nOF\nTHIS\nON\nA\nSINGLE\nLINE ONLY. " +
+ "ESCAPE ALL NEWLINE,SHOW\nALL\nOF\nTHIS\nON\nA\nSINGLE\n" +
+ "LINE ONLY. ESCAPE ALL NEWLINE",
+ variablesViewLabel: "Array[3]"
+ },
+
+ // 13
+ {
+ input: '({0: "a", 1: "b"})',
+ output: 'Object [ "a", "b" ]',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object[2]",
+ },
+
+ // 14
+ {
+ input: '({0: "a", 42: "b"})',
+ output: '[ "a", <9 empty slots>, 33 more\u2026 ]',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object[43]",
+ },
+
+ // 15
+ {
+ input: '({0: "a", 1: "b", 2: "c", 3: "d", 4: "e", 5: "f", 6: "g", ' +
+ '7: "h", 8: "i", 9: "j", 10: "k", 11: "l"})',
+ output: 'Object [ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", ' +
+ "2 more\u2026 ]",
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object[12]",
+ },
+
+ // 16
+ {
+ input: '({0: "a", 1: "b", 2: "c", 3: "d", 4: "e", 5: "f", 6: "g", ' +
+ '7: "h", 8: "i", 9: "j", 10: "k", 11: "l", m: "n"})',
+ output: 'Object { 0: "a", 1: "b", 2: "c", 3: "d", 4: "e", 5: "f", ' +
+ '6: "g", 7: "h", 8: "i", 9: "j", 3 more\u2026 }',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object",
+ },
+
+ // 17
+ {
+ input: '({" ": "a"})',
+ output: 'Object { : "a" }',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object",
+ },
+
+ // 18
+ {
+ input: '({})',
+ output: 'Object { }',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object",
+ },
+
+ // 19
+ {
+ input: '({length: 0})',
+ output: 'Object [ ]',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object[0]",
+ },
+
+ // 20
+ {
+ input: '({length: 1})',
+ output: '[ <1 empty slot> ]',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object[1]",
+ },
+
+ // 21
+ {
+ input: '({0: "a", 1: "b", length: 1})',
+ output: 'Object { 0: "a", 1: "b", length: 1 }',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object",
+ },
+
+ // 22
+ {
+ input: '({0: "a", 1: "b", length: 2})',
+ output: 'Object [ "a", "b" ]',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object[2]",
+ },
+
+ // 23
+ {
+ input: '({0: "a", 1: "b", length: 3})',
+ output: '[ "a", "b", <1 empty slot> ]',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object[3]",
+ },
+
+ // 24
+ {
+ input: '({0: "a", 2: "b", length: 2})',
+ output: 'Object { 0: "a", 2: "b", length: 2 }',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object",
+ },
+
+ // 25
+ {
+ input: '({0: "a", 2: "b", length: 3})',
+ output: '[ "a", <1 empty slot>, "b" ]',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object[3]",
+ },
+
+ // 26
+ {
+ input: '({0: "a", b: "b", length: 1})',
+ output: 'Object { 0: "a", b: "b", length: 1 }',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object",
+ },
+
+ // 27
+ {
+ input: '({0: "a", b: "b", length: 2})',
+ output: 'Object { 0: "a", b: "b", length: 2 }',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object",
+ },
+
+ // 28
+ {
+ input: '({42: "a"})',
+ output: 'Object { 42: "a" }',
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object",
+ },
+];
+
+function test() {
+ requestLongerTimeout(2);
+ Task.spawn(function* () {
+ let {tab} = yield loadTab(TEST_URI);
+ let hud = yield openConsole(tab);
+ return checkOutputForInputs(hud, inputTests);
+ }).then(finishUp);
+}
+
+function finishUp() {
+ inputTests = null;
+ finishTest();
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_copy_newlines.js b/devtools/client/webconsole/test/browser_webconsole_output_copy_newlines.js
new file mode 100644
index 000000000..22de843f9
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_copy_newlines.js
@@ -0,0 +1,72 @@
+/* -*- 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/ */
+
+// Test that multiple messages are copied into the clipboard and that they are
+// separated by new lines. See bug 916997.
+
+"use strict";
+
+add_task(function* () {
+ const TEST_URI = "data:text/html;charset=utf8,<p>hello world, bug 916997";
+ let clipboardValue = "";
+
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+ hud.jsterm.clearOutput();
+
+ let controller = top.document.commandDispatcher
+ .getControllerForCommand("cmd_copy");
+ is(controller.isCommandEnabled("cmd_copy"), false, "cmd_copy is disabled");
+
+ content.console.log("Hello world! bug916997a");
+ content.console.log("Hello world 2! bug916997b");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "Hello world! bug916997a",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }, {
+ text: "Hello world 2! bug916997b",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ hud.ui.output.selectAllMessages();
+ hud.outputNode.focus();
+
+ goUpdateCommand("cmd_copy");
+ controller = top.document.commandDispatcher
+ .getControllerForCommand("cmd_copy");
+ is(controller.isCommandEnabled("cmd_copy"), true, "cmd_copy is enabled");
+
+ let selection = hud.iframeWindow.getSelection() + "";
+ info("selection '" + selection + "'");
+
+ waitForClipboard((str) => {
+ clipboardValue = str;
+ return str.indexOf("bug916997a") > -1 && str.indexOf("bug916997b") > -1;
+ },
+ () => {
+ goDoCommand("cmd_copy");
+ },
+ () => {
+ info("clipboard value '" + clipboardValue + "'");
+ let lines = clipboardValue.trim().split("\n");
+ is(hud.outputNode.children.length, 2, "number of messages");
+ is(lines.length, hud.outputNode.children.length, "number of lines");
+ isnot(lines[0].indexOf("bug916997a"), -1,
+ "first message text includes 'bug916997a'");
+ isnot(lines[1].indexOf("bug916997b"), -1,
+ "second message text includes 'bug916997b'");
+ is(lines[0].indexOf("bug916997b"), -1,
+ "first message text does not include 'bug916997b'");
+ },
+ () => {
+ info("last clipboard value: '" + clipboardValue + "'");
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_01.js b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_01.js
new file mode 100644
index 000000000..097eb3b37
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_01.js
@@ -0,0 +1,122 @@
+/* -*- 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/ */
+
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejections should be fixed.
+
+"use strict";
+
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed(null);
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed(
+ "TypeError: this.toolbox is null");
+
+// Test the webconsole output for various types of DOM Nodes.
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-output-dom-elements.html";
+
+var inputTests = [
+ {
+ input: "testBodyNode()",
+ output: '<body class="body-class" id="body-id">',
+ printOutput: "[object HTMLBodyElement]",
+ inspectable: true,
+ noClick: true,
+ inspectorIcon: true
+ },
+
+ {
+ input: "testDocumentElement()",
+ output: '<html dir="ltr" lang="en-US">',
+ printOutput: "[object HTMLHtmlElement]",
+ inspectable: true,
+ noClick: true,
+ inspectorIcon: true
+ },
+
+ {
+ input: "testDocument()",
+ output: "HTMLDocument \u2192 " + TEST_URI,
+ printOutput: "[object HTMLDocument]",
+ inspectable: true,
+ noClick: true,
+ inspectorIcon: false
+ },
+
+ {
+ input: "testNode()",
+ output: '<p some-attribute="some-value">',
+ printOutput: "[object HTMLParagraphElement]",
+ inspectable: true,
+ noClick: true,
+ inspectorIcon: true
+ },
+
+ {
+ input: "testNodeList()",
+ output: "NodeList [ <p>, <p#lots-of-attributes>, <iframe>, " +
+ "<div.some.classname.here.with.more.classnames.here>, " +
+ "<svg>, <clipPath>, <rect>, <script> ]",
+ printOutput: "[object NodeList]",
+ inspectable: true,
+ noClick: true,
+ inspectorIcon: true
+ },
+
+ {
+ input: "testNodeInIframe()",
+ output: "<p>",
+ printOutput: "[object HTMLParagraphElement]",
+ inspectable: true,
+ noClick: true,
+ inspectorIcon: true
+ },
+
+ {
+ input: "testLotsOfAttributes()",
+ output: '<p id="lots-of-attributes" a="" b="" c="" d="" e="" f="" g="" ' +
+ 'h="" i="" j="" k="" l="" m="" n="">',
+ printOutput: "[object HTMLParagraphElement]",
+ inspectable: true,
+ noClick: true,
+ inspectorIcon: true
+ },
+
+ {
+ input: "testDocumentFragment()",
+ output: "DocumentFragment [ <span.foo>, <div#fragdiv> ]",
+ printOutput: "[object DocumentFragment]",
+ inspectable: true,
+ noClick: true,
+ inspectorIcon: false
+ },
+
+ {
+ input: "testNodeInDocumentFragment()",
+ output: '<span class="foo" data-lolz="hehe">',
+ printOutput: "[object HTMLSpanElement]",
+ inspectable: true,
+ noClick: true,
+ inspectorIcon: false
+ },
+
+ {
+ input: "testUnattachedNode()",
+ output: '<p class="such-class" data-data="such-data">',
+ printOutput: "[object HTMLParagraphElement]",
+ inspectable: true,
+ noClick: true,
+ inspectorIcon: false
+ },
+];
+
+function test() {
+ requestLongerTimeout(2);
+ Task.spawn(function* () {
+ let {tab} = yield loadTab(TEST_URI);
+ let hud = yield openConsole(tab);
+ yield checkOutputForInputs(hud, inputTests);
+ }).then(finishTest);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_02.js b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_02.js
new file mode 100644
index 000000000..51fe89e01
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_02.js
@@ -0,0 +1,66 @@
+/* -*- 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/ */
+
+// Test the inspector links in the webconsole output for DOM Nodes actually
+// open the inspector and select the right node.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-output-dom-elements.html";
+
+const TEST_DATA = [
+ {
+ // The first test shouldn't be returning the body element as this is the
+ // default selected node, so re-selecting it won't fire the
+ // inspector-updated event
+ input: "testNode()",
+ output: '<p some-attribute="some-value">',
+ displayName: "p",
+ attrs: [{name: "some-attribute", value: "some-value"}]
+ },
+ {
+ input: "testBodyNode()",
+ output: '<body class="body-class" id="body-id">',
+ displayName: "body",
+ attrs: [
+ {
+ name: "class", value: "body-class"
+ },
+ {
+ name: "id", value: "body-id"
+ }
+ ]
+ },
+ {
+ input: "testNodeInIframe()",
+ output: "<p>",
+ displayName: "p",
+ attrs: []
+ },
+ {
+ input: "testDocumentElement()",
+ output: '<html dir="ltr" lang="en-US">',
+ displayName: "html",
+ attrs: [
+ {
+ name: "dir",
+ value: "ltr"
+ },
+ {
+ name: "lang",
+ value: "en-US"
+ }
+ ]
+ }
+];
+
+function test() {
+ Task.spawn(function* () {
+ let {tab} = yield loadTab(TEST_URI);
+ let hud = yield openConsole(tab);
+ yield checkDomElementHighlightingForInputs(hud, TEST_DATA);
+ }).then(finishTest);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_03.js b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_03.js
new file mode 100644
index 000000000..b5dd125d1
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_03.js
@@ -0,0 +1,70 @@
+/* -*- 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/ */
+
+// Test that inspector links in webconsole outputs for DOM Nodes highlight
+// the actual DOM Nodes on hover
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-output-dom-elements.html";
+
+function test() {
+ Task.spawn(function* () {
+ let {tab} = yield loadTab(TEST_URI);
+ let hud = yield openConsole(tab);
+ let toolbox = gDevTools.getToolbox(hud.target);
+
+ // Loading the inspector panel at first, to make it possible to listen for
+ // new node selections
+ yield toolbox.loadTool("inspector");
+ toolbox.getPanel("inspector");
+
+ info("Executing 'testNode()' in the web console to output a DOM Node");
+ let [result] = yield jsEval("testNode()", hud, {
+ text: '<p some-attribute="some-value">'
+ });
+
+ let elementNodeWidget = yield getWidget(result);
+
+ let nodeFront = yield hoverOverWidget(elementNodeWidget, toolbox);
+ let attrs = nodeFront.attributes;
+ is(nodeFront.tagName, "P", "The correct node was highlighted");
+ is(attrs[0].name, "some-attribute", "The correct node was highlighted");
+ is(attrs[0].value, "some-value", "The correct node was highlighted");
+ }).then(finishTest);
+}
+
+function jsEval(input, hud, message) {
+ hud.jsterm.execute(input);
+ return waitForMessages({
+ webconsole: hud,
+ messages: [message]
+ });
+}
+
+function* getWidget(result) {
+ info("Getting the output ElementNode widget");
+
+ let msg = [...result.matched][0];
+ let elementNodeWidget = [...msg._messageObject.widgets][0];
+ ok(elementNodeWidget, "ElementNode widget found in the output");
+
+ info("Waiting for the ElementNode widget to be linked to the inspector");
+ yield elementNodeWidget.linkToInspector();
+
+ return elementNodeWidget;
+}
+
+function* hoverOverWidget(widget, toolbox) {
+ info("Hovering over the output to highlight the node");
+
+ let onHighlight = toolbox.once("node-highlight");
+ EventUtils.sendMouseEvent({type: "mouseover"}, widget.element,
+ widget.element.ownerDocument.defaultView);
+ let nodeFront = yield onHighlight;
+ ok(true, "The highlighter was shown on a node");
+ return nodeFront;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_04.js b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_04.js
new file mode 100644
index 000000000..c7eb94902
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_04.js
@@ -0,0 +1,113 @@
+/* -*- 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/ */
+
+// Test that inspector links in the webconsole output for DOM Nodes do not try
+// to highlight or select nodes once they have been detached
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-output-dom-elements.html";
+
+const TEST_DATA = [
+ {
+ // The first test shouldn't be returning the body element as this is the
+ // default selected node, so re-selecting it won't fire the
+ // inspector-updated event
+ input: "testNode()",
+ output: '<p some-attribute="some-value">'
+ },
+ {
+ input: "testSvgNode()",
+ output: '<clipPath>'
+ },
+ {
+ input: "testBodyNode()",
+ output: '<body class="body-class" id="body-id">'
+ },
+ {
+ input: "testNodeInIframe()",
+ output: "<p>"
+ },
+ {
+ input: "testDocumentElement()",
+ output: '<html dir="ltr" lang="en-US">'
+ }
+];
+
+const PREF = "devtools.webconsole.persistlog";
+
+function test() {
+ Services.prefs.setBoolPref(PREF, true);
+ registerCleanupFunction(() => Services.prefs.clearUserPref(PREF));
+
+ Task.spawn(function* () {
+ let {tab} = yield loadTab(TEST_URI);
+ let hud = yield openConsole(tab);
+ let toolbox = gDevTools.getToolbox(hud.target);
+
+ info("Executing the test data");
+ let widgets = [];
+ for (let data of TEST_DATA) {
+ let [result] = yield jsEval(data.input, hud, {text: data.output});
+ let {widget} = yield getWidgetAndMessage(result);
+ widgets.push(widget);
+ }
+
+ info("Reloading the page");
+ yield reloadPage();
+
+ info("Iterating over the ElementNode widgets");
+ for (let widget of widgets) {
+ // Verify that openNodeInInspector rejects since the associated dom node
+ // doesn't exist anymore
+ yield widget.openNodeInInspector().then(() => {
+ ok(false, "The openNodeInInspector promise resolved");
+ }, () => {
+ ok(true, "The openNodeInInspector promise rejected as expected");
+ });
+ yield toolbox.selectTool("webconsole");
+
+ // Verify that highlightDomNode rejects too, for the same reason
+ yield widget.highlightDomNode().then(() => {
+ ok(false, "The highlightDomNode promise resolved");
+ }, () => {
+ ok(true, "The highlightDomNode promise rejected as expected");
+ });
+ }
+ }).then(finishTest);
+}
+
+function jsEval(input, hud, message) {
+ info("Executing '" + input + "' in the web console");
+ hud.jsterm.execute(input);
+ return waitForMessages({
+ webconsole: hud,
+ messages: [message]
+ });
+}
+
+function* getWidgetAndMessage(result) {
+ info("Getting the output ElementNode widget");
+
+ let msg = [...result.matched][0];
+ let widget = [...msg._messageObject.widgets][0];
+ ok(widget, "ElementNode widget found in the output");
+
+ info("Waiting for the ElementNode widget to be linked to the inspector");
+ yield widget.linkToInspector();
+
+ return {widget: widget, msg: msg};
+}
+
+function reloadPage() {
+ let def = promise.defer();
+ gBrowser.selectedBrowser.addEventListener("load", function onload() {
+ gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+ def.resolve();
+ }, true);
+ content.location.reload();
+ return def.promise;
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_05.js b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_05.js
new file mode 100644
index 000000000..9d35ef984
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_05.js
@@ -0,0 +1,47 @@
+/* -*- 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/ */
+
+"use strict";
+
+// Test the inspector links in the webconsole output for namespaced elements
+// actually open the inspector and select the right node.
+
+const XHTML = `
+ <!DOCTYPE html>
+ <html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <body>
+ <svg:svg width="100" height="100">
+ <svg:clipPath id="clip">
+ <svg:rect id="rectangle" x="0" y="0" width="10" height="5"></svg:rect>
+ </svg:clipPath>
+ <svg:circle cx="0" cy="0" r="5"></svg:circle>
+ </svg:svg>
+ </body>
+ </html>
+`;
+
+const TEST_URI = "data:application/xhtml+xml;charset=utf-8," + encodeURI(XHTML);
+
+const TEST_DATA = [
+ {
+ input: 'document.querySelector("clipPath")',
+ output: '<svg:clipPath id="clip">',
+ displayName: "svg:clipPath"
+ },
+ {
+ input: 'document.querySelector("circle")',
+ output: '<svg:circle cx="0" cy="0" r="5">',
+ displayName: "svg:circle"
+ },
+];
+
+function test() {
+ Task.spawn(function* () {
+ let {tab} = yield loadTab(TEST_URI);
+ let hud = yield openConsole(tab);
+ yield checkDomElementHighlightingForInputs(hud, TEST_DATA);
+ }).then(finishTest);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_events.js b/devtools/client/webconsole/test/browser_webconsole_output_events.js
new file mode 100644
index 000000000..9bd04bfc7
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_events.js
@@ -0,0 +1,54 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+
+"use strict";
+
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("null");
+
+// Test the webconsole output for DOM events.
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-output-events.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ hud.jsterm.clearOutput();
+ hud.jsterm.execute("testDOMEvents()");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "testDOMEvents() output",
+ text: "undefined",
+ category: CATEGORY_OUTPUT,
+ }],
+ });
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "console.log() output for mousemove",
+ text: /eventLogger mousemove { target: .+, buttons: 0, clientX: \d+, clientY: \d+, layerX: \d+, layerY: \d+ }/,
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "console.log() output for keypress",
+ text: /eventLogger keypress Shift { target: .+, key: .+, charCode: \d+, keyCode: \d+ }/,
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_order.js b/devtools/client/webconsole/test/browser_webconsole_output_order.js
new file mode 100644
index 000000000..66fa74cb0
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_order.js
@@ -0,0 +1,47 @@
+/* -*- 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 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/test-console.html";
+
+add_task(function* () {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole();
+
+ let jsterm = hud.jsterm;
+
+ jsterm.clearOutput();
+ jsterm.execute("console.log('foo', 'bar');");
+
+ let [functionCall, consoleMessage, result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "console.log('foo', 'bar');",
+ category: CATEGORY_INPUT,
+ },
+ {
+ text: "foo bar",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ },
+ {
+ text: "undefined",
+ category: CATEGORY_OUTPUT,
+ }]
+ });
+
+ let fncallNode = [...functionCall.matched][0];
+ let consoleMessageNode = [...consoleMessage.matched][0];
+ let resultNode = [...result.matched][0];
+ is(fncallNode.nextElementSibling, consoleMessageNode,
+ "console.log() is followed by 'foo' 'bar'");
+ is(consoleMessageNode.nextElementSibling, resultNode,
+ "'foo' 'bar' is followed by undefined");
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_regexp.js b/devtools/client/webconsole/test/browser_webconsole_output_regexp.js
new file mode 100644
index 000000000..2d6e767e9
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_regexp.js
@@ -0,0 +1,35 @@
+/* -*- 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/ */
+
+// Test the webconsole output for various types of objects.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-output-regexp.html";
+
+var inputTests = [
+ // 0
+ {
+ input: "/foo/igym",
+ output: "/foo/gimy",
+ printOutput: "Error: source called",
+ inspectable: true,
+ },
+];
+
+function test() {
+ requestLongerTimeout(2);
+ Task.spawn(function* () {
+ let {tab} = yield loadTab(TEST_URI);
+ let hud = yield openConsole(tab);
+ return checkOutputForInputs(hud, inputTests);
+ }).then(finishUp);
+}
+
+function finishUp() {
+ inputTests = null;
+ finishTest();
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_table.js b/devtools/client/webconsole/test/browser_webconsole_output_table.js
new file mode 100644
index 000000000..372afb28d
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_output_table.js
@@ -0,0 +1,199 @@
+/* -*- 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 console.table() works as intended.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/test-console-table.html";
+
+const TEST_DATA = [
+ {
+ command: "console.table(languages1)",
+ data: [
+ { _index: 0, name: "\"JavaScript\"", fileExtension: "Array[1]" },
+ { _index: 1, name: "Object", fileExtension: "\".ts\"" },
+ { _index: 2, name: "\"CoffeeScript\"", fileExtension: "\".coffee\"" }
+ ],
+ columns: { _index: "(index)", name: "name", fileExtension: "fileExtension" }
+ },
+ {
+ command: "console.table(languages1, 'name')",
+ data: [
+ { _index: 0, name: "\"JavaScript\"", fileExtension: "Array[1]" },
+ { _index: 1, name: "Object", fileExtension: "\".ts\"" },
+ { _index: 2, name: "\"CoffeeScript\"", fileExtension: "\".coffee\"" }
+ ],
+ columns: { _index: "(index)", name: "name" }
+ },
+ {
+ command: "console.table(languages1, ['name'])",
+ data: [
+ { _index: 0, name: "\"JavaScript\"", fileExtension: "Array[1]" },
+ { _index: 1, name: "Object", fileExtension: "\".ts\"" },
+ { _index: 2, name: "\"CoffeeScript\"", fileExtension: "\".coffee\"" }
+ ],
+ columns: { _index: "(index)", name: "name" }
+ },
+ {
+ command: "console.table(languages2)",
+ data: [
+ { _index: "csharp", name: "\"C#\"", paradigm: "\"object-oriented\"" },
+ { _index: "fsharp", name: "\"F#\"", paradigm: "\"functional\"" }
+ ],
+ columns: { _index: "(index)", name: "name", paradigm: "paradigm" }
+ },
+ {
+ command: "console.table([[1, 2], [3, 4]])",
+ data: [
+ { _index: 0, 0: "1", 1: "2" },
+ { _index: 1, 0: "3", 1: "4" }
+ ],
+ columns: { _index: "(index)", 0: "0", 1: "1" }
+ },
+ {
+ command: "console.table({a: [1, 2], b: [3, 4]})",
+ data: [
+ { _index: "a", 0: "1", 1: "2" },
+ { _index: "b", 0: "3", 1: "4" }
+ ],
+ columns: { _index: "(index)", 0: "0", 1: "1" }
+ },
+ {
+ command: "console.table(family)",
+ data: [
+ { _index: "mother", firstName: "\"Susan\"", lastName: "\"Doyle\"",
+ age: "32" },
+ { _index: "father", firstName: "\"John\"", lastName: "\"Doyle\"",
+ age: "33" },
+ { _index: "daughter", firstName: "\"Lily\"", lastName: "\"Doyle\"",
+ age: "5" },
+ { _index: "son", firstName: "\"Mike\"", lastName: "\"Doyle\"", age: "8" },
+ ],
+ columns: { _index: "(index)", firstName: "firstName", lastName: "lastName",
+ age: "age" }
+ },
+ {
+ command: "console.table(family, [])",
+ data: [
+ { _index: "mother", firstName: "\"Susan\"", lastName: "\"Doyle\"",
+ age: "32" },
+ { _index: "father", firstName: "\"John\"", lastName: "\"Doyle\"",
+ age: "33" },
+ { _index: "daughter", firstName: "\"Lily\"", lastName: "\"Doyle\"",
+ age: "5" },
+ { _index: "son", firstName: "\"Mike\"", lastName: "\"Doyle\"", age: "8" },
+ ],
+ columns: { _index: "(index)" }
+ },
+ {
+ command: "console.table(family, ['firstName', 'lastName'])",
+ data: [
+ { _index: "mother", firstName: "\"Susan\"", lastName: "\"Doyle\"",
+ age: "32" },
+ { _index: "father", firstName: "\"John\"", lastName: "\"Doyle\"",
+ age: "33" },
+ { _index: "daughter", firstName: "\"Lily\"", lastName: "\"Doyle\"",
+ age: "5" },
+ { _index: "son", firstName: "\"Mike\"", lastName: "\"Doyle\"", age: "8" },
+ ],
+ columns: { _index: "(index)", firstName: "firstName", lastName: "lastName" }
+ },
+ {
+ command: "console.table(mySet)",
+ data: [
+ { _index: 0, _value: "1" },
+ { _index: 1, _value: "5" },
+ { _index: 2, _value: "\"some text\"" },
+ { _index: 3, _value: "null" },
+ { _index: 4, _value: "undefined" }
+ ],
+ columns: { _index: "(iteration index)", _value: "Values" }
+ },
+ {
+ command: "console.table(myMap)",
+ data: [
+ { _index: 0, _key: "\"a string\"",
+ _value: "\"value associated with 'a string'\"" },
+ { _index: 1, _key: "5", _value: "\"value associated with 5\"" },
+ ],
+ columns: { _index: "(iteration index)", _key: "Key", _value: "Values" }
+ },
+ {
+ command: "console.table(weakset)",
+ data: [
+ { _value: "String" },
+ { _value: "String" },
+ ],
+ columns: { _index: "(iteration index)", _value: "Values" },
+ couldBeOutOfOrder: true,
+ },
+ {
+ command: "console.table(weakmap)",
+ data: [
+ { _key: "String", _value: "\"oh no\"" },
+ { _key: "String", _value: "23" },
+ ],
+ columns: { _index: "(iteration index)", _key: "Key", _value: "Values" },
+ couldBeOutOfOrder: true,
+ },
+];
+
+add_task(function* () {
+ const {tab} = yield loadTab(TEST_URI);
+ let hud = yield openConsole(tab);
+
+ for (let testdata of TEST_DATA) {
+ hud.jsterm.clearOutput();
+
+ info("Executing " + testdata.command);
+
+ let onTableRender = once(hud.ui, "messages-table-rendered");
+ hud.jsterm.execute(testdata.command);
+ yield onTableRender;
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: testdata.command + " output",
+ consoleTable: true
+ }],
+ });
+
+ let node = [...result.matched][0];
+ ok(node, "found trace log node");
+
+ let obj = node._messageObject;
+ ok(obj, "console.trace message object");
+
+ ok(obj._data, "found table data object");
+
+ let data = obj._data.map(entries => {
+ let entryResult = {};
+
+ for (let key of Object.keys(entries)) {
+ // If the results can be out of order, then ignore _index.
+ if (!testdata.couldBeOutOfOrder || key !== "_index") {
+ entryResult[key] = entries[key] instanceof HTMLElement ?
+ entries[key].textContent : entries[key];
+ }
+ }
+
+ return entryResult;
+ });
+
+ if (testdata.couldBeOutOfOrder) {
+ data = data.map(e => e.toSource()).sort().join(",");
+ let expected = testdata.data.map(e => e.toSource()).sort().join(",");
+ is(data, expected, "table data is correct");
+ } else {
+ is(data.toSource(), testdata.data.toSource(), "table data is correct");
+ }
+ ok(obj._columns, "found table column object");
+ is(obj._columns.toSource(), testdata.columns.toSource(),
+ "table column is correct");
+ }
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_promise.js b/devtools/client/webconsole/test/browser_webconsole_promise.js
new file mode 100644
index 000000000..59cd287ca
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_promise.js
@@ -0,0 +1,35 @@
+/* -*- 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/ */
+
+// Bug 1148759 - Test the webconsole can display promises inside objects.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,test for console and promises";
+
+var inputTests = [
+ // 0
+ {
+ input: "({ x: Promise.resolve() })",
+ output: "Object { x: Promise }",
+ printOutput: "[object Object]",
+ inspectable: true,
+ variablesViewLabel: "Object"
+ },
+];
+
+function test() {
+ requestLongerTimeout(2);
+
+ Task.spawn(function* () {
+ let {tab} = yield loadTab(TEST_URI);
+ let hud = yield openConsole(tab);
+ return checkOutputForInputs(hud, inputTests);
+ }).then(finishUp);
+}
+
+function finishUp() {
+ finishTest();
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_property_provider.js b/devtools/client/webconsole/test/browser_webconsole_property_provider.js
new file mode 100644
index 000000000..0c9b4c4e3
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_property_provider.js
@@ -0,0 +1,46 @@
+/* -*- 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 the property provider, which is part of the code completion
+// infrastructure.
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<p>test the JS property provider";
+
+function test() {
+ loadTab(TEST_URI).then(testPropertyProvider);
+}
+
+function testPropertyProvider({browser}) {
+ browser.removeEventListener("load", testPropertyProvider, true);
+ let {JSPropertyProvider} = require("devtools/shared/webconsole/js-property-provider");
+
+ let tmp = Cu.import("resource://gre/modules/jsdebugger.jsm", {});
+ tmp.addDebuggerToGlobal(tmp);
+ let dbg = new tmp.Debugger();
+ let dbgWindow = dbg.addDebuggee(content);
+
+ let completion = JSPropertyProvider(dbgWindow, null, "thisIsNotDefined");
+ is(completion.matches.length, 0, "no match for 'thisIsNotDefined");
+
+ // This is a case the PropertyProvider can't handle. Should return null.
+ completion = JSPropertyProvider(dbgWindow, null, "window[1].acb");
+ is(completion, null, "no match for 'window[1].acb");
+
+ // A very advanced completion case.
+ let strComplete =
+ "function a() { }document;document.getElementById(window.locatio";
+ completion = JSPropertyProvider(dbgWindow, null, strComplete);
+ ok(completion.matches.length == 2, "two matches found");
+ ok(completion.matchProp == "locatio", "matching part is 'test'");
+ let matches = completion.matches;
+ matches.sort();
+ ok(matches[0] == "location", "the first match is 'location'");
+ ok(matches[1] == "locationbar", "the second match is 'locationbar'");
+
+ dbg.removeDebuggee(content);
+ finishTest();
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_reflow.js b/devtools/client/webconsole/test/browser_webconsole_reflow.js
new file mode 100644
index 000000000..86caa10e0
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_reflow.js
@@ -0,0 +1,33 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for " +
+ "reflow activity";
+
+add_task(function* () {
+ let { browser } = yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ function onReflowListenersReady() {
+ browser.contentDocument.body.style.display = "none";
+ browser.contentDocument.body.clientTop;
+ }
+
+ Services.prefs.setBoolPref("devtools.webconsole.filter.csslog", true);
+ hud.ui._updateReflowActivityListener(onReflowListenersReady);
+ Services.prefs.clearUserPref("devtools.webconsole.filter.csslog");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: /reflow: /,
+ category: CATEGORY_CSS,
+ severity: SEVERITY_LOG,
+ }],
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_scratchpad_panel_link.js b/devtools/client/webconsole/test/browser_webconsole_scratchpad_panel_link.js
new file mode 100644
index 000000000..566af8d42
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_scratchpad_panel_link.js
@@ -0,0 +1,76 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,<p>test Scratchpad panel " +
+ "linking</p>";
+
+var { Tools } = require("devtools/client/definitions");
+var { isTargetSupported } = Tools.scratchpad;
+
+function pushPrefEnv() {
+ let deferred = promise.defer();
+ let options = {"set":
+ [["devtools.scratchpad.enabled", true]
+ ]};
+ SpecialPowers.pushPrefEnv(options, deferred.resolve);
+ return deferred.promise;
+}
+
+add_task(function* () {
+ waitForExplicitFinish();
+
+ yield pushPrefEnv();
+
+ yield loadTab(TEST_URI);
+
+ info("Opening toolbox with Scratchpad panel");
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target, "scratchpad", "window");
+
+ let scratchpadPanel = toolbox.getPanel("scratchpad");
+ let { scratchpad } = scratchpadPanel;
+ is(toolbox.getCurrentPanel(), scratchpadPanel,
+ "Scratchpad is currently selected panel");
+
+ info("Switching to webconsole panel");
+
+ let webconsolePanel = yield toolbox.selectTool("webconsole");
+ let { hud } = webconsolePanel;
+ is(toolbox.getCurrentPanel(), webconsolePanel,
+ "Webconsole is currently selected panel");
+
+ info("console.log()ing from Scratchpad");
+
+ scratchpad.setText("console.log('foobar-from-scratchpad')");
+ scratchpad.run();
+ let messages = yield waitForMessages({
+ webconsole: hud,
+ messages: [{ text: "foobar-from-scratchpad" }]
+ });
+
+ info("Clicking link to switch to and focus Scratchpad");
+
+ let [matched] = [...messages[0].matched];
+ ok(matched, "Found logged message from Scratchpad");
+ let anchor = matched.querySelector(".message-location .frame-link-filename");
+
+ toolbox.on("scratchpad-selected", function selected() {
+ toolbox.off("scratchpad-selected", selected);
+
+ is(toolbox.getCurrentPanel(), scratchpadPanel,
+ "Clicking link switches to Scratchpad panel");
+
+ is(Services.ww.activeWindow, toolbox.win.parent,
+ "Scratchpad's toolbox is focused");
+
+ Tools.scratchpad.isTargetSupported = isTargetSupported;
+ finish();
+ });
+
+ EventUtils.synthesizeMouse(anchor, 2, 2, {}, hud.iframeWindow);
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_script_errordoc_urls.js b/devtools/client/webconsole/test/browser_webconsole_script_errordoc_urls.js
new file mode 100644
index 000000000..779d80376
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_script_errordoc_urls.js
@@ -0,0 +1,67 @@
+/* -*- 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/ */
+
+// Ensure that [Learn More] links appear alongside any errors listed
+// in "errordocs.js". Note: this only tests script execution.
+
+"use strict";
+
+const ErrorDocs = require("devtools/server/actors/errordocs");
+
+function makeURIData(script) {
+ return `data:text/html;charset=utf8,<script>${script}</script>`;
+}
+
+const TestData = [
+ {
+ jsmsg: "JSMSG_READ_ONLY",
+ script: "'use strict'; (Object.freeze({name: 'Elsa', score: 157})).score = 0;",
+ isException: true,
+ },
+ {
+ jsmsg: "JSMSG_STMT_AFTER_RETURN",
+ script: "function a() { return; 1 + 1; };",
+ isException: false,
+ }
+];
+
+add_task(function* () {
+ yield loadTab("data:text/html;charset=utf8,errordoc tests");
+
+ let hud = yield openConsole();
+
+ for (let i = 0; i < TestData.length; i++) {
+ yield testScriptError(hud, TestData[i]);
+ }
+});
+
+function* testScriptError(hud, testData) {
+ if (testData.isException === true) {
+ expectUncaughtException();
+ }
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, makeURIData(testData.script));
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ category: CATEGORY_JS
+ }
+ ]
+ });
+
+ // grab the most current error doc URL
+ let url = ErrorDocs.GetURL({ errorMessageName: testData.jsmsg });
+
+ let hrefs = {};
+ for (let link of hud.jsterm.outputNode.querySelectorAll("a")) {
+ hrefs[link.href] = true;
+ }
+
+ ok(url in hrefs, `Expected a link to ${url}.`);
+
+ hud.jsterm.clearOutput();
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_show_subresource_security_errors.js b/devtools/client/webconsole/test/browser_webconsole_show_subresource_security_errors.js
new file mode 100644
index 000000000..43cb96bdc
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_show_subresource_security_errors.js
@@ -0,0 +1,39 @@
+/* -*- 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/ */
+
+// Ensure non-toplevel security errors are displayed
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console subresource STS " +
+ "warning test";
+const TEST_DOC = "https://example.com/browser/devtools/client/webconsole/" +
+ "test/test_bug1092055_shouldwarn.html";
+const SAMPLE_MSG = "specified a header that could not be parsed successfully.";
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({
+ set: [["dom.ipc.processCount", 1]]
+ });
+});
+
+add_task(function* () {
+ let { browser } = yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ hud.jsterm.clearOutput();
+
+ let loaded = loadBrowser(browser);
+ BrowserTestUtils.loadURI(browser, TEST_DOC);
+ yield loaded;
+
+ yield waitForSuccess({
+ name: "Subresource STS warning displayed successfully",
+ validator: function () {
+ return hud.outputNode.textContent.indexOf(SAMPLE_MSG) > -1;
+ }
+ });
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_shows_reqs_in_netmonitor.js b/devtools/client/webconsole/test/browser_webconsole_shows_reqs_in_netmonitor.js
new file mode 100644
index 000000000..b66d5afff
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_shows_reqs_in_netmonitor.js
@@ -0,0 +1,73 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf8,Test that the web console " +
+ "displays requests that have been recorded in the " +
+ "netmonitor, even if the console hadn't opened yet.";
+
+const TEST_FILE = "test-network-request.html";
+const TEST_PATH = "http://example.com/browser/devtools/client/webconsole/" +
+ "test/" + TEST_FILE;
+
+const NET_PREF = "devtools.webconsole.filter.networkinfo";
+Services.prefs.setBoolPref(NET_PREF, true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(NET_PREF);
+});
+
+add_task(function* () {
+ let { tab, browser } = yield loadTab(TEST_URI);
+
+ let target = TargetFactory.forTab(tab);
+ let toolbox = yield gDevTools.showToolbox(target, "netmonitor");
+ info("Network panel is open.");
+
+ yield loadDocument(browser);
+ info("Document loaded.");
+
+ // Test that the request appears in the network panel.
+ testNetmonitor(toolbox);
+
+ // Test that the request appears in the console.
+ let hud = yield openConsole();
+ info("Web console is open");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "network message",
+ text: TEST_FILE,
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG
+ }
+ ]
+ });
+});
+
+function loadDocument(browser) {
+ let deferred = promise.defer();
+
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ deferred.resolve();
+ }, true);
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_PATH);
+
+ return deferred.promise;
+}
+
+function testNetmonitor(toolbox) {
+ let monitor = toolbox.getCurrentPanel();
+ let { RequestsMenu } = monitor.panelWin.NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+ is(RequestsMenu.itemCount, 1, "Network request appears in the network panel");
+
+ let item = RequestsMenu.getItemAtIndex(0);
+ is(item.attachment.method, "GET", "The attached method is correct.");
+ is(item.attachment.url, TEST_PATH, "The attached url is correct.");
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_split.js b/devtools/client/webconsole/test/browser_webconsole_split.js
new file mode 100644
index 000000000..0242d94b4
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_split.js
@@ -0,0 +1,268 @@
+/* -*- 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/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for splitting";
+
+function test() {
+ waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [["dom.ipc.processCount", 1]]}, runTest);
+}
+
+function runTest() {
+ // Test is slow on Linux EC2 instances - Bug 962931
+ requestLongerTimeout(2);
+
+ let {Toolbox} = require("devtools/client/framework/toolbox");
+ let toolbox;
+
+ loadTab(TEST_URI).then(testConsoleLoadOnDifferentPanel);
+
+ function testConsoleLoadOnDifferentPanel() {
+ info("About to check console loads even when non-webconsole panel is open");
+
+ openPanel("inspector").then(() => {
+ toolbox.on("webconsole-ready", () => {
+ ok(true, "Webconsole has been triggered as loaded while another tool " +
+ "is active");
+ testKeyboardShortcuts();
+ });
+
+ // Opens split console.
+ toolbox.toggleSplitConsole();
+ });
+ }
+
+ function testKeyboardShortcuts() {
+ info("About to check that panel responds to ESCAPE keyboard shortcut");
+
+ toolbox.once("split-console", () => {
+ ok(true, "Split console has been triggered via ESCAPE keypress");
+ checkAllTools();
+ });
+
+ // Closes split console.
+ EventUtils.sendKey("ESCAPE", toolbox.win);
+ }
+
+ function checkAllTools() {
+ info("About to check split console with each panel individually.");
+
+ Task.spawn(function* () {
+ yield openAndCheckPanel("jsdebugger");
+ yield openAndCheckPanel("inspector");
+ yield openAndCheckPanel("styleeditor");
+ yield openAndCheckPanel("performance");
+ yield openAndCheckPanel("netmonitor");
+
+ yield checkWebconsolePanelOpened();
+ testBottomHost();
+ });
+ }
+
+ function getCurrentUIState() {
+ let win = toolbox.win;
+ let deck = toolbox.doc.querySelector("#toolbox-deck");
+ let webconsolePanel = toolbox.webconsolePanel;
+ let splitter = toolbox.doc.querySelector("#toolbox-console-splitter");
+
+ let containerHeight = parseFloat(win.getComputedStyle(deck.parentNode)
+ .getPropertyValue("height"));
+ let deckHeight = parseFloat(win.getComputedStyle(deck)
+ .getPropertyValue("height"));
+ let webconsoleHeight = parseFloat(win.getComputedStyle(webconsolePanel)
+ .getPropertyValue("height"));
+ let splitterVisibility = !splitter.getAttribute("hidden");
+ let openedConsolePanel = toolbox.currentToolId === "webconsole";
+ let cmdButton = toolbox.doc.querySelector("#command-button-splitconsole");
+
+ return {
+ deckHeight: deckHeight,
+ containerHeight: containerHeight,
+ webconsoleHeight: webconsoleHeight,
+ splitterVisibility: splitterVisibility,
+ openedConsolePanel: openedConsolePanel,
+ buttonSelected: cmdButton.hasAttribute("checked")
+ };
+ }
+
+ function checkWebconsolePanelOpened() {
+ info("About to check special cases when webconsole panel is open.");
+
+ let deferred = promise.defer();
+
+ // Start with console split, so we can test for transition to main panel.
+ toolbox.toggleSplitConsole();
+
+ let currentUIState = getCurrentUIState();
+
+ ok(currentUIState.splitterVisibility,
+ "Splitter is visible when console is split");
+ ok(currentUIState.deckHeight > 0,
+ "Deck has a height > 0 when console is split");
+ ok(currentUIState.webconsoleHeight > 0,
+ "Web console has a height > 0 when console is split");
+ ok(!currentUIState.openedConsolePanel,
+ "The console panel is not the current tool");
+ ok(currentUIState.buttonSelected, "The command button is selected");
+
+ openPanel("webconsole").then(() => {
+ currentUIState = 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");
+ ok(currentUIState.buttonSelected,
+ "The command button is still selected.");
+
+ // Make sure splitting console does nothing while webconsole is opened
+ toolbox.toggleSplitConsole();
+
+ currentUIState = 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");
+ ok(currentUIState.buttonSelected,
+ "The command button is still selected.");
+
+ // Make sure that split state is saved after opening another panel
+ openPanel("inspector").then(() => {
+ currentUIState = getCurrentUIState();
+ ok(currentUIState.splitterVisibility,
+ "Splitter is visible when console is split");
+ ok(currentUIState.deckHeight > 0,
+ "Deck has a height > 0 when console is split");
+ ok(currentUIState.webconsoleHeight > 0,
+ "Web console has a height > 0 when console is split");
+ ok(!currentUIState.openedConsolePanel,
+ "The console panel is not the current tool");
+ ok(currentUIState.buttonSelected,
+ "The command button is still selected.");
+
+ toolbox.toggleSplitConsole();
+ deferred.resolve();
+ });
+ });
+ return deferred.promise;
+ }
+
+ function openPanel(toolId) {
+ let deferred = promise.defer();
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, toolId).then(function (box) {
+ toolbox = box;
+ deferred.resolve();
+ }).then(null, console.error);
+ return deferred.promise;
+ }
+
+ function openAndCheckPanel(toolId) {
+ let deferred = promise.defer();
+ openPanel(toolId).then(() => {
+ info("Checking toolbox for " + toolId);
+ checkToolboxUI(toolbox.getCurrentPanel());
+ deferred.resolve();
+ });
+ return deferred.promise;
+ }
+
+ function checkToolboxUI() {
+ let currentUIState = 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");
+ ok(!currentUIState.buttonSelected, "The command button is not selected.");
+
+ toolbox.toggleSplitConsole();
+
+ currentUIState = getCurrentUIState();
+
+ ok(currentUIState.splitterVisibility,
+ "Splitter is visible when console is split");
+ ok(currentUIState.deckHeight > 0,
+ "Deck has a height > 0 when console is split");
+ ok(currentUIState.webconsoleHeight > 0,
+ "Web console has a height > 0 when console is split");
+ is(Math.round(currentUIState.deckHeight + currentUIState.webconsoleHeight),
+ currentUIState.containerHeight,
+ "Everything adds up to container height");
+ ok(!currentUIState.openedConsolePanel,
+ "The console panel is not the current tool");
+ ok(currentUIState.buttonSelected, "The command button is selected.");
+
+ toolbox.toggleSplitConsole();
+
+ currentUIState = 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");
+ ok(!currentUIState.buttonSelected, "The command button is not selected.");
+ }
+
+ function testBottomHost() {
+ checkHostType(Toolbox.HostType.BOTTOM);
+
+ checkToolboxUI();
+
+ toolbox.switchHost(Toolbox.HostType.SIDE).then(testSidebarHost);
+ }
+
+ function testSidebarHost() {
+ checkHostType(Toolbox.HostType.SIDE);
+
+ checkToolboxUI();
+
+ toolbox.switchHost(Toolbox.HostType.WINDOW).then(testWindowHost);
+ }
+
+ function testWindowHost() {
+ checkHostType(Toolbox.HostType.WINDOW);
+
+ checkToolboxUI();
+
+ toolbox.switchHost(Toolbox.HostType.BOTTOM).then(testDestroy);
+ }
+
+ function checkHostType(hostType) {
+ is(toolbox.hostType, hostType, "host type is " + hostType);
+
+ let pref = Services.prefs.getCharPref("devtools.toolbox.host");
+ is(pref, hostType, "host pref is " + hostType);
+ }
+
+ function testDestroy() {
+ toolbox.destroy().then(function () {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target).then(finish);
+ });
+ }
+
+ function finish() {
+ toolbox = null;
+ finishTest();
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_split_escape_key.js b/devtools/client/webconsole/test/browser_webconsole_split_escape_key.js
new file mode 100644
index 000000000..f71efb99e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_split_escape_key.js
@@ -0,0 +1,158 @@
+/* -*- 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/ */
+
+ "use strict";
+
+ function test() {
+ info("Test various cases where the escape key should hide the split console.");
+
+ let toolbox;
+ let hud;
+ let jsterm;
+ let hudMessages;
+ let variablesView;
+
+ Task.spawn(runner).then(finish);
+
+ function* runner() {
+ let {tab} = yield loadTab("data:text/html;charset=utf-8,<p>Web Console " +
+ "test for splitting");
+ let target = TargetFactory.forTab(tab);
+ toolbox = yield gDevTools.showToolbox(target, "inspector");
+
+ yield testCreateSplitConsoleAfterEscape();
+
+ yield showAutoCompletePopoup();
+
+ yield testHideAutoCompletePopupAfterEscape();
+
+ yield executeJS();
+ yield clickMessageAndShowVariablesView();
+ jsterm.focus();
+
+ yield testHideVariablesViewAfterEscape();
+
+ yield clickMessageAndShowVariablesView();
+ yield startPropertyEditor();
+
+ yield testCancelPropertyEditorAfterEscape();
+ yield testHideVariablesViewAfterEscape();
+ yield testHideSplitConsoleAfterEscape();
+ }
+
+ function testCreateSplitConsoleAfterEscape() {
+ let result = toolbox.once("webconsole-ready", () => {
+ hud = toolbox.getPanel("webconsole").hud;
+ jsterm = hud.jsterm;
+ ok(toolbox.splitConsole, "Split console is created.");
+ });
+
+ let contentWindow = toolbox.win;
+ contentWindow.focus();
+ EventUtils.sendKey("ESCAPE", contentWindow);
+
+ return result;
+ }
+
+ function testHideSplitConsoleAfterEscape() {
+ let result = toolbox.once("split-console", () => {
+ ok(!toolbox.splitConsole, "Split console is hidden.");
+ });
+ EventUtils.sendKey("ESCAPE", toolbox.win);
+
+ return result;
+ }
+
+ function testHideVariablesViewAfterEscape() {
+ let result = jsterm.once("sidebar-closed", () => {
+ ok(!hud.ui.jsterm.sidebar,
+ "Variables view is hidden.");
+ ok(toolbox.splitConsole,
+ "Split console is open after hiding the variables view.");
+ });
+ EventUtils.sendKey("ESCAPE", toolbox.win);
+
+ return result;
+ }
+
+ function testHideAutoCompletePopupAfterEscape() {
+ let deferred = promise.defer();
+ let popup = jsterm.autocompletePopup;
+
+ popup.once("popup-closed", () => {
+ ok(!popup.isOpen,
+ "Auto complete popup is hidden.");
+ ok(toolbox.splitConsole,
+ "Split console is open after hiding the autocomplete popup.");
+
+ deferred.resolve();
+ });
+
+ EventUtils.sendKey("ESCAPE", toolbox.win);
+
+ return deferred.promise;
+ }
+
+ function testCancelPropertyEditorAfterEscape() {
+ EventUtils.sendKey("ESCAPE", variablesView.window);
+ ok(hud.ui.jsterm.sidebar,
+ "Variables view is open after canceling property editor.");
+ ok(toolbox.splitConsole,
+ "Split console is open after editing.");
+ }
+
+ function* executeJS() {
+ jsterm.execute("var foo = { bar: \"baz\" }; foo;");
+ hudMessages = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "Object { bar: \"baz\" }",
+ category: CATEGORY_OUTPUT,
+ objects: true
+ }],
+ });
+ }
+
+ function clickMessageAndShowVariablesView() {
+ let result = jsterm.once("variablesview-fetched", (event, vview) => {
+ variablesView = vview;
+ });
+
+ let clickable = hudMessages[0].clickableElements[0];
+ EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow);
+
+ return result;
+ }
+
+ function* startPropertyEditor() {
+ let results = yield findVariableViewProperties(variablesView, [
+ {name: "bar", value: "baz"}
+ ], {webconsole: hud});
+ results[0].matchedProp.focus();
+ EventUtils.synthesizeKey("VK_RETURN", variablesView.window);
+ }
+
+ function showAutoCompletePopoup() {
+ let onPopupShown = jsterm.autocompletePopup.once("popup-opened");
+
+ jsterm.focus();
+ jsterm.setInputValue("document.location.");
+ EventUtils.sendKey("TAB", hud.iframeWindow);
+
+ return onPopupShown;
+ }
+
+ function finish() {
+ toolbox.destroy().then(() => {
+ toolbox = null;
+ hud = null;
+ jsterm = null;
+ hudMessages = null;
+ variablesView = null;
+
+ finishTest();
+ });
+ }
+ }
diff --git a/devtools/client/webconsole/test/browser_webconsole_split_focus.js b/devtools/client/webconsole/test/browser_webconsole_split_focus.js
new file mode 100644
index 000000000..ff65229c9
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_split_focus.js
@@ -0,0 +1,66 @@
+/* -*- 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/ */
+
+ "use strict";
+
+ function test() {
+ info("Test that the split console state is persisted");
+
+ let toolbox;
+ let TEST_URI = "data:text/html;charset=utf-8,<p>Web Console test for " +
+ "splitting</p>";
+
+ Task.spawn(runner).then(finish);
+
+ function* runner() {
+ info("Opening a tab while there is no user setting on split console pref");
+ let {tab} = yield loadTab(TEST_URI);
+ let target = TargetFactory.forTab(tab);
+ toolbox = yield gDevTools.showToolbox(target, "inspector");
+
+ ok(!toolbox.splitConsole, "Split console is hidden by default");
+
+ info("Focusing the search box before opening the split console");
+ let inspector = toolbox.getPanel("inspector");
+ inspector.searchBox.focus();
+
+ let activeElement = getActiveElement(inspector.panelDoc);
+ is(activeElement, inspector.searchBox, "Search box is focused");
+
+ yield toolbox.openSplitConsole();
+
+ ok(toolbox.splitConsole, "Split console is now visible");
+
+ // Use the binding element since jsterm.inputNode is a XUL textarea element.
+ activeElement = getActiveElement(toolbox.doc);
+ activeElement = activeElement.ownerDocument.getBindingParent(activeElement);
+ let inputNode = toolbox.getPanel("webconsole").hud.jsterm.inputNode;
+ is(activeElement, inputNode, "Split console input is focused by default");
+
+ yield 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");
+
+ yield toolbox.destroy();
+ }
+
+ function getActiveElement(doc) {
+ let activeElement = doc.activeElement;
+ while (activeElement && activeElement.contentDocument) {
+ activeElement = activeElement.contentDocument.activeElement;
+ }
+ return activeElement;
+ }
+
+ function finish() {
+ toolbox = TEST_URI = null;
+ Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
+ Services.prefs.clearUserPref("devtools.toolbox.splitconsoleHeight");
+ finishTest();
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_split_persist.js b/devtools/client/webconsole/test/browser_webconsole_split_persist.js
new file mode 100644
index 000000000..e11bd4811
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_split_persist.js
@@ -0,0 +1,119 @@
+/* -*- 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/ */
+
+ "use strict";
+
+ function test() {
+ info("Test that the split console state is persisted");
+
+ let toolbox;
+ let TEST_URI = "data:text/html;charset=utf-8,<p>Web Console test for " +
+ "splitting</p>";
+
+ Task.spawn(runner).then(finish);
+
+ function* runner() {
+ info("Opening a tab while there is no user setting on split console pref");
+ let {tab} = yield loadTab(TEST_URI);
+ let target = TargetFactory.forTab(tab);
+ toolbox = yield gDevTools.showToolbox(target, "inspector");
+
+ ok(!toolbox.splitConsole, "Split console is hidden by default.");
+ ok(!isCommandButtonChecked(), "Split console button is unchecked by " +
+ "default.");
+ yield toggleSplitConsoleWithEscape();
+ ok(toolbox.splitConsole, "Split console is now visible.");
+ ok(isCommandButtonChecked(), "Split console button is now checked.");
+ ok(getVisiblePrefValue(), "Visibility pref is true");
+
+ is(getHeightPrefValue(), toolbox.webconsolePanel.height,
+ "Panel height matches the pref");
+ toolbox.webconsolePanel.height = 200;
+
+ yield toolbox.destroy();
+
+ info("Opening a tab while there is a true user setting on split console " +
+ "pref");
+ ({tab} = yield loadTab(TEST_URI));
+ target = TargetFactory.forTab(tab);
+ toolbox = yield gDevTools.showToolbox(target, "inspector");
+
+ ok(toolbox.splitConsole, "Split console is visible by default.");
+ ok(isCommandButtonChecked(), "Split console button is checked by default.");
+ is(getHeightPrefValue(), 200, "Height is set based on panel height after " +
+ "closing");
+
+ // Use the binding element since jsterm.inputNode is a XUL textarea element.
+ let activeElement = getActiveElement(toolbox.doc);
+ activeElement = activeElement.ownerDocument.getBindingParent(activeElement);
+ let inputNode = toolbox.getPanel("webconsole").hud.jsterm.inputNode;
+ is(activeElement, inputNode, "Split console input is focused by default");
+
+ toolbox.webconsolePanel.height = 1;
+ ok(toolbox.webconsolePanel.clientHeight > 1,
+ "The actual height of the console is bound with a min height");
+
+ toolbox.webconsolePanel.height = 10000;
+ ok(toolbox.webconsolePanel.clientHeight < 10000,
+ "The actual height of the console is bound with a max height");
+
+ yield toggleSplitConsoleWithEscape();
+ ok(!toolbox.splitConsole, "Split console is now hidden.");
+ ok(!isCommandButtonChecked(), "Split console button is now unchecked.");
+ ok(!getVisiblePrefValue(), "Visibility pref is false");
+
+ yield toolbox.destroy();
+
+ is(getHeightPrefValue(), 10000,
+ "Height is set based on panel height after closing");
+
+ info("Opening a tab while there is a false user setting on split " +
+ "console pref");
+ ({tab} = yield loadTab(TEST_URI));
+ target = TargetFactory.forTab(tab);
+ toolbox = yield gDevTools.showToolbox(target, "inspector");
+
+ ok(!toolbox.splitConsole, "Split console is hidden by default.");
+ ok(!getVisiblePrefValue(), "Visibility pref is false");
+
+ yield toolbox.destroy();
+ }
+
+ function getActiveElement(doc) {
+ let activeElement = doc.activeElement;
+ while (activeElement && activeElement.contentDocument) {
+ activeElement = activeElement.contentDocument.activeElement;
+ }
+ return activeElement;
+ }
+
+ function getVisiblePrefValue() {
+ return Services.prefs.getBoolPref("devtools.toolbox.splitconsoleEnabled");
+ }
+
+ function getHeightPrefValue() {
+ return Services.prefs.getIntPref("devtools.toolbox.splitconsoleHeight");
+ }
+
+ function isCommandButtonChecked() {
+ return toolbox.doc.querySelector("#command-button-splitconsole")
+ .hasAttribute("checked");
+ }
+
+ function toggleSplitConsoleWithEscape() {
+ let onceSplitConsole = toolbox.once("split-console");
+ let contentWindow = toolbox.win;
+ contentWindow.focus();
+ EventUtils.sendKey("ESCAPE", contentWindow);
+ return onceSplitConsole;
+ }
+
+ function finish() {
+ toolbox = TEST_URI = null;
+ Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
+ Services.prefs.clearUserPref("devtools.toolbox.splitconsoleHeight");
+ finishTest();
+ }
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_start_netmon_first.js b/devtools/client/webconsole/test/browser_webconsole_start_netmon_first.js
new file mode 100644
index 000000000..a10acf9b2
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_start_netmon_first.js
@@ -0,0 +1,38 @@
+/* -*- 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/ */
+
+// Check that the webconsole works if the network monitor is first opened, then
+// the user switches to the webconsole. See bug 970914.
+
+"use strict";
+
+function test() {
+ Task.spawn(runner).then(finishTest);
+
+ function* runner() {
+ const {tab} = yield loadTab("data:text/html;charset=utf8,<p>hello");
+
+ const hud = yield openConsole(tab);
+
+ hud.jsterm.execute("console.log('foobar bug970914')");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "console.log",
+ text: "foobar bug970914",
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ let text = hud.outputNode.textContent;
+ isnot(text.indexOf("foobar bug970914"), -1,
+ "console.log message confirmed");
+ ok(!/logging API|disabled by a script/i.test(text),
+ "no warning about disabled console API");
+ }
+}
+
diff --git a/devtools/client/webconsole/test/browser_webconsole_strict_mode_errors.js b/devtools/client/webconsole/test/browser_webconsole_strict_mode_errors.js
new file mode 100644
index 000000000..c8f2200f9
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_strict_mode_errors.js
@@ -0,0 +1,83 @@
+/* -*- 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/ */
+
+// Check that "use strict" JS errors generate errors, not warnings.
+
+"use strict";
+
+add_task(function* () {
+ // On e10s, the exception is triggered in child process
+ // and is ignored by test harness
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+ yield loadTab("data:text/html;charset=utf8,<script>'use strict';var arguments;</script>");
+
+ let hud = yield openConsole();
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ text: "SyntaxError: 'arguments' can't be defined or assigned to in strict mode code",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ },
+ ],
+ });
+
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "data:text/html;charset="
+ + "utf8,<script>'use strict';function f(a, a) {};</script>");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ text: "SyntaxError: duplicate formal argument a",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ },
+ ],
+ });
+
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "data:text/html;charset="
+ + "utf8,<script>'use strict';var o = {get p() {}};o.p = 1;</script>");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ text: "TypeError: setting a property that has only a getter",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ },
+ ],
+ });
+
+ if (!Services.appinfo.browserTabsRemoteAutostart) {
+ expectUncaughtException();
+ }
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser,
+ "data:text/html;charset=utf8,<script>'use strict';v = 1;</script>");
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ text: "ReferenceError: assignment to undeclared variable v",
+ category: CATEGORY_JS,
+ severity: SEVERITY_ERROR,
+ },
+ ],
+ });
+
+ hud = null;
+});
diff --git a/devtools/client/webconsole/test/browser_webconsole_trackingprotection_errors.js b/devtools/client/webconsole/test/browser_webconsole_trackingprotection_errors.js
new file mode 100644
index 000000000..eafeee18e
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_trackingprotection_errors.js
@@ -0,0 +1,54 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Load a page with tracking elements that get blocked and make sure that a
+// 'learn more' link shows up in the webconsole.
+
+"use strict";
+
+const TEST_URI = "http://tracking.example.org/browser/devtools/client/" +
+ "webconsole/test/test-trackingprotection-securityerrors.html";
+const LEARN_MORE_URI = "https://developer.mozilla.org/Firefox/Privacy/" +
+ "Tracking_Protection" + DOCS_GA_PARAMS;
+const PREF = "privacy.trackingprotection.enabled";
+
+const {UrlClassifierTestUtils} = Cu.import("resource://testing-common/UrlClassifierTestUtils.jsm", {});
+
+registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(PREF);
+ UrlClassifierTestUtils.cleanupTestTrackers();
+});
+
+add_task(function* testMessagesAppear() {
+ yield UrlClassifierTestUtils.addTestTrackers();
+ Services.prefs.setBoolPref(PREF, true);
+
+ let { browser } = yield loadTab(TEST_URI);
+
+ let hud = yield openConsole();
+
+ let results = yield waitForMessages({
+ webconsole: hud,
+ messages: [
+ {
+ name: "Was blocked because tracking protection is enabled",
+ text: "The resource at \u201chttp://tracking.example.com/\u201d was " +
+ "blocked because tracking protection is enabled",
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_WARNING,
+ objects: true,
+ },
+ ],
+ });
+
+ yield testClickOpenNewTab(hud, results[0]);
+});
+
+function testClickOpenNewTab(hud, match) {
+ let warningNode = match.clickableElements[0];
+ ok(warningNode, "link element");
+ ok(warningNode.classList.contains("learn-more-link"), "link class name");
+ return simulateMessageLinkClick(warningNode, LEARN_MORE_URI);
+}
diff --git a/devtools/client/webconsole/test/browser_webconsole_view_source.js b/devtools/client/webconsole/test/browser_webconsole_view_source.js
new file mode 100644
index 000000000..a81b58acc
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_webconsole_view_source.js
@@ -0,0 +1,52 @@
+/* -*- 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 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/test-mixedcontent-securityerrors.html";
+
+add_task(function* () {
+ yield actuallyTest();
+});
+
+add_task(function* () {
+ Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+ yield actuallyTest();
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+var actuallyTest = Task.async(function*() {
+ yield loadTab(TEST_URI);
+ let hud = yield openConsole(null);
+ info("console opened");
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: "Blocked loading mixed active content",
+ category: CATEGORY_SECURITY,
+ severity: SEVERITY_ERROR,
+ }],
+ });
+
+ let msg = [...result.matched][0];
+ ok(msg, "error message");
+ let locationNode = msg.querySelector(".message-location .frame-link-filename");
+ ok(locationNode, "location node");
+
+ let onTabOpen = waitForTab();
+
+ EventUtils.sendMouseEvent({ type: "click" }, locationNode);
+
+ let tab = yield onTabOpen;
+ ok(true, "the view source tab was opened in response to clicking the location node");
+ gBrowser.removeTab(tab);
+});
diff --git a/devtools/client/webconsole/test/head.js b/devtools/client/webconsole/test/head.js
new file mode 100644
index 000000000..519cb78b0
--- /dev/null
+++ b/devtools/client/webconsole/test/head.js
@@ -0,0 +1,1844 @@
+/* -*- 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/ */
+
+/* import-globals-from ../../framework/test/shared-head.js */
+"use strict";
+
+// shared-head.js handles imports, constants, and utility functions
+Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js", this);
+
+var {Utils: WebConsoleUtils} = require("devtools/client/webconsole/utils");
+var {Messages} = require("devtools/client/webconsole/console-output");
+const asyncStorage = require("devtools/shared/async-storage");
+const HUDService = require("devtools/client/webconsole/hudservice");
+
+// Services.prefs.setBoolPref("devtools.debugger.log", true);
+
+var gPendingOutputTest = 0;
+
+// The various categories of messages.
+const CATEGORY_NETWORK = 0;
+const CATEGORY_CSS = 1;
+const CATEGORY_JS = 2;
+const CATEGORY_WEBDEV = 3;
+const CATEGORY_INPUT = 4;
+const CATEGORY_OUTPUT = 5;
+const CATEGORY_SECURITY = 6;
+const CATEGORY_SERVER = 7;
+
+// The possible message severities.
+const SEVERITY_ERROR = 0;
+const SEVERITY_WARNING = 1;
+const SEVERITY_INFO = 2;
+const SEVERITY_LOG = 3;
+
+// The indent of a console group in pixels.
+const GROUP_INDENT = 12;
+
+const WEBCONSOLE_STRINGS_URI = "devtools/client/locales/webconsole.properties";
+var WCUL10n = new WebConsoleUtils.L10n(WEBCONSOLE_STRINGS_URI);
+
+const DOCS_GA_PARAMS = "?utm_source=mozilla" +
+ "&utm_medium=firefox-console-errors" +
+ "&utm_campaign=default";
+
+flags.testing = true;
+
+function loadTab(url) {
+ let deferred = promise.defer();
+
+ let tab = gBrowser.selectedTab = gBrowser.addTab(url);
+ let browser = gBrowser.getBrowserForTab(tab);
+
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ deferred.resolve({tab: tab, browser: browser});
+ }, true);
+
+ return deferred.promise;
+}
+
+function loadBrowser(browser) {
+ return BrowserTestUtils.browserLoaded(browser);
+}
+
+function closeTab(tab) {
+ let deferred = promise.defer();
+
+ let container = gBrowser.tabContainer;
+
+ container.addEventListener("TabClose", function onTabClose() {
+ container.removeEventListener("TabClose", onTabClose, true);
+ deferred.resolve(null);
+ }, true);
+
+ gBrowser.removeTab(tab);
+
+ return deferred.promise;
+}
+
+/**
+ * Load the page and return the associated HUD.
+ *
+ * @param string uri
+ * The URI of the page to load.
+ * @param string consoleType [optional]
+ * The console type, either "browserConsole" or "webConsole". Defaults to
+ * "webConsole".
+ * @return object
+ * The HUD associated with the console
+ */
+function* loadPageAndGetHud(uri, consoleType) {
+ let { browser } = yield loadTab("data:text/html;charset=utf-8,Loading tab for tests");
+
+ let hud;
+ if (consoleType === "browserConsole") {
+ hud = yield HUDService.openBrowserConsoleOrFocus();
+ } else {
+ hud = yield openConsole();
+ }
+
+ ok(hud, "Console was opened");
+
+ let loaded = loadBrowser(browser);
+ yield BrowserTestUtils.loadURI(gBrowser.selectedBrowser, uri);
+ yield loaded;
+
+ yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: uri,
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ return hud;
+}
+
+function afterAllTabsLoaded(callback, win) {
+ win = win || window;
+
+ let stillToLoad = 0;
+
+ function onLoad() {
+ this.removeEventListener("load", onLoad, true);
+ stillToLoad--;
+ if (!stillToLoad) {
+ callback();
+ }
+ }
+
+ for (let a = 0; a < win.gBrowser.tabs.length; a++) {
+ let browser = win.gBrowser.tabs[a].linkedBrowser;
+ if (browser.webProgress.isLoadingDocument) {
+ stillToLoad++;
+ browser.addEventListener("load", onLoad, true);
+ }
+ }
+
+ if (!stillToLoad) {
+ callback();
+ }
+}
+
+/**
+ * Check if a log entry exists in the HUD output node.
+ *
+ * @param {Element} outputNode
+ * the HUD output node.
+ * @param {string} matchString
+ * the string you want to check if it exists in the output node.
+ * @param {string} msg
+ * the message describing the test
+ * @param {boolean} [onlyVisible=false]
+ * find only messages that are visible, not hidden by the filter.
+ * @param {boolean} [failIfFound=false]
+ * fail the test if the string is found in the output node.
+ * @param {string} cssClass [optional]
+ * find only messages with the given CSS class.
+ */
+function testLogEntry(outputNode, matchString, msg, onlyVisible,
+ failIfFound, cssClass) {
+ let selector = ".message";
+ // Skip entries that are hidden by the filter.
+ if (onlyVisible) {
+ selector += ":not(.filtered-by-type):not(.filtered-by-string)";
+ }
+ if (cssClass) {
+ selector += "." + aClass;
+ }
+
+ let msgs = outputNode.querySelectorAll(selector);
+ let found = false;
+ for (let i = 0, n = msgs.length; i < n; i++) {
+ let message = msgs[i].textContent.indexOf(matchString);
+ if (message > -1) {
+ found = true;
+ break;
+ }
+ }
+
+ is(found, !failIfFound, msg);
+}
+
+/**
+ * A convenience method to call testLogEntry().
+ *
+ * @param str string
+ * The string to find.
+ */
+function findLogEntry(str) {
+ testLogEntry(outputNode, str, "found " + str);
+}
+
+/**
+ * Open the Web Console for the given tab.
+ *
+ * @param nsIDOMElement [tab]
+ * Optional tab element for which you want open the Web Console. The
+ * default tab is taken from the global variable |tab|.
+ * @param function [callback]
+ * Optional function to invoke after the Web Console completes
+ * initialization (web-console-created).
+ * @return object
+ * A promise that is resolved once the web console is open.
+ */
+var openConsole = function (tab) {
+ let webconsoleOpened = promise.defer();
+ let target = TargetFactory.forTab(tab || gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "webconsole").then(toolbox => {
+ let hud = toolbox.getCurrentPanel().hud;
+ hud.jsterm._lazyVariablesView = false;
+ webconsoleOpened.resolve(hud);
+ });
+ return webconsoleOpened.promise;
+};
+
+/**
+ * Close the Web Console for the given tab.
+ *
+ * @param nsIDOMElement [tab]
+ * Optional tab element for which you want close the Web Console. The
+ * default tab is taken from the global variable |tab|.
+ * @param function [callback]
+ * Optional function to invoke after the Web Console completes
+ * closing (web-console-destroyed).
+ * @return object
+ * A promise that is resolved once the web console is closed.
+ */
+var closeConsole = Task.async(function* (tab) {
+ let target = TargetFactory.forTab(tab || gBrowser.selectedTab);
+ let toolbox = gDevTools.getToolbox(target);
+ if (toolbox) {
+ yield toolbox.destroy();
+ }
+});
+
+/**
+ * Listen for a new tab to open and return a promise that resolves when one
+ * does and completes the load event.
+ * @return a promise that resolves to the tab object
+ */
+var waitForTab = Task.async(function* () {
+ info("Waiting for a tab to open");
+ yield once(gBrowser.tabContainer, "TabOpen");
+ let tab = gBrowser.selectedTab;
+ let browser = tab.linkedBrowser;
+ yield once(browser, "load", true);
+ info("The tab load completed");
+ return tab;
+});
+
+/**
+ * Dump the output of all open Web Consoles - used only for debugging purposes.
+ */
+function dumpConsoles() {
+ if (gPendingOutputTest) {
+ console.log("dumpConsoles start");
+ for (let [, hud] of HUDService.consoles) {
+ if (!hud.outputNode) {
+ console.debug("no output content for", hud.hudId);
+ continue;
+ }
+
+ console.debug("output content for", hud.hudId);
+ for (let elem of hud.outputNode.childNodes) {
+ dumpMessageElement(elem);
+ }
+ }
+ console.log("dumpConsoles end");
+
+ gPendingOutputTest = 0;
+ }
+}
+
+/**
+ * Dump to output debug information for the given webconsole message.
+ *
+ * @param nsIDOMNode message
+ * The message element you want to display.
+ */
+function dumpMessageElement(message) {
+ let text = message.textContent;
+ let repeats = message.querySelector(".message-repeats");
+ if (repeats) {
+ repeats = repeats.getAttribute("value");
+ }
+ console.debug("id", message.getAttribute("id"),
+ "date", message.timestamp,
+ "class", message.className,
+ "category", message.category,
+ "severity", message.severity,
+ "repeats", repeats,
+ "clipboardText", message.clipboardText,
+ "text", text);
+}
+
+var finishTest = Task.async(function* () {
+ dumpConsoles();
+
+ let browserConsole = HUDService.getBrowserConsole();
+ if (browserConsole) {
+ if (browserConsole.jsterm) {
+ browserConsole.jsterm.clearOutput(true);
+ }
+ yield HUDService.toggleBrowserConsole();
+ }
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ yield gDevTools.closeToolbox(target);
+
+ finish();
+});
+
+// Always use the 'old' frontend for tests that rely on it
+Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.webconsole.new-frontend-enabled");
+});
+
+registerCleanupFunction(function* () {
+ flags.testing = false;
+
+ // Remove stored console commands in between tests
+ yield asyncStorage.removeItem("webConsoleHistory");
+
+ dumpConsoles();
+
+ let browserConsole = HUDService.getBrowserConsole();
+ if (browserConsole) {
+ if (browserConsole.jsterm) {
+ browserConsole.jsterm.clearOutput(true);
+ }
+ yield HUDService.toggleBrowserConsole();
+ }
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ yield gDevTools.closeToolbox(target);
+
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+});
+
+waitForExplicitFinish();
+
+/**
+ * Polls a given function waiting for it to become true.
+ *
+ * @param object options
+ * Options object with the following properties:
+ * - validator
+ * A validator function that returns a boolean. This is called every few
+ * milliseconds to check if the result is true. When it is true, the
+ * promise is resolved and polling stops. If validator never returns
+ * true, then polling timeouts after several tries and the promise is
+ * rejected.
+ * - name
+ * Name of test. This is used to generate the success and failure
+ * messages.
+ * - timeout
+ * Timeout for validator function, in milliseconds. Default is 5000.
+ * @return object
+ * A Promise object that is resolved based on the validator function.
+ */
+function waitForSuccess(options) {
+ let deferred = promise.defer();
+ let start = Date.now();
+ let timeout = options.timeout || 5000;
+ let {validator} = options;
+
+ function wait() {
+ if ((Date.now() - start) > timeout) {
+ // Log the failure.
+ ok(false, "Timed out while waiting for: " + options.name);
+ deferred.reject(null);
+ return;
+ }
+
+ if (validator(options)) {
+ ok(true, options.name);
+ deferred.resolve(null);
+ } else {
+ setTimeout(wait, 100);
+ }
+ }
+
+ setTimeout(wait, 100);
+
+ return deferred.promise;
+}
+
+var openInspector = Task.async(function* (tab = gBrowser.selectedTab) {
+ let target = TargetFactory.forTab(tab);
+ let toolbox = yield gDevTools.showToolbox(target, "inspector");
+ return toolbox.getCurrentPanel();
+});
+
+/**
+ * Find variables or properties in a VariablesView instance.
+ *
+ * @param object view
+ * The VariablesView instance.
+ * @param array rules
+ * The array of rules you want to match. Each rule is an object with:
+ * - name (string|regexp): property name to match.
+ * - value (string|regexp): property value to match.
+ * - isIterator (boolean): check if the property is an iterator.
+ * - isGetter (boolean): check if the property is a getter.
+ * - isGenerator (boolean): check if the property is a generator.
+ * - dontMatch (boolean): make sure the rule doesn't match any property.
+ * @param object options
+ * Options for matching:
+ * - webconsole: the WebConsole instance we work with.
+ * @return object
+ * A promise object that is resolved when all the rules complete
+ * matching. The resolved callback is given an array of all the rules
+ * you wanted to check. Each rule has a new property: |matchedProp|
+ * which holds a reference to the Property object instance from the
+ * VariablesView. If the rule did not match, then |matchedProp| is
+ * undefined.
+ */
+function findVariableViewProperties(view, rules, options) {
+ // Initialize the search.
+ function init() {
+ // Separate out the rules that require expanding properties throughout the
+ // view.
+ let expandRules = [];
+ let filterRules = rules.filter((rule) => {
+ if (typeof rule.name == "string" && rule.name.indexOf(".") > -1) {
+ expandRules.push(rule);
+ return false;
+ }
+ return true;
+ });
+
+ // Search through the view those rules that do not require any properties to
+ // be expanded. Build the array of matchers, outstanding promises to be
+ // resolved.
+ let outstanding = [];
+ finder(filterRules, view, outstanding);
+
+ // Process the rules that need to expand properties.
+ let lastStep = processExpandRules.bind(null, expandRules);
+
+ // Return the results - a promise resolved to hold the updated rules array.
+ let returnResults = onAllRulesMatched.bind(null, rules);
+
+ return promise.all(outstanding).then(lastStep).then(returnResults);
+ }
+
+ function onMatch(prop, rule, matched) {
+ if (matched && !rule.matchedProp) {
+ rule.matchedProp = prop;
+ }
+ }
+
+ function finder(rules, vars, promises) {
+ for (let [, prop] of vars) {
+ for (let rule of rules) {
+ let matcher = matchVariablesViewProperty(prop, rule, options);
+ promises.push(matcher.then(onMatch.bind(null, prop, rule)));
+ }
+ }
+ }
+
+ function processExpandRules(rules) {
+ let rule = rules.shift();
+ if (!rule) {
+ return promise.resolve(null);
+ }
+
+ let deferred = promise.defer();
+ let expandOptions = {
+ rootVariable: view,
+ expandTo: rule.name,
+ webconsole: options.webconsole,
+ };
+
+ variablesViewExpandTo(expandOptions).then(function onSuccess(prop) {
+ let name = rule.name;
+ let lastName = name.split(".").pop();
+ rule.name = lastName;
+
+ let matched = matchVariablesViewProperty(prop, rule, options);
+ return matched.then(onMatch.bind(null, prop, rule)).then(function () {
+ rule.name = name;
+ });
+ }, function onFailure() {
+ return promise.resolve(null);
+ }).then(processExpandRules.bind(null, rules)).then(function () {
+ deferred.resolve(null);
+ });
+
+ return deferred.promise;
+ }
+
+ function onAllRulesMatched(rules) {
+ for (let rule of rules) {
+ let matched = rule.matchedProp;
+ if (matched && !rule.dontMatch) {
+ ok(true, "rule " + rule.name + " matched for property " + matched.name);
+ } else if (matched && rule.dontMatch) {
+ ok(false, "rule " + rule.name + " should not match property " +
+ matched.name);
+ } else {
+ ok(rule.dontMatch, "rule " + rule.name + " did not match any property");
+ }
+ }
+ return rules;
+ }
+
+ return init();
+}
+
+/**
+ * Check if a given Property object from the variables view matches the given
+ * rule.
+ *
+ * @param object prop
+ * The variable's view Property instance.
+ * @param object rule
+ * Rules for matching the property. See findVariableViewProperties() for
+ * details.
+ * @param object options
+ * Options for matching. See findVariableViewProperties().
+ * @return object
+ * A promise that is resolved when all the checks complete. Resolution
+ * result is a boolean that tells your promise callback the match
+ * result: true or false.
+ */
+function matchVariablesViewProperty(prop, rule, options) {
+ function resolve(result) {
+ return promise.resolve(result);
+ }
+
+ if (rule.name) {
+ let match = rule.name instanceof RegExp ?
+ rule.name.test(prop.name) :
+ prop.name == rule.name;
+ if (!match) {
+ return resolve(false);
+ }
+ }
+
+ if (rule.value) {
+ let displayValue = prop.displayValue;
+ if (prop.displayValueClassName == "token-string") {
+ displayValue = displayValue.substring(1, displayValue.length - 1);
+ }
+
+ let match = rule.value instanceof RegExp ?
+ rule.value.test(displayValue) :
+ displayValue == rule.value;
+ if (!match) {
+ info("rule " + rule.name + " did not match value, expected '" +
+ rule.value + "', found '" + displayValue + "'");
+ return resolve(false);
+ }
+ }
+
+ if ("isGetter" in rule) {
+ let isGetter = !!(prop.getter && prop.get("get"));
+ if (rule.isGetter != isGetter) {
+ info("rule " + rule.name + " getter test failed");
+ return resolve(false);
+ }
+ }
+
+ if ("isGenerator" in rule) {
+ let isGenerator = prop.displayValue == "Generator";
+ if (rule.isGenerator != isGenerator) {
+ info("rule " + rule.name + " generator test failed");
+ return resolve(false);
+ }
+ }
+
+ let outstanding = [];
+
+ if ("isIterator" in rule) {
+ let isIterator = isVariableViewPropertyIterator(prop, options.webconsole);
+ outstanding.push(isIterator.then((result) => {
+ if (result != rule.isIterator) {
+ info("rule " + rule.name + " iterator test failed");
+ }
+ return result == rule.isIterator;
+ }));
+ }
+
+ outstanding.push(promise.resolve(true));
+
+ return promise.all(outstanding).then(function _onMatchDone(results) {
+ let ruleMatched = results.indexOf(false) == -1;
+ return resolve(ruleMatched);
+ });
+}
+
+/**
+ * Check if the given variables view property is an iterator.
+ *
+ * @param object prop
+ * The Property instance you want to check.
+ * @param object webConsole
+ * The WebConsole instance to work with.
+ * @return object
+ * A promise that is resolved when the check completes. The resolved
+ * callback is given a boolean: true if the property is an iterator, or
+ * false otherwise.
+ */
+function isVariableViewPropertyIterator(prop, webConsole) {
+ if (prop.displayValue == "Iterator") {
+ return promise.resolve(true);
+ }
+
+ let deferred = promise.defer();
+
+ variablesViewExpandTo({
+ rootVariable: prop,
+ expandTo: "__proto__.__iterator__",
+ webconsole: webConsole,
+ }).then(function onSuccess() {
+ deferred.resolve(true);
+ }, function onFailure() {
+ deferred.resolve(false);
+ });
+
+ return deferred.promise;
+}
+
+/**
+ * Recursively expand the variables view up to a given property.
+ *
+ * @param options
+ * Options for view expansion:
+ * - rootVariable: start from the given scope/variable/property.
+ * - expandTo: string made up of property names you want to expand.
+ * For example: "body.firstChild.nextSibling" given |rootVariable:
+ * document|.
+ * - webconsole: a WebConsole instance. If this is not provided all
+ * property expand() calls will be considered sync. Things may fail!
+ * @return object
+ * A promise that is resolved only when the last property in |expandTo|
+ * is found, and rejected otherwise. Resolution reason is always the
+ * last property - |nextSibling| in the example above. Rejection is
+ * always the last property that was found.
+ */
+function variablesViewExpandTo(options) {
+ let root = options.rootVariable;
+ let expandTo = options.expandTo.split(".");
+ let jsterm = (options.webconsole || {}).jsterm;
+ let lastDeferred = promise.defer();
+
+ function fetch(prop) {
+ if (!prop.onexpand) {
+ ok(false, "property " + prop.name + " cannot be expanded: !onexpand");
+ return promise.reject(prop);
+ }
+
+ let deferred = promise.defer();
+
+ if (prop._fetched || !jsterm) {
+ executeSoon(function () {
+ deferred.resolve(prop);
+ });
+ } else {
+ jsterm.once("variablesview-fetched", function _onFetchProp() {
+ executeSoon(() => deferred.resolve(prop));
+ });
+ }
+
+ prop.expand();
+
+ return deferred.promise;
+ }
+
+ function getNext(prop) {
+ let name = expandTo.shift();
+ let newProp = prop.get(name);
+
+ if (expandTo.length > 0) {
+ ok(newProp, "found property " + name);
+ if (newProp) {
+ fetch(newProp).then(getNext, fetchError);
+ } else {
+ lastDeferred.reject(prop);
+ }
+ } else if (newProp) {
+ lastDeferred.resolve(newProp);
+ } else {
+ lastDeferred.reject(prop);
+ }
+ }
+
+ function fetchError(prop) {
+ lastDeferred.reject(prop);
+ }
+
+ if (!root._fetched) {
+ fetch(root).then(getNext, fetchError);
+ } else {
+ getNext(root);
+ }
+
+ return lastDeferred.promise;
+}
+
+/**
+ * Update the content of a property in the variables view.
+ *
+ * @param object options
+ * Options for the property update:
+ * - property: the property you want to change.
+ * - field: string that tells what you want to change:
+ * - use "name" to change the property name,
+ * - or "value" to change the property value.
+ * - string: the new string to write into the field.
+ * - webconsole: reference to the Web Console instance we work with.
+ * @return object
+ * A Promise object that is resolved once the property is updated.
+ */
+var updateVariablesViewProperty = Task.async(function* (options) {
+ let view = options.property._variablesView;
+ view.window.focus();
+ options.property.focus();
+
+ switch (options.field) {
+ case "name":
+ EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true }, view.window);
+ break;
+ case "value":
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.window);
+ break;
+ default:
+ throw new Error("options.field is incorrect");
+ }
+
+ let deferred = promise.defer();
+
+ executeSoon(() => {
+ EventUtils.synthesizeKey("A", { accelKey: true }, view.window);
+
+ for (let c of options.string) {
+ EventUtils.synthesizeKey(c, {}, view.window);
+ }
+
+ if (options.webconsole) {
+ options.webconsole.jsterm.once("variablesview-fetched")
+ .then((varView) => deferred.resolve(varView));
+ }
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.window);
+
+ if (!options.webconsole) {
+ executeSoon(() => {
+ deferred.resolve(null);
+ });
+ }
+ });
+
+ return deferred.promise;
+});
+
+/**
+ * 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.
+ * - panelWin: the window object of the panel iframe.
+ */
+function openDebugger(options = {}) {
+ if (!options.tab) {
+ options.tab = gBrowser.selectedTab;
+ }
+
+ let deferred = promise.defer();
+
+ let target = TargetFactory.forTab(options.tab);
+ let toolbox = gDevTools.getToolbox(target);
+ let dbgPanelAlreadyOpen = toolbox && toolbox.getPanel("jsdebugger");
+
+ gDevTools.showToolbox(target, "jsdebugger").then(function onSuccess(tool) {
+ let panel = tool.getCurrentPanel();
+ let panelWin = panel.panelWin;
+
+ panel._view.Variables.lazyEmpty = false;
+
+ let resolveObject = {
+ target: target,
+ toolbox: tool,
+ panel: panel,
+ panelWin: panelWin,
+ };
+
+ if (dbgPanelAlreadyOpen) {
+ deferred.resolve(resolveObject);
+ } else {
+ panelWin.DebuggerController.waitForSourcesLoaded().then(() => {
+ deferred.resolve(resolveObject);
+ });
+ }
+ }, function onFailure(reason) {
+ console.debug("failed to open the toolbox for 'jsdebugger'", reason);
+ deferred.reject(reason);
+ });
+
+ return deferred.promise;
+}
+
+/**
+ * Returns true if the caret in the debugger editor is placed at the specified
+ * position.
+ * @param panel The debugger panel.
+ * @param {number} line The line number.
+ * @param {number} [col] The column number.
+ * @returns {boolean}
+ */
+function isDebuggerCaretPos(panel, line, col = 1) {
+ let editor = panel.panelWin.DebuggerView.editor;
+ let cursor = editor.getCursor();
+
+ // Source editor starts counting line and column numbers from 0.
+ info("Current editor caret position: " + (cursor.line + 1) + ", " +
+ (cursor.ch + 1));
+ return cursor.line == (line - 1) && cursor.ch == (col - 1);
+}
+
+/**
+ * Wait for messages in the Web Console output.
+ *
+ * @param object options
+ * Options for what you want to wait for:
+ * - webconsole: the webconsole instance you work with.
+ * - matchCondition: "any" or "all". Default: "all". The promise
+ * returned by this function resolves when all of the messages are
+ * matched, if the |matchCondition| is "all". If you set the condition to
+ * "any" then the promise is resolved by any message rule that matches,
+ * irrespective of order - waiting for messages stops whenever any rule
+ * matches.
+ * - messages: an array of objects that tells which messages to wait for.
+ * Properties:
+ * - text: string or RegExp to match the textContent of each new
+ * message.
+ * - noText: string or RegExp that must not match in the message
+ * textContent.
+ * - repeats: the number of message repeats, as displayed by the Web
+ * Console.
+ * - category: match message category. See CATEGORY_* constants at
+ * the top of this file.
+ * - severity: match message severity. See SEVERITY_* constants at
+ * the top of this file.
+ * - count: how many unique web console messages should be matched by
+ * this rule.
+ * - consoleTrace: boolean, set to |true| to match a console.trace()
+ * message. Optionally this can be an object of the form
+ * { file, fn, line } that can match the specified file, function
+ * and/or line number in the trace message.
+ * - consoleTime: string that matches a console.time() timer name.
+ * Provide this if you want to match a console.time() message.
+ * - consoleTimeEnd: same as above, but for console.timeEnd().
+ * - consoleDir: boolean, set to |true| to match a console.dir()
+ * message.
+ * - consoleGroup: boolean, set to |true| to match a console.group()
+ * message.
+ * - consoleTable: boolean, set to |true| to match a console.table()
+ * message.
+ * - longString: boolean, set to |true} to match long strings in the
+ * message.
+ * - collapsible: boolean, set to |true| to match messages that can
+ * be collapsed/expanded.
+ * - type: match messages that are instances of the given object. For
+ * example, you can point to Messages.NavigationMarker to match any
+ * such message.
+ * - objects: boolean, set to |true| if you expect inspectable
+ * objects in the message.
+ * - source: object of the shape { url, line }. This is used to
+ * match the source URL and line number of the error message or
+ * console API call.
+ * - prefix: prefix text to check for in the prefix element.
+ * - stacktrace: array of objects of the form { file, fn, line } that
+ * can match frames in the stacktrace associated with the message.
+ * - groupDepth: number used to check the depth of the message in
+ * a group.
+ * - url: URL to match for network requests.
+ * @return object
+ * A promise object is returned once the messages you want are found.
+ * The promise is resolved with the array of rule objects you give in
+ * the |messages| property. Each objects is the same as provided, with
+ * additional properties:
+ * - matched: a Set of web console messages that matched the rule.
+ * - clickableElements: a list of inspectable objects. This is available
+ * if any of the following properties are present in the rule:
+ * |consoleTrace| or |objects|.
+ * - longStrings: a list of long string ellipsis elements you can click
+ * in the message element, to expand a long string. This is available
+ * only if |longString| is present in the matching rule.
+ */
+function waitForMessages(options) {
+ info("Waiting for messages...");
+
+ gPendingOutputTest++;
+ let webconsole = options.webconsole;
+ let rules = WebConsoleUtils.cloneObject(options.messages, true);
+ let rulesMatched = 0;
+ let listenerAdded = false;
+ let deferred = promise.defer();
+ options.matchCondition = options.matchCondition || "all";
+
+ function checkText(rule, text) {
+ let result = false;
+ if (Array.isArray(rule)) {
+ result = rule.every((s) => checkText(s, text));
+ } else if (typeof rule == "string") {
+ result = text.indexOf(rule) > -1;
+ } else if (rule instanceof RegExp) {
+ result = rule.test(text);
+ } else {
+ result = rule == text;
+ }
+ return result;
+ }
+
+ function checkConsoleTable(rule, element) {
+ let elemText = element.textContent;
+
+ if (!checkText("console.table():", elemText)) {
+ return false;
+ }
+
+ rule.category = CATEGORY_WEBDEV;
+ rule.severity = SEVERITY_LOG;
+ rule.type = Messages.ConsoleTable;
+
+ return true;
+ }
+
+ function checkConsoleTrace(rule, element) {
+ let elemText = element.textContent;
+ let trace = rule.consoleTrace;
+
+ if (!checkText("console.trace():", elemText)) {
+ return false;
+ }
+
+ rule.category = CATEGORY_WEBDEV;
+ rule.severity = SEVERITY_LOG;
+ rule.type = Messages.ConsoleTrace;
+
+ if (!rule.stacktrace && typeof trace == "object" && trace !== true) {
+ if (Array.isArray(trace)) {
+ rule.stacktrace = trace;
+ } else {
+ rule.stacktrace = [trace];
+ }
+ }
+
+ return true;
+ }
+
+ function checkConsoleTime(rule, element) {
+ let elemText = element.textContent;
+ let time = rule.consoleTime;
+
+ if (!checkText(time + ": timer started", elemText)) {
+ return false;
+ }
+
+ rule.category = CATEGORY_WEBDEV;
+ rule.severity = SEVERITY_LOG;
+
+ return true;
+ }
+
+ function checkConsoleTimeEnd(rule, element) {
+ let elemText = element.textContent;
+ let time = rule.consoleTimeEnd;
+ let regex = new RegExp(time + ": -?\\d+([,.]\\d+)?ms");
+
+ if (!checkText(regex, elemText)) {
+ return false;
+ }
+
+ rule.category = CATEGORY_WEBDEV;
+ rule.severity = SEVERITY_LOG;
+
+ return true;
+ }
+
+ function checkConsoleDir(rule, element) {
+ if (!element.classList.contains("inlined-variables-view")) {
+ return false;
+ }
+
+ let elemText = element.textContent;
+ if (!checkText(rule.consoleDir, elemText)) {
+ return false;
+ }
+
+ let iframe = element.querySelector("iframe");
+ if (!iframe) {
+ ok(false, "console.dir message has no iframe");
+ return false;
+ }
+
+ return true;
+ }
+
+ function checkConsoleGroup(rule) {
+ if (!isNaN(parseInt(rule.consoleGroup, 10))) {
+ rule.groupDepth = rule.consoleGroup;
+ }
+ rule.category = CATEGORY_WEBDEV;
+ rule.severity = SEVERITY_LOG;
+
+ return true;
+ }
+
+ function checkSource(rule, element) {
+ let location = getRenderedSource(element);
+ if (!location) {
+ return false;
+ }
+
+ if (!checkText(rule.source.url, location.url)) {
+ return false;
+ }
+
+ if ("line" in rule.source && location.line != rule.source.line) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function checkCollapsible(rule, element) {
+ let msg = element._messageObject;
+ if (!msg || !!msg.collapsible != rule.collapsible) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function checkStacktrace(rule, element) {
+ let stack = rule.stacktrace;
+ let frames = element.querySelectorAll(".stacktrace > .stack-trace > .frame-link");
+ if (!frames.length) {
+ return false;
+ }
+
+ for (let i = 0; i < stack.length; i++) {
+ let frame = frames[i];
+ let expected = stack[i];
+ if (!frame) {
+ ok(false, "expected frame #" + i + " but didnt find it");
+ return false;
+ }
+
+ if (expected.file) {
+ let url = frame.getAttribute("data-url");
+ if (!checkText(expected.file, url)) {
+ ok(false, "frame #" + i + " does not match file name: " +
+ expected.file + " != " + url);
+ displayErrorContext(rule, element);
+ return false;
+ }
+ }
+
+ if (expected.fn) {
+ let fn = frame.querySelector(".frame-link-function-display-name").textContent;
+ if (!checkText(expected.fn, fn)) {
+ ok(false, "frame #" + i + " does not match the function name: " +
+ expected.fn + " != " + fn);
+ displayErrorContext(rule, element);
+ return false;
+ }
+ }
+
+ if (expected.line) {
+ let line = frame.getAttribute("data-line");
+ if (!checkText(expected.line, line)) {
+ ok(false, "frame #" + i + " does not match the line number: " +
+ expected.line + " != " + line);
+ displayErrorContext(rule, element);
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ function hasXhrLabel(element) {
+ let xhr = element.querySelector(".xhr");
+ if (!xhr) {
+ return false;
+ }
+ return true;
+ }
+
+ function checkMessage(rule, element) {
+ let elemText = element.textContent;
+
+ if (rule.text && !checkText(rule.text, elemText)) {
+ return false;
+ }
+
+ if (rule.noText && checkText(rule.noText, elemText)) {
+ return false;
+ }
+
+ if (rule.consoleTable && !checkConsoleTable(rule, element)) {
+ return false;
+ }
+
+ if (rule.consoleTrace && !checkConsoleTrace(rule, element)) {
+ return false;
+ }
+
+ if (rule.consoleTime && !checkConsoleTime(rule, element)) {
+ return false;
+ }
+
+ if (rule.consoleTimeEnd && !checkConsoleTimeEnd(rule, element)) {
+ return false;
+ }
+
+ if (rule.consoleDir && !checkConsoleDir(rule, element)) {
+ return false;
+ }
+
+ if (rule.consoleGroup && !checkConsoleGroup(rule, element)) {
+ return false;
+ }
+
+ if (rule.source && !checkSource(rule, element)) {
+ return false;
+ }
+
+ if ("collapsible" in rule && !checkCollapsible(rule, element)) {
+ return false;
+ }
+
+ if (rule.isXhr && !hasXhrLabel(element)) {
+ return false;
+ }
+
+ if (!rule.isXhr && hasXhrLabel(element)) {
+ return false;
+ }
+
+ let partialMatch = !!(rule.consoleTrace || rule.consoleTime ||
+ rule.consoleTimeEnd);
+
+ // The rule tries to match the newer types of messages, based on their
+ // object constructor.
+ if (rule.type) {
+ if (!element._messageObject ||
+ !(element._messageObject instanceof rule.type)) {
+ if (partialMatch) {
+ ok(false, "message type for rule: " + displayRule(rule));
+ displayErrorContext(rule, element);
+ }
+ return false;
+ }
+ partialMatch = true;
+ }
+
+ if ("category" in rule && element.category != rule.category) {
+ if (partialMatch) {
+ is(element.category, rule.category,
+ "message category for rule: " + displayRule(rule));
+ displayErrorContext(rule, element);
+ }
+ return false;
+ }
+
+ if ("severity" in rule && element.severity != rule.severity) {
+ if (partialMatch) {
+ is(element.severity, rule.severity,
+ "message severity for rule: " + displayRule(rule));
+ displayErrorContext(rule, element);
+ }
+ return false;
+ }
+
+ if (rule.text) {
+ partialMatch = true;
+ }
+
+ if (rule.stacktrace && !checkStacktrace(rule, element)) {
+ if (partialMatch) {
+ ok(false, "failed to match stacktrace for rule: " + displayRule(rule));
+ displayErrorContext(rule, element);
+ }
+ return false;
+ }
+
+ if (rule.category == CATEGORY_NETWORK && "url" in rule &&
+ !checkText(rule.url, element.url)) {
+ return false;
+ }
+
+ if ("repeats" in rule) {
+ let repeats = element.querySelector(".message-repeats");
+ if (!repeats || repeats.getAttribute("value") != rule.repeats) {
+ return false;
+ }
+ }
+
+ if ("groupDepth" in rule) {
+ let indentNode = element.querySelector(".indent");
+ let indent = (GROUP_INDENT * rule.groupDepth) + "px";
+ if (!indentNode || indentNode.style.width != indent) {
+ is(indentNode.style.width, indent,
+ "group depth check failed for message rule: " + displayRule(rule));
+ return false;
+ }
+ }
+
+ if ("longString" in rule) {
+ let longStrings = element.querySelectorAll(".longStringEllipsis");
+ if (rule.longString != !!longStrings[0]) {
+ if (partialMatch) {
+ is(!!longStrings[0], rule.longString,
+ "long string existence check failed for message rule: " +
+ displayRule(rule));
+ displayErrorContext(rule, element);
+ }
+ return false;
+ }
+ rule.longStrings = longStrings;
+ }
+
+ if ("objects" in rule) {
+ let clickables = element.querySelectorAll(".message-body a");
+ if (rule.objects != !!clickables[0]) {
+ if (partialMatch) {
+ is(!!clickables[0], rule.objects,
+ "objects existence check failed for message rule: " +
+ displayRule(rule));
+ displayErrorContext(rule, element);
+ }
+ return false;
+ }
+ rule.clickableElements = clickables;
+ }
+
+ if ("prefix" in rule) {
+ let prefixNode = element.querySelector(".prefix");
+ is(prefixNode && prefixNode.textContent, rule.prefix, "Check prefix");
+ }
+
+ let count = rule.count || 1;
+ if (!rule.matched) {
+ rule.matched = new Set();
+ }
+ rule.matched.add(element);
+
+ return rule.matched.size == count;
+ }
+
+ function onMessagesAdded(event, newMessages) {
+ for (let msg of newMessages) {
+ let elem = msg.node;
+ let location = getRenderedSource(elem);
+ if (location && location.url) {
+ let url = location.url;
+ // Prevent recursion with the browser console and any potential
+ // messages coming from head.js.
+ if (url.indexOf("devtools/client/webconsole/test/head.js") != -1) {
+ continue;
+ }
+ }
+
+ for (let rule of rules) {
+ if (rule._ruleMatched) {
+ continue;
+ }
+
+ let matched = checkMessage(rule, elem);
+ if (matched) {
+ rule._ruleMatched = true;
+ rulesMatched++;
+ ok(1, "matched rule: " + displayRule(rule));
+ if (maybeDone()) {
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ function allRulesMatched() {
+ return options.matchCondition == "all" && rulesMatched == rules.length ||
+ options.matchCondition == "any" && rulesMatched > 0;
+ }
+
+ function maybeDone() {
+ if (allRulesMatched()) {
+ if (listenerAdded) {
+ webconsole.ui.off("new-messages", onMessagesAdded);
+ }
+ gPendingOutputTest--;
+ deferred.resolve(rules);
+ return true;
+ }
+ return false;
+ }
+
+ function testCleanup() {
+ if (allRulesMatched()) {
+ return;
+ }
+
+ if (webconsole.ui) {
+ webconsole.ui.off("new-messages", onMessagesAdded);
+ }
+
+ for (let rule of rules) {
+ if (!rule._ruleMatched) {
+ ok(false, "failed to match rule: " + displayRule(rule));
+ }
+ }
+ }
+
+ function displayRule(rule) {
+ return rule.name || rule.text;
+ }
+
+ function displayErrorContext(rule, element) {
+ console.log("error occured during rule " + displayRule(rule));
+ console.log("while checking the following message");
+ dumpMessageElement(element);
+ }
+
+ executeSoon(() => {
+ let messages = [];
+ for (let elem of webconsole.outputNode.childNodes) {
+ messages.push({
+ node: elem,
+ update: false,
+ });
+ }
+
+ onMessagesAdded("new-messages", messages);
+
+ if (!allRulesMatched()) {
+ listenerAdded = true;
+ registerCleanupFunction(testCleanup);
+ webconsole.ui.on("new-messages", onMessagesAdded);
+ }
+ });
+
+ return deferred.promise;
+}
+
+function whenDelayedStartupFinished(win, callback) {
+ Services.obs.addObserver(function observer(subject, topic) {
+ if (win == subject) {
+ Services.obs.removeObserver(observer, topic);
+ executeSoon(callback);
+ }
+ }, "browser-delayed-startup-finished", false);
+}
+
+/**
+ * Check the web console output for the given inputs. Each input is checked for
+ * the expected JS eval result, the result of calling print(), the result of
+ * console.log(). The JS eval result is also checked if it opens the variables
+ * view on click.
+ *
+ * @param object hud
+ * The web console instance to work with.
+ * @param array inputTests
+ * An array of input tests. An input test element is an object. Each
+ * object has the following properties:
+ * - input: string, JS input value to execute.
+ *
+ * - output: string|RegExp, expected JS eval result.
+ *
+ * - inspectable: boolean, when true, the test runner expects the JS eval
+ * result is an object that can be clicked for inspection.
+ *
+ * - noClick: boolean, when true, the test runner does not click the JS
+ * eval result. Some objects, like |window|, have a lot of properties and
+ * opening vview for them is very slow (they can cause timeouts in debug
+ * builds).
+ *
+ * - consoleOutput: string|RegExp, optional, expected consoleOutput
+ * If not provided consoleOuput = output;
+ *
+ * - printOutput: string|RegExp, optional, expected output for
+ * |print(input)|. If this is not provided, printOutput = output.
+ *
+ * - variablesViewLabel: string|RegExp, optional, the expected variables
+ * view label when the object is inspected. If this is not provided, then
+ * |output| is used.
+ *
+ * - inspectorIcon: boolean, when true, the test runner expects the
+ * result widget to contain an inspectorIcon element (className
+ * open-inspector).
+ *
+ * - expectedTab: string, optional, the full URL of the new tab which
+ * must open. If this is not provided, any new tabs that open will cause
+ * a test failure.
+ */
+function checkOutputForInputs(hud, inputTests) {
+ let container = gBrowser.tabContainer;
+
+ function* runner() {
+ for (let [i, entry] of inputTests.entries()) {
+ info("checkInput(" + i + "): " + entry.input);
+ yield checkInput(entry);
+ }
+ container = null;
+ }
+
+ function* checkInput(entry) {
+ yield checkConsoleLog(entry);
+ yield checkPrintOutput(entry);
+ yield checkJSEval(entry);
+ }
+
+ function* checkConsoleLog(entry) {
+ info("Logging");
+ hud.jsterm.clearOutput();
+ hud.jsterm.execute("console.log(" + entry.input + ")");
+
+ let consoleOutput = "consoleOutput" in entry ?
+ entry.consoleOutput : entry.output;
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "console.log() output: " + consoleOutput,
+ text: consoleOutput,
+ category: CATEGORY_WEBDEV,
+ severity: SEVERITY_LOG,
+ }],
+ });
+
+ let msg = [...result.matched][0];
+
+ if (entry.consoleLogClick) {
+ yield checkObjectClick(entry, msg);
+ }
+
+ if (typeof entry.inspectorIcon == "boolean") {
+ info("Checking Inspector Link");
+ yield checkLinkToInspector(entry.inspectorIcon, msg);
+ }
+ }
+
+ function checkPrintOutput(entry) {
+ info("Printing");
+ hud.jsterm.clearOutput();
+ hud.jsterm.execute("print(" + entry.input + ")");
+
+ let printOutput = entry.printOutput || entry.output;
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "print() output: " + printOutput,
+ text: printOutput,
+ category: CATEGORY_OUTPUT,
+ }],
+ });
+ }
+
+ function* checkJSEval(entry) {
+ info("Evaluating");
+ hud.jsterm.clearOutput();
+ hud.jsterm.execute(entry.input);
+
+ let evalOutput = entry.evalOutput || entry.output;
+
+ let [result] = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ name: "JS eval output: " + entry.evalOutput,
+ text: entry.evalOutput,
+ category: CATEGORY_OUTPUT,
+ }],
+ });
+
+ let msg = [...result.matched][0];
+ if (!entry.noClick) {
+ yield checkObjectClick(entry, msg);
+ }
+ if (typeof entry.inspectorIcon == "boolean") {
+ info("Checking Inspector Link: " + entry.input);
+ yield checkLinkToInspector(entry.inspectorIcon, msg);
+ }
+ }
+
+ function* checkObjectClick(entry, msg) {
+ info("Clicking");
+ let body;
+ if (entry.getClickableNode) {
+ body = entry.getClickableNode(msg);
+ } else {
+ body = msg.querySelector(".message-body a") ||
+ msg.querySelector(".message-body");
+ }
+ ok(body, "the message body");
+
+ let deferredVariablesView = promise.defer();
+ entry._onVariablesViewOpen = onVariablesViewOpen.bind(null, entry,
+ deferredVariablesView);
+ hud.jsterm.on("variablesview-open", entry._onVariablesViewOpen);
+
+ let deferredTab = promise.defer();
+ entry._onTabOpen = onTabOpen.bind(null, entry, deferredTab);
+ container.addEventListener("TabOpen", entry._onTabOpen, true);
+
+ body.scrollIntoView();
+
+ if (!entry.suppressClick) {
+ EventUtils.synthesizeMouse(body, 2, 2, {}, hud.iframeWindow);
+ }
+
+ if (entry.inspectable) {
+ info("message body tagName '" + body.tagName + "' className '" +
+ body.className + "'");
+ yield deferredVariablesView.promise;
+ } else {
+ hud.jsterm.off("variablesview-open", entry._onVariablesView);
+ entry._onVariablesView = null;
+ }
+
+ if (entry.expectedTab) {
+ yield deferredTab.promise;
+ } else {
+ container.removeEventListener("TabOpen", entry._onTabOpen, true);
+ entry._onTabOpen = null;
+ }
+
+ yield promise.resolve(null);
+ }
+
+ function onVariablesViewOpen(entry, {resolve, reject}, event, view, options) {
+ info("Variables view opened");
+ let label = entry.variablesViewLabel || entry.output;
+ if (typeof label == "string" && options.label != label) {
+ return;
+ }
+ if (label instanceof RegExp && !label.test(options.label)) {
+ return;
+ }
+
+ hud.jsterm.off("variablesview-open", entry._onVariablesViewOpen);
+ entry._onVariablesViewOpen = null;
+ ok(entry.inspectable, "variables view was shown");
+
+ resolve(null);
+ }
+
+ function onTabOpen(entry, {resolve, reject}, event) {
+ container.removeEventListener("TabOpen", entry._onTabOpen, true);
+ entry._onTabOpen = null;
+ let tab = event.target;
+ let browser = gBrowser.getBrowserForTab(tab);
+
+ Task.spawn(function* () {
+ yield loadBrowser(browser);
+ let uri = yield ContentTask.spawn(browser, {}, function* () {
+ return content.location.href;
+ });
+ ok(entry.expectedTab && entry.expectedTab == uri,
+ "opened tab '" + uri + "', expected tab '" + entry.expectedTab + "'");
+ yield closeTab(tab);
+ }).then(resolve, reject);
+ }
+
+ return Task.spawn(runner);
+}
+
+/**
+ * Check the web console DOM element output for the given inputs.
+ * Each input is checked for the expected JS eval result. The JS eval result is
+ * also checked if it opens the inspector with the correct node selected on
+ * inspector icon click
+ *
+ * @param object hud
+ * The web console instance to work with.
+ * @param array inputTests
+ * An array of input tests. An input test element is an object. Each
+ * object has the following properties:
+ * - input: string, JS input value to execute.
+ *
+ * - output: string, expected JS eval result.
+ *
+ * - displayName: string, expected NodeFront's displayName.
+ *
+ * - attr: Array, expected NodeFront's attributes
+ */
+function checkDomElementHighlightingForInputs(hud, inputs) {
+ function* runner() {
+ let toolbox = gDevTools.getToolbox(hud.target);
+
+ // Loading the inspector panel at first, to make it possible to listen for
+ // new node selections
+ yield toolbox.selectTool("inspector");
+ let inspector = toolbox.getCurrentPanel();
+ yield toolbox.selectTool("webconsole");
+
+ info("Iterating over the test data");
+ for (let data of inputs) {
+ let [result] = yield jsEval(data.input, {text: data.output});
+ let {msg} = yield checkWidgetAndMessage(result);
+ yield checkNodeHighlight(toolbox, inspector, msg, data);
+ }
+ }
+
+ function jsEval(input, message) {
+ info("Executing '" + input + "' in the web console");
+
+ hud.jsterm.clearOutput();
+ hud.jsterm.execute(input);
+
+ return waitForMessages({
+ webconsole: hud,
+ messages: [message]
+ });
+ }
+
+ function* checkWidgetAndMessage(result) {
+ info("Getting the output ElementNode widget");
+
+ let msg = [...result.matched][0];
+ let widget = [...msg._messageObject.widgets][0];
+ ok(widget, "ElementNode widget found in the output");
+
+ info("Waiting for the ElementNode widget to be linked to the inspector");
+ yield widget.linkToInspector();
+
+ return {widget, msg};
+ }
+
+ function* checkNodeHighlight(toolbox, inspector, msg, testData) {
+ let inspectorIcon = msg.querySelector(".open-inspector");
+ ok(inspectorIcon, "Inspector icon found in the ElementNode widget");
+
+ 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");
+ let onNodeHighlight = toolbox.once("node-highlight");
+
+ EventUtils.synthesizeMouseAtCenter(inspectorIcon, {},
+ inspectorIcon.ownerDocument.defaultView);
+ yield onInspectorSelected;
+ yield onInspectorUpdated;
+ yield onNodeHighlight;
+ let nodeFront = yield onNewNode;
+
+ ok(true, "Inspector selected and new node got selected");
+
+ is(nodeFront.displayName, testData.displayName,
+ "The correct node was highlighted");
+
+ if (testData.attrs) {
+ let attrs = nodeFront.attributes;
+ for (let i in testData.attrs) {
+ is(attrs[i].name, testData.attrs[i].name,
+ "Expected attribute's name is present");
+ is(attrs[i].value, testData.attrs[i].value,
+ "Expected attribute's value is present");
+ }
+ }
+
+ info("Unhighlight the node by moving away from the markup view");
+ let onNodeUnhighlight = toolbox.once("node-unhighlight");
+ let btn = inspector.toolbox.doc.querySelector(".toolbox-dock-button");
+ EventUtils.synthesizeMouseAtCenter(btn, {type: "mousemove"},
+ inspector.toolbox.win);
+ yield onNodeUnhighlight;
+
+ info("Switching back to the console");
+ yield toolbox.selectTool("webconsole");
+ }
+
+ return Task.spawn(runner);
+}
+
+/**
+ * Finish the request and resolve with the request object.
+ *
+ * @param {Function} predicate A predicate function that takes the request
+ * object as an argument and returns true if the request was the expected one,
+ * false otherwise. The returned promise is resolved ONLY if the predicate
+ * matches a request. Defaults to accepting any request.
+ * @return promise
+ * @resolves The request object.
+ */
+function waitForFinishedRequest(predicate = () => true) {
+ registerCleanupFunction(function () {
+ HUDService.lastFinishedRequest.callback = null;
+ });
+
+ return new Promise(resolve => {
+ HUDService.lastFinishedRequest.callback = request => {
+ // Check if this is the expected request
+ if (predicate(request)) {
+ // Match found. Clear the listener.
+ HUDService.lastFinishedRequest.callback = null;
+
+ resolve(request);
+ } else {
+ info(`Ignoring unexpected request ${JSON.stringify(request, null, 2)}`);
+ }
+ };
+ });
+}
+
+/**
+ * Wait for eventName on target.
+ * @param {Object} target An observable object that either supports on/off or
+ * addEventListener/removeEventListener
+ * @param {String} eventName
+ * @param {Boolean} useCapture Optional for addEventListener/removeEventListener
+ * @return A promise that resolves when the event has been handled
+ */
+function once(target, eventName, useCapture = false) {
+ info("Waiting for event: '" + eventName + "' on " + target + ".");
+
+ let deferred = promise.defer();
+
+ for (let [add, remove] of [
+ ["addEventListener", "removeEventListener"],
+ ["addListener", "removeListener"],
+ ["on", "off"]
+ ]) {
+ if ((add in target) && (remove in target)) {
+ target[add](eventName, function onEvent(...aArgs) {
+ target[remove](eventName, onEvent, useCapture);
+ deferred.resolve.apply(deferred, aArgs);
+ }, useCapture);
+ break;
+ }
+ }
+
+ return deferred.promise;
+}
+
+/**
+ * Checks a link to the inspector
+ *
+ * @param {boolean} hasLinkToInspector Set to true if the message should
+ * link to the inspector panel.
+ * @param {element} msg The message to test.
+ */
+function checkLinkToInspector(hasLinkToInspector, msg) {
+ let elementNodeWidget = [...msg._messageObject.widgets][0];
+ if (!elementNodeWidget) {
+ ok(!hasLinkToInspector, "The message has no ElementNode widget");
+ return true;
+ }
+
+ return elementNodeWidget.linkToInspector().then(() => {
+ // linkToInspector resolved, check for the .open-inspector element
+ if (hasLinkToInspector) {
+ ok(msg.querySelectorAll(".open-inspector").length,
+ "The ElementNode widget is linked to the inspector");
+ } else {
+ ok(!msg.querySelectorAll(".open-inspector").length,
+ "The ElementNode widget isn't linked to the inspector");
+ }
+ }, () => {
+ // linkToInspector promise rejected, node not linked to inspector
+ ok(!hasLinkToInspector,
+ "The ElementNode widget isn't linked to the inspector");
+ });
+}
+
+function getSourceActor(sources, URL) {
+ let item = sources.getItemForAttachment(a => a.source.url === URL);
+ return item && item.value;
+}
+
+/**
+ * Make a request against an actor and resolve with the packet.
+ * @param object client
+ * The client to use when making the request.
+ * @param function requestType
+ * The client request function to run.
+ * @param array args
+ * The arguments to pass into the function.
+ */
+function getPacket(client, requestType, args) {
+ return new Promise(resolve => {
+ client[requestType](...args, packet => resolve(packet));
+ });
+}
+
+/**
+ * Verify that clicking on a link from a popup notification message tries to
+ * open the expected URL.
+ */
+function simulateMessageLinkClick(element, expectedLink) {
+ let deferred = promise.defer();
+
+ // Invoke the click event and check if a new tab would
+ // open to the correct page.
+ let oldOpenUILinkIn = window.openUILinkIn;
+ window.openUILinkIn = function (link) {
+ if (link == expectedLink) {
+ ok(true, "Clicking the message link opens the desired page");
+ window.openUILinkIn = oldOpenUILinkIn;
+ deferred.resolve();
+ }
+ };
+
+ let event = new MouseEvent("click", {
+ detail: 1,
+ button: 0,
+ bubbles: true,
+ cancelable: true
+ });
+ element.dispatchEvent(event);
+
+ return deferred.promise;
+}
+
+function getRenderedSource(root) {
+ let location = root.querySelector(".message-location .frame-link");
+ return location ? {
+ url: location.getAttribute("data-url"),
+ line: location.getAttribute("data-line"),
+ column: location.getAttribute("data-column"),
+ } : null;
+}
diff --git a/devtools/client/webconsole/test/test-autocomplete-in-stackframe.html b/devtools/client/webconsole/test/test-autocomplete-in-stackframe.html
new file mode 100644
index 000000000..ba5212de3
--- /dev/null
+++ b/devtools/client/webconsole/test/test-autocomplete-in-stackframe.html
@@ -0,0 +1,50 @@
+<!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>
+ var foo1 = "globalFoo";
+
+ var foo1Obj = {
+ prop1: "111",
+ prop2: {
+ prop21: "212121"
+ }
+ };
+
+ function firstCall()
+ {
+ var foo2 = "fooFirstCall";
+
+ var foo2Obj = {
+ prop1: {
+ prop11: "111111"
+ }
+ };
+
+ secondCall();
+ }
+
+ function secondCall()
+ {
+ var foo3 = "fooSecondCall";
+
+ var foo3Obj = {
+ prop1: {
+ prop11: "313131"
+ }
+ };
+
+ debugger;
+ }
+ </script>
+ </head>
+ <body>
+ <p>Hello world!</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-585956-console-trace.html b/devtools/client/webconsole/test/test-bug-585956-console-trace.html
new file mode 100644
index 000000000..e658ba633
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-585956-console-trace.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head><meta charset="utf-8">
+ <title>Web Console test for bug 585956 - console.trace()</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<script type="application/javascript">
+window.foobar585956c = function(a) {
+ console.trace();
+ return a+"c";
+};
+
+function foobar585956b(a) {
+ return foobar585956c(a+"b");
+}
+
+function foobar585956a(omg) {
+ return foobar585956b(omg + "a");
+}
+
+foobar585956a("omg");
+</script>
+ </head>
+ <body>
+ <p>Web Console test for bug 585956 - console.trace().</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-593003-iframe-wrong-hud-iframe.html b/devtools/client/webconsole/test/test-bug-593003-iframe-wrong-hud-iframe.html
new file mode 100644
index 000000000..ebf9c515f
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-593003-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/test-bug-593003-iframe-wrong-hud.html b/devtools/client/webconsole/test/test-bug-593003-iframe-wrong-hud.html
new file mode 100644
index 000000000..8e47cf20f
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-593003-iframe-wrong-hud.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>WebConsole test: iframe associated to the wrong HUD</title>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <p>WebConsole test: iframe associated to the wrong HUD.</p>
+ <iframe
+ src="http://example.com/browser/devtools/client/webconsole/test/test-bug-593003-iframe-wrong-hud-iframe.html"></iframe>
+ </body>
+ </html>
diff --git a/devtools/client/webconsole/test/test-bug-595934-canvas-css.html b/devtools/client/webconsole/test/test-bug-595934-canvas-css.html
new file mode 100644
index 000000000..3c9cf03a5
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-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-bug-595934-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/test-bug-595934-canvas-css.js b/devtools/client/webconsole/test/test-bug-595934-canvas-css.js
new file mode 100644
index 000000000..ee1ebd425
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-canvas-css.js
@@ -0,0 +1,10 @@
+/* -*- 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/ */
+
+window.addEventListener("DOMContentLoaded", function () {
+ var canvas = document.querySelector("canvas");
+ var context = canvas.getContext("2d");
+ context.strokeStyle = "foobarCanvasCssParser";
+}, false);
diff --git a/devtools/client/webconsole/test/test-bug-595934-css-loader.css b/devtools/client/webconsole/test/test-bug-595934-css-loader.css
new file mode 100644
index 000000000..b4224430f
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-css-loader.css
@@ -0,0 +1,10 @@
+/*
+ * 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/test-bug-595934-css-loader.css^headers^ b/devtools/client/webconsole/test/test-bug-595934-css-loader.css^headers^
new file mode 100644
index 000000000..e7be84a71
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-css-loader.css^headers^
@@ -0,0 +1 @@
+Content-Type: image/png
diff --git a/devtools/client/webconsole/test/test-bug-595934-css-loader.html b/devtools/client/webconsole/test/test-bug-595934-css-loader.html
new file mode 100644
index 000000000..6bb0d54c5
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-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-bug-595934-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/test-bug-595934-css-parser.css b/devtools/client/webconsole/test/test-bug-595934-css-parser.css
new file mode 100644
index 000000000..f6db82398
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-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/test-bug-595934-css-parser.html b/devtools/client/webconsole/test/test-bug-595934-css-parser.html
new file mode 100644
index 000000000..a4ea74ba3
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-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-bug-595934-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/test-bug-595934-empty-getelementbyid.html b/devtools/client/webconsole/test/test-bug-595934-empty-getelementbyid.html
new file mode 100644
index 000000000..a70f9011b
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-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-bug-595934-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/test-bug-595934-empty-getelementbyid.js b/devtools/client/webconsole/test/test-bug-595934-empty-getelementbyid.js
new file mode 100644
index 000000000..bf9ccece9
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-empty-getelementbyid.js
@@ -0,0 +1,8 @@
+/* -*- 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/ */
+
+window.addEventListener("load", function () {
+ document.getElementById("");
+}, false);
diff --git a/devtools/client/webconsole/test/test-bug-595934-html.html b/devtools/client/webconsole/test/test-bug-595934-html.html
new file mode 100644
index 000000000..fe35afef6
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-html.html
@@ -0,0 +1,16 @@
+<!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/test-bug-595934-image.html b/devtools/client/webconsole/test/test-bug-595934-image.html
new file mode 100644
index 000000000..312ecd49f
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-image.html
@@ -0,0 +1,15 @@
+<!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-bug-595934-image.jpg" alt="corrupted image"></p>
+ </body>
+</html>
+
+
diff --git a/devtools/client/webconsole/test/test-bug-595934-image.jpg b/devtools/client/webconsole/test/test-bug-595934-image.jpg
new file mode 100644
index 000000000..947e5f11b
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-image.jpg
Binary files differ
diff --git a/devtools/client/webconsole/test/test-bug-595934-imagemap.html b/devtools/client/webconsole/test/test-bug-595934-imagemap.html
new file mode 100644
index 000000000..007c3c01b
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-imagemap.html
@@ -0,0 +1,17 @@
+<!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/test-bug-595934-malformedxml-external.html b/devtools/client/webconsole/test/test-bug-595934-malformedxml-external.html
new file mode 100644
index 000000000..2fd8beac5
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-malformedxml-external.html
@@ -0,0 +1,19 @@
+<!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"><!--
+ var req = new XMLHttpRequest();
+ req.open("GET", "test-bug-595934-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/test-bug-595934-malformedxml-external.xml b/devtools/client/webconsole/test/test-bug-595934-malformedxml-external.xml
new file mode 100644
index 000000000..4812786f1
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-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/test-bug-595934-malformedxml.xhtml b/devtools/client/webconsole/test/test-bug-595934-malformedxml.xhtml
new file mode 100644
index 000000000..62689c567
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-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/test-bug-595934-svg.xhtml b/devtools/client/webconsole/test/test-bug-595934-svg.xhtml
new file mode 100644
index 000000000..572382c64
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-svg.xhtml
@@ -0,0 +1,17 @@
+<!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/test-bug-595934-workers.html b/devtools/client/webconsole/test/test-bug-595934-workers.html
new file mode 100644
index 000000000..baf5a6215
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-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">
+ var myWorker = new Worker("test-bug-595934-workers.js");
+ myWorker.postMessage("hello world");
+ </script>
+ </body>
+</html>
+
diff --git a/devtools/client/webconsole/test/test-bug-595934-workers.js b/devtools/client/webconsole/test/test-bug-595934-workers.js
new file mode 100644
index 000000000..d23f080af
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-595934-workers.js
@@ -0,0 +1,14 @@
+/* -*- 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/ */
+
+/* global fooBarWorker*/
+/* eslint-disable no-unused-vars*/
+
+"use strict";
+
+var onmessage = function () {
+ fooBarWorker();
+};
+
diff --git a/devtools/client/webconsole/test/test-bug-597136-external-script-errors.html b/devtools/client/webconsole/test/test-bug-597136-external-script-errors.html
new file mode 100644
index 000000000..25bdeecc5
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-597136-external-script-errors.html
@@ -0,0 +1,25 @@
+<!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-bug-597136-external-script-errors.js"></script>
+ </body>
+</html>
+
diff --git a/devtools/client/webconsole/test/test-bug-597136-external-script-errors.js b/devtools/client/webconsole/test/test-bug-597136-external-script-errors.js
new file mode 100644
index 000000000..00821e38e
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-597136-external-script-errors.js
@@ -0,0 +1,9 @@
+/* -*- 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() {
+ bogus.g();
+}
+
diff --git a/devtools/client/webconsole/test/test-bug-597756-reopen-closed-tab.html b/devtools/client/webconsole/test/test-bug-597756-reopen-closed-tab.html
new file mode 100644
index 000000000..68e19e677
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-597756-reopen-closed-tab.html
@@ -0,0 +1,18 @@
+<!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"><!--
+ fooBug597756_error.bar();
+ // --></script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-599725-response-headers.sjs b/devtools/client/webconsole/test/test-bug-599725-response-headers.sjs
new file mode 100644
index 000000000..2e78d6b7b
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-599725-response-headers.sjs
@@ -0,0 +1,25 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function handleRequest(request, response)
+{
+ var Etag = '"4c881ab-b03-435f0a0f9ef00"';
+ var IfNoneMatch = request.hasHeader("If-None-Match")
+ ? request.getHeader("If-None-Match")
+ : "";
+
+ var page = "<!DOCTYPE html><html><body><p>hello world!</p></body></html>";
+
+ response.setHeader("Etag", Etag, false);
+
+ if (IfNoneMatch == Etag) {
+ response.setStatusLine(request.httpVersion, "304", "Not Modified");
+ }
+ else {
+ response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Content-Length", page.length + "", false);
+ response.write(page);
+ }
+}
diff --git a/devtools/client/webconsole/test/test-bug-600183-charset.html b/devtools/client/webconsole/test/test-bug-600183-charset.html
new file mode 100644
index 000000000..040490a6b
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-600183-charset.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US"><head>
+ <meta charset="gb2312">
+ <title>Console HTTP test page (chinese)</title>
+ </head>
+ <body>
+ <p>µÄÎʺò!</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-600183-charset.html^headers^ b/devtools/client/webconsole/test/test-bug-600183-charset.html^headers^
new file mode 100644
index 000000000..9f3e2302f
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-600183-charset.html^headers^
@@ -0,0 +1 @@
+Content-Type: text/html; charset=gb2312
diff --git a/devtools/client/webconsole/test/test-bug-601177-log-levels.html b/devtools/client/webconsole/test/test-bug-601177-log-levels.html
new file mode 100644
index 000000000..a59213907
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-601177-log-levels.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 601177: log levels</title>
+ <script src="test-bug-601177-log-levels.js" type="text/javascript"></script>
+ <script type="text/javascript"><!--
+ window.undefinedPropertyBug601177;
+ // --></script>
+ <!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+ </head>
+ <body>
+ <h1>Web Console test for bug 601177: log levels</h1>
+ <img src="test-image.png?bug601177">
+ <img src="foobar-known-to-fail.png?bug601177">
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-601177-log-levels.js b/devtools/client/webconsole/test/test-bug-601177-log-levels.js
new file mode 100644
index 000000000..afeb13ff6
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-601177-log-levels.js
@@ -0,0 +1,8 @@
+/* -*- 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/ */
+
+foobarBug601177strictError = "strict error";
+
+window.foobarBug601177exception();
diff --git a/devtools/client/webconsole/test/test-bug-603750-websocket.html b/devtools/client/webconsole/test/test-bug-603750-websocket.html
new file mode 100644
index 000000000..f0097dd77
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-603750-websocket.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 603750 - 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 bug 595934 - Web Socket errors.</p>
+ <iframe src="data:text/html;charset=utf-8,hello world!"></iframe>
+ <script type="text/javascript" src="test-bug-603750-websocket.js"></script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-603750-websocket.js b/devtools/client/webconsole/test/test-bug-603750-websocket.js
new file mode 100644
index 000000000..3d92c506b
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-603750-websocket.js
@@ -0,0 +1,20 @@
+/* -*- 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/ */
+
+"use strict";
+
+window.addEventListener("load", function () {
+ let ws1 = new WebSocket("ws://0.0.0.0:81");
+ ws1.onopen = function () {
+ ws1.send("test 1");
+ ws1.close();
+ };
+
+ let ws2 = new window.frames[0].WebSocket("ws://0.0.0.0:82");
+ ws2.onopen = function () {
+ ws2.send("test 2");
+ ws2.close();
+ };
+}, false);
diff --git a/devtools/client/webconsole/test/test-bug-609872-cd-iframe-child.html b/devtools/client/webconsole/test/test-bug-609872-cd-iframe-child.html
new file mode 100644
index 000000000..451eba21e
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-609872-cd-iframe-child.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>test for bug 609872 - iframe child</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <p>test for bug 609872 - iframe child</p>
+ <script>window.foobarBug609872 = 'child!';</script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-609872-cd-iframe-parent.html b/devtools/client/webconsole/test/test-bug-609872-cd-iframe-parent.html
new file mode 100644
index 000000000..fdb636b97
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-609872-cd-iframe-parent.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>test for bug 609872 - iframe parent</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <p>test for bug 609872 - iframe parent</p>
+ <script>window.foobarBug609872 = 'parent!';</script>
+ <iframe src="test-bug-609872-cd-iframe-child.html"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-613013-console-api-iframe.html b/devtools/client/webconsole/test/test-bug-613013-console-api-iframe.html
new file mode 100644
index 000000000..edf40e80e
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-613013-console-api-iframe.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>test for bug 613013</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"><!--
+ (function () {
+ var iframe = document.createElement('iframe');
+ iframe.src = 'data:text/html;charset=utf-8,little iframe';
+ document.body.appendChild(iframe);
+
+ console.log("foobarBug613013");
+ })();
+ // --></script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-618078-network-exceptions.html b/devtools/client/webconsole/test/test-bug-618078-network-exceptions.html
new file mode 100644
index 000000000..ac755e1b9
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-618078-network-exceptions.html
@@ -0,0 +1,24 @@
+<!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">
+ var req = new XMLHttpRequest();
+ req.open('GET', 'http://example.com', true);
+ req.onreadystatechange = function() {
+ if (req.readyState == 4) {
+ bug618078exception();
+ }
+ };
+ req.send(null);
+ </script>
+ </head>
+ <body>
+ <p>Web Console test for bug 618078 - exception in async network request
+ callback.</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-621644-jsterm-dollar.html b/devtools/client/webconsole/test/test-bug-621644-jsterm-dollar.html
new file mode 100644
index 000000000..09c986703
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-621644-jsterm-dollar.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 621644</title>
+ <script>
+ function $(elem) {
+ return elem.innerHTML;
+ }
+ function $$(doc) {
+ return doc.title;
+ }
+ </script>
+ <!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+ </head>
+ <body>
+ <h1>Web Console test for bug 621644</h1>
+ <p>hello world!</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-630733-response-redirect-headers.sjs b/devtools/client/webconsole/test/test-bug-630733-response-redirect-headers.sjs
new file mode 100644
index 000000000..f92e0fe65
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-630733-response-redirect-headers.sjs
@@ -0,0 +1,16 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function handleRequest(request, response)
+{
+ var page = "<!DOCTYPE html><html><body><p>hello world! bug 630733</p></body></html>";
+
+ response.setStatusLine(request.httpVersion, "301", "Moved Permanently");
+ response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Content-Length", page.length + "", false);
+ response.setHeader("x-foobar-bug630733", "bazbaz", false);
+ response.setHeader("Location", "/redirect-from-bug-630733", false);
+ response.write(page);
+}
diff --git a/devtools/client/webconsole/test/test-bug-632275-getters.html b/devtools/client/webconsole/test/test-bug-632275-getters.html
new file mode 100644
index 000000000..349c301f3
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-632275-getters.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 632275 - getters</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<script type="application/javascript;version=1.8">
+ document.foobar = {
+ _val: 5,
+ get val() { return ++this._val; }
+ };
+</script>
+
+ </head>
+ <body>
+ <p>Web Console test for bug 632275 - getters.</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-632347-iterators-generators.html b/devtools/client/webconsole/test/test-bug-632347-iterators-generators.html
new file mode 100644
index 000000000..1eddcf350
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-632347-iterators-generators.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 632347 - iterators and generators</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<script type="application/javascript;version=1.8">
+(function(){
+function genFunc() {
+ var a = 5;
+ while (a < 10) {
+ yield a++;
+ }
+}
+
+window._container = {};
+
+_container.gen1 = genFunc();
+_container.gen1.next();
+
+var obj = { foo: "bar", baz: "baaz", hay: "stack" };
+_container.iter1 = Iterator(obj);
+
+function Range(low, high) {
+ this.low = low;
+ this.high = high;
+}
+
+function RangeIterator(range) {
+ this.range = range;
+ this.current = this.range.low;
+}
+
+RangeIterator.prototype.next = function() {
+ if (this.current > this.range.high) {
+ throw StopIteration;
+ } else {
+ return this.current++;
+ }
+}
+
+Range.prototype.__iterator__ = function() {
+ return new RangeIterator(this);
+}
+
+_container.iter2 = new Range(3, 15);
+
+_container.gen2 = (function* () { for (let i in _container.iter2) yield i * 2; })();
+})();
+</script>
+ </head>
+ <body>
+ <p>Web Console test for bug 632347 - iterators and generators.</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-644419-log-limits.html b/devtools/client/webconsole/test/test-bug-644419-log-limits.html
new file mode 100644
index 000000000..21d99ba14
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-644419-log-limits.html
@@ -0,0 +1,21 @@
+<!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 644419: console log limits</title>
+ </head>
+ <body>
+ <h1>Test for bug 644419: Console should have user-settable log limits for
+ each message category</h1>
+
+ <script type="text/javascript">
+ function foo() {
+ bar.baz();
+ }
+ foo();
+ </script>
+ </body>
+</html>
+
diff --git a/devtools/client/webconsole/test/test-bug-646025-console-file-location.html b/devtools/client/webconsole/test/test-bug-646025-console-file-location.html
new file mode 100644
index 000000000..7c80f1446
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-646025-console-file-location.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US"><head>
+ <meta charset="utf-8">
+ <title>Console file location test</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script src="test-file-location.js"></script>
+ </head>
+ <body>
+ <h1>Web Console File Location Test Page</h1>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-658368-time-methods.html b/devtools/client/webconsole/test/test-bug-658368-time-methods.html
new file mode 100644
index 000000000..cc50b6313
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-658368-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">
+ function foo() {
+ console.timeEnd("aTimer");
+ }
+ console.time("aTimer");
+ foo();
+ console.time("bTimer");
+ </script>
+ </body>
+</html>
+
diff --git a/devtools/client/webconsole/test/test-bug-737873-mixedcontent.html b/devtools/client/webconsole/test/test-bug-737873-mixedcontent.html
new file mode 100644
index 000000000..db83274f0
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-737873-mixedcontent.html
@@ -0,0 +1,15 @@
+<!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>
+ </body>
+</html>
+
diff --git a/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-inner.html b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-inner.html
new file mode 100644
index 000000000..ccb363ed9
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-752559-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/test-bug-752559-ineffective-iframe-sandbox-warning-nested1.html b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested1.html
new file mode 100644
index 000000000..b9939fe83
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested1.html
@@ -0,0 +1,14 @@
+<!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/test-bug-752559-ineffective-iframe-sandbox-warning-inner.html"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested2.html b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested2.html
new file mode 100644
index 000000000..7678d15fe
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested2.html
@@ -0,0 +1,14 @@
+<!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/test-bug-752559-ineffective-iframe-sandbox-warning-inner.html" sandbox="allow-scripts allow-same-origin"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning0.html b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning0.html
new file mode 100644
index 000000000..233a6cb70
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-752559-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-bug-752559-ineffective-iframe-sandbox-warning-inner.html" sandbox="allow-scripts allow-same-origin"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning1.html b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning1.html
new file mode 100644
index 000000000..da0d58819
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-752559-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-bug-752559-ineffective-iframe-sandbox-warning-inner.html" sandbox="allow-scripts"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning2.html b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning2.html
new file mode 100644
index 000000000..f33f0a6dc
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-752559-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-bug-752559-ineffective-iframe-sandbox-warning-inner.html" sandbox="allow-same-origin"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning3.html b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning3.html
new file mode 100644
index 000000000..c0ff6994a
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning3.html
@@ -0,0 +1,14 @@
+<!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="http://www.example.com/browser/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-inner.html" sandbox="allow-scripts allow-same-origin"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning4.html b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning4.html
new file mode 100644
index 000000000..84e0b6c72
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning4.html
@@ -0,0 +1,14 @@
+<!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
+src="http://www.example.com/browser/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning-nested1.html" sandbox="allow-scripts allow-same-origin"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning5.html b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning5.html
new file mode 100644
index 000000000..72d86931a
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-752559-ineffective-iframe-sandbox-warning5.html
@@ -0,0 +1,14 @@
+<!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/test-bug-752559-ineffective-iframe-sandbox-warning-nested2.html"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-762593-insecure-passwords-about-blank-web-console-warning.html b/devtools/client/webconsole/test/test-bug-762593-insecure-passwords-about-blank-web-console-warning.html
new file mode 100644
index 000000000..d7bcd45d6
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-762593-insecure-passwords-about-blank-web-console-warning.html
@@ -0,0 +1,28 @@
+<!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>
+ 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/test-bug-762593-insecure-passwords-web-console-warning.html b/devtools/client/webconsole/test/test-bug-762593-insecure-passwords-web-console-warning.html
new file mode 100644
index 000000000..f473303f4
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-762593-insecure-passwords-web-console-warning.html
@@ -0,0 +1,16 @@
+<!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
+ ="http://example.com/browser/devtools/client/webconsole/test/test-iframe-762593-insecure-frame.html">
+ </iframe>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-766001-console-log.js b/devtools/client/webconsole/test/test-bug-766001-console-log.js
new file mode 100644
index 000000000..a4be0cb15
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-766001-console-log.js
@@ -0,0 +1,10 @@
+/* -*- 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 onLoad123() {
+ console.log("Blah Blah");
+}
+
+window.addEventListener("load", onLoad123, false);
diff --git a/devtools/client/webconsole/test/test-bug-766001-js-console-links.html b/devtools/client/webconsole/test/test-bug-766001-js-console-links.html
new file mode 100644
index 000000000..6a6ac6008
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-766001-js-console-links.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 766001 : Open 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-bug-766001-js-errors.js"></script>
+ <script type="text/javascript" src="test-bug-766001-console-log.js"></script>
+ </head>
+ <body>
+ <p>Web Console test for bug 766001 : Open JS/Console call Links in Debugger.</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-766001-js-errors.js b/devtools/client/webconsole/test/test-bug-766001-js-errors.js
new file mode 100644
index 000000000..85321813a
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-766001-js-errors.js
@@ -0,0 +1,8 @@
+/* -*- 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/ */
+
+window.addEventListener("load", function () {
+ document.bar();
+}, false);
diff --git a/devtools/client/webconsole/test/test-bug-782653-css-errors-1.css b/devtools/client/webconsole/test/test-bug-782653-css-errors-1.css
new file mode 100644
index 000000000..ad7fd1999
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-782653-css-errors-1.css
@@ -0,0 +1,10 @@
+/*
+ * 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/test-bug-782653-css-errors-2.css b/devtools/client/webconsole/test/test-bug-782653-css-errors-2.css
new file mode 100644
index 000000000..91b14137a
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-782653-css-errors-2.css
@@ -0,0 +1,10 @@
+/*
+ * 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/test-bug-782653-css-errors.html b/devtools/client/webconsole/test/test-bug-782653-css-errors.html
new file mode 100644
index 000000000..7ca11fc34
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-782653-css-errors.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 782653 : Open 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-bug-782653-css-errors-1.css">
+ <link rel="stylesheet" href="test-bug-782653-css-errors-2.css">
+ </head>
+ <body>
+ <p>Web Console test for bug 782653 : Open CSS Links in Style Editor.</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-837351-security-errors.html b/devtools/client/webconsole/test/test-bug-837351-security-errors.html
new file mode 100644
index 000000000..db83274f0
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-837351-security-errors.html
@@ -0,0 +1,15 @@
+<!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>
+ </body>
+</html>
+
diff --git a/devtools/client/webconsole/test/test-bug-859170-longstring-hang.html b/devtools/client/webconsole/test/test-bug-859170-longstring-hang.html
new file mode 100644
index 000000000..51bc0de28
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-859170-longstring-hang.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head><meta charset="utf-8">
+ <title>Web Console test for bug 859170 - very long strings hang the browser</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<script type="application/javascript">
+(function() {
+var longString = "abbababazomglolztest";
+for (var i = 0; i < 10; i++) {
+ longString += longString + longString;
+}
+
+longString = "foobar" + (new Array(9000)).join("a") + "foobaz" +
+ longString + "boom!";
+console.log(longString);
+})();
+</script>
+ </head>
+ <body>
+ <p>Web Console test for bug 859170 - very long strings hang the browser.</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-869003-iframe.html b/devtools/client/webconsole/test/test-bug-869003-iframe.html
new file mode 100644
index 000000000..5a29728e5
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-869003-iframe.html
@@ -0,0 +1,20 @@
+<!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"><!--
+ window.onload = function testConsoleLogging()
+ {
+ var o = { hello: "world!", bug: 869003 };
+ console.log("foobar", o);
+ };
+ // --></script>
+ </head>
+ <body>
+ <p>Make sure users can inspect objects from cross-domain iframes.</p>
+ <p>Iframe window.</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-869003-top-window.html b/devtools/client/webconsole/test/test-bug-869003-top-window.html
new file mode 100644
index 000000000..a2da438f6
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-869003-top-window.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 869003</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <p>Make sure users can inspect objects from cross-domain iframes.</p>
+ <p>Top window.</p>
+ <iframe src="http://example.org/browser/devtools/client/webconsole/test/test-bug-869003-iframe.html"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug-952277-highlight-nodes-in-vview.html b/devtools/client/webconsole/test/test-bug-952277-highlight-nodes-in-vview.html
new file mode 100644
index 000000000..de297d9b5
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-952277-highlight-nodes-in-vview.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 952277 - Highlighting and selecting nodes from the variablesview</title>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <p>Web Console test for bug 952277 - Highlighting and selecting nodes from the variablesview</p>
+ <p>Web Console test for bug 952277 - Highlighting and selecting nodes from the variablesview</p>
+ <p>Web Console test for bug 952277 - Highlighting and selecting nodes from the variablesview</p>
+ </body>
+</html>
+
diff --git a/devtools/client/webconsole/test/test-bug-989025-iframe-parent.html b/devtools/client/webconsole/test/test-bug-989025-iframe-parent.html
new file mode 100644
index 000000000..54a4e9038
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug-989025-iframe-parent.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>test for bug 989025 - iframe parent</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <p>test for bug 989025 - iframe parent</p>
+ <iframe src="http://mochi.test:8888/browser/devtools/client/webconsole/test/test-bug-609872-cd-iframe-child.html"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug_1050691_click_function_to_source.html b/devtools/client/webconsole/test/test-bug_1050691_click_function_to_source.html
new file mode 100644
index 000000000..912e301f0
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug_1050691_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-bug_1050691_click_function_to_source.js"></script>
+ </head>
+ <body></body>
+</html>
diff --git a/devtools/client/webconsole/test/test-bug_1050691_click_function_to_source.js b/devtools/client/webconsole/test/test-bug_1050691_click_function_to_source.js
new file mode 100644
index 000000000..1eddf0d6e
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug_1050691_click_function_to_source.js
@@ -0,0 +1,10 @@
+/**
+ * this
+ * is
+ * a
+ * function
+ */
+function foo() {
+ console.log(foo);
+}
+
diff --git a/devtools/client/webconsole/test/test-bug_923281_console_log_filter.html b/devtools/client/webconsole/test/test-bug_923281_console_log_filter.html
new file mode 100644
index 000000000..f2d650a5d
--- /dev/null
+++ b/devtools/client/webconsole/test/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/test-bug_923281_test1.js b/devtools/client/webconsole/test/test-bug_923281_test1.js
new file mode 100644
index 000000000..1c07f1155
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug_923281_test1.js
@@ -0,0 +1,7 @@
+/* -*- 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/ */
+
+console.log("Sample log.");
+console.log("This log should be filtered when filtered for test2.js.");
diff --git a/devtools/client/webconsole/test/test-bug_923281_test2.js b/devtools/client/webconsole/test/test-bug_923281_test2.js
new file mode 100644
index 000000000..7ac85b387
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug_923281_test2.js
@@ -0,0 +1,8 @@
+/* -*- 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/ */
+
+"use strict";
+
+console.log("This is a random text.");
diff --git a/devtools/client/webconsole/test/test-bug_939783_console_trace_duplicates.html b/devtools/client/webconsole/test/test-bug_939783_console_trace_duplicates.html
new file mode 100644
index 000000000..ab44de09f
--- /dev/null
+++ b/devtools/client/webconsole/test/test-bug_939783_console_trace_duplicates.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test for bug 939783 - different console.trace() calls
+ wrongly filtered as duplicates</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<script type="application/javascript">
+function foo1() {
+ foo2();
+}
+
+function foo1b() {
+ foo2();
+}
+
+function foo2() {
+ foo3();
+}
+
+function foo3() {
+ console.trace();
+}
+
+foo1(); foo1();
+foo1b();
+
+</script>
+ </head>
+ <body>
+ <p>Web Console test for bug 939783 - different console.trace() calls
+ wrongly filtered as duplicates</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-certificate-messages.html b/devtools/client/webconsole/test/test-certificate-messages.html
new file mode 100644
index 000000000..b0419a6fc
--- /dev/null
+++ b/devtools/client/webconsole/test/test-certificate-messages.html
@@ -0,0 +1,22 @@
+<!--
+ 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>
+ 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/test-closure-optimized-out.html b/devtools/client/webconsole/test/test-closure-optimized-out.html
new file mode 100644
index 000000000..3ad4e8fc0
--- /dev/null
+++ b/devtools/client/webconsole/test/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">
+ window.addEventListener("load", function onload() {
+ window.removeEventListener("load", onload);
+ function clickHandler(event) {
+ button.removeEventListener("click", clickHandler, false);
+ function outer(arg) {
+ var 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;
+ };
+ }
+
+ var f = outer(42);
+ f();
+ }
+ var button = document.querySelector("button");
+ button.addEventListener("click", clickHandler, false);
+ });
+ </script>
+
+ </head>
+ <body>
+ <button>Click me!</button>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-closures.html b/devtools/client/webconsole/test/test-closures.html
new file mode 100644
index 000000000..4fadade20
--- /dev/null
+++ b/devtools/client/webconsole/test/test-closures.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'/>
+ <title>Console Test for Closure Inspection</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script type="text/javascript">
+ function injectPerson() {
+ var PersonFactory = function _pfactory(name) {
+ var foo = 10;
+ return {
+ getName: function() { return name; },
+ getFoo: function() { foo = Date.now(); return foo; }
+ };
+ };
+ window.george = new PersonFactory("George");
+ debugger;
+ }
+ </script>
+
+ </head>
+ <body>
+ <button onclick="injectPerson()">Test</button>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-api-stackframe.html b/devtools/client/webconsole/test/test-console-api-stackframe.html
new file mode 100644
index 000000000..df7fef9b1
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-api-stackframe.html
@@ -0,0 +1,32 @@
+<!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 920116 - stacktraces for console API messages</title>
+ <script>
+ function firstCall() {
+ secondCall();
+ }
+
+ function secondCall() {
+ thirdCall();
+ }
+
+ function thirdCall() {
+ console.log("foo-log");
+ console.error("foo-error");
+ console.exception("foo-exception");
+ console.assert("red" == "blue", "foo-assert");
+ }
+
+ window.onload = firstCall;
+ </script>
+ </head>
+ <body>
+ <p>Hello world!</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-assert.html b/devtools/client/webconsole/test/test-console-assert.html
new file mode 100644
index 000000000..b104d72d4
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-assert.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+ <meta charset="utf-8">
+ <title>console.assert() test</title>
+ <script type="text/javascript">
+ function test() {
+ console.log("start");
+ console.assert(false, "false assert");
+ console.assert(0, "falsy assert");
+ console.assert(true, "true assert");
+ console.log("end");
+ }
+ </script>
+ </head>
+ <body>
+ <p>test console.assert()</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-clear.html b/devtools/client/webconsole/test/test-console-clear.html
new file mode 100644
index 000000000..8009db858
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-clear.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US"><head>
+ <meta charset="utf-8">
+ <title>Console.clear() tests</title>
+ <script type="text/javascript">
+ console.log("log1");
+ console.log("log2");
+ console.clear();
+
+ window.objFromPage = { a: 1 };
+ </script>
+ </head>
+ <body>
+ <h1 id="header">Clear Demo</h1>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-column.html b/devtools/client/webconsole/test/test-console-column.html
new file mode 100644
index 000000000..ff9cc81e1
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-column.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <meta charset="utf-8">
+ <title>Console test</title>
+
+ <script type="text/javascript">
+ console.info("INLINE SCRIPT:"); console.log('Further');
+ console.warn("I'm warning you, he will eat up all yr bacon.");
+ console.error("Error Message");
+ console.log('Rainbooooww');
+ console.log('NYAN CATZ');
+ </script>
+ </head>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-count-external-file.js b/devtools/client/webconsole/test/test-console-count-external-file.js
new file mode 100644
index 000000000..cca9e2f10
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-count-external-file.js
@@ -0,0 +1,11 @@
+/* eslint-disable no-unused-vars */
+
+"use strict";
+
+function counterExternalFile() {
+ console.count("console.count() testcounter");
+}
+function externalCountersWithoutLabel() {
+ console.count();
+ console.count();
+}
diff --git a/devtools/client/webconsole/test/test-console-count.html b/devtools/client/webconsole/test/test-console-count.html
new file mode 100644
index 000000000..e6db0ebb0
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-count.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+ <meta charset="utf-8">
+ <title>console.count() test</title>
+ <script src="test-console-count-external-file.js"></script>
+ <script tyoe="text/javascript">
+ function counterSeperateScriptTag() {
+ console.count("console.count() testcounter");
+ }
+ </script>
+ <script type="text/javascript">
+ function counterNoLabel() {
+ console.count();
+ }
+ function countersWithoutLabel() {
+ console.count();
+ console.count();
+ }
+ function counterWithLabel() {
+ console.count("console.count() testcounter");
+ }
+ function testLocal() {
+ console.log("start");
+ counterNoLabel();
+ counterNoLabel();
+ countersWithoutLabel();
+ counterWithLabel();
+ counterWithLabel();
+ counterSeperateScriptTag();
+ counterSeperateScriptTag();
+ console.log("end");
+ }
+ function testExternal() {
+ console.log("start");
+ counterExternalFile();
+ counterExternalFile();
+ externalCountersWithoutLabel();
+ console.log("end");
+ }
+ </script>
+ </head>
+ <body>
+ <p>test console.count()</p>
+ <button id="local" onclick="testLocal();">
+ test local console.count() calls
+ </button>
+ <button id="external" onclick="testExternal();">
+ test external console.count() calls
+ </button>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-extras.html b/devtools/client/webconsole/test/test-console-extras.html
new file mode 100644
index 000000000..8685b1a80
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-extras.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US"><head>
+ <meta charset="utf-8">
+ <title>Console extended API test</title>
+ <script type="text/javascript">
+ function test() {
+ console.log("start");
+ console.clear();
+ console.log("end");
+ }
+ </script>
+ </head>
+ <body>
+ <h1 id="header">Heads Up Display Demo</h1>
+ <button onclick="test();">Test Extended API</button>
+ <div id="myDiv"></div>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-output-02.html b/devtools/client/webconsole/test/test-console-output-02.html
new file mode 100644
index 000000000..ad90f0ebf
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-output-02.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html dir="ltr" lang="en-US">
+<head>
+ <meta charset="utf-8">
+ <title>Test the web console output - 02</title>
+ <!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+</head>
+<body>
+ <p>hello world!</p>
+ <script type="text/javascript">
+function testfn1() { return 42; }
+
+var testobj1 = {
+ testfn2: function() { return 42; },
+};
+
+function testfn3() { return 42; }
+testfn3.displayName = "testfn3DisplayName";
+
+var array1 = [1, 2, 3, "a", "b", "c", "4", "5"];
+
+var array2 = ["a", document, document.body, document.body.dataset,
+ document.body.classList];
+
+var array3 = [1, window, null, "a", "b", undefined, false, "", -Infinity, testfn3, testobj1, "foo", "bar"];
+
+var array4 = new Array(5);
+array4.push("test");
+array4.push(array4);
+
+var typedarray1 = new Int32Array([1, 287, 8651, 40983, 8754]);
+
+var set1 = new Set([1, 2, null, array3, "a", "b", undefined, document.head]);
+set1.add(set1);
+
+var bunnies = new String("bunnies")
+var weakset = new WeakSet([bunnies, document.head]);
+
+var testobj2 = {a: "b", c: "d", e: 1, f: "2"};
+testobj2.foo = testobj1;
+testobj2.bar = testobj2;
+Object.defineProperty(testobj2, "getterTest", {
+ enumerable: true,
+ get: function() {
+ return 42;
+ },
+});
+
+var testobj3 = {a: "b", c: "d", e: 1, f: "2", g: true, h: null, i: undefined,
+ j: "", k: document.styleSheets, l: document.body.childNodes,
+ o: new Array(125), m: document.head};
+
+var testobj4 = {a: "b", c: "d"};
+Object.defineProperty(testobj4, "nonEnumerable", { value: "hello world" });
+
+var map1 = new Map([["a", "b"], [document.body.children, testobj2]]);
+map1.set(map1, set1);
+
+var weakmap = new WeakMap([[bunnies, 23], [document.body.children, testobj2]]);
+
+ </script>
+</body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-output-03.html b/devtools/client/webconsole/test/test-console-output-03.html
new file mode 100644
index 000000000..9dcf051a6
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-output-03.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html dir="ltr" lang="en-US">
+<head>
+ <meta charset="utf-8">
+ <title>Test the web console output - 03</title>
+ <!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+</head>
+<body>
+ <p>hello world!</p>
+ <script type="text/javascript">
+function testBodyClassName() {
+ document.body.className = "test1 tezt2";
+ return document.body;
+}
+
+function testBodyID() {
+ document.body.id = 'foobarid';
+ return document.body;
+}
+
+function testBodyDataset() {
+ document.body.dataset.preview = 'zuzu"<a>foo';
+ return document.body;
+}
+ </script>
+</body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-output-04.html b/devtools/client/webconsole/test/test-console-output-04.html
new file mode 100644
index 000000000..bb4345277
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-output-04.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html dir="ltr" lang="en-US">
+<head>
+ <meta charset="utf-8">
+ <title>Test the web console output - 04</title>
+ <!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+</head>
+<body>
+ <p>hello world!</p>
+ <script type="text/javascript">
+function testTextNode() {
+ return document.querySelector("p").childNodes[0];
+}
+
+function testCommentNode() {
+ return document.head.childNodes[5];
+}
+
+function testDocumentFragment() {
+ var frag = document.createDocumentFragment();
+
+ var div = document.createElement("div");
+ div.id = "foo1";
+ div.className = "bar";
+ frag.appendChild(div);
+
+ var span = document.createElement("span");
+ span.id = "foo2";
+ span.textContent = "hello world";
+ div.appendChild(span);
+
+ var div2 = document.createElement("div");
+ div2.id = "foo3";
+ frag.appendChild(div2);
+
+ return frag;
+}
+
+function testError() {
+ try {
+ window.foobar("a");
+ } catch (ex) {
+ return ex;
+ }
+ return null;
+}
+
+function testDOMException() {
+ try {
+ var foo = document.querySelector("foo;()bar!");
+ } catch (ex) {
+ return ex;
+ }
+ return null;
+}
+
+function testCSSStyleDeclaration() {
+ document.body.style = 'color: green; font-size: 2em';
+ return document.body.style;
+}
+
+function testStyleSheetList() {
+ var style = document.querySelector("style");
+ if (!style) {
+ style = document.createElement("style");
+ style.textContent = "p, div { color: blue; font-weight: bold }\n" +
+ "@media print { p { background-color: yellow } }";
+ document.head.appendChild(style);
+ }
+ return document.styleSheets;
+}
+ </script>
+</body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-output-dom-elements.html b/devtools/client/webconsole/test/test-console-output-dom-elements.html
new file mode 100644
index 000000000..5acabfa3f
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-output-dom-elements.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html dir="ltr" lang="en-US">
+<head>
+ <meta charset="utf-8">
+ <title>Test the web console output - dom elements</title>
+ <!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+</head>
+<body class="body-class" id="body-id">
+ <p some-attribute="some-value">hello world!</p>
+ <p id="lots-of-attributes" a b c d e f g h i j k l m n></p>
+ <!--
+ Be sure we have a charset in our iframe's data URI, otherwise we get the following extra
+ console output message:
+ "The character encoding of a framed document was not declared. The document may appear different if viewed without the document framing it."
+ This wouldn't be a big deal, but when we look for a "<p>" in our `waitForMessage` helper,
+ this extra encoding warning line contains the data URI source, returning a message
+ that was unexpected
+ -->
+ <iframe src="data:text/html;charset=US-ASCII,<p>hello from iframe</p>"></iframe>
+ <div class="some classname here with more classnames here"></div>
+ <svg>
+ <clipPath>
+ <rect x="0" y="0" width="10" height="5"></rect>
+ </clipPath>
+ </svg>
+ <script type="text/javascript">
+function testBodyNode() {
+ return document.body;
+}
+
+function testDocumentElement() {
+ return document.documentElement;
+}
+
+function testLotsOfAttributes() {
+ return document.querySelector("#lots-of-attributes");
+}
+
+function testDocument() {
+ return document;
+}
+
+function testNode() {
+ return document.querySelector("p");
+}
+
+function testSvgNode() {
+ return document.querySelector("clipPath");
+}
+
+function testNodeList() {
+ return document.querySelectorAll("body *");
+}
+
+function testNodeInIframe() {
+ return document.querySelector("iframe").contentWindow.document.querySelector("p");
+}
+
+function testDocumentFragment() {
+ var frag = document.createDocumentFragment();
+
+ var span = document.createElement("span");
+ span.className = 'foo';
+ span.dataset.lolz = 'hehe';
+
+ var div = document.createElement('div')
+ div.id = 'fragdiv';
+
+ frag.appendChild(span);
+ frag.appendChild(div);
+
+ return frag;
+}
+
+function testNodeInDocumentFragment() {
+ var frag = testDocumentFragment();
+ return frag.firstChild;
+}
+
+function testUnattachedNode() {
+ var p = document.createElement("p");
+ p.className = "such-class";
+ p.dataset.data = "such-data";
+ return p;
+}
+ </script>
+</body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-output-events.html b/devtools/client/webconsole/test/test-console-output-events.html
new file mode 100644
index 000000000..908a86fab
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-output-events.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html dir="ltr" lang="en-US">
+<head>
+ <meta charset="utf-8">
+ <title>Test the web console output for DOM events</title>
+ <!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+</head>
+<body>
+ <p>hello world!</p>
+
+ <script type="text/javascript">
+function testDOMEvents() {
+ function eventLogger(ev) {
+ console.log("eventLogger", ev);
+ }
+ document.addEventListener("mousemove", eventLogger);
+ document.addEventListener("keypress", eventLogger);
+
+ synthesizeMouseMove();
+ synthesizeKeyPress("a", {shiftKey: true});
+}
+
+function synthesizeMouseMove(element) {
+ var mouseEvent = document.createEvent("MouseEvent");
+ mouseEvent.initMouseEvent("mousemove", true, true, window, 0, 0, 0, 0, 0,
+ false, false, false, false, 0, null);
+
+ document.dispatchEvent(mouseEvent);
+}
+
+function synthesizeKeyPress(key, options) {
+ var keyboardEvent = document.createEvent("KeyboardEvent");
+ keyboardEvent.initKeyEvent("keypress", true, true, window, false, false,
+ options.shiftKey, false, key.charCodeAt(0), 0);
+ document.dispatchEvent(keyboardEvent);
+}
+ </script>
+</body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-output-regexp.html b/devtools/client/webconsole/test/test-console-output-regexp.html
new file mode 100644
index 000000000..e62680d90
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-output-regexp.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html dir="ltr" lang="en-US">
+<head>
+ <meta charset="utf-8">
+ <title>Test the web console output for RegExp</title>
+ <!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+</head>
+<body>
+ <p>hello world!</p>
+
+ <script type="text/javascript">
+Object.defineProperty(RegExp.prototype, "flags", {
+ get: function() { throw Error("flags called"); }
+})
+Object.defineProperty(RegExp.prototype, "source", {
+ get: function() { throw Error("source called"); },
+})
+ </script>
+</body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-replaced-api.html b/devtools/client/webconsole/test/test-console-replaced-api.html
new file mode 100644
index 000000000..2b05d023a
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-replaced-api.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 replaced API</title>
+ </head>
+ <body>
+ <h1 id="header">Web Console Replace API Test</h1>
+ <script type="text/javascript">
+ window.console = {log: function (msg){}, info: function (msg){}, warn: function (msg){}, error: function (msg){}};
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-server-logging-array.sjs b/devtools/client/webconsole/test/test-console-server-logging-array.sjs
new file mode 100644
index 000000000..bba394264
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-server-logging-array.sjs
@@ -0,0 +1,32 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function handleRequest(request, response)
+{
+ var page = "<!DOCTYPE html><html>" +
+ "<head><meta charset='utf-8'></head>" +
+ "<body><p>hello world!</p></body>" +
+ "</html>";
+
+ var data = {
+ "version": "4.1.0",
+ "columns": ["log", "backtrace", "type"],
+ "rows":[[
+ [{ "best": "Firefox", "reckless": "Chrome", "new_ie": "Safari", "new_new_ie": "Edge"}],
+ "C:\\src\\www\\serverlogging\\test7.php:4:1",
+ ""
+ ]],
+ };
+
+ // Put log into headers.
+ var value = b64EncodeUnicode(JSON.stringify(data));
+ response.setHeader("X-ChromeLogger-Data", value, false);
+
+ response.write(page);
+}
+
+function b64EncodeUnicode(str) {
+ return btoa(unescape(encodeURIComponent(str)));
+}
diff --git a/devtools/client/webconsole/test/test-console-server-logging.sjs b/devtools/client/webconsole/test/test-console-server-logging.sjs
new file mode 100644
index 000000000..7177e7185
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-server-logging.sjs
@@ -0,0 +1,32 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function handleRequest(request, response)
+{
+ var page = "<!DOCTYPE html><html>" +
+ "<head><meta charset='utf-8'></head>" +
+ "<body><p>hello world!</p></body>" +
+ "</html>";
+
+ var data = {
+ "version": "4.1.0",
+ "columns": ["log", "backtrace", "type"],
+ "rows": [[
+ ["values: %s %o %i %f %s","string",{"a":10,"___class_name":"Object"},123,1.12, "\u2713"],
+ "C:\\src\\www\\serverlogging\\test7.php:4:1",
+ ""
+ ]]
+ };
+
+ // Put log into headers.
+ var value = b64EncodeUnicode(JSON.stringify(data));
+ response.setHeader("X-ChromeLogger-Data", value, false);
+
+ response.write(page);
+}
+
+function b64EncodeUnicode(str) {
+ return btoa(unescape(encodeURIComponent(str)));
+}
diff --git a/devtools/client/webconsole/test/test-console-table.html b/devtools/client/webconsole/test/test-console-table.html
new file mode 100644
index 000000000..461dedcde
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-table.html
@@ -0,0 +1,63 @@
+<!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 899753 - console.table support</title>
+ <script>
+ var languages1 = [
+ { name: "JavaScript", fileExtension: [".js"] },
+ { name: { a: "TypeScript" }, fileExtension: ".ts" },
+ { name: "CoffeeScript", fileExtension: ".coffee" }
+ ];
+
+ var languages2 = {
+ csharp: { name: "C#", paradigm: "object-oriented" },
+ fsharp: { name: "F#", paradigm: "functional" }
+ };
+
+ function Person(firstName, lastName, age)
+ {
+ this.firstName = firstName;
+ this.lastName = lastName;
+ this.age = age;
+ }
+
+ var family = {};
+ family.mother = new Person("Susan", "Doyle", 32);
+ family.father = new Person("John", "Doyle", 33);
+ family.daughter = new Person("Lily", "Doyle", 5);
+ family.son = new Person("Mike", "Doyle", 8);
+
+ var myMap = new Map();
+
+ myMap.set("a string", "value associated with 'a string'");
+ myMap.set(5, "value associated with 5");
+
+ var mySet = new Set();
+
+ mySet.add(1);
+ mySet.add(5);
+ mySet.add("some text");
+ mySet.add(null);
+ mySet.add(undefined);
+
+ // These are globals and so won't be reclaimed by the GC.
+ var bunnies = new String("bunnies");
+ var lizards = new String("lizards");
+
+ var weakmap = new WeakMap();
+ weakmap.set(bunnies, 23);
+ weakmap.set(lizards, "oh no");
+
+ var weakset = new WeakSet([bunnies, lizards]);
+
+ </script>
+ </head>
+ <body>
+ <p>Hello world!</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-trace-async.html b/devtools/client/webconsole/test/test-console-trace-async.html
new file mode 100644
index 000000000..c7b895455
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-trace-async.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head><meta charset="utf-8">
+ <title>Web Console test for bug 1200832 - console.trace() async stacks</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<script type="application/javascript">
+function inner() {
+ console.trace();
+}
+
+function time1() {
+ new Promise(function(resolve, reject) {
+ setTimeout(resolve, 10);
+ }).then(inner);
+}
+
+setTimeout(time1, 10);
+</script>
+ </head>
+ <body>
+ <p>Web Console test for bug 1200832 - console.trace() async stacks</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console-workers.html b/devtools/client/webconsole/test/test-console-workers.html
new file mode 100644
index 000000000..f4b286ae5
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-workers.html
@@ -0,0 +1,13 @@
+<!-- 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">
+var sw = new SharedWorker('data:application/javascript,console.log("foo-bar-shared-worker");');
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-console.html b/devtools/client/webconsole/test/test-console.html
new file mode 100644
index 000000000..b294a3ba1
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US"><head>
+ <meta charset="utf-8">
+ <title>Console test</title>
+ <script type="text/javascript">
+ var fooObj = {
+ testProp: "testValue"
+ };
+
+ function test() {
+ var str = "Dolske Digs Bacon, Now and Forevermore."
+ for (var i=0; i < 5; i++) {
+ console.log(str);
+ }
+ }
+
+ function testTrace() {
+ console.log("bug 1100562");
+ console.trace();
+ }
+
+ console.info("INLINE SCRIPT:");
+ test();
+ console.warn("I'm warning you, he will eat up all yr bacon.");
+ console.error("Error Message");
+ </script>
+ </head>
+ <body>
+ <h1 id="header">Heads Up Display Demo</h1>
+ <button onclick="test();">Log stuff about Dolske</button>
+ <button id="testTrace" onclick="testTrace();">Log stuff with stacktrace</button>
+ <div id="myDiv"></div>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-consoleiframes.html b/devtools/client/webconsole/test/test-consoleiframes.html
new file mode 100644
index 000000000..a8176f93a
--- /dev/null
+++ b/devtools/client/webconsole/test/test-consoleiframes.html
@@ -0,0 +1,13 @@
+<html>
+<head>
+ <script>
+ 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> \ No newline at end of file
diff --git a/devtools/client/webconsole/test/test-cu-reporterror.js b/devtools/client/webconsole/test/test-cu-reporterror.js
new file mode 100644
index 000000000..6e2f9d262
--- /dev/null
+++ b/devtools/client/webconsole/test/test-cu-reporterror.js
@@ -0,0 +1,4 @@
+function a() {
+ Components.utils.reportError("bug1141222");
+}
+a();
diff --git a/devtools/client/webconsole/test/test-data.json b/devtools/client/webconsole/test/test-data.json
new file mode 100644
index 000000000..471d240b5
--- /dev/null
+++ b/devtools/client/webconsole/test/test-data.json
@@ -0,0 +1 @@
+{ id: "test JSON data", myArray: [ "foo", "bar", "baz", "biff" ] } \ No newline at end of file
diff --git a/devtools/client/webconsole/test/test-data.json^headers^ b/devtools/client/webconsole/test/test-data.json^headers^
new file mode 100644
index 000000000..7b5e82d4b
--- /dev/null
+++ b/devtools/client/webconsole/test/test-data.json^headers^
@@ -0,0 +1 @@
+Content-Type: application/json
diff --git a/devtools/client/webconsole/test/test-duplicate-error.html b/devtools/client/webconsole/test/test-duplicate-error.html
new file mode 100644
index 000000000..1b2691672
--- /dev/null
+++ b/devtools/client/webconsole/test/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"><!--
+ fooDuplicateError1.bar();
+ // --></script>
+ </body>
+</html>
+
diff --git a/devtools/client/webconsole/test/test-encoding-ISO-8859-1.html b/devtools/client/webconsole/test/test-encoding-ISO-8859-1.html
new file mode 100644
index 000000000..cf19629f4
--- /dev/null
+++ b/devtools/client/webconsole/test/test-encoding-ISO-8859-1.html
@@ -0,0 +1,7 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="ISO-8859-1">
+</head>
+<body>üöä</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/webconsole/test/test-error.html b/devtools/client/webconsole/test/test-error.html
new file mode 100644
index 000000000..abf62a3f1
--- /dev/null
+++ b/devtools/client/webconsole/test/test-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 error test</title>
+ </head>
+ <body>
+ <h1>Heads Up Display - error test</h1>
+ <p><button>generate error</button></p>
+
+ <script type="text/javascript"><!--
+ var button = document.getElementsByTagName("button")[0];
+
+ button.addEventListener("click", function clicker () {
+ button.removeEventListener("click", clicker, false);
+ fooBazBaz.bar();
+ }, false);
+ // --></script>
+ </body>
+</html>
+
diff --git a/devtools/client/webconsole/test/test-eval-in-stackframe.html b/devtools/client/webconsole/test/test-eval-in-stackframe.html
new file mode 100644
index 000000000..ec1bf3f30
--- /dev/null
+++ b/devtools/client/webconsole/test/test-eval-in-stackframe.html
@@ -0,0 +1,39 @@
+<!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>
+ var foo = "globalFooBug783499";
+ var fooObj = {
+ testProp: "testValue",
+ };
+
+ function firstCall()
+ {
+ var foo = "fooFirstCall";
+ var foo3 = "foo3FirstCall";
+ secondCall();
+ }
+
+ function secondCall()
+ {
+ var foo2 = "foo2SecondCall";
+ var fooObj = {
+ testProp2: "testValue2",
+ };
+ var fooObj2 = {
+ testProp22: "testValue22",
+ };
+ debugger;
+ }
+ </script>
+ </head>
+ <body>
+ <p>Hello world!</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-exception-stackframe.html b/devtools/client/webconsole/test/test-exception-stackframe.html
new file mode 100644
index 000000000..0a6dea4ca
--- /dev/null
+++ b/devtools/client/webconsole/test/test-exception-stackframe.html
@@ -0,0 +1,43 @@
+<!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 1184172 - stacktraces for exceptions</title>
+ <script>
+ function firstCall() {
+ secondCall();
+ }
+
+ // Check anonymous functions
+ var secondCall = function () {
+ thirdCall();
+ }
+
+ function thirdCall() {
+ nonExistingMethodCall();
+ }
+
+ function domAPI() {
+ document.querySelector("buggy;selector");
+ }
+
+ function domException() {
+ throw new DOMException("DOMException");
+ }
+ window.addEventListener("load", firstCall);
+ window.addEventListener("load", function onLoadDomAPI() {
+ domAPI();
+ });
+ window.addEventListener("load", function onLoadDomException() {
+ domException();
+ });
+ </script>
+ </head>
+ <body>
+ <p>Hello world!</p>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-file-location.js b/devtools/client/webconsole/test/test-file-location.js
new file mode 100644
index 000000000..d9879a356
--- /dev/null
+++ b/devtools/client/webconsole/test/test-file-location.js
@@ -0,0 +1,12 @@
+/* -*- 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/ */
+
+"use strict";
+
+console.log("message for level log");
+console.info("message for level info");
+console.warn("message for level warn");
+console.error("message for level error");
+console.debug("message for level debug");
diff --git a/devtools/client/webconsole/test/test-filter.html b/devtools/client/webconsole/test/test-filter.html
new file mode 100644
index 000000000..219177bb2
--- /dev/null
+++ b/devtools/client/webconsole/test/test-filter.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US"><head>
+ <meta charset="utf-8">
+ <title>Console test</title>
+ <script type="text/javascript">
+ </script>
+ </head>
+ <body>
+ <h1>Heads Up Display Filter Test Page</h1>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-for-of.html b/devtools/client/webconsole/test/test-for-of.html
new file mode 100644
index 000000000..876010c9e
--- /dev/null
+++ b/devtools/client/webconsole/test/test-for-of.html
@@ -0,0 +1,8 @@
+<!DOCTYPE HTML>
+<html>
+<meta charset="utf-8">
+<body>
+<h1>a</h1>
+<div><p>b</p></div>
+<h2>c</h2>
+<p>d</p>
diff --git a/devtools/client/webconsole/test/test-iframe-762593-insecure-form-action.html b/devtools/client/webconsole/test/test-iframe-762593-insecure-form-action.html
new file mode 100644
index 000000000..d14b5cdd7
--- /dev/null
+++ b/devtools/client/webconsole/test/test-iframe-762593-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/test-iframe-762593-insecure-frame.html b/devtools/client/webconsole/test/test-iframe-762593-insecure-frame.html
new file mode 100644
index 000000000..dde47a78e
--- /dev/null
+++ b/devtools/client/webconsole/test/test-iframe-762593-insecure-frame.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 1</h1>
+ <p>This frame is served with an insecure password field.</p>
+ <iframe src=
+ "http://example.com/browser/devtools/client/webconsole/test/test-iframe-762593-insecure-form-action.html">
+ </iframe>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-iframe1.html b/devtools/client/webconsole/test/test-iframe1.html
new file mode 100644
index 000000000..4dd4eddfe
--- /dev/null
+++ b/devtools/client/webconsole/test/test-iframe1.html
@@ -0,0 +1,10 @@
+<html>
+<head>
+ <script>
+ console.log("iframe 1");
+ </script>
+</head>
+<body>
+<h1>iframe 1</h1>
+</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/webconsole/test/test-iframe2.html b/devtools/client/webconsole/test/test-iframe2.html
new file mode 100644
index 000000000..c15884795
--- /dev/null
+++ b/devtools/client/webconsole/test/test-iframe2.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+ <script>
+ console.log("iframe 2");
+ blah;
+ </script>
+</head>
+<body>
+<h1>iframe 2</h1>
+</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/webconsole/test/test-iframe3.html b/devtools/client/webconsole/test/test-iframe3.html
new file mode 100644
index 000000000..f0df8b669
--- /dev/null
+++ b/devtools/client/webconsole/test/test-iframe3.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+ <script>
+ console.log("iframe 3");
+ </script>
+</head>
+<body>
+<h1>iframe 3</h1>
+<iframe src="test-iframe1.html"></iframe>
+</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/webconsole/test/test-image.png b/devtools/client/webconsole/test/test-image.png
new file mode 100644
index 000000000..769c63634
--- /dev/null
+++ b/devtools/client/webconsole/test/test-image.png
Binary files differ
diff --git a/devtools/client/webconsole/test/test-mixedcontent-securityerrors.html b/devtools/client/webconsole/test/test-mixedcontent-securityerrors.html
new file mode 100644
index 000000000..cb8cfdaaf
--- /dev/null
+++ b/devtools/client/webconsole/test/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/test-mutation.html b/devtools/client/webconsole/test/test-mutation.html
new file mode 100644
index 000000000..e80933b06
--- /dev/null
+++ b/devtools/client/webconsole/test/test-mutation.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf-8">
+ <title>Console mutation test</title>
+ <script>
+ window.onload = function (){
+ var node = document.createElement("div");
+ document.body.appendChild(node);
+ };
+ </script>
+ </head>
+ <body>
+ <h1>Heads Up Display DOM Mutation Test Page</h1>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-network-request.html b/devtools/client/webconsole/test/test-network-request.html
new file mode 100644
index 000000000..7cb736296
--- /dev/null
+++ b/devtools/client/webconsole/test/test-network-request.html
@@ -0,0 +1,40 @@
+<!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"><!--
+ function makeXhr(aMethod, aUrl, aRequestBody, aCallback) {
+ var xmlhttp = new XMLHttpRequest();
+ xmlhttp.open(aMethod, aUrl, true);
+ xmlhttp.onreadystatechange = function() {
+ if (aCallback && xmlhttp.readyState == 4) {
+ aCallback();
+ }
+ };
+ xmlhttp.send(aRequestBody);
+ }
+
+ function testXhrGet(aCallback) {
+ makeXhr('get', 'test-data.json', null, aCallback);
+ }
+
+ function testXhrWarn(aCallback) {
+ makeXhr('get', 'http://example.com/browser/devtools/client/netmonitor/test/sjs_cors-test-server.sjs', null, aCallback);
+ }
+
+ function testXhrPost(aCallback) {
+ makeXhr('post', 'test-data.json', "Hello world!", aCallback);
+ }
+ // --></script>
+ </head>
+ <body>
+ <h1>Heads Up Display HTTP Logging Testpage</h1>
+ <h2>This page is used to test the HTTP logging.</h2>
+
+ <form action="https://example.com/browser/devtools/client/webconsole/test/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/test-network.html b/devtools/client/webconsole/test/test-network.html
new file mode 100644
index 000000000..69d3422e3
--- /dev/null
+++ b/devtools/client/webconsole/test/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/test-observe-http-ajax.html b/devtools/client/webconsole/test/test-observe-http-ajax.html
new file mode 100644
index 000000000..5abcefdad
--- /dev/null
+++ b/devtools/client/webconsole/test/test-observe-http-ajax.html
@@ -0,0 +1,17 @@
+<!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">
+ function test() {
+ var xmlhttp = new XMLHttpRequest();
+ xmlhttp.open('get', 'test-data.json', false);
+ xmlhttp.send(null);
+ }
+ </script>
+ </head>
+ <body onload="test();">
+ <h1>Heads Up Display HTTP & AJAX Test Page</h1>
+ <h2>This page fires an ajax request so we can see the http logging of the console</h2>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-own-console.html b/devtools/client/webconsole/test/test-own-console.html
new file mode 100644
index 000000000..d1d18ebc2
--- /dev/null
+++ b/devtools/client/webconsole/test/test-own-console.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+<head>
+<meta charset="utf-8">
+<script>
+ var _console = {
+ foo: "bar"
+ }
+
+ window.console = _console;
+
+ function loadIFrame() {
+ var iframe = document.body.querySelector("iframe");
+ iframe.addEventListener("load", function() {
+ iframe.removeEventListener("load", arguments.callee, true);
+ }, true);
+
+ iframe.setAttribute("src", "test-console.html");
+ }
+</script>
+</head>
+<body>
+ <iframe></iframe>
+</body>
diff --git a/devtools/client/webconsole/test/test-property-provider.html b/devtools/client/webconsole/test/test-property-provider.html
new file mode 100644
index 000000000..532b00f44
--- /dev/null
+++ b/devtools/client/webconsole/test/test-property-provider.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US"><head>
+ <meta charset="utf-8">
+ <title>Property provider test</title>
+ <script>
+ var testObj = {
+ testProp: 'testValue'
+ };
+ </script>
+ </head>
+ <body>
+ <h1>Heads Up Property Provider Test Page</h1>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-repeated-messages.html b/devtools/client/webconsole/test/test-repeated-messages.html
new file mode 100644
index 000000000..b19c9485e
--- /dev/null
+++ b/devtools/client/webconsole/test/test-repeated-messages.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ <title>Test for bugs 720180, 800510, 865288 and 1218089</title>
+ <script>
+ function testConsole() {
+ // same line and column number
+ for(var i = 0; i < 2; i++) {
+ console.log("foo repeat");
+ }
+ console.log("foo repeat");
+ console.error("foo repeat");
+ }
+ function testConsoleObjects() {
+ for (var i = 0; i < 3; i++) {
+ var o = { id: "abba" + i };
+ console.log("abba", o);
+ }
+ }
+ function testConsoleFalsyValues(){
+ [NaN, undefined, null].forEach(function(item, index){
+ console.log(item);
+ });
+ [NaN, NaN].forEach(function(item, index){
+ console.log(item);
+ });
+ [undefined, undefined].forEach(function(item, index){
+ console.log(item);
+ });
+ [null, null].forEach(function(item, index){
+ console.log(item);
+ });
+ }
+ </script>
+ <style>
+ body {
+ background-image: foobarz;
+ }
+ p {
+ background-image: foobarz;
+ }
+ </style>
+ <!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+ </head>
+ <body>
+ <p>Hello world!</p>
+ </body>
+</html>
+
diff --git a/devtools/client/webconsole/test/test-result-format-as-string.html b/devtools/client/webconsole/test/test-result-format-as-string.html
new file mode 100644
index 000000000..c3ab78ee7
--- /dev/null
+++ b/devtools/client/webconsole/test/test-result-format-as-string.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Web Console test: jsterm eval format as a string</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body>
+ <p>Make sure js eval results are formatted as strings.</p>
+ <script>
+ document.querySelector("p").toSource = function() {
+ var element = document.createElement("div");
+ element.id = "foobar";
+ element.textContent = "bug772506_content";
+ element.setAttribute("onmousemove",
+ "(function () {" +
+ " gBrowser._bug772506 = 'foobar';" +
+ "})();"
+ );
+ return element;
+ };
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-trackingprotection-securityerrors.html b/devtools/client/webconsole/test/test-trackingprotection-securityerrors.html
new file mode 100644
index 000000000..17f0e459e
--- /dev/null
+++ b/devtools/client/webconsole/test/test-trackingprotection-securityerrors.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="http://tracking.example.com/"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/test/test-webconsole-error-observer.html b/devtools/client/webconsole/test/test-webconsole-error-observer.html
new file mode 100644
index 000000000..8466bc6f2
--- /dev/null
+++ b/devtools/client/webconsole/test/test-webconsole-error-observer.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf-8">
+ <title>WebConsoleErrorObserver test - bug 611032</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <script type="text/javascript">
+ console.log("log Bazzle");
+ console.info("info Bazzle");
+ console.warn("warn Bazzle");
+ console.error("error Bazzle");
+
+ var foo = {};
+ foo.bazBug611032();
+ </script>
+ <style type="text/css">
+ .foo { color: cssColorBug611032; }
+ </style>
+ </head>
+ <body>
+ <h1>WebConsoleErrorObserver test</h1>
+ </body>
+</html>
+
diff --git a/devtools/client/webconsole/test/test_bug1045902_console_csp_ignore_reflected_xss_message.html b/devtools/client/webconsole/test/test_bug1045902_console_csp_ignore_reflected_xss_message.html
new file mode 100644
index 000000000..bf63601bf
--- /dev/null
+++ b/devtools/client/webconsole/test/test_bug1045902_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/test_bug1045902_console_csp_ignore_reflected_xss_message.html^headers^ b/devtools/client/webconsole/test/test_bug1045902_console_csp_ignore_reflected_xss_message.html^headers^
new file mode 100644
index 000000000..0b234f0e8
--- /dev/null
+++ b/devtools/client/webconsole/test/test_bug1045902_console_csp_ignore_reflected_xss_message.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: reflected-xss filter;
diff --git a/devtools/client/webconsole/test/test_bug1092055_shouldwarn.html b/devtools/client/webconsole/test/test_bug1092055_shouldwarn.html
new file mode 100644
index 000000000..ebb7773cb
--- /dev/null
+++ b/devtools/client/webconsole/test/test_bug1092055_shouldwarn.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_bug1092055_shouldwarn.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/test_bug1092055_shouldwarn.js b/devtools/client/webconsole/test/test_bug1092055_shouldwarn.js
new file mode 100644
index 000000000..c7d5cec14
--- /dev/null
+++ b/devtools/client/webconsole/test/test_bug1092055_shouldwarn.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/test_bug1092055_shouldwarn.js^headers^ b/devtools/client/webconsole/test/test_bug1092055_shouldwarn.js^headers^
new file mode 100644
index 000000000..f99377fc6
--- /dev/null
+++ b/devtools/client/webconsole/test/test_bug1092055_shouldwarn.js^headers^
@@ -0,0 +1 @@
+Strict-Transport-Security: some complete nonsense
diff --git a/devtools/client/webconsole/test/test_bug_1010953_cspro.html b/devtools/client/webconsole/test/test_bug_1010953_cspro.html
new file mode 100644
index 000000000..83ac6391f
--- /dev/null
+++ b/devtools/client/webconsole/test/test_bug_1010953_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/test_bug_1010953_cspro.js"></script>
+
+<!-- this image allowed only be the CSP report-only header. -->
+<img src="http://some.example.com/test.png">
+</body>
+</html> \ No newline at end of file
diff --git a/devtools/client/webconsole/test/test_bug_1010953_cspro.html^headers^ b/devtools/client/webconsole/test/test_bug_1010953_cspro.html^headers^
new file mode 100644
index 000000000..03056e2cb
--- /dev/null
+++ b/devtools/client/webconsole/test/test_bug_1010953_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/test_bug_1247459_violation.html b/devtools/client/webconsole/test/test_bug_1247459_violation.html
new file mode 100644
index 000000000..fdda4eb26
--- /dev/null
+++ b/devtools/client/webconsole/test/test_bug_1247459_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/test_bug_770099_violation.html b/devtools/client/webconsole/test/test_bug_770099_violation.html
new file mode 100644
index 000000000..ccbded87a
--- /dev/null
+++ b/devtools/client/webconsole/test/test_bug_770099_violation.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Test for Bug 770099 - policy violation</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=770099">Mozilla Bug 770099</a>
+<img src="http://some.example.com/test.png">
+</body>
+</html>
diff --git a/devtools/client/webconsole/test/test_bug_770099_violation.html^headers^ b/devtools/client/webconsole/test/test_bug_770099_violation.html^headers^
new file mode 100644
index 000000000..4c6fa3c26
--- /dev/null
+++ b/devtools/client/webconsole/test/test_bug_770099_violation.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: default-src 'self'
diff --git a/devtools/client/webconsole/test/test_hpkp-invalid-headers.sjs b/devtools/client/webconsole/test/test_hpkp-invalid-headers.sjs
new file mode 100644
index 000000000..cd0e18523
--- /dev/null
+++ b/devtools/client/webconsole/test/test_hpkp-invalid-headers.sjs
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response)
+{
+ response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
+
+ let issue;
+ switch (request.queryString) {
+ case "badSyntax":
+ response.setHeader("Public-Key-Pins", "\"");
+ issue = "is not syntactically correct.";
+ break;
+ case "noMaxAge":
+ response.setHeader("Public-Key-Pins", "max-age444");
+ issue = "does not include a max-age directive.";
+ break;
+ case "invalidIncludeSubDomains":
+ response.setHeader("Public-Key-Pins", "includeSubDomains=abc");
+ issue = "includes an invalid includeSubDomains directive.";
+ break;
+ case "invalidMaxAge":
+ response.setHeader("Public-Key-Pins", "max-age=abc");
+ issue = "includes an invalid max-age directive.";
+ break;
+ case "multipleIncludeSubDomains":
+ response.setHeader("Public-Key-Pins",
+ "includeSubDomains; includeSubDomains");
+ issue = "includes multiple includeSubDomains directives.";
+ break;
+ case "multipleMaxAge":
+ response.setHeader("Public-Key-Pins",
+ "max-age=444; max-age=999");
+ issue = "includes multiple max-age directives.";
+ break;
+ case "multipleReportURIs":
+ response.setHeader("Public-Key-Pins",
+ 'report-uri="http://example.com"; ' +
+ 'report-uri="http://example.com"');
+ issue = "includes multiple report-uri directives.";
+ break;
+ case "pinsetDoesNotMatch":
+ response.setHeader(
+ "Public-Key-Pins",
+ 'max-age=999; ' +
+ 'pin-sha256="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; ' +
+ 'pin-sha256="BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="');
+ issue = "does not include a matching pin.";
+ break;
+ }
+
+ response.write("This page is served with a PKP header that " + issue);
+}
diff --git a/devtools/client/webconsole/test/test_hsts-invalid-headers.sjs b/devtools/client/webconsole/test/test_hsts-invalid-headers.sjs
new file mode 100644
index 000000000..9e3ea7624
--- /dev/null
+++ b/devtools/client/webconsole/test/test_hsts-invalid-headers.sjs
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response)
+{
+ response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
+
+ let issue;
+ switch (request.queryString) {
+ case "badSyntax":
+ response.setHeader("Strict-Transport-Security", "\"");
+ issue = "is not syntactically correct.";
+ break;
+ case "noMaxAge":
+ response.setHeader("Strict-Transport-Security", "max-age444");
+ issue = "does not include a max-age directive.";
+ break;
+ case "invalidIncludeSubDomains":
+ response.setHeader("Strict-Transport-Security", "includeSubDomains=abc");
+ issue = "includes an invalid includeSubDomains directive.";
+ break;
+ case "invalidMaxAge":
+ response.setHeader("Strict-Transport-Security", "max-age=abc");
+ issue = "includes an invalid max-age directive.";
+ break;
+ case "multipleIncludeSubDomains":
+ response.setHeader("Strict-Transport-Security",
+ "includeSubDomains; includeSubDomains");
+ issue = "includes multiple includeSubDomains directives.";
+ break;
+ case "multipleMaxAge":
+ response.setHeader("Strict-Transport-Security",
+ "max-age=444; max-age=999");
+ issue = "includes multiple max-age directives.";
+ break;
+ }
+
+ response.write("This page is served with a STS header that " + issue);
+}
diff --git a/devtools/client/webconsole/test/testscript.js b/devtools/client/webconsole/test/testscript.js
new file mode 100644
index 000000000..849b03d86
--- /dev/null
+++ b/devtools/client/webconsole/test/testscript.js
@@ -0,0 +1,2 @@
+"use strict";
+console.log("running network console logging tests");
diff --git a/devtools/client/webconsole/utils.js b/devtools/client/webconsole/utils.js
new file mode 100644
index 000000000..ae2f1809f
--- /dev/null
+++ b/devtools/client/webconsole/utils.js
@@ -0,0 +1,395 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft= javascript ts=2 et sw=2 tw=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";
+
+const {Cc, Ci, Cu, components} = require("chrome");
+const Services = require("Services");
+const {LocalizationHelper} = require("devtools/shared/l10n");
+
+// Match the function name from the result of toString() or toSource().
+//
+// Examples:
+// (function foobar(a, b) { ...
+// function foobar2(a) { ...
+// function() { ...
+const REGEX_MATCH_FUNCTION_NAME = /^\(?function\s+([^(\s]+)\s*\(/;
+
+// Number of terminal entries for the self-xss prevention to go away
+const CONSOLE_ENTRY_THRESHOLD = 5;
+
+const CONSOLE_WORKER_IDS = exports.CONSOLE_WORKER_IDS = [
+ "SharedWorker",
+ "ServiceWorker",
+ "Worker"
+];
+
+var WebConsoleUtils = {
+
+ /**
+ * Wrap a string in an nsISupportsString object.
+ *
+ * @param string string
+ * @return nsISupportsString
+ */
+ supportsString: function (string) {
+ let str = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ str.data = string;
+ return str;
+ },
+
+ /**
+ * Clone an object.
+ *
+ * @param object object
+ * The object you want cloned.
+ * @param boolean recursive
+ * Tells if you want to dig deeper into the object, to clone
+ * recursively.
+ * @param function [filter]
+ * Optional, filter function, called for every property. Three
+ * arguments are passed: key, value and object. Return true if the
+ * property should be added to the cloned object. Return false to skip
+ * the property.
+ * @return object
+ * The cloned object.
+ */
+ cloneObject: function (object, recursive, filter) {
+ if (typeof object != "object") {
+ return object;
+ }
+
+ let temp;
+
+ if (Array.isArray(object)) {
+ temp = [];
+ Array.forEach(object, function (value, index) {
+ if (!filter || filter(index, value, object)) {
+ temp.push(recursive ? WebConsoleUtils.cloneObject(value) : value);
+ }
+ });
+ } else {
+ temp = {};
+ for (let key in object) {
+ let value = object[key];
+ if (object.hasOwnProperty(key) &&
+ (!filter || filter(key, value, object))) {
+ temp[key] = recursive ? WebConsoleUtils.cloneObject(value) : value;
+ }
+ }
+ }
+
+ return temp;
+ },
+
+ /**
+ * Copies certain style attributes from one element to another.
+ *
+ * @param nsIDOMNode from
+ * The target node.
+ * @param nsIDOMNode to
+ * The destination node.
+ */
+ copyTextStyles: function (from, to) {
+ let win = from.ownerDocument.defaultView;
+ let style = win.getComputedStyle(from);
+ to.style.fontFamily = style.getPropertyCSSValue("font-family").cssText;
+ to.style.fontSize = style.getPropertyCSSValue("font-size").cssText;
+ to.style.fontWeight = style.getPropertyCSSValue("font-weight").cssText;
+ to.style.fontStyle = style.getPropertyCSSValue("font-style").cssText;
+ },
+
+ /**
+ * Create a grip for the given value. If the value is an object,
+ * an object wrapper will be created.
+ *
+ * @param mixed value
+ * The value you want to create a grip for, before sending it to the
+ * client.
+ * @param function objectWrapper
+ * If the value is an object then the objectWrapper function is
+ * invoked to give us an object grip. See this.getObjectGrip().
+ * @return mixed
+ * The value grip.
+ */
+ createValueGrip: function (value, objectWrapper) {
+ switch (typeof value) {
+ case "boolean":
+ return value;
+ case "string":
+ return objectWrapper(value);
+ case "number":
+ if (value === Infinity) {
+ return { type: "Infinity" };
+ } else if (value === -Infinity) {
+ return { type: "-Infinity" };
+ } else if (Number.isNaN(value)) {
+ return { type: "NaN" };
+ } else if (!value && 1 / value === -Infinity) {
+ return { type: "-0" };
+ }
+ return value;
+ case "undefined":
+ return { type: "undefined" };
+ case "object":
+ if (value === null) {
+ return { type: "null" };
+ }
+ // Fall through.
+ case "function":
+ return objectWrapper(value);
+ default:
+ console.error("Failed to provide a grip for value of " + typeof value
+ + ": " + value);
+ return null;
+ }
+ },
+
+ /**
+ * Determine if the given request mixes HTTP with HTTPS content.
+ *
+ * @param string request
+ * Location of the requested content.
+ * @param string location
+ * Location of the current page.
+ * @return boolean
+ * True if the content is mixed, false if not.
+ */
+ isMixedHTTPSRequest: function (request, location) {
+ try {
+ let requestURI = Services.io.newURI(request, null, null);
+ let contentURI = Services.io.newURI(location, null, null);
+ return (contentURI.scheme == "https" && requestURI.scheme != "https");
+ } catch (ex) {
+ return false;
+ }
+ },
+
+ /**
+ * Helper function to deduce the name of the provided function.
+ *
+ * @param funtion function
+ * The function whose name will be returned.
+ * @return string
+ * Function name.
+ */
+ getFunctionName: function (func) {
+ let name = null;
+ if (func.name) {
+ name = func.name;
+ } else {
+ let desc;
+ try {
+ desc = func.getOwnPropertyDescriptor("displayName");
+ } catch (ex) {
+ // Ignore.
+ }
+ if (desc && typeof desc.value == "string") {
+ name = desc.value;
+ }
+ }
+ if (!name) {
+ try {
+ let str = (func.toString() || func.toSource()) + "";
+ name = (str.match(REGEX_MATCH_FUNCTION_NAME) || [])[1];
+ } catch (ex) {
+ // Ignore.
+ }
+ }
+ return name;
+ },
+
+ /**
+ * Get the object class name. For example, the |window| object has the Window
+ * class name (based on [object Window]).
+ *
+ * @param object object
+ * The object you want to get the class name for.
+ * @return string
+ * The object class name.
+ */
+ getObjectClassName: function (object) {
+ if (object === null) {
+ return "null";
+ }
+ if (object === undefined) {
+ return "undefined";
+ }
+
+ let type = typeof object;
+ if (type != "object") {
+ // Grip class names should start with an uppercase letter.
+ return type.charAt(0).toUpperCase() + type.substr(1);
+ }
+
+ let className;
+
+ try {
+ className = ((object + "").match(/^\[object (\S+)\]$/) || [])[1];
+ if (!className) {
+ className = ((object.constructor + "")
+ .match(/^\[object (\S+)\]$/) || [])[1];
+ }
+ if (!className && typeof object.constructor == "function") {
+ className = this.getFunctionName(object.constructor);
+ }
+ } catch (ex) {
+ // Ignore.
+ }
+
+ return className;
+ },
+
+ /**
+ * Check if the given value is a grip with an actor.
+ *
+ * @param mixed grip
+ * Value you want to check if it is a grip with an actor.
+ * @return boolean
+ * True if the given value is a grip with an actor.
+ */
+ isActorGrip: function (grip) {
+ return grip && typeof (grip) == "object" && grip.actor;
+ },
+
+ /**
+ * 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);
+ }
+ },
+ /**
+ * The inputNode "paste" event handler generator. Helps prevent
+ * self-xss attacks
+ *
+ * @param nsIDOMElement inputField
+ * @param nsIDOMElement notificationBox
+ * @returns A function to be added as a handler to 'paste' and
+ *'drop' events on the input field
+ */
+ pasteHandlerGen: function (inputField, notificationBox, msg, okstring) {
+ let handler = function (event) {
+ if (WebConsoleUtils.usageCount >= CONSOLE_ENTRY_THRESHOLD) {
+ inputField.removeEventListener("paste", handler);
+ inputField.removeEventListener("drop", handler);
+ return true;
+ }
+ if (notificationBox.getNotificationWithValue("selfxss-notification")) {
+ event.preventDefault();
+ event.stopPropagation();
+ return false;
+ }
+
+ let notification = notificationBox.appendNotification(msg,
+ "selfxss-notification", null,
+ notificationBox.PRIORITY_WARNING_HIGH, null,
+ function (eventType) {
+ // Cleanup function if notification is dismissed
+ if (eventType == "removed") {
+ inputField.removeEventListener("keyup", pasteKeyUpHandler);
+ }
+ });
+
+ function pasteKeyUpHandler(event2) {
+ let value = inputField.value || inputField.textContent;
+ if (value.includes(okstring)) {
+ notificationBox.removeNotification(notification);
+ inputField.removeEventListener("keyup", pasteKeyUpHandler);
+ WebConsoleUtils.usageCount = CONSOLE_ENTRY_THRESHOLD;
+ }
+ }
+ inputField.addEventListener("keyup", pasteKeyUpHandler);
+
+ event.preventDefault();
+ event.stopPropagation();
+ return false;
+ };
+ return handler;
+ },
+};
+
+exports.Utils = WebConsoleUtils;
+
+// Localization
+
+WebConsoleUtils.L10n = function (bundleURI) {
+ this._helper = new LocalizationHelper(bundleURI);
+};
+
+WebConsoleUtils.L10n.prototype = {
+ /**
+ * 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: function (milliseconds) {
+ let d = new Date(milliseconds ? milliseconds : null);
+ let hours = d.getHours(), minutes = d.getMinutes();
+ let seconds = d.getSeconds();
+ milliseconds = d.getMilliseconds();
+ let parameters = [hours, minutes, seconds, milliseconds];
+ return this.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: function (name) {
+ try {
+ return this._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: function (name, array) {
+ try {
+ return this._helper.getFormatStr(name, ...array);
+ } catch (ex) {
+ console.error("Failed to format string: " + name);
+ throw ex;
+ }
+ },
+};
diff --git a/devtools/client/webconsole/webconsole.js b/devtools/client/webconsole/webconsole.js
new file mode 100644
index 000000000..bd7f90a0e
--- /dev/null
+++ b/devtools/client/webconsole/webconsole.js
@@ -0,0 +1,3658 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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";
+
+const {Cc, Ci, Cu} = require("chrome");
+
+const {Utils: WebConsoleUtils, CONSOLE_WORKER_IDS} =
+ require("devtools/client/webconsole/utils");
+const { getSourceNames } = require("devtools/client/shared/source-utils");
+const BrowserLoaderModule = {};
+Cu.import("resource://devtools/client/shared/browser-loader.js", BrowserLoaderModule);
+
+const promise = require("promise");
+const Services = require("Services");
+const ErrorDocs = require("devtools/server/actors/errordocs");
+const Telemetry = require("devtools/client/shared/telemetry");
+
+loader.lazyServiceGetter(this, "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1",
+ "nsIClipboardHelper");
+loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
+loader.lazyRequireGetter(this, "AutocompletePopup", "devtools/client/shared/autocomplete-popup", true);
+loader.lazyRequireGetter(this, "ToolSidebar", "devtools/client/framework/sidebar", true);
+loader.lazyRequireGetter(this, "ConsoleOutput", "devtools/client/webconsole/console-output", true);
+loader.lazyRequireGetter(this, "Messages", "devtools/client/webconsole/console-output", true);
+loader.lazyRequireGetter(this, "EnvironmentClient", "devtools/shared/client/main", true);
+loader.lazyRequireGetter(this, "ObjectClient", "devtools/shared/client/main", true);
+loader.lazyRequireGetter(this, "system", "devtools/shared/system");
+loader.lazyRequireGetter(this, "JSTerm", "devtools/client/webconsole/jsterm", true);
+loader.lazyRequireGetter(this, "gSequenceId", "devtools/client/webconsole/jsterm", true);
+loader.lazyImporter(this, "VariablesView", "resource://devtools/client/shared/widgets/VariablesView.jsm");
+loader.lazyImporter(this, "VariablesViewController", "resource://devtools/client/shared/widgets/VariablesViewController.jsm");
+loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
+loader.lazyRequireGetter(this, "KeyShortcuts", "devtools/client/shared/key-shortcuts", true);
+loader.lazyRequireGetter(this, "ZoomKeys", "devtools/client/shared/zoom-keys");
+
+const {PluralForm} = require("devtools/shared/plural-form");
+const STRINGS_URI = "devtools/client/locales/webconsole.properties";
+var l10n = new WebConsoleUtils.L10n(STRINGS_URI);
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+const MIXED_CONTENT_LEARN_MORE = "https://developer.mozilla.org/docs/Web/Security/Mixed_content";
+
+const IGNORED_SOURCE_URLS = ["debugger eval code"];
+
+// The amount of time in milliseconds that we wait before performing a live
+// search.
+const SEARCH_DELAY = 200;
+
+// The number of lines that are displayed in the console output by default, for
+// each category. The user can change this number by adjusting the hidden
+// "devtools.hud.loglimit.{network,cssparser,exception,console}" preferences.
+const DEFAULT_LOG_LIMIT = 1000;
+
+// The various categories of messages. We start numbering at zero so we can
+// use these as indexes into the MESSAGE_PREFERENCE_KEYS matrix below.
+const CATEGORY_NETWORK = 0;
+const CATEGORY_CSS = 1;
+const CATEGORY_JS = 2;
+const CATEGORY_WEBDEV = 3;
+// always on
+const CATEGORY_INPUT = 4;
+// always on
+const CATEGORY_OUTPUT = 5;
+const CATEGORY_SECURITY = 6;
+const CATEGORY_SERVER = 7;
+
+// The possible message severities. As before, we start at zero so we can use
+// these as indexes into MESSAGE_PREFERENCE_KEYS.
+const SEVERITY_ERROR = 0;
+const SEVERITY_WARNING = 1;
+const SEVERITY_INFO = 2;
+const SEVERITY_LOG = 3;
+
+// The fragment of a CSS class name that identifies each category.
+const CATEGORY_CLASS_FRAGMENTS = [
+ "network",
+ "cssparser",
+ "exception",
+ "console",
+ "input",
+ "output",
+ "security",
+ "server",
+];
+
+// The fragment of a CSS class name that identifies each severity.
+const SEVERITY_CLASS_FRAGMENTS = [
+ "error",
+ "warn",
+ "info",
+ "log",
+];
+
+// The preference keys to use for each category/severity combination, indexed
+// first by category (rows) and then by severity (columns) in the following
+// order:
+//
+// [ Error, Warning, Info, Log ]
+//
+// Most of these rather idiosyncratic names are historical and predate the
+// division of message type into "category" and "severity".
+const MESSAGE_PREFERENCE_KEYS = [
+ // Network
+ [ "network", "netwarn", "netxhr", "networkinfo", ],
+ // CSS
+ [ "csserror", "cssparser", null, "csslog", ],
+ // JS
+ [ "exception", "jswarn", null, "jslog", ],
+ // Web Developer
+ [ "error", "warn", "info", "log", ],
+ // Input
+ [ null, null, null, null, ],
+ // Output
+ [ null, null, null, null, ],
+ // Security
+ [ "secerror", "secwarn", null, null, ],
+ // Server Logging
+ [ "servererror", "serverwarn", "serverinfo", "serverlog", ],
+];
+
+// A mapping from the console API log event levels to the Web Console
+// severities.
+const LEVELS = {
+ error: SEVERITY_ERROR,
+ exception: SEVERITY_ERROR,
+ assert: SEVERITY_ERROR,
+ warn: SEVERITY_WARNING,
+ info: SEVERITY_INFO,
+ log: SEVERITY_LOG,
+ clear: SEVERITY_LOG,
+ trace: SEVERITY_LOG,
+ table: SEVERITY_LOG,
+ debug: SEVERITY_LOG,
+ dir: SEVERITY_LOG,
+ dirxml: SEVERITY_LOG,
+ group: SEVERITY_LOG,
+ groupCollapsed: SEVERITY_LOG,
+ groupEnd: SEVERITY_LOG,
+ time: SEVERITY_LOG,
+ timeEnd: SEVERITY_LOG,
+ count: SEVERITY_LOG
+};
+
+// This array contains the prefKey for the workers and it must keep them in the
+// same order as CONSOLE_WORKER_IDS
+const WORKERTYPES_PREFKEYS =
+ [ "sharedworkers", "serviceworkers", "windowlessworkers" ];
+
+// The lowest HTTP response code (inclusive) that is considered an error.
+const MIN_HTTP_ERROR_CODE = 400;
+// The highest HTTP response code (inclusive) that is considered an error.
+const MAX_HTTP_ERROR_CODE = 599;
+
+// The indent of a console group in pixels.
+const GROUP_INDENT = 12;
+
+// The number of messages to display in a single display update. If we display
+// too many messages at once we slow down the Firefox UI too much.
+const MESSAGES_IN_INTERVAL = DEFAULT_LOG_LIMIT;
+
+// The delay (in milliseconds) between display updates - tells how often we
+// should *try* to push new messages to screen. This value is optimistic,
+// updates won't always happen. Keep this low so the Web Console output feels
+// live.
+const OUTPUT_INTERVAL = 20;
+
+// The maximum amount of time (in milliseconds) that can be spent doing cleanup
+// inside of the flush output callback. If things don't get cleaned up in this
+// time, then it will start again the next time it is called.
+const MAX_CLEANUP_TIME = 10;
+
+// When the output queue has more than MESSAGES_IN_INTERVAL items we throttle
+// output updates to this number of milliseconds. So during a lot of output we
+// update every N milliseconds given here.
+const THROTTLE_UPDATES = 1000;
+
+// The preference prefix for all of the Web Console filters.
+const FILTER_PREFS_PREFIX = "devtools.webconsole.filter.";
+
+// The minimum font size.
+const MIN_FONT_SIZE = 10;
+
+const PREF_CONNECTION_TIMEOUT = "devtools.debugger.remote-timeout";
+const PREF_PERSISTLOG = "devtools.webconsole.persistlog";
+const PREF_MESSAGE_TIMESTAMP = "devtools.webconsole.timestampMessages";
+const PREF_NEW_FRONTEND_ENABLED = "devtools.webconsole.new-frontend-enabled";
+
+/**
+ * A WebConsoleFrame 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 WebConsoleFrame is responsible for the actual Web Console UI
+ * implementation.
+ *
+ * @constructor
+ * @param object webConsoleOwner
+ * The WebConsole owner object.
+ */
+function WebConsoleFrame(webConsoleOwner) {
+ this.owner = webConsoleOwner;
+ this.hudId = this.owner.hudId;
+ this.isBrowserConsole = this.owner._browserConsole;
+
+ this.window = this.owner.iframeWindow;
+
+ this._repeatNodes = {};
+ this._outputQueue = [];
+ this._itemDestroyQueue = [];
+ this._pruneCategoriesQueue = {};
+ this.filterPrefs = {};
+
+ this.output = new ConsoleOutput(this);
+
+ this.unmountMessage = this.unmountMessage.bind(this);
+ this._toggleFilter = this._toggleFilter.bind(this);
+ this.resize = this.resize.bind(this);
+ this._onPanelSelected = this._onPanelSelected.bind(this);
+ this._flushMessageQueue = this._flushMessageQueue.bind(this);
+ this._onToolboxPrefChanged = this._onToolboxPrefChanged.bind(this);
+ this._onUpdateListeners = this._onUpdateListeners.bind(this);
+
+ this._outputTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._outputTimerInitialized = false;
+
+ let require = BrowserLoaderModule.BrowserLoader({
+ window: this.window,
+ useOnlyShared: true
+ }).require;
+
+ this.React = require("devtools/client/shared/vendor/react");
+ this.ReactDOM = require("devtools/client/shared/vendor/react-dom");
+ this.FrameView = this.React.createFactory(require("devtools/client/shared/components/frame"));
+ this.StackTraceView = this.React.createFactory(require("devtools/client/shared/components/stack-trace"));
+
+ this._telemetry = new Telemetry();
+
+ EventEmitter.decorate(this);
+}
+exports.WebConsoleFrame = WebConsoleFrame;
+
+WebConsoleFrame.prototype = {
+ /**
+ * The WebConsole instance that owns this frame.
+ * @see hudservice.js::WebConsole
+ * @type object
+ */
+ owner: null,
+
+ /**
+ * Proxy between the Web Console and the remote Web Console instance. This
+ * object holds methods used for connecting, listening and disconnecting from
+ * the remote server, using the remote debugging protocol.
+ *
+ * @see WebConsoleConnectionProxy
+ * @type object
+ */
+ proxy: null,
+
+ /**
+ * Getter for the xul:popupset that holds any popups we open.
+ * @type nsIDOMElement
+ */
+ get popupset() {
+ return this.owner.mainPopupSet;
+ },
+
+ /**
+ * Holds the initialization promise object.
+ * @private
+ * @type object
+ */
+ _initDefer: null,
+
+ /**
+ * Last time when we displayed any message in the output.
+ *
+ * @private
+ * @type number
+ * Timestamp in milliseconds since the Unix epoch.
+ */
+ _lastOutputFlush: 0,
+
+ /**
+ * Message nodes are stored here in a queue for later display.
+ *
+ * @private
+ * @type array
+ */
+ _outputQueue: null,
+
+ /**
+ * Keep track of the categories we need to prune from time to time.
+ *
+ * @private
+ * @type array
+ */
+ _pruneCategoriesQueue: null,
+
+ /**
+ * Function invoked whenever the output queue is emptied. This is used by some
+ * tests.
+ *
+ * @private
+ * @type function
+ */
+ _flushCallback: null,
+
+ /**
+ * Timer used for flushing the messages output queue.
+ *
+ * @private
+ * @type nsITimer
+ */
+ _outputTimer: null,
+ _outputTimerInitialized: null,
+
+ /**
+ * Store for tracking repeated nodes.
+ * @private
+ * @type object
+ */
+ _repeatNodes: null,
+
+ /**
+ * Preferences for filtering messages by type.
+ * @see this._initDefaultFilterPrefs()
+ * @type object
+ */
+ filterPrefs: null,
+
+ /**
+ * Prefix used for filter preferences.
+ * @private
+ * @type string
+ */
+ _filterPrefsPrefix: FILTER_PREFS_PREFIX,
+
+ /**
+ * The nesting depth of the currently active console group.
+ */
+ groupDepth: 0,
+
+ /**
+ * The current target location.
+ * @type string
+ */
+ contentLocation: "",
+
+ /**
+ * The JSTerm object that manage the console's input.
+ * @see JSTerm
+ * @type object
+ */
+ jsterm: null,
+
+ /**
+ * The element that holds all of the messages we display.
+ * @type nsIDOMElement
+ */
+ outputNode: null,
+
+ /**
+ * The ConsoleOutput instance that manages all output.
+ * @type object
+ */
+ output: null,
+
+ /**
+ * The input element that allows the user to filter messages by string.
+ * @type nsIDOMElement
+ */
+ filterBox: null,
+
+ /**
+ * Getter for the debugger WebConsoleClient.
+ * @type object
+ */
+ get webConsoleClient() {
+ return this.proxy ? this.proxy.webConsoleClient : null;
+ },
+
+ _destroyer: null,
+
+ _saveRequestAndResponseBodies: true,
+ _throttleData: null,
+
+ // Chevron width at the starting of Web Console's input box.
+ _chevronWidth: 0,
+ // Width of the monospace characters in Web Console's input box.
+ _inputCharWidth: 0,
+
+ /**
+ * Setter for saving of network request and response bodies.
+ *
+ * @param boolean value
+ * The new value you want to set.
+ */
+ setSaveRequestAndResponseBodies: function (value) {
+ if (!this.webConsoleClient) {
+ // Don't continue if the webconsole disconnected.
+ return promise.resolve(null);
+ }
+
+ let deferred = promise.defer();
+ let newValue = !!value;
+ let toSet = {
+ "NetworkMonitor.saveRequestAndResponseBodies": newValue,
+ };
+
+ // Make sure the web console client connection is established first.
+ this.webConsoleClient.setPreferences(toSet, response => {
+ if (!response.error) {
+ this._saveRequestAndResponseBodies = newValue;
+ deferred.resolve(response);
+ } else {
+ deferred.reject(response.error);
+ }
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Setter for throttling data.
+ *
+ * @param boolean value
+ * The new value you want to set; @see NetworkThrottleManager.
+ */
+ setThrottleData: function(value) {
+ if (!this.webConsoleClient) {
+ // Don't continue if the webconsole disconnected.
+ return promise.resolve(null);
+ }
+
+ let deferred = promise.defer();
+ let toSet = {
+ "NetworkMonitor.throttleData": value,
+ };
+
+ // Make sure the web console client connection is established first.
+ this.webConsoleClient.setPreferences(toSet, response => {
+ if (!response.error) {
+ this._throttleData = value;
+ deferred.resolve(response);
+ } else {
+ deferred.reject(response.error);
+ }
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Getter for the persistent logging preference.
+ * @type boolean
+ */
+ get persistLog() {
+ // 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.
+ return this.isBrowserConsole ||
+ Services.prefs.getBoolPref(PREF_PERSISTLOG);
+ },
+
+ /**
+ * Initialize the WebConsoleFrame instance.
+ * @return object
+ * A promise object that resolves once the frame is ready to use.
+ */
+ init: function () {
+ this._initUI();
+ let connectionInited = this._initConnection();
+
+ // Don't reject if the history fails to load for some reason.
+ // This would be fine, the panel will just start with empty history.
+ let allReady = this.jsterm.historyLoaded.catch(() => {}).then(() => {
+ return connectionInited;
+ });
+
+ // This notification is only used in tests. Don't chain it onto
+ // the returned promise because the console panel needs to be attached
+ // to the toolbox before the web-console-created event is receieved.
+ let notifyObservers = () => {
+ let id = WebConsoleUtils.supportsString(this.hudId);
+ Services.obs.notifyObservers(id, "web-console-created", null);
+ };
+ allReady.then(notifyObservers, notifyObservers);
+
+ if (this.NEW_CONSOLE_OUTPUT_ENABLED) {
+ allReady.then(this.newConsoleOutput.init);
+ }
+
+ return allReady;
+ },
+
+ /**
+ * Connect to the server using the remote debugging protocol.
+ *
+ * @private
+ * @return object
+ * A promise object that is resolved/reject based on the connection
+ * result.
+ */
+ _initConnection: function () {
+ if (this._initDefer) {
+ return this._initDefer.promise;
+ }
+
+ this._initDefer = promise.defer();
+ this.proxy = new WebConsoleConnectionProxy(this, this.owner.target);
+
+ this.proxy.connect().then(() => {
+ // on success
+ this._initDefer.resolve(this);
+ }, (reason) => {
+ // on failure
+ let node = this.createMessageNode(CATEGORY_JS, SEVERITY_ERROR,
+ reason.error + ": " + reason.message);
+ this.outputMessage(CATEGORY_JS, node, [reason]);
+ this._initDefer.reject(reason);
+ });
+
+ return this._initDefer.promise;
+ },
+
+ /**
+ * Find the Web Console UI elements and setup event listeners as needed.
+ * @private
+ */
+ _initUI: function () {
+ this.document = this.window.document;
+ this.rootElement = this.document.documentElement;
+ this.NEW_CONSOLE_OUTPUT_ENABLED = !this.isBrowserConsole
+ && !this.owner.target.chrome
+ && Services.prefs.getBoolPref(PREF_NEW_FRONTEND_ENABLED);
+
+ this.outputNode = this.document.getElementById("output-container");
+ this.outputWrapper = this.document.getElementById("output-wrapper");
+ this.completeNode = this.document.querySelector(".jsterm-complete-node");
+ this.inputNode = this.document.querySelector(".jsterm-input-node");
+
+ // In the old frontend, the area that scrolls is outputWrapper, but in the new
+ // frontend this will be reassigned.
+ this.outputScroller = this.outputWrapper;
+
+ // Update the character width and height needed for the popup offset
+ // calculations.
+ this._updateCharSize();
+
+ this.jsterm = new JSTerm(this);
+ this.jsterm.init();
+
+ let toolbox = gDevTools.getToolbox(this.owner.target);
+
+ if (this.NEW_CONSOLE_OUTPUT_ENABLED) {
+ // @TODO Remove this once JSTerm is handled with React/Redux.
+ this.window.jsterm = this.jsterm;
+
+ // Remove context menu for now (see Bug 1307239).
+ this.outputWrapper.removeAttribute("context");
+
+ // XXX: We should actually stop output from happening on old output
+ // panel, but for now let's just hide it.
+ this.experimentalOutputNode = this.outputNode.cloneNode();
+ this.experimentalOutputNode.removeAttribute("tabindex");
+ this.outputNode.hidden = true;
+ this.outputNode.parentNode.appendChild(this.experimentalOutputNode);
+ // @TODO Once the toolbox has been converted to React, see if passing
+ // in JSTerm is still necessary.
+
+ this.newConsoleOutput = new this.window.NewConsoleOutput(
+ this.experimentalOutputNode, this.jsterm, toolbox, this.owner, this.document);
+
+ let filterToolbar = this.document.querySelector(".hud-console-filter-toolbar");
+ filterToolbar.hidden = true;
+ } else {
+ // Register the controller to handle "select all" properly.
+ this._commandController = new CommandController(this);
+ this.window.controllers.insertControllerAt(0, this._commandController);
+
+ this._contextMenuHandler = new ConsoleContextMenu(this);
+
+ this._initDefaultFilterPrefs();
+ this.filterBox = this.document.querySelector(".hud-filter-box");
+ this._setFilterTextBoxEvents();
+ this._initFilterButtons();
+ let clearButton =
+ this.document.getElementsByClassName("webconsole-clear-console-button")[0];
+ clearButton.addEventListener("command", () => {
+ this.owner._onClearButton();
+ this.jsterm.clearOutput(true);
+ });
+
+ }
+
+ this.resize();
+ this.window.addEventListener("resize", this.resize, true);
+ this.jsterm.on("sidebar-opened", this.resize);
+ this.jsterm.on("sidebar-closed", this.resize);
+
+ if (toolbox) {
+ toolbox.on("webconsole-selected", this._onPanelSelected);
+ }
+
+ /*
+ * Focus the input line whenever the output area is clicked.
+ */
+ this.outputWrapper.addEventListener("click", (event) => {
+ // Do not focus on middle/right-click or 2+ clicks.
+ if (event.detail !== 1 || event.button !== 0) {
+ return;
+ }
+
+ // Do not focus if something is selected
+ let selection = this.window.getSelection();
+ if (selection && !selection.isCollapsed) {
+ return;
+ }
+
+ // Do not focus if a link was clicked
+ if (event.target.nodeName.toLowerCase() === "a" ||
+ event.target.parentNode.nodeName.toLowerCase() === "a") {
+ return;
+ }
+
+ // Do not focus if a search input was clicked on the new frontend
+ if (this.NEW_CONSOLE_OUTPUT_ENABLED &&
+ event.target.nodeName.toLowerCase() === "input" &&
+ event.target.getAttribute("type").toLowerCase() === "search") {
+ return;
+ }
+
+ this.jsterm.focus();
+ });
+
+ // Toggle the timestamp on preference change
+ gDevTools.on("pref-changed", this._onToolboxPrefChanged);
+ this._onToolboxPrefChanged("pref-changed", {
+ pref: PREF_MESSAGE_TIMESTAMP,
+ newValue: Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP),
+ });
+
+ this._initShortcuts();
+
+ // focus input node
+ this.jsterm.focus();
+ },
+
+ /**
+ * Resizes the output node to fit the output wrapped.
+ * We need this because it makes the layout a lot faster than
+ * using -moz-box-flex and 100% width. See Bug 1237368.
+ */
+ resize: function () {
+ if (this.NEW_CONSOLE_OUTPUT_ENABLED) {
+ this.experimentalOutputNode.style.width =
+ this.outputWrapper.clientWidth + "px";
+ } else {
+ this.outputNode.style.width = this.outputWrapper.clientWidth + "px";
+ }
+ },
+
+ /**
+ * Sets the focus to JavaScript input field when the web console tab is
+ * selected or when there is a split console present.
+ * @private
+ */
+ _onPanelSelected: function () {
+ this.jsterm.focus();
+ },
+
+ /**
+ * Initialize the default filter preferences.
+ * @private
+ */
+ _initDefaultFilterPrefs: function () {
+ let prefs = ["network", "networkinfo", "csserror", "cssparser", "csslog",
+ "exception", "jswarn", "jslog", "error", "info", "warn", "log",
+ "secerror", "secwarn", "netwarn", "netxhr", "sharedworkers",
+ "serviceworkers", "windowlessworkers", "servererror",
+ "serverwarn", "serverinfo", "serverlog"];
+
+ for (let pref of prefs) {
+ this.filterPrefs[pref] = Services.prefs.getBoolPref(
+ this._filterPrefsPrefix + pref);
+ }
+ },
+
+ _initShortcuts: function() {
+ var shortcuts = new KeyShortcuts({
+ window: this.window
+ });
+
+ shortcuts.on(l10n.getStr("webconsole.find.key"),
+ (name, event) => {
+ this.filterBox.focus();
+ event.preventDefault();
+ });
+
+ let clearShortcut;
+ if (system.constants.platform === "macosx") {
+ clearShortcut = l10n.getStr("webconsole.clear.keyOSX");
+ } else {
+ clearShortcut = l10n.getStr("webconsole.clear.key");
+ }
+ shortcuts.on(clearShortcut,
+ () => this.jsterm.clearOutput(true));
+
+ if (this.isBrowserConsole) {
+ shortcuts.on(l10n.getStr("webconsole.close.key"),
+ this.window.close.bind(this.window));
+
+ ZoomKeys.register(this.window);
+ }
+ },
+
+ /**
+ * Attach / detach reflow listeners depending on the checked status
+ * of the `CSS > Log` menuitem.
+ *
+ * @param function [callback=null]
+ * Optional function to invoke when the listener has been
+ * added/removed.
+ */
+ _updateReflowActivityListener: function (callback) {
+ if (this.webConsoleClient) {
+ let pref = this._filterPrefsPrefix + "csslog";
+ if (Services.prefs.getBoolPref(pref)) {
+ this.webConsoleClient.startListeners(["ReflowActivity"], callback);
+ } else {
+ this.webConsoleClient.stopListeners(["ReflowActivity"], callback);
+ }
+ }
+ },
+
+ /**
+ * Attach / detach server logging listener depending on the filter
+ * preferences. If the user isn't interested in the server logs at
+ * all the listener is not registered.
+ *
+ * @param function [callback=null]
+ * Optional function to invoke when the listener has been
+ * added/removed.
+ */
+ _updateServerLoggingListener: function (callback) {
+ if (!this.webConsoleClient) {
+ return null;
+ }
+
+ let startListener = false;
+ let prefs = ["servererror", "serverwarn", "serverinfo", "serverlog"];
+ for (let i = 0; i < prefs.length; i++) {
+ if (this.filterPrefs[prefs[i]]) {
+ startListener = true;
+ break;
+ }
+ }
+
+ if (startListener) {
+ this.webConsoleClient.startListeners(["ServerLogging"], callback);
+ } else {
+ this.webConsoleClient.stopListeners(["ServerLogging"], callback);
+ }
+ },
+
+ /**
+ * Sets the events for the filter input field.
+ * @private
+ */
+ _setFilterTextBoxEvents: function () {
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ let timerEvent = this.adjustVisibilityOnSearchStringChange.bind(this);
+
+ let onChange = function _onChange() {
+ // To improve responsiveness, we let the user finish typing before we
+ // perform the search.
+ timer.cancel();
+ timer.initWithCallback(timerEvent, SEARCH_DELAY,
+ Ci.nsITimer.TYPE_ONE_SHOT);
+ };
+
+ this.filterBox.addEventListener("command", onChange, false);
+ this.filterBox.addEventListener("input", onChange, false);
+ },
+
+ /**
+ * Creates one of the filter buttons on the toolbar.
+ *
+ * @private
+ * @param nsIDOMNode aParent
+ * The node to which the filter button should be appended.
+ * @param object aDescriptor
+ * A descriptor that contains info about the button. Contains "name",
+ * "category", and "prefKey" properties, and optionally a "severities"
+ * property.
+ */
+ _initFilterButtons: function () {
+ let categories = this.document
+ .querySelectorAll(".webconsole-filter-button[category]");
+ Array.forEach(categories, function (button) {
+ button.addEventListener("contextmenu", () => {
+ button.open = true;
+ }, false);
+ button.addEventListener("click", this._toggleFilter, false);
+
+ let someChecked = false;
+ let severities = button.querySelectorAll("menuitem[prefKey]");
+ Array.forEach(severities, function (menuItem) {
+ menuItem.addEventListener("command", this._toggleFilter, false);
+
+ let prefKey = menuItem.getAttribute("prefKey");
+ let checked = this.filterPrefs[prefKey];
+ menuItem.setAttribute("checked", checked);
+ someChecked = someChecked || checked;
+ }, this);
+
+ button.setAttribute("checked", someChecked);
+ button.setAttribute("aria-pressed", someChecked);
+ }, this);
+
+ if (!this.isBrowserConsole) {
+ // The Browser Console displays nsIConsoleMessages which are messages that
+ // end up in the JS category, but they are not errors or warnings, they
+ // are just log messages. The Web Console does not show such messages.
+ let jslog = this.document.querySelector("menuitem[prefKey=jslog]");
+ jslog.hidden = true;
+ }
+
+ if (Services.appinfo.OS == "Darwin") {
+ let net = this.document.querySelector("toolbarbutton[category=net]");
+ let accesskey = net.getAttribute("accesskeyMacOSX");
+ net.setAttribute("accesskey", accesskey);
+
+ let logging =
+ this.document.querySelector("toolbarbutton[category=logging]");
+ logging.removeAttribute("accesskey");
+
+ let serverLogging =
+ this.document.querySelector("toolbarbutton[category=server]");
+ serverLogging.removeAttribute("accesskey");
+ }
+ },
+
+ /**
+ * Calculates the width and height of a single character of the input box.
+ * This will be used in opening the popup at the correct offset.
+ *
+ * @private
+ */
+ _updateCharSize: function () {
+ let doc = this.document;
+ let tempLabel = doc.createElementNS(XHTML_NS, "span");
+ let style = tempLabel.style;
+ style.position = "fixed";
+ style.padding = "0";
+ style.margin = "0";
+ style.width = "auto";
+ style.color = "transparent";
+ WebConsoleUtils.copyTextStyles(this.inputNode, tempLabel);
+ tempLabel.textContent = "x";
+ doc.documentElement.appendChild(tempLabel);
+ this._inputCharWidth = tempLabel.offsetWidth;
+ tempLabel.parentNode.removeChild(tempLabel);
+ // Calculate the width of the chevron placed at the beginning of the input
+ // box. Remove 4 more pixels to accomodate the padding of the popup.
+ this._chevronWidth = +doc.defaultView.getComputedStyle(this.inputNode)
+ .paddingLeft.replace(/[^0-9.]/g, "") - 4;
+ },
+
+ /**
+ * The event handler that is called whenever a user switches a filter on or
+ * off.
+ *
+ * @private
+ * @param nsIDOMEvent event
+ * The event that triggered the filter change.
+ */
+ _toggleFilter: function (event) {
+ let target = event.target;
+ let tagName = target.tagName;
+ // Prevent toggle if generated from a contextmenu event (right click)
+ let isRightClick = (event.button === 2);
+ if (tagName != event.currentTarget.tagName || isRightClick) {
+ return;
+ }
+
+ switch (tagName) {
+ case "toolbarbutton": {
+ let originalTarget = event.originalTarget;
+ let classes = originalTarget.classList;
+
+ if (originalTarget.localName !== "toolbarbutton") {
+ // Oddly enough, the click event is sent to the menu button when
+ // selecting a menu item with the mouse. Detect this case and bail
+ // out.
+ break;
+ }
+
+ if (!classes.contains("toolbarbutton-menubutton-button") &&
+ originalTarget.getAttribute("type") === "menu-button") {
+ // This is a filter button with a drop-down. The user clicked the
+ // drop-down, so do nothing. (The menu will automatically appear
+ // without our intervention.)
+ break;
+ }
+
+ // Toggle on the targeted filter button, and if the user alt clicked,
+ // toggle off all other filter buttons and their associated filters.
+ let state = target.getAttribute("checked") !== "true";
+ if (event.getModifierState("Alt")) {
+ let buttons = this.document
+ .querySelectorAll(".webconsole-filter-button");
+ Array.forEach(buttons, (button) => {
+ if (button !== target) {
+ button.setAttribute("checked", false);
+ button.setAttribute("aria-pressed", false);
+ this._setMenuState(button, false);
+ }
+ });
+ state = true;
+ }
+ target.setAttribute("checked", state);
+ target.setAttribute("aria-pressed", state);
+
+ // This is a filter button with a drop-down, and the user clicked the
+ // main part of the button. Go through all the severities and toggle
+ // their associated filters.
+ this._setMenuState(target, state);
+
+ // CSS reflow logging can decrease web page performance.
+ // Make sure the option is always unchecked when the CSS filter button
+ // is selected. See bug 971798.
+ if (target.getAttribute("category") == "css" && state) {
+ let csslogMenuItem = target.querySelector("menuitem[prefKey=csslog]");
+ csslogMenuItem.setAttribute("checked", false);
+ this.setFilterState("csslog", false);
+ }
+
+ break;
+ }
+
+ case "menuitem": {
+ let state = target.getAttribute("checked") !== "true";
+ target.setAttribute("checked", state);
+
+ let prefKey = target.getAttribute("prefKey");
+ this.setFilterState(prefKey, state);
+
+ // Adjust the state of the button appropriately.
+ let menuPopup = target.parentNode;
+
+ let someChecked = false;
+ let menuItem = menuPopup.firstChild;
+ while (menuItem) {
+ if (menuItem.hasAttribute("prefKey") &&
+ menuItem.getAttribute("checked") === "true") {
+ someChecked = true;
+ break;
+ }
+ menuItem = menuItem.nextSibling;
+ }
+ let toolbarButton = menuPopup.parentNode;
+ toolbarButton.setAttribute("checked", someChecked);
+ toolbarButton.setAttribute("aria-pressed", someChecked);
+ break;
+ }
+ }
+ },
+
+ /**
+ * Set the menu attributes for a specific toggle button.
+ *
+ * @private
+ * @param XULElement target
+ * Button with drop down items to be toggled.
+ * @param boolean state
+ * True if the menu item is being toggled on, and false otherwise.
+ */
+ _setMenuState: function (target, state) {
+ let menuItems = target.querySelectorAll("menuitem");
+ Array.forEach(menuItems, (item) => {
+ item.setAttribute("checked", state);
+ let prefKey = item.getAttribute("prefKey");
+ this.setFilterState(prefKey, state);
+ });
+ },
+
+ /**
+ * Set the filter state for a specific toggle button.
+ *
+ * @param string toggleType
+ * @param boolean state
+ * @returns void
+ */
+ setFilterState: function (toggleType, state) {
+ this.filterPrefs[toggleType] = state;
+ this.adjustVisibilityForMessageType(toggleType, state);
+
+ Services.prefs.setBoolPref(this._filterPrefsPrefix + toggleType, state);
+
+ if (this._updateListenersTimeout) {
+ clearTimeout(this._updateListenersTimeout);
+ }
+
+ this._updateListenersTimeout = setTimeout(
+ this._onUpdateListeners, 200);
+ },
+
+ /**
+ * Get the filter state for a specific toggle button.
+ *
+ * @param string toggleType
+ * @returns boolean
+ */
+ getFilterState: function (toggleType) {
+ return this.filterPrefs[toggleType];
+ },
+
+ /**
+ * Called when a logging filter changes. Allows to stop/start
+ * listeners according to the current filter state.
+ */
+ _onUpdateListeners: function () {
+ this._updateReflowActivityListener();
+ this._updateServerLoggingListener();
+ },
+
+ /**
+ * Check that the passed string matches the filter arguments.
+ *
+ * @param String str
+ * to search for filter words in.
+ * @param String filter
+ * is a string containing all of the words to filter on.
+ * @returns boolean
+ */
+ stringMatchesFilters: function (str, filter) {
+ if (!filter || !str) {
+ return true;
+ }
+
+ let searchStr = str.toLowerCase();
+ let filterStrings = filter.toLowerCase().split(/\s+/);
+ return !filterStrings.some(function (f) {
+ return searchStr.indexOf(f) == -1;
+ });
+ },
+
+ /**
+ * Turns the display of log nodes on and off appropriately to reflect the
+ * adjustment of the message type filter named by @prefKey.
+ *
+ * @param string prefKey
+ * The preference key for the message type being filtered: one of the
+ * values in the MESSAGE_PREFERENCE_KEYS table.
+ * @param boolean state
+ * True if the filter named by @messageType is being turned on; false
+ * otherwise.
+ * @returns void
+ */
+ adjustVisibilityForMessageType: function (prefKey, state) {
+ let outputNode = this.outputNode;
+ let doc = this.document;
+
+ // Look for message nodes (".message") with the given preference key
+ // (filter="error", filter="cssparser", etc.) and add or remove the
+ // "filtered-by-type" class, which turns on or off the display.
+
+ let attribute = WORKERTYPES_PREFKEYS.indexOf(prefKey) == -1
+ ? "filter" : "workerType";
+
+ let xpath = ".//*[contains(@class, 'message') and " +
+ "@" + attribute + "='" + prefKey + "']";
+ let result = doc.evaluate(xpath, outputNode, null,
+ Ci.nsIDOMXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
+ for (let i = 0; i < result.snapshotLength; i++) {
+ let node = result.snapshotItem(i);
+ if (state) {
+ node.classList.remove("filtered-by-type");
+ } else {
+ node.classList.add("filtered-by-type");
+ }
+ }
+ },
+
+ /**
+ * Turns the display of log nodes on and off appropriately to reflect the
+ * adjustment of the search string.
+ */
+ adjustVisibilityOnSearchStringChange: function () {
+ let nodes = this.outputNode.getElementsByClassName("message");
+ let searchString = this.filterBox.value;
+
+ for (let i = 0, n = nodes.length; i < n; ++i) {
+ let node = nodes[i];
+
+ // hide nodes that match the strings
+ let text = node.textContent;
+
+ // if the text matches the words in aSearchString...
+ if (this.stringMatchesFilters(text, searchString)) {
+ node.classList.remove("filtered-by-string");
+ } else {
+ node.classList.add("filtered-by-string");
+ }
+ }
+
+ this.resize();
+ },
+
+ /**
+ * Applies the user's filters to a newly-created message node via CSS
+ * classes.
+ *
+ * @param nsIDOMNode node
+ * The newly-created message node.
+ * @return boolean
+ * True if the message was filtered or false otherwise.
+ */
+ filterMessageNode: function (node) {
+ let isFiltered = false;
+
+ // Filter by the message type.
+ let prefKey = MESSAGE_PREFERENCE_KEYS[node.category][node.severity];
+ if (prefKey && !this.getFilterState(prefKey)) {
+ // The node is filtered by type.
+ node.classList.add("filtered-by-type");
+ isFiltered = true;
+ }
+
+ // Filter by worker type
+ if ("workerType" in node && !this.getFilterState(node.workerType)) {
+ node.classList.add("filtered-by-type");
+ isFiltered = true;
+ }
+
+ // Filter on the search string.
+ let search = this.filterBox.value;
+ let text = node.clipboardText;
+
+ // if string matches the filter text
+ if (!this.stringMatchesFilters(text, search)) {
+ node.classList.add("filtered-by-string");
+ isFiltered = true;
+ }
+
+ if (isFiltered && node.classList.contains("inlined-variables-view")) {
+ node.classList.add("hidden-message");
+ }
+
+ return isFiltered;
+ },
+
+ /**
+ * Merge the attributes of repeated nodes.
+ *
+ * @param nsIDOMNode original
+ * The Original Node. The one being merged into.
+ */
+ mergeFilteredMessageNode: function (original) {
+ let repeatNode = original.getElementsByClassName("message-repeats")[0];
+ if (!repeatNode) {
+ // no repeat node, return early.
+ return;
+ }
+
+ let occurrences = parseInt(repeatNode.getAttribute("value"), 10) + 1;
+ repeatNode.setAttribute("value", occurrences);
+ repeatNode.textContent = occurrences;
+ let str = l10n.getStr("messageRepeats.tooltip2");
+ repeatNode.title = PluralForm.get(occurrences, str)
+ .replace("#1", occurrences);
+ },
+
+ /**
+ * Filter the message node from the output if it is a repeat.
+ *
+ * @private
+ * @param nsIDOMNode node
+ * The message node to be filtered or not.
+ * @returns nsIDOMNode|null
+ * Returns the duplicate node if the message was filtered, null
+ * otherwise.
+ */
+ _filterRepeatedMessage: function (node) {
+ let repeatNode = node.getElementsByClassName("message-repeats")[0];
+ if (!repeatNode) {
+ return null;
+ }
+
+ let uid = repeatNode._uid;
+ let dupeNode = null;
+
+ if (node.category == CATEGORY_CSS ||
+ node.category == CATEGORY_SECURITY) {
+ dupeNode = this._repeatNodes[uid];
+ if (!dupeNode) {
+ this._repeatNodes[uid] = node;
+ }
+ } else if ((node.category == CATEGORY_WEBDEV ||
+ node.category == CATEGORY_JS) &&
+ node.category != CATEGORY_NETWORK &&
+ !node.classList.contains("inlined-variables-view")) {
+ let lastMessage = this.outputNode.lastChild;
+ if (!lastMessage) {
+ return null;
+ }
+
+ let lastRepeatNode =
+ lastMessage.getElementsByClassName("message-repeats")[0];
+ if (lastRepeatNode && lastRepeatNode._uid == uid) {
+ dupeNode = lastMessage;
+ }
+ }
+
+ if (dupeNode) {
+ this.mergeFilteredMessageNode(dupeNode);
+ // Even though this node was never rendered, we create the location
+ // nodes before rendering, so we still have to clean up any
+ // React components
+ this.unmountMessage(node);
+ return dupeNode;
+ }
+
+ return null;
+ },
+
+ /**
+ * Display cached messages that may have been collected before the UI is
+ * displayed.
+ *
+ * @param array remoteMessages
+ * Array of cached messages coming from the remote Web Console
+ * content instance.
+ */
+ displayCachedMessages: function (remoteMessages) {
+ if (!remoteMessages.length) {
+ return;
+ }
+
+ remoteMessages.forEach(function (message) {
+ switch (message._type) {
+ case "PageError": {
+ let category = Utils.categoryForScriptError(message);
+ this.outputMessage(category, this.reportPageError,
+ [category, message]);
+ break;
+ }
+ case "LogMessage":
+ this.handleLogMessage(message);
+ break;
+ case "ConsoleAPI":
+ this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage,
+ [message]);
+ break;
+ case "NetworkEvent":
+ this.outputMessage(CATEGORY_NETWORK, this.logNetEvent, [message]);
+ break;
+ }
+ }, this);
+ },
+
+ /**
+ * Logs a message to the Web Console that originates from the Web Console
+ * server.
+ *
+ * @param object message
+ * The message received from the server.
+ * @return nsIDOMElement|null
+ * The message element to display in the Web Console output.
+ */
+ logConsoleAPIMessage: function (message) {
+ let body = null;
+ let clipboardText = null;
+ let sourceURL = message.filename;
+ let sourceLine = message.lineNumber;
+ let level = message.level;
+ let args = message.arguments;
+ let objectActors = new Set();
+ let node = null;
+
+ // Gather the actor IDs.
+ args.forEach((value) => {
+ if (WebConsoleUtils.isActorGrip(value)) {
+ objectActors.add(value.actor);
+ }
+ });
+
+ switch (level) {
+ case "log":
+ case "info":
+ case "warn":
+ case "error":
+ case "exception":
+ case "assert":
+ case "debug": {
+ let msg = new Messages.ConsoleGeneric(message);
+ node = msg.init(this.output).render().element;
+ break;
+ }
+ case "table": {
+ let msg = new Messages.ConsoleTable(message);
+ node = msg.init(this.output).render().element;
+ break;
+ }
+ case "trace": {
+ let msg = new Messages.ConsoleTrace(message);
+ node = msg.init(this.output).render().element;
+ break;
+ }
+ case "clear": {
+ body = l10n.getStr("consoleCleared");
+ clipboardText = body;
+ break;
+ }
+ case "dir": {
+ body = { arguments: args };
+ let clipboardArray = [];
+ args.forEach((value) => {
+ clipboardArray.push(VariablesView.getString(value));
+ });
+ clipboardText = clipboardArray.join(" ");
+ break;
+ }
+ case "dirxml": {
+ // We just alias console.dirxml() with console.log().
+ message.level = "log";
+ return this.logConsoleAPIMessage(message);
+ }
+ case "group":
+ case "groupCollapsed":
+ clipboardText = body = message.groupName;
+ this.groupDepth++;
+ break;
+
+ case "groupEnd":
+ if (this.groupDepth > 0) {
+ this.groupDepth--;
+ }
+ break;
+
+ case "time": {
+ let timer = message.timer;
+ if (!timer) {
+ return null;
+ }
+ if (timer.error) {
+ console.error(new Error(l10n.getStr(timer.error)));
+ return null;
+ }
+ body = l10n.getFormatStr("timerStarted", [timer.name]);
+ clipboardText = body;
+ break;
+ }
+
+ case "timeEnd": {
+ let timer = message.timer;
+ if (!timer) {
+ return null;
+ }
+ let duration = Math.round(timer.duration * 100) / 100;
+ body = l10n.getFormatStr("timeEnd", [timer.name, duration]);
+ clipboardText = body;
+ break;
+ }
+
+ case "count": {
+ let counter = message.counter;
+ if (!counter) {
+ return null;
+ }
+ if (counter.error) {
+ console.error(l10n.getStr(counter.error));
+ return null;
+ }
+ let msg = new Messages.ConsoleGeneric(message);
+ node = msg.init(this.output).render().element;
+ break;
+ }
+
+ case "timeStamp": {
+ // console.timeStamp() doesn't need to display anything.
+ return null;
+ }
+
+ default:
+ console.error(new Error("Unknown Console API log level: " + level));
+ return null;
+ }
+
+ // Release object actors for arguments coming from console API methods that
+ // we ignore their arguments.
+ switch (level) {
+ case "group":
+ case "groupCollapsed":
+ case "groupEnd":
+ case "time":
+ case "timeEnd":
+ case "count":
+ for (let actor of objectActors) {
+ this._releaseObject(actor);
+ }
+ objectActors.clear();
+ }
+
+ if (level == "groupEnd") {
+ // no need to continue
+ return null;
+ }
+
+ if (!node) {
+ node = this.createMessageNode(CATEGORY_WEBDEV, LEVELS[level], body,
+ sourceURL, sourceLine, clipboardText,
+ level, message.timeStamp);
+ if (message.private) {
+ node.setAttribute("private", true);
+ }
+ }
+
+ if (objectActors.size > 0) {
+ node._objectActors = objectActors;
+
+ if (!node._messageObject) {
+ let repeatNode = node.getElementsByClassName("message-repeats")[0];
+ repeatNode._uid += [...objectActors].join("-");
+ }
+ }
+
+ let workerTypeID = CONSOLE_WORKER_IDS.indexOf(message.workerType);
+ if (workerTypeID != -1) {
+ node.workerType = WORKERTYPES_PREFKEYS[workerTypeID];
+ node.setAttribute("workerType", WORKERTYPES_PREFKEYS[workerTypeID]);
+ }
+
+ return node;
+ },
+
+ /**
+ * Handle ConsoleAPICall objects received from the server. This method outputs
+ * the window.console API call.
+ *
+ * @param object message
+ * The console API message received from the server.
+ */
+ handleConsoleAPICall: function (message) {
+ this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage, [message]);
+ },
+
+ /**
+ * Reports an error in the page source, either JavaScript or CSS.
+ *
+ * @param nsIScriptError scriptError
+ * The error message to report.
+ * @return nsIDOMElement|undefined
+ * The message element to display in the Web Console output.
+ */
+ reportPageError: function (category, scriptError) {
+ // Warnings and legacy strict errors become warnings; other types become
+ // errors.
+ let severity = "error";
+ if (scriptError.warning || scriptError.strict) {
+ severity = "warning";
+ } else if (scriptError.info) {
+ severity = "log";
+ }
+
+ switch (category) {
+ case CATEGORY_CSS:
+ category = "css";
+ break;
+ case CATEGORY_SECURITY:
+ category = "security";
+ break;
+ default:
+ category = "js";
+ break;
+ }
+
+ let objectActors = new Set();
+
+ // Gather the actor IDs.
+ for (let prop of ["errorMessage", "lineText"]) {
+ let grip = scriptError[prop];
+ if (WebConsoleUtils.isActorGrip(grip)) {
+ objectActors.add(grip.actor);
+ }
+ }
+
+ let errorMessage = scriptError.errorMessage;
+ if (errorMessage.type && errorMessage.type == "longString") {
+ errorMessage = errorMessage.initial;
+ }
+
+ let displayOrigin = scriptError.sourceName;
+
+ // TLS errors are related to the connection and not the resource; therefore
+ // it makes sense to only display the protcol, host and port (prePath).
+ // This also means messages are grouped for a single origin.
+ if (scriptError.category && scriptError.category == "SHA-1 Signature") {
+ let sourceURI = Services.io.newURI(scriptError.sourceName, null, null)
+ .QueryInterface(Ci.nsIURL);
+ displayOrigin = sourceURI.prePath;
+ }
+
+ // Create a new message
+ let msg = new Messages.Simple(errorMessage, {
+ location: {
+ url: displayOrigin,
+ line: scriptError.lineNumber,
+ column: scriptError.columnNumber
+ },
+ stack: scriptError.stacktrace,
+ category: category,
+ severity: severity,
+ timestamp: scriptError.timeStamp,
+ private: scriptError.private,
+ filterDuplicates: true
+ });
+
+ let node = msg.init(this.output).render().element;
+
+ // Select the body of the message node that is displayed in the console
+ let msgBody = node.getElementsByClassName("message-body")[0];
+
+ // Add the more info link node to messages that belong to certain categories
+ if (scriptError.exceptionDocURL) {
+ this.addLearnMoreWarningNode(msgBody, scriptError.exceptionDocURL);
+ }
+
+ // Collect telemetry data regarding JavaScript errors
+ this._telemetry.logKeyed("DEVTOOLS_JAVASCRIPT_ERROR_DISPLAYED",
+ scriptError.errorMessageName,
+ true);
+
+ if (objectActors.size > 0) {
+ node._objectActors = objectActors;
+ }
+
+ return node;
+ },
+
+ /**
+ * Handle PageError objects received from the server. This method outputs the
+ * given error.
+ *
+ * @param nsIScriptError pageError
+ * The error received from the server.
+ */
+ handlePageError: function (pageError) {
+ let category = Utils.categoryForScriptError(pageError);
+ this.outputMessage(category, this.reportPageError, [category, pageError]);
+ },
+
+ /**
+ * Handle log messages received from the server. This method outputs the given
+ * message.
+ *
+ * @param object packet
+ * The message packet received from the server.
+ */
+ handleLogMessage: function (packet) {
+ if (packet.message) {
+ this.outputMessage(CATEGORY_JS, this._reportLogMessage, [packet]);
+ }
+ },
+
+ /**
+ * Display log messages received from the server.
+ *
+ * @private
+ * @param object packet
+ * The message packet received from the server.
+ * @return nsIDOMElement
+ * The message element to render for the given log message.
+ */
+ _reportLogMessage: function (packet) {
+ let msg = packet.message;
+ if (msg.type && msg.type == "longString") {
+ msg = msg.initial;
+ }
+ let node = this.createMessageNode(CATEGORY_JS, SEVERITY_LOG, msg, null,
+ null, null, null, packet.timeStamp);
+ if (WebConsoleUtils.isActorGrip(packet.message)) {
+ node._objectActors = new Set([packet.message.actor]);
+ }
+ return node;
+ },
+
+ /**
+ * Log network event.
+ *
+ * @param object networkInfo
+ * The network request information to log.
+ * @return nsIDOMElement|null
+ * The message element to display in the Web Console output.
+ */
+ logNetEvent: function (networkInfo) {
+ let actorId = networkInfo.actor;
+ let request = networkInfo.request;
+ let clipboardText = request.method + " " + request.url;
+ let severity = SEVERITY_LOG;
+ if (networkInfo.isXHR) {
+ clipboardText = request.method + " XHR " + request.url;
+ severity = SEVERITY_INFO;
+ }
+ let mixedRequest =
+ WebConsoleUtils.isMixedHTTPSRequest(request.url, this.contentLocation);
+ if (mixedRequest) {
+ severity = SEVERITY_WARNING;
+ }
+
+ let methodNode = this.document.createElementNS(XHTML_NS, "span");
+ methodNode.className = "method";
+ methodNode.textContent = request.method + " ";
+
+ let messageNode = this.createMessageNode(CATEGORY_NETWORK, severity,
+ methodNode, null, null,
+ clipboardText, null,
+ networkInfo.timeStamp);
+ if (networkInfo.private) {
+ messageNode.setAttribute("private", true);
+ }
+ messageNode._connectionId = actorId;
+ messageNode.url = request.url;
+
+ let body = methodNode.parentNode;
+ body.setAttribute("aria-haspopup", true);
+
+ if (networkInfo.isXHR) {
+ let xhrNode = this.document.createElementNS(XHTML_NS, "span");
+ xhrNode.className = "xhr";
+ xhrNode.textContent = l10n.getStr("webConsoleXhrIndicator");
+ body.appendChild(xhrNode);
+ body.appendChild(this.document.createTextNode(" "));
+ }
+
+ let displayUrl = request.url;
+ let pos = displayUrl.indexOf("?");
+ if (pos > -1) {
+ displayUrl = displayUrl.substr(0, pos);
+ }
+
+ let urlNode = this.document.createElementNS(XHTML_NS, "a");
+ urlNode.className = "url";
+ urlNode.setAttribute("title", request.url);
+ urlNode.href = request.url;
+ urlNode.textContent = displayUrl;
+ urlNode.draggable = false;
+ body.appendChild(urlNode);
+ body.appendChild(this.document.createTextNode(" "));
+
+ if (mixedRequest) {
+ messageNode.classList.add("mixed-content");
+ this.makeMixedContentNode(body);
+ }
+
+ let statusNode = this.document.createElementNS(XHTML_NS, "a");
+ statusNode.className = "status";
+ body.appendChild(statusNode);
+
+ let onClick = () => this.openNetworkPanel(networkInfo.actor);
+
+ this._addMessageLinkCallback(urlNode, onClick);
+ this._addMessageLinkCallback(statusNode, onClick);
+
+ networkInfo.node = messageNode;
+
+ this._updateNetMessage(actorId);
+
+ if (this.window.NetRequest) {
+ this.window.NetRequest.onNetworkEvent({
+ consoleFrame: this,
+ response: networkInfo,
+ node: messageNode,
+ update: false
+ });
+ }
+
+ return messageNode;
+ },
+
+ /**
+ * Create a mixed content warning Node.
+ *
+ * @param linkNode
+ * Parent to the requested urlNode.
+ */
+ makeMixedContentNode: function (linkNode) {
+ let mixedContentWarning =
+ "[" + l10n.getStr("webConsoleMixedContentWarning") + "]";
+
+ // Mixed content warning message links to a Learn More page
+ let mixedContentWarningNode = this.document.createElementNS(XHTML_NS, "a");
+ mixedContentWarningNode.title = MIXED_CONTENT_LEARN_MORE;
+ mixedContentWarningNode.href = MIXED_CONTENT_LEARN_MORE;
+ mixedContentWarningNode.className = "learn-more-link";
+ mixedContentWarningNode.textContent = mixedContentWarning;
+ mixedContentWarningNode.draggable = false;
+
+ linkNode.appendChild(mixedContentWarningNode);
+
+ this._addMessageLinkCallback(mixedContentWarningNode, (event) => {
+ event.stopPropagation();
+ this.owner.openLink(MIXED_CONTENT_LEARN_MORE);
+ });
+ },
+
+ /*
+ * Appends a clickable warning node to the node passed
+ * as a parameter to the function. When a user clicks on the appended
+ * warning node, the browser navigates to the provided url.
+ *
+ * @param node
+ * The node to which we will be adding a clickable warning node.
+ * @param url
+ * The url which points to the page where the user can learn more
+ * about security issues associated with the specific message that's
+ * being logged.
+ */
+ addLearnMoreWarningNode: function (node, url) {
+ let moreInfoLabel = "[" + l10n.getStr("webConsoleMoreInfoLabel") + "]";
+
+ let warningNode = this.document.createElementNS(XHTML_NS, "a");
+ warningNode.title = url.split("?")[0];
+ warningNode.href = url;
+ warningNode.draggable = false;
+ warningNode.textContent = moreInfoLabel;
+ warningNode.className = "learn-more-link";
+
+ this._addMessageLinkCallback(warningNode, (event) => {
+ event.stopPropagation();
+ this.owner.openLink(url);
+ });
+
+ node.appendChild(warningNode);
+ },
+
+ /**
+ * Log file activity.
+ *
+ * @param string fileURI
+ * The file URI that was loaded.
+ * @return nsIDOMElement|undefined
+ * The message element to display in the Web Console output.
+ */
+ logFileActivity: function (fileURI) {
+ let urlNode = this.document.createElementNS(XHTML_NS, "a");
+ urlNode.setAttribute("title", fileURI);
+ urlNode.className = "url";
+ urlNode.textContent = fileURI;
+ urlNode.draggable = false;
+ urlNode.href = fileURI;
+
+ let outputNode = this.createMessageNode(CATEGORY_NETWORK, SEVERITY_LOG,
+ urlNode, null, null, fileURI);
+
+ this._addMessageLinkCallback(urlNode, () => {
+ this.owner.viewSource(fileURI);
+ });
+
+ return outputNode;
+ },
+
+ /**
+ * Handle the file activity messages coming from the remote Web Console.
+ *
+ * @param string fileURI
+ * The file URI that was requested.
+ */
+ handleFileActivity: function (fileURI) {
+ this.outputMessage(CATEGORY_NETWORK, this.logFileActivity, [fileURI]);
+ },
+
+ /**
+ * Handle the reflow activity messages coming from the remote Web Console.
+ *
+ * @param object msg
+ * An object holding information about a reflow batch.
+ */
+ logReflowActivity: function (message) {
+ let {start, end, sourceURL, sourceLine} = message;
+ let duration = Math.round((end - start) * 100) / 100;
+ let node = this.document.createElementNS(XHTML_NS, "span");
+ if (sourceURL) {
+ node.textContent =
+ l10n.getFormatStr("reflow.messageWithLink", [duration]);
+ let a = this.document.createElementNS(XHTML_NS, "a");
+ a.href = "#";
+ a.draggable = "false";
+ let filename = getSourceNames(sourceURL).short;
+ let functionName = message.functionName ||
+ l10n.getStr("stacktrace.anonymousFunction");
+ a.textContent = l10n.getFormatStr("reflow.messageLinkText",
+ [functionName, filename, sourceLine]);
+ this._addMessageLinkCallback(a, () => {
+ this.owner.viewSourceInDebugger(sourceURL, sourceLine);
+ });
+ node.appendChild(a);
+ } else {
+ node.textContent =
+ l10n.getFormatStr("reflow.messageWithNoLink", [duration]);
+ }
+ return this.createMessageNode(CATEGORY_CSS, SEVERITY_LOG, node);
+ },
+
+ handleReflowActivity: function (message) {
+ this.outputMessage(CATEGORY_CSS, this.logReflowActivity, [message]);
+ },
+
+ /**
+ * Inform user that the window.console API has been replaced by a script
+ * in a content page.
+ */
+ logWarningAboutReplacedAPI: function () {
+ let node = this.createMessageNode(CATEGORY_JS, SEVERITY_WARNING,
+ l10n.getStr("ConsoleAPIDisabled"));
+ this.outputMessage(CATEGORY_JS, node);
+ },
+
+ /**
+ * Handle the network events coming from the remote Web Console.
+ *
+ * @param object networkInfo
+ * The network request information.
+ */
+ handleNetworkEvent: function (networkInfo) {
+ this.outputMessage(CATEGORY_NETWORK, this.logNetEvent, [networkInfo]);
+ },
+
+ /**
+ * Handle network event updates coming from the server.
+ *
+ * @param object networkInfo
+ * The network request information.
+ * @param object packet
+ * Update details.
+ */
+ handleNetworkEventUpdate: function (networkInfo, packet) {
+ if (networkInfo.node && this._updateNetMessage(packet.from)) {
+ if (this.window.NetRequest) {
+ this.window.NetRequest.onNetworkEvent({
+ client: this.webConsoleClient,
+ response: packet,
+ node: networkInfo.node,
+ update: true
+ });
+ }
+
+ this.emit("new-messages", new Set([{
+ update: true,
+ node: networkInfo.node,
+ response: packet,
+ }]));
+ }
+
+ // For unit tests we pass the HTTP activity object to the test callback,
+ // once requests complete.
+ if (this.owner.lastFinishedRequestCallback &&
+ networkInfo.updates.indexOf("responseContent") > -1 &&
+ networkInfo.updates.indexOf("eventTimings") > -1) {
+ this.owner.lastFinishedRequestCallback(networkInfo, this);
+ }
+ },
+
+ /**
+ * Update an output message to reflect the latest state of a network request,
+ * given a network event actor ID.
+ *
+ * @private
+ * @param string actorId
+ * The network event actor ID for which you want to update the message.
+ * @return boolean
+ * |true| if the message node was updated, or |false| otherwise.
+ */
+ _updateNetMessage: function (actorId) {
+ let networkInfo = this.webConsoleClient.getNetworkRequest(actorId);
+ if (!networkInfo || !networkInfo.node) {
+ return false;
+ }
+
+ let messageNode = networkInfo.node;
+ let updates = networkInfo.updates;
+ let hasEventTimings = updates.indexOf("eventTimings") > -1;
+ let hasResponseStart = updates.indexOf("responseStart") > -1;
+ let request = networkInfo.request;
+ let methodText = (networkInfo.isXHR) ?
+ request.method + " XHR" : request.method;
+ let response = networkInfo.response;
+ let updated = false;
+
+ if (hasEventTimings || hasResponseStart) {
+ let status = [];
+ if (response.httpVersion && response.status) {
+ status = [response.httpVersion, response.status, response.statusText];
+ }
+ if (hasEventTimings) {
+ status.push(l10n.getFormatStr("NetworkPanel.durationMS",
+ [networkInfo.totalTime]));
+ }
+ let statusText = "[" + status.join(" ") + "]";
+
+ let statusNode = messageNode.getElementsByClassName("status")[0];
+ statusNode.textContent = statusText;
+
+ messageNode.clipboardText = [methodText, request.url, statusText]
+ .join(" ");
+
+ if (hasResponseStart && response.status >= MIN_HTTP_ERROR_CODE &&
+ response.status <= MAX_HTTP_ERROR_CODE) {
+ this.setMessageType(messageNode, CATEGORY_NETWORK, SEVERITY_ERROR);
+ }
+
+ updated = true;
+ }
+
+ if (messageNode._netPanel) {
+ messageNode._netPanel.update();
+ }
+
+ return updated;
+ },
+
+ /**
+ * Opens the network monitor and highlights the specified request.
+ *
+ * @param string requestId
+ * The actor ID of the network request.
+ */
+ openNetworkPanel: function (requestId) {
+ let toolbox = gDevTools.getToolbox(this.owner.target);
+ // The browser console doesn't have a toolbox.
+ if (!toolbox) {
+ return;
+ }
+ return toolbox.selectTool("netmonitor").then(panel => {
+ return panel.panelWin.NetMonitorController.inspectRequest(requestId);
+ });
+ },
+
+ /**
+ * Handler for page location changes.
+ *
+ * @param string uri
+ * New page location.
+ * @param string title
+ * New page title.
+ */
+ onLocationChange: function (uri, title) {
+ this.contentLocation = uri;
+ if (this.owner.onLocationChange) {
+ this.owner.onLocationChange(uri, title);
+ }
+ },
+
+ /**
+ * Handler for the tabNavigated notification.
+ *
+ * @param string event
+ * Event name.
+ * @param object packet
+ * Notification packet received from the server.
+ */
+ handleTabNavigated: function (event, packet) {
+ if (event == "will-navigate") {
+ if (this.persistLog) {
+ if (this.NEW_CONSOLE_OUTPUT_ENABLED) {
+ // Add a _type to hit convertCachedPacket.
+ packet._type = true;
+ this.newConsoleOutput.dispatchMessageAdd(packet);
+ } else {
+ let marker = new Messages.NavigationMarker(packet, Date.now());
+ this.output.addMessage(marker);
+ }
+ } else {
+ this.jsterm.clearOutput();
+ }
+ }
+
+ if (packet.url) {
+ this.onLocationChange(packet.url, packet.title);
+ }
+
+ if (event == "navigate" && !packet.nativeConsoleAPI) {
+ this.logWarningAboutReplacedAPI();
+ }
+ },
+
+ /**
+ * Output a message node. This filters a node appropriately, then sends it to
+ * the output, regrouping and pruning output as necessary.
+ *
+ * Note: this call is async - the given message node may not be displayed when
+ * you call this method.
+ *
+ * @param integer category
+ * The category of the message you want to output. See the CATEGORY_*
+ * constants.
+ * @param function|nsIDOMElement methodOrNode
+ * The method that creates the message element to send to the output or
+ * the actual element. If a method is given it will be bound to the HUD
+ * object and the arguments will be |args|.
+ * @param array [args]
+ * If a method is given to output the message element then the method
+ * will be invoked with the list of arguments given here. The last
+ * object in this array should be the packet received from the
+ * back end.
+ */
+ outputMessage: function (category, methodOrNode, args) {
+ if (!this._outputQueue.length) {
+ // If the queue is empty we consider that now was the last output flush.
+ // This avoid an immediate output flush when the timer executes.
+ this._lastOutputFlush = Date.now();
+ }
+
+ this._outputQueue.push([category, methodOrNode, args]);
+
+ this._initOutputTimer();
+ },
+
+ /**
+ * Try to flush the output message queue. This takes the messages in the
+ * output queue and displays them. Outputting stops at MESSAGES_IN_INTERVAL.
+ * Further output is queued to happen later - see OUTPUT_INTERVAL.
+ *
+ * @private
+ */
+ _flushMessageQueue: function () {
+ this._outputTimerInitialized = false;
+ if (!this._outputTimer) {
+ return;
+ }
+
+ let startTime = Date.now();
+ let timeSinceFlush = startTime - this._lastOutputFlush;
+ let shouldThrottle = this._outputQueue.length > MESSAGES_IN_INTERVAL &&
+ timeSinceFlush < THROTTLE_UPDATES;
+
+ // Determine how many messages we can display now.
+ let toDisplay = Math.min(this._outputQueue.length, MESSAGES_IN_INTERVAL);
+
+ // If there aren't any messages to display (because of throttling or an
+ // empty queue), then take care of some cleanup. Destroy items that were
+ // pruned from the outputQueue before being displayed.
+ if (shouldThrottle || toDisplay < 1) {
+ while (this._itemDestroyQueue.length) {
+ if ((Date.now() - startTime) > MAX_CLEANUP_TIME) {
+ break;
+ }
+ this._destroyItem(this._itemDestroyQueue.pop());
+ }
+
+ this._initOutputTimer();
+ return;
+ }
+
+ // Try to prune the message queue.
+ let shouldPrune = false;
+ if (this._outputQueue.length > toDisplay && this._pruneOutputQueue()) {
+ toDisplay = Math.min(this._outputQueue.length, toDisplay);
+ shouldPrune = true;
+ }
+
+ let batch = this._outputQueue.splice(0, toDisplay);
+ let outputNode = this.outputNode;
+ let lastVisibleNode = null;
+ let scrollNode = this.outputWrapper;
+ let hudIdSupportsString = WebConsoleUtils.supportsString(this.hudId);
+
+ // We won't bother to try to restore scroll position if this is showing
+ // a lot of messages at once (and there are still items in the queue).
+ // It is going to purge whatever you were looking at anyway.
+ let scrolledToBottom =
+ shouldPrune || Utils.isOutputScrolledToBottom(outputNode, scrollNode);
+
+ // Output the current batch of messages.
+ let messages = new Set();
+ for (let i = 0; i < batch.length; i++) {
+ let item = batch[i];
+ let result = this._outputMessageFromQueue(hudIdSupportsString, item);
+ if (result) {
+ messages.add({
+ node: result.isRepeated ? result.isRepeated : result.node,
+ response: result.message,
+ update: !!result.isRepeated,
+ });
+
+ if (result.visible && result.node == this.outputNode.lastChild) {
+ lastVisibleNode = result.node;
+ }
+ }
+ }
+
+ let oldScrollHeight = 0;
+ let removedNodes = 0;
+
+ // Prune messages from the DOM, but only if needed.
+ if (shouldPrune || !this._outputQueue.length) {
+ // Only bother measuring the scrollHeight if not scrolled to bottom,
+ // since the oldScrollHeight will not be used if it is.
+ if (!scrolledToBottom) {
+ oldScrollHeight = scrollNode.scrollHeight;
+ }
+
+ let categories = Object.keys(this._pruneCategoriesQueue);
+ categories.forEach(function _pruneOutput(category) {
+ removedNodes += this.pruneOutputIfNecessary(category);
+ }, this);
+ this._pruneCategoriesQueue = {};
+ }
+
+ let isInputOutput = lastVisibleNode &&
+ (lastVisibleNode.category == CATEGORY_INPUT ||
+ lastVisibleNode.category == CATEGORY_OUTPUT);
+
+ // Scroll to the new node if it is not filtered, and if the output node is
+ // scrolled at the bottom or if the new node is a jsterm input/output
+ // message.
+ if (lastVisibleNode && (scrolledToBottom || isInputOutput)) {
+ Utils.scrollToVisible(lastVisibleNode);
+ } else if (!scrolledToBottom && removedNodes > 0 &&
+ oldScrollHeight != scrollNode.scrollHeight) {
+ // If there were pruned messages and if scroll is not at the bottom, then
+ // we need to adjust the scroll location.
+ scrollNode.scrollTop -= oldScrollHeight - scrollNode.scrollHeight;
+ }
+
+ if (messages.size) {
+ this.emit("new-messages", messages);
+ }
+
+ // If the output queue is empty, then run _flushCallback.
+ if (this._outputQueue.length === 0 && this._flushCallback) {
+ if (this._flushCallback() === false) {
+ this._flushCallback = null;
+ }
+ }
+
+ this._initOutputTimer();
+
+ // Resize the output area in case a vertical scrollbar has been added
+ this.resize();
+
+ this._lastOutputFlush = Date.now();
+ },
+
+ /**
+ * Initialize the output timer.
+ * @private
+ */
+ _initOutputTimer: function () {
+ let panelIsDestroyed = !this._outputTimer;
+ let alreadyScheduled = this._outputTimerInitialized;
+ let nothingToDo = !this._itemDestroyQueue.length &&
+ !this._outputQueue.length;
+
+ // Don't schedule a callback in the following cases:
+ if (panelIsDestroyed || alreadyScheduled || nothingToDo) {
+ return;
+ }
+
+ this._outputTimerInitialized = true;
+ this._outputTimer.initWithCallback(this._flushMessageQueue,
+ OUTPUT_INTERVAL,
+ Ci.nsITimer.TYPE_ONE_SHOT);
+ },
+
+ /**
+ * Output a message from the queue.
+ *
+ * @private
+ * @param nsISupportsString hudIdSupportsString
+ * The HUD ID as an nsISupportsString.
+ * @param array item
+ * An item from the output queue - this item represents a message.
+ * @return object
+ * An object that holds the following properties:
+ * - node: the DOM element of the message.
+ * - isRepeated: the DOM element of the original message, if this is
+ * a repeated message, otherwise null.
+ * - visible: boolean that tells if the message is visible.
+ */
+ _outputMessageFromQueue: function (hudIdSupportsString, item) {
+ let [, methodOrNode, args] = item;
+
+ // The last object in the args array should be message
+ // object or response packet received from the server.
+ let message = (args && args.length) ? args[args.length - 1] : null;
+
+ let node = typeof methodOrNode == "function" ?
+ methodOrNode.apply(this, args || []) :
+ methodOrNode;
+ if (!node) {
+ return null;
+ }
+
+ let isFiltered = this.filterMessageNode(node);
+
+ let isRepeated = this._filterRepeatedMessage(node);
+
+ // If a clear message is processed while the webconsole is opened, the UI
+ // should be cleared.
+ // Do not clear the output if the current frame is owned by a Browser Console.
+ if (message && message.level == "clear" && !this.isBrowserConsole) {
+ // Do not clear the consoleStorage here as it has been cleared already
+ // by the clear method, only clear the UI.
+ this.jsterm.clearOutput(false);
+ }
+
+ let visible = !isRepeated && !isFiltered;
+ if (!isRepeated) {
+ this.outputNode.appendChild(node);
+ this._pruneCategoriesQueue[node.category] = true;
+
+ let nodeID = node.getAttribute("id");
+ Services.obs.notifyObservers(hudIdSupportsString,
+ "web-console-message-created", nodeID);
+ }
+
+ if (node._onOutput) {
+ node._onOutput();
+ delete node._onOutput;
+ }
+
+ return {
+ visible: visible,
+ node: node,
+ isRepeated: isRepeated,
+ message: message
+ };
+ },
+
+ /**
+ * Prune the queue of messages to display. This avoids displaying messages
+ * that will be removed at the end of the queue anyway.
+ * @private
+ */
+ _pruneOutputQueue: function () {
+ let nodes = {};
+
+ // Group the messages per category.
+ this._outputQueue.forEach(function (item, index) {
+ let [category] = item;
+ if (!(category in nodes)) {
+ nodes[category] = [];
+ }
+ nodes[category].push(index);
+ }, this);
+
+ let pruned = 0;
+
+ // Loop through the categories we found and prune if needed.
+ for (let category in nodes) {
+ let limit = Utils.logLimitForCategory(category);
+ let indexes = nodes[category];
+ if (indexes.length > limit) {
+ let n = Math.max(0, indexes.length - limit);
+ pruned += n;
+ for (let i = n - 1; i >= 0; i--) {
+ this._itemDestroyQueue.push(this._outputQueue[indexes[i]]);
+ this._outputQueue.splice(indexes[i], 1);
+ }
+ }
+ }
+
+ return pruned;
+ },
+
+ /**
+ * Destroy an item that was once in the outputQueue but isn't needed
+ * after all.
+ *
+ * @private
+ * @param array item
+ * The item you want to destroy. Does not remove it from the output
+ * queue.
+ */
+ _destroyItem: function (item) {
+ // TODO: handle object releasing in a more elegant way once all console
+ // messages use the new API - bug 778766.
+ let [category, methodOrNode, args] = item;
+ if (typeof methodOrNode != "function" && methodOrNode._objectActors) {
+ for (let actor of methodOrNode._objectActors) {
+ this._releaseObject(actor);
+ }
+ methodOrNode._objectActors.clear();
+ }
+
+ if (methodOrNode == this.output._flushMessageQueue &&
+ args[0]._objectActors) {
+ for (let arg of args) {
+ if (!arg._objectActors) {
+ continue;
+ }
+ for (let actor of arg._objectActors) {
+ this._releaseObject(actor);
+ }
+ arg._objectActors.clear();
+ }
+ }
+
+ if (category == CATEGORY_NETWORK) {
+ let connectionId = null;
+ if (methodOrNode == this.logNetEvent) {
+ connectionId = args[0].actor;
+ } else if (typeof methodOrNode != "function") {
+ connectionId = methodOrNode._connectionId;
+ }
+ if (connectionId &&
+ this.webConsoleClient.hasNetworkRequest(connectionId)) {
+ this.webConsoleClient.removeNetworkRequest(connectionId);
+ this._releaseObject(connectionId);
+ }
+ } else if (category == CATEGORY_WEBDEV &&
+ methodOrNode == this.logConsoleAPIMessage) {
+ args[0].arguments.forEach((value) => {
+ if (WebConsoleUtils.isActorGrip(value)) {
+ this._releaseObject(value.actor);
+ }
+ });
+ } else if (category == CATEGORY_JS &&
+ methodOrNode == this.reportPageError) {
+ let pageError = args[1];
+ for (let prop of ["errorMessage", "lineText"]) {
+ let grip = pageError[prop];
+ if (WebConsoleUtils.isActorGrip(grip)) {
+ this._releaseObject(grip.actor);
+ }
+ }
+ } else if (category == CATEGORY_JS &&
+ methodOrNode == this._reportLogMessage) {
+ if (WebConsoleUtils.isActorGrip(args[0].message)) {
+ this._releaseObject(args[0].message.actor);
+ }
+ }
+ },
+
+ /**
+ * Cleans up a message via a node that may or may not
+ * have actually been rendered in the DOM. Currently, only
+ * cleans up React components.
+ *
+ * @param nsIDOMNode node
+ * The message node you want to clean up.
+ */
+ unmountMessage(node) {
+ // Unmount the Frame component with the message location
+ let locationNode = node.querySelector(".message-location");
+ if (locationNode) {
+ this.ReactDOM.unmountComponentAtNode(locationNode);
+ }
+
+ // Unmount the StackTrace component if present in the message
+ let stacktraceNode = node.querySelector(".stacktrace");
+ if (stacktraceNode) {
+ this.ReactDOM.unmountComponentAtNode(stacktraceNode);
+ }
+ },
+
+ /**
+ * Ensures that the number of message nodes of type category don't exceed that
+ * category's line limit by removing old messages as needed.
+ *
+ * @param integer category
+ * The category of message nodes to prune if needed.
+ * @return number
+ * The number of removed nodes.
+ */
+ pruneOutputIfNecessary: function (category) {
+ let logLimit = Utils.logLimitForCategory(category);
+ let messageNodes = this.outputNode.querySelectorAll(".message[category=" +
+ CATEGORY_CLASS_FRAGMENTS[category] + "]");
+ let n = Math.max(0, messageNodes.length - logLimit);
+ [...messageNodes].slice(0, n).forEach(this.removeOutputMessage, this);
+ return n;
+ },
+
+ /**
+ * Remove a given message from the output.
+ *
+ * @param nsIDOMNode node
+ * The message node you want to remove.
+ */
+ removeOutputMessage: function (node) {
+ if (node._messageObject) {
+ node._messageObject.destroy();
+ }
+
+ if (node._objectActors) {
+ for (let actor of node._objectActors) {
+ this._releaseObject(actor);
+ }
+ node._objectActors.clear();
+ }
+
+ if (node.category == CATEGORY_CSS ||
+ node.category == CATEGORY_SECURITY) {
+ let repeatNode = node.getElementsByClassName("message-repeats")[0];
+ if (repeatNode && repeatNode._uid) {
+ delete this._repeatNodes[repeatNode._uid];
+ }
+ } else if (node._connectionId &&
+ node.category == CATEGORY_NETWORK) {
+ this.webConsoleClient.removeNetworkRequest(node._connectionId);
+ this._releaseObject(node._connectionId);
+ } else if (node.classList.contains("inlined-variables-view")) {
+ let view = node._variablesView;
+ if (view) {
+ view.controller.releaseActors();
+ }
+ node._variablesView = null;
+ }
+
+ this.unmountMessage(node);
+
+ node.remove();
+ },
+
+ /**
+ * Given a category and message body, creates a DOM node to represent an
+ * incoming message. The timestamp is automatically added.
+ *
+ * @param number category
+ * The category of the message: one of the CATEGORY_* constants.
+ * @param number severity
+ * The severity of the message: one of the SEVERITY_* constants;
+ * @param string|nsIDOMNode body
+ * The body of the message, either a simple string or a DOM node.
+ * @param string sourceURL [optional]
+ * The URL of the source file that emitted the error.
+ * @param number sourceLine [optional]
+ * The line number on which the error occurred. If zero or omitted,
+ * there is no line number associated with this message.
+ * @param string clipboardText [optional]
+ * The text that should be copied to the clipboard when this node is
+ * copied. If omitted, defaults to the body text. If `body` is not
+ * a string, then the clipboard text must be supplied.
+ * @param number level [optional]
+ * The level of the console API message.
+ * @param number timestamp [optional]
+ * The timestamp to use for this message node. If omitted, the current
+ * date and time is used.
+ * @return nsIDOMNode
+ * The message node: a DIV ready to be inserted into the Web Console
+ * output node.
+ */
+ createMessageNode: function (category, severity, body, sourceURL, sourceLine,
+ clipboardText, level, timestamp) {
+ if (typeof body != "string" && clipboardText == null && body.innerText) {
+ clipboardText = body.innerText;
+ }
+
+ let indentNode = this.document.createElementNS(XHTML_NS, "span");
+ indentNode.className = "indent";
+
+ // Apply the current group by indenting appropriately.
+ let indent = this.groupDepth * GROUP_INDENT;
+ indentNode.style.width = indent + "px";
+
+ // Make the icon container, which is a vertical box. Its purpose is to
+ // ensure that the icon stays anchored at the top of the message even for
+ // long multi-line messages.
+ let iconContainer = this.document.createElementNS(XHTML_NS, "span");
+ iconContainer.className = "icon";
+
+ // Create the message body, which contains the actual text of the message.
+ let bodyNode = this.document.createElementNS(XHTML_NS, "span");
+ bodyNode.className = "message-body-wrapper message-body devtools-monospace";
+
+ // Store the body text, since it is needed later for the variables view.
+ let storedBody = body;
+ // If a string was supplied for the body, turn it into a DOM node and an
+ // associated clipboard string now.
+ clipboardText = clipboardText ||
+ (body + (sourceURL ? " @ " + sourceURL : "") +
+ (sourceLine ? ":" + sourceLine : ""));
+
+ timestamp = timestamp || Date.now();
+
+ // Create the containing node and append all its elements to it.
+ let node = this.document.createElementNS(XHTML_NS, "div");
+ node.id = "console-msg-" + gSequenceId();
+ node.className = "message";
+ node.clipboardText = clipboardText;
+ node.timestamp = timestamp;
+ this.setMessageType(node, category, severity);
+
+ if (body instanceof Ci.nsIDOMNode) {
+ bodyNode.appendChild(body);
+ } else {
+ let str = undefined;
+ if (level == "dir") {
+ str = VariablesView.getString(body.arguments[0]);
+ } else {
+ str = body;
+ }
+
+ if (str !== undefined) {
+ body = this.document.createTextNode(str);
+ bodyNode.appendChild(body);
+ }
+ }
+
+ // Add the message repeats node only when needed.
+ let repeatNode = null;
+ if (category != CATEGORY_INPUT &&
+ category != CATEGORY_OUTPUT &&
+ category != CATEGORY_NETWORK &&
+ !(category == CATEGORY_CSS && severity == SEVERITY_LOG)) {
+ repeatNode = this.document.createElementNS(XHTML_NS, "span");
+ repeatNode.setAttribute("value", "1");
+ repeatNode.className = "message-repeats";
+ repeatNode.textContent = 1;
+ repeatNode._uid = [bodyNode.textContent, category, severity, level,
+ sourceURL, sourceLine].join(":");
+ }
+
+ // Create the timestamp.
+ let timestampNode = this.document.createElementNS(XHTML_NS, "span");
+ timestampNode.className = "timestamp devtools-monospace";
+
+ let timestampString = l10n.timestampString(timestamp);
+ timestampNode.textContent = timestampString + " ";
+
+ // Create the source location (e.g. www.example.com:6) that sits on the
+ // right side of the message, if applicable.
+ let locationNode;
+ if (sourceURL && IGNORED_SOURCE_URLS.indexOf(sourceURL) == -1) {
+ locationNode = this.createLocationNode({url: sourceURL,
+ line: sourceLine});
+ }
+
+ node.appendChild(timestampNode);
+ node.appendChild(indentNode);
+ node.appendChild(iconContainer);
+
+ // Display the variables view after the message node.
+ if (level == "dir") {
+ let options = {
+ objectActor: storedBody.arguments[0],
+ targetElement: bodyNode,
+ hideFilterInput: true,
+ };
+ this.jsterm.openVariablesView(options).then((view) => {
+ node._variablesView = view;
+ if (node.classList.contains("hidden-message")) {
+ node.classList.remove("hidden-message");
+ }
+ });
+
+ node.classList.add("inlined-variables-view");
+ }
+
+ node.appendChild(bodyNode);
+ if (repeatNode) {
+ node.appendChild(repeatNode);
+ }
+ if (locationNode) {
+ node.appendChild(locationNode);
+ }
+ node.appendChild(this.document.createTextNode("\n"));
+
+ return node;
+ },
+
+ /**
+ * Creates the anchor that displays the textual location of an incoming
+ * message.
+ *
+ * @param {Object} location
+ * An object containing url, line and column number of the message source.
+ * @return {Element}
+ * The new anchor element, ready to be added to the message node.
+ */
+ createLocationNode: function (location) {
+ let locationNode = this.document.createElementNS(XHTML_NS, "div");
+ locationNode.className = "message-location devtools-monospace";
+
+ // Make the location clickable.
+ let onClick = ({ url, line }) => {
+ let category = locationNode.closest(".message").category;
+ let target = null;
+
+ if (/^Scratchpad\/\d+$/.test(url)) {
+ target = "scratchpad";
+ } else if (category === CATEGORY_CSS) {
+ target = "styleeditor";
+ } else if (category === CATEGORY_JS || category === CATEGORY_WEBDEV) {
+ target = "jsdebugger";
+ } else if (/\.js$/.test(url)) {
+ // If it ends in .js, let's attempt to open in debugger
+ // anyway, as this falls back to normal view-source.
+ target = "jsdebugger";
+ } else {
+ // Point everything else to debugger, if source not available,
+ // it will fall back to view-source.
+ target = "jsdebugger";
+ }
+
+ switch (target) {
+ case "scratchpad":
+ this.owner.viewSourceInScratchpad(url, line);
+ return;
+ case "jsdebugger":
+ this.owner.viewSourceInDebugger(url, line);
+ return;
+ case "styleeditor":
+ this.owner.viewSourceInStyleEditor(url, line);
+ return;
+ }
+ // No matching tool found; use old school view-source
+ this.owner.viewSource(url, line);
+ };
+
+ const toolbox = gDevTools.getToolbox(this.owner.target);
+
+ let { url, line, column } = location;
+ let source = url ? url.split(" -> ").pop() : "";
+
+ this.ReactDOM.render(this.FrameView({
+ frame: { source, line, column },
+ showEmptyPathAsHost: true,
+ onClick,
+ sourceMapService: toolbox ? toolbox._sourceMapService : null,
+ }), locationNode);
+
+ return locationNode;
+ },
+
+ /**
+ * Adjusts the category and severity of the given message.
+ *
+ * @param nsIDOMNode messageNode
+ * The message node to alter.
+ * @param number category
+ * The category for the message; one of the CATEGORY_ constants.
+ * @param number severity
+ * The severity for the message; one of the SEVERITY_ constants.
+ * @return void
+ */
+ setMessageType: function (messageNode, category, severity) {
+ messageNode.category = category;
+ messageNode.severity = severity;
+ messageNode.setAttribute("category", CATEGORY_CLASS_FRAGMENTS[category]);
+ messageNode.setAttribute("severity", SEVERITY_CLASS_FRAGMENTS[severity]);
+ messageNode.setAttribute("filter",
+ MESSAGE_PREFERENCE_KEYS[category][severity]);
+ },
+
+ /**
+ * Add the mouse event handlers needed to make a link.
+ *
+ * @private
+ * @param nsIDOMNode node
+ * The node for which you want to add the event handlers.
+ * @param function callback
+ * The function you want to invoke on click.
+ */
+ _addMessageLinkCallback: function (node, callback) {
+ node.addEventListener("mousedown", (event) => {
+ this._mousedown = true;
+ this._startX = event.clientX;
+ this._startY = event.clientY;
+ }, false);
+
+ node.addEventListener("click", (event) => {
+ let mousedown = this._mousedown;
+ this._mousedown = false;
+
+ event.preventDefault();
+
+ // Do not allow middle/right-click or 2+ clicks.
+ if (event.detail != 1 || event.button != 0) {
+ return;
+ }
+
+ // If this event started with a mousedown event and it ends at a different
+ // location, we consider this text selection.
+ if (mousedown &&
+ (this._startX != event.clientX) &&
+ (this._startY != event.clientY)) {
+ this._startX = this._startY = undefined;
+ return;
+ }
+
+ this._startX = this._startY = undefined;
+
+ callback.call(this, event);
+ }, false);
+ },
+
+ /**
+ * Handler for the pref-changed event coming from the toolbox.
+ * Currently this function only handles the timestamps preferences.
+ *
+ * @private
+ * @param object event
+ * This parameter is a string that holds the event name
+ * pref-changed in this case.
+ * @param object data
+ * This is the pref-changed data object.
+ */
+ _onToolboxPrefChanged: function (event, data) {
+ if (data.pref == PREF_MESSAGE_TIMESTAMP) {
+ if (data.newValue) {
+ this.outputNode.classList.remove("hideTimestamps");
+ } else {
+ this.outputNode.classList.add("hideTimestamps");
+ }
+ }
+ },
+
+ /**
+ * Copies the selected items to the system clipboard.
+ *
+ * @param object options
+ * - linkOnly:
+ * An optional flag to copy only URL without other meta-information.
+ * Default is false.
+ * - contextmenu:
+ * An optional flag to copy the last clicked item which brought
+ * up the context menu if nothing is selected. Default is false.
+ */
+ copySelectedItems: function (options) {
+ options = options || { linkOnly: false, contextmenu: false };
+
+ // Gather up the selected items and concatenate their clipboard text.
+ let strings = [];
+
+ let children = this.output.getSelectedMessages();
+ if (!children.length && options.contextmenu) {
+ children = [this._contextMenuHandler.lastClickedMessage];
+ }
+
+ for (let item of children) {
+ // Ensure the selected item hasn't been filtered by type or string.
+ if (!item.classList.contains("filtered-by-type") &&
+ !item.classList.contains("filtered-by-string")) {
+ if (options.linkOnly) {
+ strings.push(item.url);
+ } else {
+ strings.push(item.clipboardText);
+ }
+ }
+ }
+
+ clipboardHelper.copyString(strings.join("\n"));
+ },
+
+ /**
+ * Object properties provider. This function gives you the properties of the
+ * remote object you want.
+ *
+ * @param string actor
+ * The object actor ID from which you want the properties.
+ * @param function callback
+ * Function you want invoked once the properties are received.
+ */
+ objectPropertiesProvider: function (actor, callback) {
+ this.webConsoleClient.inspectObjectProperties(actor,
+ function (response) {
+ if (response.error) {
+ console.error("Failed to retrieve the object properties from the " +
+ "server. Error: " + response.error);
+ return;
+ }
+ callback(response.properties);
+ });
+ },
+
+ /**
+ * Release an actor.
+ *
+ * @private
+ * @param string actor
+ * The actor ID you want to release.
+ */
+ _releaseObject: function (actor) {
+ if (this.proxy) {
+ this.proxy.releaseActor(actor);
+ }
+ },
+
+ /**
+ * Open the selected item's URL in a new tab.
+ */
+ openSelectedItemInTab: function () {
+ let item = this.output.getSelectedMessages(1)[0] ||
+ this._contextMenuHandler.lastClickedMessage;
+
+ if (!item || !item.url) {
+ return;
+ }
+
+ this.owner.openLink(item.url);
+ },
+
+ /**
+ * Destroy the WebConsoleFrame object. Call this method to avoid memory leaks
+ * when the Web Console is closed.
+ *
+ * @return object
+ * A promise that is resolved when the WebConsoleFrame instance is
+ * destroyed.
+ */
+ destroy: function () {
+ if (this._destroyer) {
+ return this._destroyer.promise;
+ }
+
+ this._destroyer = promise.defer();
+
+ let toolbox = gDevTools.getToolbox(this.owner.target);
+ if (toolbox) {
+ toolbox.off("webconsole-selected", this._onPanelSelected);
+ }
+
+ gDevTools.off("pref-changed", this._onToolboxPrefChanged);
+ this.window.removeEventListener("resize", this.resize, true);
+
+ this._repeatNodes = {};
+ this._outputQueue.forEach(this._destroyItem, this);
+ this._outputQueue = [];
+ this._itemDestroyQueue.forEach(this._destroyItem, this);
+ this._itemDestroyQueue = [];
+ this._pruneCategoriesQueue = {};
+ this.webConsoleClient.clearNetworkRequests();
+
+ // Unmount any currently living frame components in DOM, since
+ // currently we only clean up messages in `this.removeOutputMessage`,
+ // via `this.pruneOutputIfNecessary`.
+ let liveMessages = this.outputNode.querySelectorAll(".message");
+ Array.prototype.forEach.call(liveMessages, this.unmountMessage);
+
+ if (this._outputTimerInitialized) {
+ this._outputTimerInitialized = false;
+ this._outputTimer.cancel();
+ }
+ this._outputTimer = null;
+ if (this.jsterm) {
+ this.jsterm.off("sidebar-opened", this.resize);
+ this.jsterm.off("sidebar-closed", this.resize);
+ this.jsterm.destroy();
+ this.jsterm = null;
+ }
+ this.output.destroy();
+ this.output = null;
+
+ this.React = this.ReactDOM = this.FrameView = null;
+
+ if (this._contextMenuHandler) {
+ this._contextMenuHandler.destroy();
+ this._contextMenuHandler = null;
+ }
+
+ this._commandController = null;
+
+ let onDestroy = () => {
+ this._destroyer.resolve(null);
+ };
+
+ if (this.proxy) {
+ this.proxy.disconnect().then(onDestroy);
+ this.proxy = null;
+ } else {
+ onDestroy();
+ }
+
+ return this._destroyer.promise;
+ },
+};
+
+/**
+ * Utils: a collection of globally used functions.
+ */
+var Utils = {
+ /**
+ * Scrolls a node so that it's visible in its containing element.
+ *
+ * @param nsIDOMNode node
+ * The node to make visible.
+ * @returns void
+ */
+ scrollToVisible: function (node) {
+ node.scrollIntoView(false);
+ },
+
+ /**
+ * Check if the given output node is scrolled to the bottom.
+ *
+ * @param nsIDOMNode outputNode
+ * @param nsIDOMNode scrollNode
+ * @return boolean
+ * True if the output node is scrolled to the bottom, or false
+ * otherwise.
+ */
+ isOutputScrolledToBottom: function (outputNode, scrollNode) {
+ let lastNodeHeight = outputNode.lastChild ?
+ outputNode.lastChild.clientHeight : 0;
+ return scrollNode.scrollTop + scrollNode.clientHeight >=
+ scrollNode.scrollHeight - lastNodeHeight / 2;
+ },
+
+ /**
+ * Determine the category of a given nsIScriptError.
+ *
+ * @param nsIScriptError scriptError
+ * The script error you want to determine the category for.
+ * @return CATEGORY_JS|CATEGORY_CSS|CATEGORY_SECURITY
+ * Depending on the script error CATEGORY_JS, CATEGORY_CSS, or
+ * CATEGORY_SECURITY can be returned.
+ */
+ categoryForScriptError: function (scriptError) {
+ let category = scriptError.category;
+
+ if (/^(?:CSS|Layout)\b/.test(category)) {
+ return CATEGORY_CSS;
+ }
+
+ switch (category) {
+ case "Mixed Content Blocker":
+ case "Mixed Content Message":
+ case "CSP":
+ case "Invalid HSTS Headers":
+ case "Invalid HPKP Headers":
+ case "SHA-1 Signature":
+ case "Insecure Password Field":
+ case "SSL":
+ case "CORS":
+ case "Iframe Sandbox":
+ case "Tracking Protection":
+ case "Sub-resource Integrity":
+ return CATEGORY_SECURITY;
+
+ default:
+ return CATEGORY_JS;
+ }
+ },
+
+ /**
+ * Retrieve the limit of messages for a specific category.
+ *
+ * @param number category
+ * The category of messages you want to retrieve the limit for. See the
+ * CATEGORY_* constants.
+ * @return number
+ * The number of messages allowed for the specific category.
+ */
+ logLimitForCategory: function (category) {
+ let logLimit = DEFAULT_LOG_LIMIT;
+
+ try {
+ let prefName = CATEGORY_CLASS_FRAGMENTS[category];
+ logLimit = Services.prefs.getIntPref("devtools.hud.loglimit." + prefName);
+ logLimit = Math.max(logLimit, 1);
+ } catch (e) {
+ // Ignore any exceptions
+ }
+
+ return logLimit;
+ },
+};
+
+// CommandController
+
+/**
+ * A controller (an instance of nsIController) that makes editing actions
+ * behave appropriately in the context of the Web Console.
+ */
+function CommandController(webConsole) {
+ this.owner = webConsole;
+}
+
+CommandController.prototype = {
+ /**
+ * Selects all the text in the HUD output.
+ */
+ selectAll: function () {
+ this.owner.output.selectAllMessages();
+ },
+
+ /**
+ * Open the URL of the selected message in a new tab.
+ */
+ openURL: function () {
+ this.owner.openSelectedItemInTab();
+ },
+
+ copyURL: function () {
+ this.owner.copySelectedItems({ linkOnly: true, contextmenu: true });
+ },
+
+ /**
+ * Copies the last clicked message.
+ */
+ copyLastClicked: function () {
+ this.owner.copySelectedItems({ linkOnly: false, contextmenu: true });
+ },
+
+ supportsCommand: function (command) {
+ if (!this.owner || !this.owner.output) {
+ return false;
+ }
+ return this.isCommandEnabled(command);
+ },
+
+ isCommandEnabled: function (command) {
+ switch (command) {
+ case "consoleCmd_openURL":
+ case "consoleCmd_copyURL": {
+ // Only enable URL-related actions if node is Net Activity.
+ let selectedItem = this.owner.output.getSelectedMessages(1)[0] ||
+ this.owner._contextMenuHandler.lastClickedMessage;
+ return selectedItem && "url" in selectedItem;
+ }
+ case "cmd_copy": {
+ // Only copy if we right-clicked the console and there's no selected
+ // text. With text selected, we want to fall back onto the default
+ // copy behavior.
+ return this.owner._contextMenuHandler.lastClickedMessage &&
+ !this.owner.output.getSelectedMessages(1)[0];
+ }
+ case "cmd_selectAll":
+ return true;
+ }
+ return false;
+ },
+
+ doCommand: function (command) {
+ switch (command) {
+ case "consoleCmd_openURL":
+ this.openURL();
+ break;
+ case "consoleCmd_copyURL":
+ this.copyURL();
+ break;
+ case "cmd_copy":
+ this.copyLastClicked();
+ break;
+ case "cmd_selectAll":
+ this.selectAll();
+ break;
+ }
+ }
+};
+
+// Web Console connection proxy
+
+/**
+ * The WebConsoleConnectionProxy handles the connection between the Web Console
+ * and the application we connect to through the remote debug protocol.
+ *
+ * @constructor
+ * @param object webConsoleFrame
+ * The WebConsoleFrame object that owns this connection proxy.
+ * @param RemoteTarget target
+ * The target that the console will connect to.
+ */
+function WebConsoleConnectionProxy(webConsoleFrame, target) {
+ this.webConsoleFrame = webConsoleFrame;
+ this.target = target;
+
+ this._onPageError = this._onPageError.bind(this);
+ this._onLogMessage = this._onLogMessage.bind(this);
+ this._onConsoleAPICall = this._onConsoleAPICall.bind(this);
+ this._onNetworkEvent = this._onNetworkEvent.bind(this);
+ this._onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this);
+ this._onFileActivity = this._onFileActivity.bind(this);
+ this._onReflowActivity = this._onReflowActivity.bind(this);
+ this._onServerLogCall = this._onServerLogCall.bind(this);
+ this._onTabNavigated = this._onTabNavigated.bind(this);
+ this._onAttachConsole = this._onAttachConsole.bind(this);
+ this._onCachedMessages = this._onCachedMessages.bind(this);
+ this._connectionTimeout = this._connectionTimeout.bind(this);
+ this._onLastPrivateContextExited =
+ this._onLastPrivateContextExited.bind(this);
+}
+
+WebConsoleConnectionProxy.prototype = {
+ /**
+ * The owning Web Console Frame instance.
+ *
+ * @see WebConsoleFrame
+ * @type object
+ */
+ webConsoleFrame: null,
+
+ /**
+ * The target that the console connects to.
+ * @type RemoteTarget
+ */
+ target: null,
+
+ /**
+ * The DebuggerClient object.
+ *
+ * @see DebuggerClient
+ * @type object
+ */
+ client: null,
+
+ /**
+ * The WebConsoleClient object.
+ *
+ * @see WebConsoleClient
+ * @type object
+ */
+ webConsoleClient: null,
+
+ /**
+ * Tells if the connection is established.
+ * @type boolean
+ */
+ connected: false,
+
+ /**
+ * Timer used for the connection.
+ * @private
+ * @type object
+ */
+ _connectTimer: null,
+
+ _connectDefer: null,
+ _disconnecter: null,
+
+ /**
+ * The WebConsoleActor ID.
+ *
+ * @private
+ * @type string
+ */
+ _consoleActor: null,
+
+ /**
+ * Tells if the window.console object of the remote web page is the native
+ * object or not.
+ * @private
+ * @type boolean
+ */
+ _hasNativeConsoleAPI: false,
+
+ /**
+ * Initialize a debugger client and connect it to the debugger server.
+ *
+ * @return object
+ * A promise object that is resolved/rejected based on the success of
+ * the connection initialization.
+ */
+ connect: function () {
+ if (this._connectDefer) {
+ return this._connectDefer.promise;
+ }
+
+ this._connectDefer = promise.defer();
+
+ let timeout = Services.prefs.getIntPref(PREF_CONNECTION_TIMEOUT);
+ this._connectTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._connectTimer.initWithCallback(this._connectionTimeout,
+ timeout, Ci.nsITimer.TYPE_ONE_SHOT);
+
+ let connPromise = this._connectDefer.promise;
+ connPromise.then(() => {
+ this._connectTimer.cancel();
+ this._connectTimer = null;
+ }, () => {
+ this._connectTimer = null;
+ });
+
+ let client = this.client = this.target.client;
+
+ if (this.target.isWorkerTarget) {
+ // XXXworkers: Not Console API yet inside of workers (Bug 1209353).
+ } else {
+ client.addListener("logMessage", this._onLogMessage);
+ client.addListener("pageError", this._onPageError);
+ client.addListener("consoleAPICall", this._onConsoleAPICall);
+ client.addListener("fileActivity", this._onFileActivity);
+ client.addListener("reflowActivity", this._onReflowActivity);
+ client.addListener("serverLogCall", this._onServerLogCall);
+ client.addListener("lastPrivateContextExited",
+ this._onLastPrivateContextExited);
+ }
+ this.target.on("will-navigate", this._onTabNavigated);
+ this.target.on("navigate", this._onTabNavigated);
+
+ this._consoleActor = this.target.form.consoleActor;
+ if (this.target.isTabActor) {
+ let tab = this.target.form;
+ this.webConsoleFrame.onLocationChange(tab.url, tab.title);
+ }
+ this._attachConsole();
+
+ return connPromise;
+ },
+
+ /**
+ * Connection timeout handler.
+ * @private
+ */
+ _connectionTimeout: function () {
+ let error = {
+ error: "timeout",
+ message: l10n.getStr("connectionTimeout"),
+ };
+
+ this._connectDefer.reject(error);
+ },
+
+ /**
+ * Attach to the Web Console actor.
+ * @private
+ */
+ _attachConsole: function () {
+ let listeners = ["PageError", "ConsoleAPI", "NetworkActivity",
+ "FileActivity"];
+ this.client.attachConsole(this._consoleActor, listeners,
+ this._onAttachConsole);
+ },
+
+ /**
+ * The "attachConsole" response handler.
+ *
+ * @private
+ * @param object response
+ * The JSON response object received from the server.
+ * @param object webConsoleClient
+ * The WebConsoleClient instance for the attached console, for the
+ * specific tab we work with.
+ */
+ _onAttachConsole: function (response, webConsoleClient) {
+ if (response.error) {
+ console.error("attachConsole failed: " + response.error + " " +
+ response.message);
+ this._connectDefer.reject(response);
+ return;
+ }
+
+ this.webConsoleClient = webConsoleClient;
+ this._hasNativeConsoleAPI = response.nativeConsoleAPI;
+
+ // There is no way to view response bodies from the Browser Console, so do
+ // not waste the memory.
+ let saveBodies = !this.webConsoleFrame.isBrowserConsole;
+ this.webConsoleFrame.setSaveRequestAndResponseBodies(saveBodies);
+
+ this.webConsoleClient.on("networkEvent", this._onNetworkEvent);
+ this.webConsoleClient.on("networkEventUpdate", this._onNetworkEventUpdate);
+
+ let msgs = ["PageError", "ConsoleAPI"];
+ this.webConsoleClient.getCachedMessages(msgs, this._onCachedMessages);
+
+ this.webConsoleFrame._onUpdateListeners();
+ },
+
+ /**
+ * Dispatch a message add on the new frontend and emit an event for tests.
+ */
+ dispatchMessageAdd: function(packet) {
+ this.webConsoleFrame.newConsoleOutput.dispatchMessageAdd(packet);
+ },
+
+ /**
+ * Batched dispatch of messages.
+ */
+ dispatchMessagesAdd: function(packets) {
+ this.webConsoleFrame.newConsoleOutput.dispatchMessagesAdd(packets);
+ },
+
+ /**
+ * The "cachedMessages" response handler.
+ *
+ * @private
+ * @param object response
+ * The JSON response object received from the server.
+ */
+ _onCachedMessages: function (response) {
+ if (response.error) {
+ console.error("Web Console getCachedMessages error: " + response.error +
+ " " + response.message);
+ this._connectDefer.reject(response);
+ return;
+ }
+
+ if (!this._connectTimer) {
+ // This happens if the promise is rejected (eg. a timeout), but the
+ // connection attempt is successful, nonetheless.
+ console.error("Web Console getCachedMessages error: invalid state.");
+ }
+
+ let messages =
+ response.messages.concat(...this.webConsoleClient.getNetworkEvents());
+ messages.sort((a, b) => a.timeStamp - b.timeStamp);
+
+ if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
+ // Filter out CSS page errors.
+ messages = messages.filter(message => !(message._type == "PageError"
+ && Utils.categoryForScriptError(message) === CATEGORY_CSS));
+ this.dispatchMessagesAdd(messages);
+ } else {
+ this.webConsoleFrame.displayCachedMessages(messages);
+ if (!this._hasNativeConsoleAPI) {
+ this.webConsoleFrame.logWarningAboutReplacedAPI();
+ }
+ }
+
+ this.connected = true;
+ this._connectDefer.resolve(this);
+ },
+
+ /**
+ * The "pageError" message type handler. We redirect any page errors to the UI
+ * for displaying.
+ *
+ * @private
+ * @param string type
+ * Message type.
+ * @param object packet
+ * The message received from the server.
+ */
+ _onPageError: function (type, packet) {
+ if (this.webConsoleFrame && packet.from == this._consoleActor) {
+ if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
+ let category = Utils.categoryForScriptError(packet.pageError);
+ if (category !== CATEGORY_CSS) {
+ this.dispatchMessageAdd(packet);
+ }
+ return;
+ }
+ this.webConsoleFrame.handlePageError(packet.pageError);
+ }
+ },
+
+ /**
+ * The "logMessage" message type handler. We redirect any message to the UI
+ * for displaying.
+ *
+ * @private
+ * @param string type
+ * Message type.
+ * @param object packet
+ * The message received from the server.
+ */
+ _onLogMessage: function (type, packet) {
+ if (this.webConsoleFrame && packet.from == this._consoleActor) {
+ this.webConsoleFrame.handleLogMessage(packet);
+ }
+ },
+
+ /**
+ * The "consoleAPICall" message type handler. We redirect any message to
+ * the UI for displaying.
+ *
+ * @private
+ * @param string type
+ * Message type.
+ * @param object packet
+ * The message received from the server.
+ */
+ _onConsoleAPICall: function (type, packet) {
+ if (this.webConsoleFrame && packet.from == this._consoleActor) {
+ if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
+ this.dispatchMessageAdd(packet);
+ } else {
+ this.webConsoleFrame.handleConsoleAPICall(packet.message);
+ }
+ }
+ },
+
+ /**
+ * The "networkEvent" message type handler. We redirect any message to
+ * the UI for displaying.
+ *
+ * @private
+ * @param string type
+ * Message type.
+ * @param object networkInfo
+ * The network request information.
+ */
+ _onNetworkEvent: function (type, networkInfo) {
+ if (this.webConsoleFrame) {
+ if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
+ this.dispatchMessageAdd(networkInfo);
+ } else {
+ this.webConsoleFrame.handleNetworkEvent(networkInfo);
+ }
+ }
+ },
+
+ /**
+ * The "networkEventUpdate" message type handler. We redirect any message to
+ * the UI for displaying.
+ *
+ * @private
+ * @param string type
+ * Message type.
+ * @param object packet
+ * The message received from the server.
+ * @param object networkInfo
+ * The network request information.
+ */
+ _onNetworkEventUpdate: function (type, { packet, networkInfo }) {
+ if (this.webConsoleFrame) {
+ this.webConsoleFrame.handleNetworkEventUpdate(networkInfo, packet);
+ }
+ },
+
+ /**
+ * The "fileActivity" message type handler. We redirect any message to
+ * the UI for displaying.
+ *
+ * @private
+ * @param string type
+ * Message type.
+ * @param object packet
+ * The message received from the server.
+ */
+ _onFileActivity: function (type, packet) {
+ if (this.webConsoleFrame && packet.from == this._consoleActor) {
+ this.webConsoleFrame.handleFileActivity(packet.uri);
+ }
+ },
+
+ _onReflowActivity: function (type, packet) {
+ if (this.webConsoleFrame && packet.from == this._consoleActor) {
+ this.webConsoleFrame.handleReflowActivity(packet);
+ }
+ },
+
+ /**
+ * The "serverLogCall" message type handler. We redirect any message to
+ * the UI for displaying.
+ *
+ * @private
+ * @param string type
+ * Message type.
+ * @param object packet
+ * The message received from the server.
+ */
+ _onServerLogCall: function (type, packet) {
+ if (this.webConsoleFrame && packet.from == this._consoleActor) {
+ this.webConsoleFrame.handleConsoleAPICall(packet.message);
+ }
+ },
+
+ /**
+ * The "lastPrivateContextExited" message type handler. When this message is
+ * received the Web Console UI is cleared.
+ *
+ * @private
+ * @param string type
+ * Message type.
+ * @param object packet
+ * The message received from the server.
+ */
+ _onLastPrivateContextExited: function (type, packet) {
+ if (this.webConsoleFrame && packet.from == this._consoleActor) {
+ this.webConsoleFrame.jsterm.clearPrivateMessages();
+ }
+ },
+
+ /**
+ * The "will-navigate" and "navigate" event handlers. We redirect any message
+ * to the UI for displaying.
+ *
+ * @private
+ * @param string event
+ * Event type.
+ * @param object packet
+ * The message received from the server.
+ */
+ _onTabNavigated: function (event, packet) {
+ if (!this.webConsoleFrame) {
+ return;
+ }
+
+ this.webConsoleFrame.handleTabNavigated(event, packet);
+ },
+
+ /**
+ * Release an object actor.
+ *
+ * @param string actor
+ * The actor ID to send the request to.
+ */
+ releaseActor: function (actor) {
+ if (this.client) {
+ this.client.release(actor);
+ }
+ },
+
+ /**
+ * Disconnect the Web Console from the remote server.
+ *
+ * @return object
+ * A promise object that is resolved when disconnect completes.
+ */
+ disconnect: function () {
+ if (this._disconnecter) {
+ return this._disconnecter.promise;
+ }
+
+ this._disconnecter = promise.defer();
+
+ if (!this.client) {
+ this._disconnecter.resolve(null);
+ return this._disconnecter.promise;
+ }
+
+ this.client.removeListener("logMessage", this._onLogMessage);
+ this.client.removeListener("pageError", this._onPageError);
+ this.client.removeListener("consoleAPICall", this._onConsoleAPICall);
+ this.client.removeListener("fileActivity", this._onFileActivity);
+ this.client.removeListener("reflowActivity", this._onReflowActivity);
+ this.client.removeListener("serverLogCall", this._onServerLogCall);
+ this.client.removeListener("lastPrivateContextExited",
+ this._onLastPrivateContextExited);
+ this.webConsoleClient.off("networkEvent", this._onNetworkEvent);
+ this.webConsoleClient.off("networkEventUpdate", this._onNetworkEventUpdate);
+ this.target.off("will-navigate", this._onTabNavigated);
+ this.target.off("navigate", this._onTabNavigated);
+
+ this.client = null;
+ this.webConsoleClient = null;
+ this.target = null;
+ this.connected = false;
+ this.webConsoleFrame = null;
+ this._disconnecter.resolve(null);
+
+ return this._disconnecter.promise;
+ },
+};
+
+// Context Menu
+
+/*
+ * ConsoleContextMenu this used to handle the visibility of context menu items.
+ *
+ * @constructor
+ * @param object owner
+ * The WebConsoleFrame instance that owns this object.
+ */
+function ConsoleContextMenu(owner) {
+ this.owner = owner;
+ this.popup = this.owner.document.getElementById("output-contextmenu");
+ this.build = this.build.bind(this);
+ this.popup.addEventListener("popupshowing", this.build);
+}
+
+ConsoleContextMenu.prototype = {
+ lastClickedMessage: null,
+
+ /*
+ * Handle to show/hide context menu item.
+ */
+ build: function (event) {
+ let metadata = this.getSelectionMetadata(event.rangeParent);
+ for (let element of this.popup.children) {
+ element.hidden = this.shouldHideMenuItem(element, metadata);
+ }
+ },
+
+ /*
+ * Get selection information from the view.
+ *
+ * @param nsIDOMElement clickElement
+ * The DOM element the user clicked on.
+ * @return object
+ * Selection metadata.
+ */
+ getSelectionMetadata: function (clickElement) {
+ let metadata = {
+ selectionType: "",
+ selection: new Set(),
+ };
+ let selectedItems = this.owner.output.getSelectedMessages();
+ if (!selectedItems.length) {
+ let clickedItem = this.owner.output.getMessageForElement(clickElement);
+ if (clickedItem) {
+ this.lastClickedMessage = clickedItem;
+ selectedItems = [clickedItem];
+ }
+ }
+
+ metadata.selectionType = selectedItems.length > 1 ? "multiple" : "single";
+
+ let selection = metadata.selection;
+ for (let item of selectedItems) {
+ switch (item.category) {
+ case CATEGORY_NETWORK:
+ selection.add("network");
+ break;
+ case CATEGORY_CSS:
+ selection.add("css");
+ break;
+ case CATEGORY_JS:
+ selection.add("js");
+ break;
+ case CATEGORY_WEBDEV:
+ selection.add("webdev");
+ break;
+ case CATEGORY_SERVER:
+ selection.add("server");
+ break;
+ }
+ }
+
+ return metadata;
+ },
+
+ /*
+ * Determine if an item should be hidden.
+ *
+ * @param nsIDOMElement menuItem
+ * @param object metadata
+ * @return boolean
+ * Whether the given item should be hidden or not.
+ */
+ shouldHideMenuItem: function (menuItem, metadata) {
+ let selectionType = menuItem.getAttribute("selectiontype");
+ if (selectionType && !metadata.selectionType == selectionType) {
+ return true;
+ }
+
+ let selection = menuItem.getAttribute("selection");
+ if (!selection) {
+ return false;
+ }
+
+ let shouldHide = true;
+ let itemData = selection.split("|");
+ for (let type of metadata.selection) {
+ // check whether this menu item should show or not.
+ if (itemData.indexOf(type) !== -1) {
+ shouldHide = false;
+ break;
+ }
+ }
+
+ return shouldHide;
+ },
+
+ /**
+ * Destroy the ConsoleContextMenu object instance.
+ */
+ destroy: function () {
+ this.popup.removeEventListener("popupshowing", this.build);
+ this.popup = null;
+ this.owner = null;
+ this.lastClickedMessage = null;
+ },
+};
diff --git a/devtools/client/webconsole/webconsole.xul b/devtools/client/webconsole/webconsole.xul
new file mode 100644
index 000000000..cd3e44d82
--- /dev/null
+++ b/devtools/client/webconsole/webconsole.xul
@@ -0,0 +1,214 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of 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 window [
+<!ENTITY % webConsoleDTD SYSTEM "chrome://devtools/locale/webConsole.dtd">
+%webConsoleDTD;
+]>
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/widgets.css"
+ type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/webconsole.css"
+ type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/components-frame.css"
+ type="text/css"?>
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="devtools-webconsole"
+ macanimationtype="document"
+ fullscreenbutton="true"
+ title="&window.title;"
+ browserConsoleTitle="&browserConsole.title;"
+ windowtype="devtools:webconsole"
+ width="900" height="350"
+ persist="screenX screenY width height sizemode">
+
+ <script type="application/javascript;version=1.8"
+ src="chrome://devtools/content/shared/theme-switching.js"/>
+ <script type="application/javascript;version=1.8"
+ src="resource://devtools/client/webconsole/new-console-output/main.js"/>
+ <script type="text/javascript" src="chrome://global/content/globalOverlay.js"/>
+ <script type="text/javascript" src="resource://devtools/client/webconsole/net/main.js"/>
+ <script type="text/javascript"><![CDATA[
+function goUpdateConsoleCommands() {
+ goUpdateCommand("consoleCmd_openURL");
+ goUpdateCommand("consoleCmd_copyURL");
+}
+ // ]]></script>
+
+ <commandset id="editMenuCommands"/>
+
+ <commandset id="consoleCommands"
+ commandupdater="true"
+ events="focus,select"
+ oncommandupdate="goUpdateConsoleCommands();">
+ <command id="consoleCmd_openURL"
+ oncommand="goDoCommand('consoleCmd_openURL');"/>
+ <command id="consoleCmd_copyURL"
+ oncommand="goDoCommand('consoleCmd_copyURL');"/>
+ </commandset>
+ <keyset id="consoleKeys">
+ </keyset>
+ <keyset id="editMenuKeys"/>
+
+ <popupset id="mainPopupSet">
+ <menupopup id="output-contextmenu" onpopupshowing="goUpdateGlobalEditMenuItems()">
+ <menuitem id="menu_openURL" label="&openURL.label;"
+ accesskey="&openURL.accesskey;" command="consoleCmd_openURL"
+ selection="network" selectionType="single"/>
+ <menuitem id="menu_copyURL" label="&copyURLCmd.label;"
+ accesskey="&copyURLCmd.accesskey;" command="consoleCmd_copyURL"
+ selection="network" selectionType="single"/>
+ <menuitem id="menu_openInVarView" label="&openInVarViewCmd.label;"
+ accesskey="&openInVarViewCmd.accesskey;" disabled="true"/>
+ <menuitem id="menu_storeAsGlobal" label="&storeAsGlobalVar.label;"
+ accesskey="&storeAsGlobalVar.accesskey;"/>
+ <menuitem id="cMenu_copy"/>
+ <menuitem id="cMenu_selectAll"/>
+ </menupopup>
+ </popupset>
+
+ <tooltip id="aHTMLTooltip" page="true"/>
+
+ <box class="hud-outer-wrapper devtools-responsive-container theme-body" flex="1">
+ <vbox class="hud-console-wrapper devtools-main-content" flex="1">
+ <toolbar class="hud-console-filter-toolbar devtools-toolbar" mode="full">
+ <toolbarbutton class="webconsole-clear-console-button devtools-toolbarbutton devtools-clear-icon"
+ tooltiptext="&btnClear.tooltip;"
+ accesskey="&btnClear.accesskey;"
+ tabindex="3"/>
+ <hbox class="devtools-toolbarbutton-group">
+ <toolbarbutton label="&btnPageNet.label;" type="menu-button"
+ category="net" class="devtools-toolbarbutton webconsole-filter-button"
+ tooltiptext="&btnPageNet.tooltip;"
+ accesskeyMacOSX="&btnPageNet.accesskeyMacOSX;"
+ accesskey="&btnPageNet.accesskey;"
+ tabindex="4">
+ <menupopup id="net-contextmenu">
+ <menuitem label="&btnConsoleErrors;" type="checkbox" autocheck="false"
+ prefKey="network"/>
+ <menuitem label="&btnConsoleWarnings;" type="checkbox" autocheck="false"
+ prefKey="netwarn"/>
+ <menuitem label="&btnConsoleXhr;" type="checkbox" autocheck="false"
+ prefKey="netxhr"/>
+ <menuitem label="&btnConsoleLog;" type="checkbox" autocheck="false"
+ prefKey="networkinfo"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton label="&btnPageCSS.label;" type="menu-button"
+ category="css" class="devtools-toolbarbutton webconsole-filter-button"
+ tooltiptext="&btnPageCSS.tooltip2;"
+ accesskey="&btnPageCSS.accesskey;"
+ tabindex="5">
+ <menupopup id="css-contextmenu">
+ <menuitem label="&btnConsoleErrors;" type="checkbox" autocheck="false"
+ prefKey="csserror"/>
+ <menuitem label="&btnConsoleWarnings;" type="checkbox"
+ autocheck="false" prefKey="cssparser"/>
+ <menuitem label="&btnConsoleReflows;" type="checkbox"
+ autocheck="false" prefKey="csslog"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton label="&btnPageJS.label;" type="menu-button"
+ category="js" class="devtools-toolbarbutton webconsole-filter-button"
+ tooltiptext="&btnPageJS.tooltip;"
+ accesskey="&btnPageJS.accesskey;"
+ tabindex="6">
+ <menupopup id="js-contextmenu">
+ <menuitem label="&btnConsoleErrors;" type="checkbox"
+ autocheck="false" prefKey="exception"/>
+ <menuitem label="&btnConsoleWarnings;" type="checkbox"
+ autocheck="false" prefKey="jswarn"/>
+ <menuitem label="&btnConsoleLog;" type="checkbox"
+ autocheck="false" prefKey="jslog"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton label="&btnPageSecurity.label;" type="menu-button"
+ category="security" class="devtools-toolbarbutton webconsole-filter-button"
+ tooltiptext="&btnPageSecurity.tooltip;"
+ accesskey="&btnPageSecurity.accesskey;"
+ tabindex="7">
+ <menupopup id="security-contextmenu">
+ <menuitem label="&btnConsoleErrors;" type="checkbox"
+ autocheck="false" prefKey="secerror"/>
+ <menuitem label="&btnConsoleWarnings;" type="checkbox"
+ autocheck="false" prefKey="secwarn"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton label="&btnPageLogging.label;" type="menu-button"
+ category="logging" class="devtools-toolbarbutton webconsole-filter-button"
+ tooltiptext="&btnPageLogging.tooltip;"
+ accesskey="&btnPageLogging.accesskey3;"
+ tabindex="8">
+ <menupopup id="logging-contextmenu">
+ <menuitem label="&btnConsoleErrors;" type="checkbox"
+ autocheck="false" prefKey="error"/>
+ <menuitem label="&btnConsoleWarnings;" type="checkbox"
+ autocheck="false" prefKey="warn"/>
+ <menuitem label="&btnConsoleInfo;" type="checkbox" autocheck="false"
+ prefKey="info"/>
+ <menuitem label="&btnConsoleLog;" type="checkbox" autocheck="false"
+ prefKey="log"/>
+ <menuseparator />
+ <menuitem label="&btnConsoleSharedWorkers;" type="checkbox"
+ autocheck="false" prefKey="sharedworkers"/>
+ <menuitem label="&btnConsoleServiceWorkers;" type="checkbox"
+ autocheck="false" prefKey="serviceworkers"/>
+ <menuitem label="&btnConsoleWindowlessWorkers;" type="checkbox"
+ autocheck="false" prefKey="windowlessworkers"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton label="&btnServerLogging.label;" type="menu-button"
+ category="server" class="devtools-toolbarbutton webconsole-filter-button"
+ tooltiptext="&btnServerLogging.tooltip;"
+ accesskey="&btnServerLogging.accesskey;"
+ tabindex="9">
+ <menupopup id="server-logging-contextmenu">
+ <menuitem label="&btnServerErrors;" type="checkbox"
+ autocheck="false" prefKey="servererror"/>
+ <menuitem label="&btnServerWarnings;" type="checkbox"
+ autocheck="false" prefKey="serverwarn"/>
+ <menuitem label="&btnServerInfo;" type="checkbox" autocheck="false"
+ prefKey="serverinfo"/>
+ <menuitem label="&btnServerLog;" type="checkbox" autocheck="false"
+ prefKey="serverlog"/>
+ </menupopup>
+ </toolbarbutton>
+ </hbox>
+
+ <spacer flex="1"/>
+
+ <textbox class="compact hud-filter-box devtools-filterinput" type="search"
+ placeholder="&filterOutput.placeholder;" tabindex="2"/>
+ </toolbar>
+
+ <hbox id="output-wrapper" flex="1" context="output-contextmenu" tooltip="aHTMLTooltip">
+ <!-- Wrapper element to make scrolling in output-container much faster.
+ See Bug 1237368 -->
+ <div xmlns="http://www.w3.org/1999/xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml" id="output-container"
+ tabindex="0" role="document" aria-live="polite" />
+ </div>
+ </hbox>
+ <notificationbox id="webconsole-notificationbox">
+ <hbox class="jsterm-input-container" style="direction:ltr">
+ <stack class="jsterm-stack-node" flex="1">
+ <textbox class="jsterm-complete-node devtools-monospace"
+ multiline="true" rows="1" tabindex="-1"/>
+ <textbox class="jsterm-input-node devtools-monospace"
+ multiline="true" rows="1" tabindex="0"
+ aria-autocomplete="list"/>
+ </stack>
+ </hbox>
+ </notificationbox>
+ </vbox>
+
+ <splitter class="devtools-side-splitter"/>
+
+ <tabbox id="webconsole-sidebar" class="devtools-sidebar-tabs" hidden="true" width="300">
+ <tabs/>
+ <tabpanels flex="1"/>
+ </tabbox>
+ </box>
+</window>