summaryrefslogtreecommitdiffstats
path: root/devtools/client/netmonitor
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/netmonitor
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/netmonitor')
-rw-r--r--devtools/client/netmonitor/.eslintrc.js15
-rw-r--r--devtools/client/netmonitor/actions/filters.js57
-rw-r--r--devtools/client/netmonitor/actions/index.js9
-rw-r--r--devtools/client/netmonitor/actions/moz.build10
-rw-r--r--devtools/client/netmonitor/actions/sidebar.js49
-rw-r--r--devtools/client/netmonitor/components/filter-buttons.js49
-rw-r--r--devtools/client/netmonitor/components/moz.build10
-rw-r--r--devtools/client/netmonitor/components/search-box.js24
-rw-r--r--devtools/client/netmonitor/components/toggle-button.js69
-rw-r--r--devtools/client/netmonitor/constants.js19
-rw-r--r--devtools/client/netmonitor/custom-request-view.js216
-rw-r--r--devtools/client/netmonitor/events.js86
-rw-r--r--devtools/client/netmonitor/filter-predicates.js129
-rw-r--r--devtools/client/netmonitor/har/har-automation.js273
-rw-r--r--devtools/client/netmonitor/har/har-builder.js491
-rw-r--r--devtools/client/netmonitor/har/har-collector.js462
-rw-r--r--devtools/client/netmonitor/har/har-exporter.js187
-rw-r--r--devtools/client/netmonitor/har/har-utils.js189
-rw-r--r--devtools/client/netmonitor/har/moz.build15
-rw-r--r--devtools/client/netmonitor/har/test/.eslintrc.js6
-rw-r--r--devtools/client/netmonitor/har/test/browser.ini12
-rw-r--r--devtools/client/netmonitor/har/test/browser_net_har_copy_all_as_har.js49
-rw-r--r--devtools/client/netmonitor/har/test/browser_net_har_post_data.js44
-rw-r--r--devtools/client/netmonitor/har/test/browser_net_har_throttle_upload.js75
-rw-r--r--devtools/client/netmonitor/har/test/head.js14
-rw-r--r--devtools/client/netmonitor/har/test/html_har_post-data-test-page.html39
-rw-r--r--devtools/client/netmonitor/har/toolbox-overlay.js85
-rw-r--r--devtools/client/netmonitor/l10n.js9
-rw-r--r--devtools/client/netmonitor/moz.build31
-rw-r--r--devtools/client/netmonitor/netmonitor-controller.js816
-rw-r--r--devtools/client/netmonitor/netmonitor-view.js1230
-rw-r--r--devtools/client/netmonitor/netmonitor.xul741
-rw-r--r--devtools/client/netmonitor/panel.js77
-rw-r--r--devtools/client/netmonitor/performance-statistics-view.js265
-rw-r--r--devtools/client/netmonitor/prefs.js14
-rw-r--r--devtools/client/netmonitor/reducers/filters.js80
-rw-r--r--devtools/client/netmonitor/reducers/index.js13
-rw-r--r--devtools/client/netmonitor/reducers/moz.build10
-rw-r--r--devtools/client/netmonitor/reducers/sidebar.js43
-rw-r--r--devtools/client/netmonitor/request-list-context-menu.js357
-rw-r--r--devtools/client/netmonitor/request-utils.js185
-rw-r--r--devtools/client/netmonitor/requests-menu-view.js1649
-rw-r--r--devtools/client/netmonitor/selectors/index.js8
-rw-r--r--devtools/client/netmonitor/selectors/moz.build8
-rw-r--r--devtools/client/netmonitor/sort-predicates.js92
-rw-r--r--devtools/client/netmonitor/store.js13
-rw-r--r--devtools/client/netmonitor/test/.eslintrc.js6
-rw-r--r--devtools/client/netmonitor/test/browser.ini156
-rw-r--r--devtools/client/netmonitor/test/browser_net_aaa_leaktest.js28
-rw-r--r--devtools/client/netmonitor/test/browser_net_accessibility-01.js87
-rw-r--r--devtools/client/netmonitor/test/browser_net_accessibility-02.js130
-rw-r--r--devtools/client/netmonitor/test/browser_net_api-calls.js39
-rw-r--r--devtools/client/netmonitor/test/browser_net_autoscroll.js75
-rw-r--r--devtools/client/netmonitor/test/browser_net_brotli.js91
-rw-r--r--devtools/client/netmonitor/test/browser_net_cached-status.js111
-rw-r--r--devtools/client/netmonitor/test/browser_net_cause.js147
-rw-r--r--devtools/client/netmonitor/test/browser_net_cause_redirect.js57
-rw-r--r--devtools/client/netmonitor/test/browser_net_charts-01.js73
-rw-r--r--devtools/client/netmonitor/test/browser_net_charts-02.js49
-rw-r--r--devtools/client/netmonitor/test/browser_net_charts-03.js106
-rw-r--r--devtools/client/netmonitor/test/browser_net_charts-04.js75
-rw-r--r--devtools/client/netmonitor/test/browser_net_charts-05.js61
-rw-r--r--devtools/client/netmonitor/test/browser_net_charts-06.js47
-rw-r--r--devtools/client/netmonitor/test/browser_net_charts-07.js63
-rw-r--r--devtools/client/netmonitor/test/browser_net_clear.js77
-rw-r--r--devtools/client/netmonitor/test/browser_net_complex-params.js195
-rw-r--r--devtools/client/netmonitor/test/browser_net_content-type.js255
-rw-r--r--devtools/client/netmonitor/test/browser_net_copy_as_curl.js88
-rw-r--r--devtools/client/netmonitor/test/browser_net_copy_headers.js72
-rw-r--r--devtools/client/netmonitor/test/browser_net_copy_image_as_data_uri.js35
-rw-r--r--devtools/client/netmonitor/test/browser_net_copy_params.js98
-rw-r--r--devtools/client/netmonitor/test/browser_net_copy_response.js35
-rw-r--r--devtools/client/netmonitor/test/browser_net_copy_svg_image_as_data_uri.js37
-rw-r--r--devtools/client/netmonitor/test/browser_net_copy_url.js31
-rw-r--r--devtools/client/netmonitor/test/browser_net_cors_requests.js33
-rw-r--r--devtools/client/netmonitor/test/browser_net_curl-utils.js228
-rw-r--r--devtools/client/netmonitor/test/browser_net_cyrillic-01.js45
-rw-r--r--devtools/client/netmonitor/test/browser_net_cyrillic-02.js44
-rw-r--r--devtools/client/netmonitor/test/browser_net_details-no-duplicated-content.js172
-rw-r--r--devtools/client/netmonitor/test/browser_net_filter-01.js264
-rw-r--r--devtools/client/netmonitor/test/browser_net_filter-02.js200
-rw-r--r--devtools/client/netmonitor/test/browser_net_filter-03.js185
-rw-r--r--devtools/client/netmonitor/test/browser_net_filter-04.js63
-rw-r--r--devtools/client/netmonitor/test/browser_net_footer-summary.js75
-rw-r--r--devtools/client/netmonitor/test/browser_net_frame.js221
-rw-r--r--devtools/client/netmonitor/test/browser_net_html-preview.js62
-rw-r--r--devtools/client/netmonitor/test/browser_net_icon-preview.js71
-rw-r--r--devtools/client/netmonitor/test/browser_net_image-tooltip.js101
-rw-r--r--devtools/client/netmonitor/test/browser_net_json-long.js98
-rw-r--r--devtools/client/netmonitor/test/browser_net_json-malformed.js77
-rw-r--r--devtools/client/netmonitor/test/browser_net_json_custom_mime.js90
-rw-r--r--devtools/client/netmonitor/test/browser_net_json_text_mime.js90
-rw-r--r--devtools/client/netmonitor/test/browser_net_jsonp.js111
-rw-r--r--devtools/client/netmonitor/test/browser_net_large-response.js55
-rw-r--r--devtools/client/netmonitor/test/browser_net_leak_on_tab_close.js17
-rw-r--r--devtools/client/netmonitor/test/browser_net_open_request_in_tab.js37
-rw-r--r--devtools/client/netmonitor/test/browser_net_page-nav.js69
-rw-r--r--devtools/client/netmonitor/test/browser_net_pane-collapse.js72
-rw-r--r--devtools/client/netmonitor/test/browser_net_pane-toggle.js74
-rw-r--r--devtools/client/netmonitor/test/browser_net_persistent_logs.js51
-rw-r--r--devtools/client/netmonitor/test/browser_net_post-data-01.js166
-rw-r--r--devtools/client/netmonitor/test/browser_net_post-data-02.js73
-rw-r--r--devtools/client/netmonitor/test/browser_net_post-data-03.js98
-rw-r--r--devtools/client/netmonitor/test/browser_net_post-data-04.js74
-rw-r--r--devtools/client/netmonitor/test/browser_net_prefs-and-l10n.js54
-rw-r--r--devtools/client/netmonitor/test/browser_net_prefs-reload.js215
-rw-r--r--devtools/client/netmonitor/test/browser_net_raw_headers.js70
-rw-r--r--devtools/client/netmonitor/test/browser_net_reload-button.js25
-rw-r--r--devtools/client/netmonitor/test/browser_net_reload-markers.js35
-rw-r--r--devtools/client/netmonitor/test/browser_net_req-resp-bodies.js68
-rw-r--r--devtools/client/netmonitor/test/browser_net_resend.js174
-rw-r--r--devtools/client/netmonitor/test/browser_net_resend_cors.js80
-rw-r--r--devtools/client/netmonitor/test/browser_net_resend_headers.js67
-rw-r--r--devtools/client/netmonitor/test/browser_net_security-details.js102
-rw-r--r--devtools/client/netmonitor/test/browser_net_security-error.js70
-rw-r--r--devtools/client/netmonitor/test/browser_net_security-icon-click.js57
-rw-r--r--devtools/client/netmonitor/test/browser_net_security-redirect.js38
-rw-r--r--devtools/client/netmonitor/test/browser_net_security-state.js119
-rw-r--r--devtools/client/netmonitor/test/browser_net_security-tab-deselect.js46
-rw-r--r--devtools/client/netmonitor/test/browser_net_security-tab-visibility.js121
-rw-r--r--devtools/client/netmonitor/test/browser_net_security-warnings.js56
-rw-r--r--devtools/client/netmonitor/test/browser_net_send-beacon-other-tab.js34
-rw-r--r--devtools/client/netmonitor/test/browser_net_send-beacon.js31
-rw-r--r--devtools/client/netmonitor/test/browser_net_service-worker-status.js87
-rw-r--r--devtools/client/netmonitor/test/browser_net_simple-init.js93
-rw-r--r--devtools/client/netmonitor/test/browser_net_simple-request-data.js247
-rw-r--r--devtools/client/netmonitor/test/browser_net_simple-request-details.js261
-rw-r--r--devtools/client/netmonitor/test/browser_net_simple-request.js72
-rw-r--r--devtools/client/netmonitor/test/browser_net_sort-01.js230
-rw-r--r--devtools/client/netmonitor/test/browser_net_sort-02.js272
-rw-r--r--devtools/client/netmonitor/test/browser_net_sort-03.js209
-rw-r--r--devtools/client/netmonitor/test/browser_net_statistics-01.js63
-rw-r--r--devtools/client/netmonitor/test/browser_net_statistics-02.js42
-rw-r--r--devtools/client/netmonitor/test/browser_net_statistics-03.js45
-rw-r--r--devtools/client/netmonitor/test/browser_net_status-codes.js213
-rw-r--r--devtools/client/netmonitor/test/browser_net_streaming-response.js69
-rw-r--r--devtools/client/netmonitor/test/browser_net_throttle.js57
-rw-r--r--devtools/client/netmonitor/test/browser_net_timeline_ticks.js142
-rw-r--r--devtools/client/netmonitor/test/browser_net_timing-division.js61
-rw-r--r--devtools/client/netmonitor/test/browser_net_truncate.js44
-rw-r--r--devtools/client/netmonitor/test/dropmarker.svg6
-rw-r--r--devtools/client/netmonitor/test/head.js518
-rw-r--r--devtools/client/netmonitor/test/html_api-calls-test-page.html46
-rw-r--r--devtools/client/netmonitor/test/html_brotli-test-page.html38
-rw-r--r--devtools/client/netmonitor/test/html_cause-test-page.html48
-rw-r--r--devtools/client/netmonitor/test/html_content-type-test-page.html48
-rw-r--r--devtools/client/netmonitor/test/html_content-type-without-cache-test-page.html52
-rw-r--r--devtools/client/netmonitor/test/html_copy-as-curl.html30
-rw-r--r--devtools/client/netmonitor/test/html_cors-test-page.html31
-rw-r--r--devtools/client/netmonitor/test/html_curl-utils.html102
-rw-r--r--devtools/client/netmonitor/test/html_custom-get-page.html44
-rw-r--r--devtools/client/netmonitor/test/html_cyrillic-test-page.html39
-rw-r--r--devtools/client/netmonitor/test/html_filter-test-page.html60
-rw-r--r--devtools/client/netmonitor/test/html_frame-subdocument.html48
-rw-r--r--devtools/client/netmonitor/test/html_frame-test-page.html49
-rw-r--r--devtools/client/netmonitor/test/html_image-tooltip-test-page.html26
-rw-r--r--devtools/client/netmonitor/test/html_infinite-get-page.html41
-rw-r--r--devtools/client/netmonitor/test/html_json-custom-mime-test-page.html38
-rw-r--r--devtools/client/netmonitor/test/html_json-long-test-page.html38
-rw-r--r--devtools/client/netmonitor/test/html_json-malformed-test-page.html38
-rw-r--r--devtools/client/netmonitor/test/html_json-text-mime-test-page.html38
-rw-r--r--devtools/client/netmonitor/test/html_jsonp-test-page.html40
-rw-r--r--devtools/client/netmonitor/test/html_navigate-test-page.html18
-rw-r--r--devtools/client/netmonitor/test/html_params-test-page.html67
-rw-r--r--devtools/client/netmonitor/test/html_post-data-test-page.html77
-rw-r--r--devtools/client/netmonitor/test/html_post-json-test-page.html39
-rw-r--r--devtools/client/netmonitor/test/html_post-raw-test-page.html40
-rw-r--r--devtools/client/netmonitor/test/html_post-raw-with-headers-test-page.html45
-rw-r--r--devtools/client/netmonitor/test/html_send-beacon.html23
-rw-r--r--devtools/client/netmonitor/test/html_simple-test-page.html18
-rw-r--r--devtools/client/netmonitor/test/html_single-get-page.html36
-rw-r--r--devtools/client/netmonitor/test/html_sorting-test-page.html18
-rw-r--r--devtools/client/netmonitor/test/html_statistics-test-page.html40
-rw-r--r--devtools/client/netmonitor/test/html_status-codes-test-page.html55
-rw-r--r--devtools/client/netmonitor/test/service-workers/status-codes-service-worker.js15
-rw-r--r--devtools/client/netmonitor/test/service-workers/status-codes.html59
-rw-r--r--devtools/client/netmonitor/test/sjs_content-type-test-server.sjs273
-rw-r--r--devtools/client/netmonitor/test/sjs_cors-test-server.sjs17
-rw-r--r--devtools/client/netmonitor/test/sjs_hsts-test-server.sjs22
-rw-r--r--devtools/client/netmonitor/test/sjs_https-redirect-test-server.sjs19
-rw-r--r--devtools/client/netmonitor/test/sjs_simple-test-server.sjs17
-rw-r--r--devtools/client/netmonitor/test/sjs_sorting-test-server.sjs26
-rw-r--r--devtools/client/netmonitor/test/sjs_status-codes-test-server.sjs56
-rw-r--r--devtools/client/netmonitor/test/sjs_truncate-test-server.sjs18
-rw-r--r--devtools/client/netmonitor/test/test-image.pngbin0 -> 580 bytes
-rw-r--r--devtools/client/netmonitor/toolbar-view.js77
186 files changed, 19826 insertions, 0 deletions
diff --git a/devtools/client/netmonitor/.eslintrc.js b/devtools/client/netmonitor/.eslintrc.js
new file mode 100644
index 000000000..6e8808a3c
--- /dev/null
+++ b/devtools/client/netmonitor/.eslintrc.js
@@ -0,0 +1,15 @@
+"use strict";
+
+module.exports = {
+ // Extend from the devtools eslintrc.
+ "extends": "../../.eslintrc.js",
+
+ "rules": {
+ // The netmonitor is being migrated to HTML and cleaned of
+ // chrome-privileged code, so this rule disallows requiring chrome
+ // code. Some files in the netmonitor disable this rule still. The
+ // goal is to enable the rule globally on all files.
+ /* eslint-disable max-len */
+ "mozilla/reject-some-requires": ["error", "^(chrome|chrome:.*|resource:.*|devtools/server/.*|.*\\.jsm|devtools/shared/platform/(chome|content)/.*)$"],
+ },
+};
diff --git a/devtools/client/netmonitor/actions/filters.js b/devtools/client/netmonitor/actions/filters.js
new file mode 100644
index 000000000..71582546a
--- /dev/null
+++ b/devtools/client/netmonitor/actions/filters.js
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {
+ TOGGLE_FILTER_TYPE,
+ ENABLE_FILTER_TYPE_ONLY,
+ SET_FILTER_TEXT,
+} = require("../constants");
+
+/**
+ * Toggle an existing filter type state.
+ * If type 'all' is specified, all the other filter types are set to false.
+ * Available filter types are defined in filters reducer.
+ *
+ * @param {string} filter - A filter type is going to be updated
+ */
+function toggleFilterType(filter) {
+ return {
+ type: TOGGLE_FILTER_TYPE,
+ filter,
+ };
+}
+
+/**
+ * Enable filter type exclusively.
+ * Except filter type is set to true, all the other filter types are set
+ * to false.
+ * Available filter types are defined in filters reducer.
+ *
+ * @param {string} filter - A filter type is going to be updated
+ */
+function enableFilterTypeOnly(filter) {
+ return {
+ type: ENABLE_FILTER_TYPE_ONLY,
+ filter,
+ };
+}
+
+/**
+ * Set filter text.
+ *
+ * @param {string} url - A filter text is going to be set
+ */
+function setFilterText(url) {
+ return {
+ type: SET_FILTER_TEXT,
+ url,
+ };
+}
+
+module.exports = {
+ toggleFilterType,
+ enableFilterTypeOnly,
+ setFilterText,
+};
diff --git a/devtools/client/netmonitor/actions/index.js b/devtools/client/netmonitor/actions/index.js
new file mode 100644
index 000000000..3f7b0bd2f
--- /dev/null
+++ b/devtools/client/netmonitor/actions/index.js
@@ -0,0 +1,9 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 sidebar = require("./sidebar");
+
+module.exports = Object.assign({}, filters, sidebar);
diff --git a/devtools/client/netmonitor/actions/moz.build b/devtools/client/netmonitor/actions/moz.build
new file mode 100644
index 000000000..477cafb41
--- /dev/null
+++ b/devtools/client/netmonitor/actions/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(
+ 'filters.js',
+ 'index.js',
+ 'sidebar.js',
+)
diff --git a/devtools/client/netmonitor/actions/sidebar.js b/devtools/client/netmonitor/actions/sidebar.js
new file mode 100644
index 000000000..7e8dca5c1
--- /dev/null
+++ b/devtools/client/netmonitor/actions/sidebar.js
@@ -0,0 +1,49 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {
+ DISABLE_TOGGLE_BUTTON,
+ SHOW_SIDEBAR,
+ TOGGLE_SIDEBAR,
+} = require("../constants");
+
+/**
+ * Change ToggleButton disabled state.
+ *
+ * @param {boolean} disabled - expected button disabled state
+ */
+function disableToggleButton(disabled) {
+ return {
+ type: DISABLE_TOGGLE_BUTTON,
+ disabled: disabled,
+ };
+}
+
+/**
+ * Change sidebar visible state.
+ *
+ * @param {boolean} visible - expected sidebar visible state
+ */
+function showSidebar(visible) {
+ return {
+ type: SHOW_SIDEBAR,
+ visible: visible,
+ };
+}
+
+/**
+ * Toggle to show/hide sidebar.
+ */
+function toggleSidebar() {
+ return {
+ type: TOGGLE_SIDEBAR,
+ };
+}
+
+module.exports = {
+ disableToggleButton,
+ showSidebar,
+ toggleSidebar,
+};
diff --git a/devtools/client/netmonitor/components/filter-buttons.js b/devtools/client/netmonitor/components/filter-buttons.js
new file mode 100644
index 000000000..f24db8c53
--- /dev/null
+++ b/devtools/client/netmonitor/components/filter-buttons.js
@@ -0,0 +1,49 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { DOM, PropTypes } = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { L10N } = require("../l10n");
+const Actions = require("../actions/index");
+
+const { button, div } = DOM;
+
+function FilterButtons({
+ filterTypes,
+ triggerFilterType,
+}) {
+ const buttons = filterTypes.entrySeq().map(([type, checked]) => {
+ let classList = ["menu-filter-button"];
+ checked && classList.push("checked");
+
+ return button({
+ id: `requests-menu-filter-${type}-button`,
+ className: classList.join(" "),
+ "data-key": type,
+ onClick: triggerFilterType,
+ onKeyDown: triggerFilterType,
+ "aria-pressed": checked,
+ }, L10N.getStr(`netmonitor.toolbar.filter.${type}`));
+ }).toArray();
+
+ return div({ id: "requests-menu-filter-buttons" }, buttons);
+}
+
+FilterButtons.PropTypes = {
+ state: PropTypes.object.isRequired,
+ triggerFilterType: PropTypes.func.iRequired,
+};
+
+module.exports = connect(
+ (state) => ({ filterTypes: state.filters.types }),
+ (dispatch) => ({
+ triggerFilterType: (evt) => {
+ if (evt.type === "keydown" && (evt.key !== "" || evt.key !== "Enter")) {
+ return;
+ }
+ dispatch(Actions.toggleFilterType(evt.target.dataset.key));
+ },
+ })
+)(FilterButtons);
diff --git a/devtools/client/netmonitor/components/moz.build b/devtools/client/netmonitor/components/moz.build
new file mode 100644
index 000000000..47ef7f026
--- /dev/null
+++ b/devtools/client/netmonitor/components/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(
+ 'filter-buttons.js',
+ 'search-box.js',
+ 'toggle-button.js',
+)
diff --git a/devtools/client/netmonitor/components/search-box.js b/devtools/client/netmonitor/components/search-box.js
new file mode 100644
index 000000000..42400e232
--- /dev/null
+++ b/devtools/client/netmonitor/components/search-box.js
@@ -0,0 +1,24 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const SearchBox = require("devtools/client/shared/components/search-box");
+const { L10N } = require("../l10n");
+const Actions = require("../actions/index");
+const { FREETEXT_FILTER_SEARCH_DELAY } = require("../constants");
+
+module.exports = connect(
+ (state) => ({
+ delay: FREETEXT_FILTER_SEARCH_DELAY,
+ keyShortcut: L10N.getStr("netmonitor.toolbar.filterFreetext.key"),
+ placeholder: L10N.getStr("netmonitor.toolbar.filterFreetext.label"),
+ type: "filter",
+ }),
+ (dispatch) => ({
+ onChange: (url) => {
+ dispatch(Actions.setFilterText(url));
+ },
+ })
+)(SearchBox);
diff --git a/devtools/client/netmonitor/components/toggle-button.js b/devtools/client/netmonitor/components/toggle-button.js
new file mode 100644
index 000000000..db546c55d
--- /dev/null
+++ b/devtools/client/netmonitor/components/toggle-button.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/. */
+/* globals NetMonitorView */
+"use strict";
+
+const { DOM, PropTypes } = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { L10N } = require("../l10n");
+const Actions = require("../actions/index");
+
+// Shortcuts
+const { button } = DOM;
+
+/**
+ * Button used to toggle sidebar
+ */
+function ToggleButton({
+ disabled,
+ onToggle,
+ visible,
+}) {
+ let className = ["devtools-button"];
+ if (!visible) {
+ className.push("pane-collapsed");
+ }
+ let titleMsg = visible ? L10N.getStr("collapseDetailsPane") :
+ L10N.getStr("expandDetailsPane");
+
+ return button({
+ id: "details-pane-toggle",
+ className: className.join(" "),
+ title: titleMsg,
+ disabled: disabled,
+ tabIndex: "0",
+ onMouseDown: onToggle,
+ });
+}
+
+ToggleButton.propTypes = {
+ disabled: PropTypes.bool.isRequired,
+ onToggle: PropTypes.func.isRequired,
+ visible: PropTypes.bool.isRequired,
+};
+
+module.exports = connect(
+ (state) => ({
+ disabled: state.sidebar.toggleButtonDisabled,
+ visible: state.sidebar.visible,
+ }),
+ (dispatch) => ({
+ onToggle: () => {
+ dispatch(Actions.toggleSidebar());
+
+ let requestsMenu = NetMonitorView.RequestsMenu;
+ let selectedIndex = requestsMenu.selectedIndex;
+
+ // Make sure there's a selection if the button is pressed, to avoid
+ // showing an empty network details pane.
+ if (selectedIndex == -1 && requestsMenu.itemCount) {
+ requestsMenu.selectedIndex = 0;
+ } else {
+ requestsMenu.selectedIndex = -1;
+ }
+ },
+ })
+)(ToggleButton);
diff --git a/devtools/client/netmonitor/constants.js b/devtools/client/netmonitor/constants.js
new file mode 100644
index 000000000..a540d74b2
--- /dev/null
+++ b/devtools/client/netmonitor/constants.js
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 general = {
+ FREETEXT_FILTER_SEARCH_DELAY: 200,
+};
+
+const actionTypes = {
+ TOGGLE_FILTER_TYPE: "TOGGLE_FILTER_TYPE",
+ ENABLE_FILTER_TYPE_ONLY: "ENABLE_FILTER_TYPE_ONLY",
+ TOGGLE_SIDEBAR: "TOGGLE_SIDEBAR",
+ SHOW_SIDEBAR: "SHOW_SIDEBAR",
+ DISABLE_TOGGLE_BUTTON: "DISABLE_TOGGLE_BUTTON",
+ SET_FILTER_TEXT: "SET_FILTER_TEXT",
+};
+
+module.exports = Object.assign({}, general, actionTypes);
diff --git a/devtools/client/netmonitor/custom-request-view.js b/devtools/client/netmonitor/custom-request-view.js
new file mode 100644
index 000000000..3159ffcc7
--- /dev/null
+++ b/devtools/client/netmonitor/custom-request-view.js
@@ -0,0 +1,216 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* globals window, dumpn, gNetwork, $, EVENTS, NetMonitorView */
+"use strict";
+
+const {Task} = require("devtools/shared/task");
+const {writeHeaderText, getKeyWithEvent} = require("./request-utils");
+
+loader.lazyRequireGetter(this, "NetworkHelper",
+ "devtools/shared/webconsole/network-helper");
+
+/**
+ * Functions handling the custom request view.
+ */
+function CustomRequestView() {
+ dumpn("CustomRequestView was instantiated");
+}
+
+CustomRequestView.prototype = {
+ /**
+ * Initialization function, called when the network monitor is started.
+ */
+ initialize: function () {
+ dumpn("Initializing the CustomRequestView");
+
+ this.updateCustomRequestEvent = getKeyWithEvent(this.onUpdate.bind(this));
+ $("#custom-pane").addEventListener("input",
+ this.updateCustomRequestEvent, false);
+ },
+
+ /**
+ * Destruction function, called when the network monitor is closed.
+ */
+ destroy: function () {
+ dumpn("Destroying the CustomRequestView");
+
+ $("#custom-pane").removeEventListener("input",
+ this.updateCustomRequestEvent, false);
+ },
+
+ /**
+ * Populates this view with the specified data.
+ *
+ * @param object data
+ * The data source (this should be the attachment of a request item).
+ * @return object
+ * Returns a promise that resolves upon population the view.
+ */
+ populate: Task.async(function* (data) {
+ $("#custom-url-value").value = data.url;
+ $("#custom-method-value").value = data.method;
+ this.updateCustomQuery(data.url);
+
+ if (data.requestHeaders) {
+ let headers = data.requestHeaders.headers;
+ $("#custom-headers-value").value = writeHeaderText(headers);
+ }
+ if (data.requestPostData) {
+ let postData = data.requestPostData.postData.text;
+ $("#custom-postdata-value").value = yield gNetwork.getString(postData);
+ }
+
+ window.emit(EVENTS.CUSTOMREQUESTVIEW_POPULATED);
+ }),
+
+ /**
+ * Handle user input in the custom request form.
+ *
+ * @param object field
+ * the field that the user updated.
+ */
+ onUpdate: function (field) {
+ let selectedItem = NetMonitorView.RequestsMenu.selectedItem;
+ let value;
+
+ switch (field) {
+ case "method":
+ value = $("#custom-method-value").value.trim();
+ selectedItem.attachment.method = value;
+ break;
+ case "url":
+ value = $("#custom-url-value").value;
+ this.updateCustomQuery(value);
+ selectedItem.attachment.url = value;
+ break;
+ case "query":
+ let query = $("#custom-query-value").value;
+ this.updateCustomUrl(query);
+ field = "url";
+ value = $("#custom-url-value").value;
+ selectedItem.attachment.url = value;
+ break;
+ case "body":
+ value = $("#custom-postdata-value").value;
+ selectedItem.attachment.requestPostData = { postData: { text: value } };
+ break;
+ case "headers":
+ let headersText = $("#custom-headers-value").value;
+ value = parseHeadersText(headersText);
+ selectedItem.attachment.requestHeaders = { headers: value };
+ break;
+ }
+
+ NetMonitorView.RequestsMenu.updateMenuView(selectedItem, field, value);
+ },
+
+ /**
+ * Update the query string field based on the url.
+ *
+ * @param object url
+ * The URL to extract query string from.
+ */
+ updateCustomQuery: function (url) {
+ let paramsArray = NetworkHelper.parseQueryString(
+ NetworkHelper.nsIURL(url).query);
+
+ if (!paramsArray) {
+ $("#custom-query").hidden = true;
+ return;
+ }
+
+ $("#custom-query").hidden = false;
+ $("#custom-query-value").value = writeQueryText(paramsArray);
+ },
+
+ /**
+ * Update the url based on the query string field.
+ *
+ * @param object queryText
+ * The contents of the query string field.
+ */
+ updateCustomUrl: function (queryText) {
+ let params = parseQueryText(queryText);
+ let queryString = writeQueryString(params);
+
+ let url = $("#custom-url-value").value;
+ let oldQuery = NetworkHelper.nsIURL(url).query;
+ let path = url.replace(oldQuery, queryString);
+
+ $("#custom-url-value").value = path;
+ }
+};
+
+/**
+ * Parse text representation of multiple HTTP headers.
+ *
+ * @param string text
+ * Text of headers
+ * @return array
+ * Array of headers info {name, value}
+ */
+function parseHeadersText(text) {
+ return parseRequestText(text, "\\S+?", ":");
+}
+
+/**
+ * Parse readable text list of a query string.
+ *
+ * @param string text
+ * Text of query string represetation
+ * @return array
+ * Array of query params {name, value}
+ */
+function parseQueryText(text) {
+ return parseRequestText(text, ".+?", "=");
+}
+
+/**
+ * Parse a text representation of a name[divider]value list with
+ * the given name regex and divider character.
+ *
+ * @param string text
+ * Text of list
+ * @return array
+ * Array of headers info {name, value}
+ */
+function parseRequestText(text, namereg, divider) {
+ let regex = new RegExp("(" + namereg + ")\\" + divider + "\\s*(.+)");
+ let pairs = [];
+
+ for (let line of text.split("\n")) {
+ let matches;
+ if (matches = regex.exec(line)) { // eslint-disable-line
+ let [, name, value] = matches;
+ pairs.push({name: name, value: value});
+ }
+ }
+ return pairs;
+}
+
+/**
+ * Write out a list of query params into a chunk of text
+ *
+ * @param array params
+ * Array of query params {name, value}
+ * @return string
+ * List of query params in text format
+ */
+function writeQueryText(params) {
+ return params.map(({name, value}) => name + "=" + value).join("\n");
+}
+
+/**
+ * Write out a list of query params into a query string
+ *
+ * @param array params
+ * Array of query params {name, value}
+ * @return string
+ * Query string that can be appended to a url.
+ */
+function writeQueryString(params) {
+ return params.map(({name, value}) => name + "=" + value).join("&");
+}
+
+exports.CustomRequestView = CustomRequestView;
diff --git a/devtools/client/netmonitor/events.js b/devtools/client/netmonitor/events.js
new file mode 100644
index 000000000..8062a2f8c
--- /dev/null
+++ b/devtools/client/netmonitor/events.js
@@ -0,0 +1,86 @@
+"use strict";
+
+// The panel's window global is an EventEmitter firing the following events:
+const EVENTS = {
+ // When the monitored target begins and finishes navigating.
+ TARGET_WILL_NAVIGATE: "NetMonitor:TargetWillNavigate",
+ TARGET_DID_NAVIGATE: "NetMonitor:TargetNavigate",
+
+ // When a network or timeline event is received.
+ // See https://developer.mozilla.org/docs/Tools/Web_Console/remoting for
+ // more information about what each packet is supposed to deliver.
+ NETWORK_EVENT: "NetMonitor:NetworkEvent",
+ TIMELINE_EVENT: "NetMonitor:TimelineEvent",
+
+ // When a network event is added to the view
+ REQUEST_ADDED: "NetMonitor:RequestAdded",
+
+ // When request headers begin and finish receiving.
+ UPDATING_REQUEST_HEADERS: "NetMonitor:NetworkEventUpdating:RequestHeaders",
+ RECEIVED_REQUEST_HEADERS: "NetMonitor:NetworkEventUpdated:RequestHeaders",
+
+ // When request cookies begin and finish receiving.
+ UPDATING_REQUEST_COOKIES: "NetMonitor:NetworkEventUpdating:RequestCookies",
+ RECEIVED_REQUEST_COOKIES: "NetMonitor:NetworkEventUpdated:RequestCookies",
+
+ // When request post data begins and finishes receiving.
+ UPDATING_REQUEST_POST_DATA: "NetMonitor:NetworkEventUpdating:RequestPostData",
+ RECEIVED_REQUEST_POST_DATA: "NetMonitor:NetworkEventUpdated:RequestPostData",
+
+ // When security information begins and finishes receiving.
+ UPDATING_SECURITY_INFO: "NetMonitor::NetworkEventUpdating:SecurityInfo",
+ RECEIVED_SECURITY_INFO: "NetMonitor::NetworkEventUpdated:SecurityInfo",
+
+ // When response headers begin and finish receiving.
+ UPDATING_RESPONSE_HEADERS: "NetMonitor:NetworkEventUpdating:ResponseHeaders",
+ RECEIVED_RESPONSE_HEADERS: "NetMonitor:NetworkEventUpdated:ResponseHeaders",
+
+ // When response cookies begin and finish receiving.
+ UPDATING_RESPONSE_COOKIES: "NetMonitor:NetworkEventUpdating:ResponseCookies",
+ RECEIVED_RESPONSE_COOKIES: "NetMonitor:NetworkEventUpdated:ResponseCookies",
+
+ // When event timings begin and finish receiving.
+ UPDATING_EVENT_TIMINGS: "NetMonitor:NetworkEventUpdating:EventTimings",
+ RECEIVED_EVENT_TIMINGS: "NetMonitor:NetworkEventUpdated:EventTimings",
+
+ // When response content begins, updates and finishes receiving.
+ STARTED_RECEIVING_RESPONSE: "NetMonitor:NetworkEventUpdating:ResponseStart",
+ UPDATING_RESPONSE_CONTENT: "NetMonitor:NetworkEventUpdating:ResponseContent",
+ RECEIVED_RESPONSE_CONTENT: "NetMonitor:NetworkEventUpdated:ResponseContent",
+
+ // When the request post params are displayed in the UI.
+ REQUEST_POST_PARAMS_DISPLAYED: "NetMonitor:RequestPostParamsAvailable",
+
+ // When the response body is displayed in the UI.
+ RESPONSE_BODY_DISPLAYED: "NetMonitor:ResponseBodyAvailable",
+
+ // When the html response preview is displayed in the UI.
+ RESPONSE_HTML_PREVIEW_DISPLAYED: "NetMonitor:ResponseHtmlPreviewAvailable",
+
+ // When the image response thumbnail is displayed in the UI.
+ RESPONSE_IMAGE_THUMBNAIL_DISPLAYED:
+ "NetMonitor:ResponseImageThumbnailAvailable",
+
+ // When a tab is selected in the NetworkDetailsView and subsequently rendered.
+ TAB_UPDATED: "NetMonitor:TabUpdated",
+
+ // Fired when Sidebar has finished being populated.
+ SIDEBAR_POPULATED: "NetMonitor:SidebarPopulated",
+
+ // Fired when NetworkDetailsView has finished being populated.
+ NETWORKDETAILSVIEW_POPULATED: "NetMonitor:NetworkDetailsViewPopulated",
+
+ // Fired when CustomRequestView has finished being populated.
+ CUSTOMREQUESTVIEW_POPULATED: "NetMonitor:CustomRequestViewPopulated",
+
+ // Fired when charts have been displayed in the PerformanceStatisticsView.
+ PLACEHOLDER_CHARTS_DISPLAYED: "NetMonitor:PlaceholderChartsDisplayed",
+ PRIMED_CACHE_CHART_DISPLAYED: "NetMonitor:PrimedChartsDisplayed",
+ EMPTY_CACHE_CHART_DISPLAYED: "NetMonitor:EmptyChartsDisplayed",
+
+ // Fired once the NetMonitorController establishes a connection to the debug
+ // target.
+ CONNECTED: "connected",
+};
+
+exports.EVENTS = EVENTS;
diff --git a/devtools/client/netmonitor/filter-predicates.js b/devtools/client/netmonitor/filter-predicates.js
new file mode 100644
index 000000000..9c8e49c62
--- /dev/null
+++ b/devtools/client/netmonitor/filter-predicates.js
@@ -0,0 +1,129 @@
+"use strict";
+
+/**
+ * Predicates used when filtering items.
+ *
+ * @param object item
+ * The filtered item.
+ * @return boolean
+ * True if the item should be visible, false otherwise.
+ */
+function all() {
+ return true;
+}
+
+function isHtml({ mimeType }) {
+ return mimeType && mimeType.includes("/html");
+}
+
+function isCss({ mimeType }) {
+ return mimeType && mimeType.includes("/css");
+}
+
+function isJs({ mimeType }) {
+ return mimeType && (
+ mimeType.includes("/ecmascript") ||
+ mimeType.includes("/javascript") ||
+ mimeType.includes("/x-javascript"));
+}
+
+function isXHR(item) {
+ // Show the request it is XHR, except if the request is a WS upgrade
+ return item.isXHR && !isWS(item);
+}
+
+function isFont({ url, mimeType }) {
+ // Fonts are a mess.
+ return (mimeType && (
+ mimeType.includes("font/") ||
+ mimeType.includes("/font"))) ||
+ url.includes(".eot") ||
+ url.includes(".ttf") ||
+ url.includes(".otf") ||
+ url.includes(".woff");
+}
+
+function isImage({ mimeType }) {
+ return mimeType && mimeType.includes("image/");
+}
+
+function isMedia({ mimeType }) {
+ // Not including images.
+ return mimeType && (
+ mimeType.includes("audio/") ||
+ mimeType.includes("video/") ||
+ mimeType.includes("model/"));
+}
+
+function isFlash({ url, mimeType }) {
+ // Flash is a mess.
+ return (mimeType && (
+ mimeType.includes("/x-flv") ||
+ mimeType.includes("/x-shockwave-flash"))) ||
+ url.includes(".swf") ||
+ url.includes(".flv");
+}
+
+function isWS({ requestHeaders, responseHeaders }) {
+ // Detect a websocket upgrade if request has an Upgrade header with value 'websocket'
+ if (!requestHeaders || !Array.isArray(requestHeaders.headers)) {
+ return false;
+ }
+
+ // Find the 'upgrade' header.
+ let upgradeHeader = requestHeaders.headers.find(header => {
+ return (header.name == "Upgrade");
+ });
+
+ // If no header found on request, check response - mainly to get
+ // something we can unit test, as it is impossible to set
+ // the Upgrade header on outgoing XHR as per the spec.
+ if (!upgradeHeader && responseHeaders &&
+ Array.isArray(responseHeaders.headers)) {
+ upgradeHeader = responseHeaders.headers.find(header => {
+ return (header.name == "Upgrade");
+ });
+ }
+
+ // Return false if there is no such header or if its value isn't 'websocket'.
+ if (!upgradeHeader || upgradeHeader.value != "websocket") {
+ return false;
+ }
+
+ return true;
+}
+
+function isOther(item) {
+ let tests = [isHtml, isCss, isJs, isXHR, isFont, isImage, isMedia, isFlash, isWS];
+ return tests.every(is => !is(item));
+}
+
+function isFreetextMatch({ url }, text) {
+ let lowerCaseUrl = url.toLowerCase();
+ let lowerCaseText = text.toLowerCase();
+ let textLength = text.length;
+ // Support negative filtering
+ if (text.startsWith("-") && textLength > 1) {
+ lowerCaseText = lowerCaseText.substring(1, textLength);
+ return !lowerCaseUrl.includes(lowerCaseText);
+ }
+
+ // no text is a positive match
+ return !text || lowerCaseUrl.includes(lowerCaseText);
+}
+
+exports.Filters = {
+ all: all,
+ html: isHtml,
+ css: isCss,
+ js: isJs,
+ xhr: isXHR,
+ fonts: isFont,
+ images: isImage,
+ media: isMedia,
+ flash: isFlash,
+ ws: isWS,
+ other: isOther,
+};
+
+exports.isFreetextMatch = isFreetextMatch;
diff --git a/devtools/client/netmonitor/har/har-automation.js b/devtools/client/netmonitor/har/har-automation.js
new file mode 100644
index 000000000..0885c4f96
--- /dev/null
+++ b/devtools/client/netmonitor/har/har-automation.js
@@ -0,0 +1,273 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+/* eslint-disable mozilla/reject-some-requires */
+const { Ci } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const { resolve } = require("promise");
+const Services = require("Services");
+
+loader.lazyRequireGetter(this, "HarCollector", "devtools/client/netmonitor/har/har-collector", true);
+loader.lazyRequireGetter(this, "HarExporter", "devtools/client/netmonitor/har/har-exporter", true);
+loader.lazyRequireGetter(this, "HarUtils", "devtools/client/netmonitor/har/har-utils", true);
+
+const prefDomain = "devtools.netmonitor.har.";
+
+// Helper tracer. Should be generic sharable by other modules (bug 1171927)
+const trace = {
+ log: function (...args) {
+ }
+};
+
+/**
+ * This object is responsible for automated HAR export. It listens
+ * for Network activity, collects all HTTP data and triggers HAR
+ * export when the page is loaded.
+ *
+ * The user needs to enable the following preference to make the
+ * auto-export work: devtools.netmonitor.har.enableAutoExportToFile
+ *
+ * HAR files are stored within directory that is specified in this
+ * preference: devtools.netmonitor.har.defaultLogDir
+ *
+ * If the default log directory preference isn't set the following
+ * directory is used by default: <profile>/har/logs
+ */
+var HarAutomation = Class({
+ // Initialization
+
+ initialize: function (toolbox) {
+ this.toolbox = toolbox;
+
+ let target = toolbox.target;
+ target.makeRemote().then(() => {
+ this.startMonitoring(target.client, target.form);
+ });
+ },
+
+ destroy: function () {
+ if (this.collector) {
+ this.collector.stop();
+ }
+
+ if (this.tabWatcher) {
+ this.tabWatcher.disconnect();
+ }
+ },
+
+ // Automation
+
+ startMonitoring: function (client, tabGrip, callback) {
+ if (!client) {
+ return;
+ }
+
+ if (!tabGrip) {
+ return;
+ }
+
+ this.debuggerClient = client;
+ this.tabClient = this.toolbox.target.activeTab;
+ this.webConsoleClient = this.toolbox.target.activeConsole;
+
+ this.tabWatcher = new TabWatcher(this.toolbox, this);
+ this.tabWatcher.connect();
+ },
+
+ pageLoadBegin: function (response) {
+ this.resetCollector();
+ },
+
+ resetCollector: function () {
+ if (this.collector) {
+ this.collector.stop();
+ }
+
+ // A page is about to be loaded, start collecting HTTP
+ // data from events sent from the backend.
+ this.collector = new HarCollector({
+ webConsoleClient: this.webConsoleClient,
+ debuggerClient: this.debuggerClient
+ });
+
+ this.collector.start();
+ },
+
+ /**
+ * A page is done loading, export collected data. Note that
+ * some requests for additional page resources might be pending,
+ * so export all after all has been properly received from the backend.
+ *
+ * This collector still works and collects any consequent HTTP
+ * traffic (e.g. XHRs) happening after the page is loaded and
+ * The additional traffic can be exported by executing
+ * triggerExport on this object.
+ */
+ pageLoadDone: function (response) {
+ trace.log("HarAutomation.pageLoadDone; ", response);
+
+ if (this.collector) {
+ this.collector.waitForHarLoad().then(collector => {
+ return this.autoExport();
+ });
+ }
+ },
+
+ autoExport: function () {
+ let autoExport = Services.prefs.getBoolPref(prefDomain +
+ "enableAutoExportToFile");
+
+ if (!autoExport) {
+ return resolve();
+ }
+
+ // Auto export to file is enabled, so save collected data
+ // into a file and use all the default options.
+ let data = {
+ fileName: Services.prefs.getCharPref(prefDomain + "defaultFileName"),
+ };
+
+ return this.executeExport(data);
+ },
+
+ // Public API
+
+ /**
+ * Export all what is currently collected.
+ */
+ triggerExport: function (data) {
+ if (!data.fileName) {
+ data.fileName = Services.prefs.getCharPref(prefDomain +
+ "defaultFileName");
+ }
+
+ return this.executeExport(data);
+ },
+
+ /**
+ * Clear currently collected data.
+ */
+ clear: function () {
+ this.resetCollector();
+ },
+
+ // HAR Export
+
+ /**
+ * Execute HAR export. This method fetches all data from the
+ * Network panel (asynchronously) and saves it into a file.
+ */
+ executeExport: function (data) {
+ let items = this.collector.getItems();
+ let form = this.toolbox.target.form;
+ let title = form.title || form.url;
+
+ let options = {
+ getString: this.getString.bind(this),
+ view: this,
+ items: items,
+ };
+
+ options.defaultFileName = data.fileName;
+ options.compress = data.compress;
+ options.title = data.title || title;
+ options.id = data.id;
+ options.jsonp = data.jsonp;
+ options.includeResponseBodies = data.includeResponseBodies;
+ options.jsonpCallback = data.jsonpCallback;
+ options.forceExport = data.forceExport;
+
+ trace.log("HarAutomation.executeExport; " + data.fileName, options);
+
+ return HarExporter.fetchHarData(options).then(jsonString => {
+ // Save the HAR file if the file name is provided.
+ if (jsonString && options.defaultFileName) {
+ let file = getDefaultTargetFile(options);
+ if (file) {
+ HarUtils.saveToFile(file, jsonString, options.compress);
+ }
+ }
+
+ return jsonString;
+ });
+ },
+
+ /**
+ * Fetches the full text of a string.
+ */
+ getString: function (stringGrip) {
+ return this.webConsoleClient.getString(stringGrip);
+ },
+});
+
+// Helpers
+
+function TabWatcher(toolbox, listener) {
+ this.target = toolbox.target;
+ this.listener = listener;
+
+ this.onTabNavigated = this.onTabNavigated.bind(this);
+}
+
+TabWatcher.prototype = {
+ // Connection
+
+ connect: function () {
+ this.target.on("navigate", this.onTabNavigated);
+ this.target.on("will-navigate", this.onTabNavigated);
+ },
+
+ disconnect: function () {
+ if (!this.target) {
+ return;
+ }
+
+ this.target.off("navigate", this.onTabNavigated);
+ this.target.off("will-navigate", this.onTabNavigated);
+ },
+
+ // Event Handlers
+
+ /**
+ * Called for each location change in the monitored tab.
+ *
+ * @param string aType
+ * Packet type.
+ * @param object aPacket
+ * Packet received from the server.
+ */
+ onTabNavigated: function (type, packet) {
+ switch (type) {
+ case "will-navigate": {
+ this.listener.pageLoadBegin(packet);
+ break;
+ }
+ case "navigate": {
+ this.listener.pageLoadDone(packet);
+ break;
+ }
+ }
+ },
+};
+
+// Protocol Helpers
+
+/**
+ * Returns target file for exported HAR data.
+ */
+function getDefaultTargetFile(options) {
+ let path = options.defaultLogDir ||
+ Services.prefs.getCharPref("devtools.netmonitor.har.defaultLogDir");
+ let folder = HarUtils.getLocalDirectory(path);
+ let fileName = HarUtils.getHarFileName(options.defaultFileName,
+ options.jsonp, options.compress);
+
+ folder.append(fileName);
+ folder.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0666", 8));
+
+ return folder;
+}
+
+// Exports from this module
+exports.HarAutomation = HarAutomation;
diff --git a/devtools/client/netmonitor/har/har-builder.js b/devtools/client/netmonitor/har/har-builder.js
new file mode 100644
index 000000000..f28e43016
--- /dev/null
+++ b/devtools/client/netmonitor/har/har-builder.js
@@ -0,0 +1,491 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { defer, all } = require("promise");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const Services = require("Services");
+const appInfo = Services.appinfo;
+const { CurlUtils } = require("devtools/client/shared/curl");
+const { getFormDataSections } = require("devtools/client/netmonitor/request-utils");
+
+loader.lazyRequireGetter(this, "NetworkHelper", "devtools/shared/webconsole/network-helper");
+
+loader.lazyGetter(this, "L10N", () => {
+ return new LocalizationHelper("devtools/client/locales/har.properties");
+});
+
+const HAR_VERSION = "1.1";
+
+/**
+ * This object is responsible for building HAR file. See HAR spec:
+ * https://dvcs.w3.org/hg/webperf/raw-file/tip/specs/HAR/Overview.html
+ * http://www.softwareishard.com/blog/har-12-spec/
+ *
+ * @param {Object} options configuration object
+ *
+ * The following options are supported:
+ *
+ * - items {Array}: List of Network requests to be exported. It is possible
+ * to use directly: NetMonitorView.RequestsMenu.items
+ *
+ * - id {String}: ID of the exported page.
+ *
+ * - title {String}: Title of the exported page.
+ *
+ * - includeResponseBodies {Boolean}: Set to true to include HTTP response
+ * bodies in the result data structure.
+ */
+var HarBuilder = function (options) {
+ this._options = options;
+ this._pageMap = [];
+};
+
+HarBuilder.prototype = {
+ // Public API
+
+ /**
+ * This is the main method used to build the entire result HAR data.
+ * The process is asynchronous since it can involve additional RDP
+ * communication (e.g. resolving long strings).
+ *
+ * @returns {Promise} A promise that resolves to the HAR object when
+ * the entire build process is done.
+ */
+ build: function () {
+ this.promises = [];
+
+ // Build basic structure for data.
+ let log = this.buildLog();
+
+ // Build entries.
+ let items = this._options.items;
+ for (let i = 0; i < items.length; i++) {
+ let file = items[i].attachment;
+ log.entries.push(this.buildEntry(log, file));
+ }
+
+ // Some data needs to be fetched from the backend during the
+ // build process, so wait till all is done.
+ let { resolve, promise } = defer();
+ all(this.promises).then(results => resolve({ log: log }));
+
+ return promise;
+ },
+
+ // Helpers
+
+ buildLog: function () {
+ return {
+ version: HAR_VERSION,
+ creator: {
+ name: appInfo.name,
+ version: appInfo.version
+ },
+ browser: {
+ name: appInfo.name,
+ version: appInfo.version
+ },
+ pages: [],
+ entries: [],
+ };
+ },
+
+ buildPage: function (file) {
+ let page = {};
+
+ // Page start time is set when the first request is processed
+ // (see buildEntry)
+ page.startedDateTime = 0;
+ page.id = "page_" + this._options.id;
+ page.title = this._options.title;
+
+ return page;
+ },
+
+ getPage: function (log, file) {
+ let id = this._options.id;
+ let page = this._pageMap[id];
+ if (page) {
+ return page;
+ }
+
+ this._pageMap[id] = page = this.buildPage(file);
+ log.pages.push(page);
+
+ return page;
+ },
+
+ buildEntry: function (log, file) {
+ let page = this.getPage(log, file);
+
+ let entry = {};
+ entry.pageref = page.id;
+ entry.startedDateTime = dateToJSON(new Date(file.startedMillis));
+ entry.time = file.endedMillis - file.startedMillis;
+
+ entry.request = this.buildRequest(file);
+ entry.response = this.buildResponse(file);
+ entry.cache = this.buildCache(file);
+ entry.timings = file.eventTimings ? file.eventTimings.timings : {};
+
+ if (file.remoteAddress) {
+ entry.serverIPAddress = file.remoteAddress;
+ }
+
+ if (file.remotePort) {
+ entry.connection = file.remotePort + "";
+ }
+
+ // Compute page load start time according to the first request start time.
+ if (!page.startedDateTime) {
+ page.startedDateTime = entry.startedDateTime;
+ page.pageTimings = this.buildPageTimings(page, file);
+ }
+
+ return entry;
+ },
+
+ buildPageTimings: function (page, file) {
+ // Event timing info isn't available
+ let timings = {
+ onContentLoad: -1,
+ onLoad: -1
+ };
+
+ return timings;
+ },
+
+ buildRequest: function (file) {
+ let request = {
+ bodySize: 0
+ };
+
+ request.method = file.method;
+ request.url = file.url;
+ request.httpVersion = file.httpVersion || "";
+
+ request.headers = this.buildHeaders(file.requestHeaders);
+ request.headers = this.appendHeadersPostData(request.headers, file);
+ request.cookies = this.buildCookies(file.requestCookies);
+
+ request.queryString = NetworkHelper.parseQueryString(
+ NetworkHelper.nsIURL(file.url).query) || [];
+
+ request.postData = this.buildPostData(file);
+
+ request.headersSize = file.requestHeaders.headersSize;
+
+ // Set request body size, but make sure the body is fetched
+ // from the backend.
+ if (file.requestPostData) {
+ this.fetchData(file.requestPostData.postData.text).then(value => {
+ request.bodySize = value.length;
+ });
+ }
+
+ return request;
+ },
+
+ /**
+ * Fetch all header values from the backend (if necessary) and
+ * build the result HAR structure.
+ *
+ * @param {Object} input Request or response header object.
+ */
+ buildHeaders: function (input) {
+ if (!input) {
+ return [];
+ }
+
+ return this.buildNameValuePairs(input.headers);
+ },
+
+ appendHeadersPostData: function (input = [], file) {
+ if (!file.requestPostData) {
+ return input;
+ }
+
+ this.fetchData(file.requestPostData.postData.text).then(value => {
+ let multipartHeaders = CurlUtils.getHeadersFromMultipartText(value);
+ for (let header of multipartHeaders) {
+ input.push(header);
+ }
+ });
+
+ return input;
+ },
+
+ buildCookies: function (input) {
+ if (!input) {
+ return [];
+ }
+
+ return this.buildNameValuePairs(input.cookies);
+ },
+
+ buildNameValuePairs: function (entries) {
+ let result = [];
+
+ // HAR requires headers array to be presented, so always
+ // return at least an empty array.
+ if (!entries) {
+ return result;
+ }
+
+ // Make sure header values are fully fetched from the server.
+ entries.forEach(entry => {
+ this.fetchData(entry.value).then(value => {
+ result.push({
+ name: entry.name,
+ value: value
+ });
+ });
+ });
+
+ return result;
+ },
+
+ buildPostData: function (file) {
+ let postData = {
+ mimeType: findValue(file.requestHeaders.headers, "content-type"),
+ params: [],
+ text: ""
+ };
+
+ if (!file.requestPostData) {
+ return postData;
+ }
+
+ if (file.requestPostData.postDataDiscarded) {
+ postData.comment = L10N.getStr("har.requestBodyNotIncluded");
+ return postData;
+ }
+
+ // Load request body from the backend.
+ this.fetchData(file.requestPostData.postData.text).then(postDataText => {
+ postData.text = postDataText;
+
+ // If we are dealing with URL encoded body, parse parameters.
+ let { headers } = file.requestHeaders;
+ if (CurlUtils.isUrlEncodedRequest({ headers, postDataText })) {
+ postData.mimeType = "application/x-www-form-urlencoded";
+
+ // Extract form parameters and produce nice HAR array.
+ getFormDataSections(
+ file.requestHeaders,
+ file.requestHeadersFromUploadStream,
+ file.requestPostData,
+ this._options.getString
+ ).then(formDataSections => {
+ formDataSections.forEach(section => {
+ let paramsArray = NetworkHelper.parseQueryString(section);
+ if (paramsArray) {
+ postData.params = [...postData.params, ...paramsArray];
+ }
+ });
+ });
+ }
+ });
+
+ return postData;
+ },
+
+ buildResponse: function (file) {
+ let response = {
+ status: 0
+ };
+
+ // Arbitrary value if it's aborted to make sure status has a number
+ if (file.status) {
+ response.status = parseInt(file.status, 10);
+ }
+
+ let responseHeaders = file.responseHeaders;
+
+ response.statusText = file.statusText || "";
+ response.httpVersion = file.httpVersion || "";
+
+ response.headers = this.buildHeaders(responseHeaders);
+ response.cookies = this.buildCookies(file.responseCookies);
+ response.content = this.buildContent(file);
+
+ let headers = responseHeaders ? responseHeaders.headers : null;
+ let headersSize = responseHeaders ? responseHeaders.headersSize : -1;
+
+ response.redirectURL = findValue(headers, "Location");
+ response.headersSize = headersSize;
+
+ // 'bodySize' is size of the received response body in bytes.
+ // Set to zero in case of responses coming from the cache (304).
+ // Set to -1 if the info is not available.
+ if (typeof file.transferredSize != "number") {
+ response.bodySize = (response.status == 304) ? 0 : -1;
+ } else {
+ response.bodySize = file.transferredSize;
+ }
+
+ return response;
+ },
+
+ buildContent: function (file) {
+ let content = {
+ mimeType: file.mimeType,
+ size: -1
+ };
+
+ let responseContent = file.responseContent;
+ if (responseContent && responseContent.content) {
+ content.size = responseContent.content.size;
+ content.encoding = responseContent.content.encoding;
+ }
+
+ let includeBodies = this._options.includeResponseBodies;
+ let contentDiscarded = responseContent ?
+ responseContent.contentDiscarded : false;
+
+ // The comment is appended only if the response content
+ // is explicitly discarded.
+ if (!includeBodies || contentDiscarded) {
+ content.comment = L10N.getStr("har.responseBodyNotIncluded");
+ return content;
+ }
+
+ if (responseContent) {
+ let text = responseContent.content.text;
+ this.fetchData(text).then(value => {
+ content.text = value;
+ });
+ }
+
+ return content;
+ },
+
+ buildCache: function (file) {
+ let cache = {};
+
+ if (!file.fromCache) {
+ return cache;
+ }
+
+ // There is no such info yet in the Net panel.
+ // cache.beforeRequest = {};
+
+ if (file.cacheEntry) {
+ cache.afterRequest = this.buildCacheEntry(file.cacheEntry);
+ } else {
+ cache.afterRequest = null;
+ }
+
+ return cache;
+ },
+
+ buildCacheEntry: function (cacheEntry) {
+ let cache = {};
+
+ cache.expires = findValue(cacheEntry, "Expires");
+ cache.lastAccess = findValue(cacheEntry, "Last Fetched");
+ cache.eTag = "";
+ cache.hitCount = findValue(cacheEntry, "Fetch Count");
+
+ return cache;
+ },
+
+ getBlockingEndTime: function (file) {
+ if (file.resolveStarted && file.connectStarted) {
+ return file.resolvingTime;
+ }
+
+ if (file.connectStarted) {
+ return file.connectingTime;
+ }
+
+ if (file.sendStarted) {
+ return file.sendingTime;
+ }
+
+ return (file.sendingTime > file.startTime) ?
+ file.sendingTime : file.waitingForTime;
+ },
+
+ // RDP Helpers
+
+ fetchData: function (string) {
+ let promise = this._options.getString(string).then(value => {
+ return value;
+ });
+
+ // Building HAR is asynchronous and not done till all
+ // collected promises are resolved.
+ this.promises.push(promise);
+
+ return promise;
+ }
+};
+
+// Helpers
+
+/**
+ * Find specified value within an array of name-value pairs
+ * (used for headers, cookies and cache entries)
+ */
+function findValue(arr, name) {
+ if (!arr) {
+ return "";
+ }
+
+ name = name.toLowerCase();
+ let result = arr.find(entry => entry.name.toLowerCase() == name);
+ return result ? result.value : "";
+}
+
+/**
+ * Generate HAR representation of a date.
+ * (YYYY-MM-DDThh:mm:ss.sTZD, e.g. 2009-07-24T19:20:30.45+01:00)
+ * See also HAR Schema: http://janodvarko.cz/har/viewer/
+ *
+ * Note: it would be great if we could utilize Date.toJSON(), but
+ * it doesn't return proper time zone offset.
+ *
+ * An example:
+ * This helper returns: 2015-05-29T16:10:30.424+02:00
+ * Date.toJSON() returns: 2015-05-29T14:10:30.424Z
+ *
+ * @param date {Date} The date object we want to convert.
+ */
+function dateToJSON(date) {
+ function f(n, c) {
+ if (!c) {
+ c = 2;
+ }
+ let s = new String(n);
+ while (s.length < c) {
+ s = "0" + s;
+ }
+ return s;
+ }
+
+ let result = date.getFullYear() + "-" +
+ f(date.getMonth() + 1) + "-" +
+ f(date.getDate()) + "T" +
+ f(date.getHours()) + ":" +
+ f(date.getMinutes()) + ":" +
+ f(date.getSeconds()) + "." +
+ f(date.getMilliseconds(), 3);
+
+ let offset = date.getTimezoneOffset();
+ let positive = offset > 0;
+
+ // Convert to positive number before using Math.floor (see issue 5512)
+ offset = Math.abs(offset);
+ let offsetHours = Math.floor(offset / 60);
+ let offsetMinutes = Math.floor(offset % 60);
+ let prettyOffset = (positive > 0 ? "-" : "+") + f(offsetHours) +
+ ":" + f(offsetMinutes);
+
+ return result + prettyOffset;
+}
+
+// Exports from this module
+exports.HarBuilder = HarBuilder;
diff --git a/devtools/client/netmonitor/har/har-collector.js b/devtools/client/netmonitor/har/har-collector.js
new file mode 100644
index 000000000..e3c510756
--- /dev/null
+++ b/devtools/client/netmonitor/har/har-collector.js
@@ -0,0 +1,462 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { defer, all } = require("promise");
+const { makeInfallible } = require("devtools/shared/DevToolsUtils");
+const Services = require("Services");
+
+// Helper tracer. Should be generic sharable by other modules (bug 1171927)
+const trace = {
+ log: function (...args) {
+ }
+};
+
+/**
+ * This object is responsible for collecting data related to all
+ * HTTP requests executed by the page (including inner iframes).
+ */
+function HarCollector(options) {
+ this.webConsoleClient = options.webConsoleClient;
+ this.debuggerClient = options.debuggerClient;
+
+ this.onNetworkEvent = this.onNetworkEvent.bind(this);
+ this.onNetworkEventUpdate = this.onNetworkEventUpdate.bind(this);
+ this.onRequestHeaders = this.onRequestHeaders.bind(this);
+ this.onRequestCookies = this.onRequestCookies.bind(this);
+ this.onRequestPostData = this.onRequestPostData.bind(this);
+ this.onResponseHeaders = this.onResponseHeaders.bind(this);
+ this.onResponseCookies = this.onResponseCookies.bind(this);
+ this.onResponseContent = this.onResponseContent.bind(this);
+ this.onEventTimings = this.onEventTimings.bind(this);
+
+ this.onPageLoadTimeout = this.onPageLoadTimeout.bind(this);
+
+ this.clear();
+}
+
+HarCollector.prototype = {
+ // Connection
+
+ start: function () {
+ this.debuggerClient.addListener("networkEvent", this.onNetworkEvent);
+ this.debuggerClient.addListener("networkEventUpdate",
+ this.onNetworkEventUpdate);
+ },
+
+ stop: function () {
+ this.debuggerClient.removeListener("networkEvent", this.onNetworkEvent);
+ this.debuggerClient.removeListener("networkEventUpdate",
+ this.onNetworkEventUpdate);
+ },
+
+ clear: function () {
+ // Any pending requests events will be ignored (they turn
+ // into zombies, since not present in the files array).
+ this.files = new Map();
+ this.items = [];
+ this.firstRequestStart = -1;
+ this.lastRequestStart = -1;
+ this.requests = [];
+ },
+
+ waitForHarLoad: function () {
+ // There should be yet another timeout e.g.:
+ // 'devtools.netmonitor.har.pageLoadTimeout'
+ // that should force export even if page isn't fully loaded.
+ let deferred = defer();
+ this.waitForResponses().then(() => {
+ trace.log("HarCollector.waitForHarLoad; DONE HAR loaded!");
+ deferred.resolve(this);
+ });
+
+ return deferred.promise;
+ },
+
+ waitForResponses: function () {
+ trace.log("HarCollector.waitForResponses; " + this.requests.length);
+
+ // All requests for additional data must be received to have complete
+ // HTTP info to generate the result HAR file. So, wait for all current
+ // promises. Note that new promises (requests) can be generated during the
+ // process of HTTP data collection.
+ return waitForAll(this.requests).then(() => {
+ // All responses are received from the backend now. We yet need to
+ // wait for a little while to see if a new request appears. If yes,
+ // lets's start gathering HTTP data again. If no, we can declare
+ // the page loaded.
+ // If some new requests appears in the meantime the promise will
+ // be rejected and we need to wait for responses all over again.
+ return this.waitForTimeout().then(() => {
+ // Page loaded!
+ }, () => {
+ trace.log("HarCollector.waitForResponses; NEW requests " +
+ "appeared during page timeout!");
+
+ // New requests executed, let's wait again.
+ return this.waitForResponses();
+ });
+ });
+ },
+
+ // Page Loaded Timeout
+
+ /**
+ * The page is loaded when there are no new requests within given period
+ * of time. The time is set in preferences:
+ * 'devtools.netmonitor.har.pageLoadedTimeout'
+ */
+ waitForTimeout: function () {
+ // The auto-export is not done if the timeout is set to zero (or less).
+ // This is useful in cases where the export is done manually through
+ // API exposed to the content.
+ let timeout = Services.prefs.getIntPref(
+ "devtools.netmonitor.har.pageLoadedTimeout");
+
+ trace.log("HarCollector.waitForTimeout; " + timeout);
+
+ this.pageLoadDeferred = defer();
+
+ if (timeout <= 0) {
+ this.pageLoadDeferred.resolve();
+ return this.pageLoadDeferred.promise;
+ }
+
+ this.pageLoadTimeout = setTimeout(this.onPageLoadTimeout, timeout);
+
+ return this.pageLoadDeferred.promise;
+ },
+
+ onPageLoadTimeout: function () {
+ trace.log("HarCollector.onPageLoadTimeout;");
+
+ // Ha, page has been loaded. Resolve the final timeout promise.
+ this.pageLoadDeferred.resolve();
+ },
+
+ resetPageLoadTimeout: function () {
+ // Remove the current timeout.
+ if (this.pageLoadTimeout) {
+ trace.log("HarCollector.resetPageLoadTimeout;");
+
+ clearTimeout(this.pageLoadTimeout);
+ this.pageLoadTimeout = null;
+ }
+
+ // Reject the current page load promise
+ if (this.pageLoadDeferred) {
+ this.pageLoadDeferred.reject();
+ this.pageLoadDeferred = null;
+ }
+ },
+
+ // Collected Data
+
+ getFile: function (actorId) {
+ return this.files.get(actorId);
+ },
+
+ getItems: function () {
+ return this.items;
+ },
+
+ // Event Handlers
+
+ onNetworkEvent: function (type, packet) {
+ // Skip events from different console actors.
+ if (packet.from != this.webConsoleClient.actor) {
+ return;
+ }
+
+ trace.log("HarCollector.onNetworkEvent; " + type, packet);
+
+ let { actor, startedDateTime, method, url, isXHR } = packet.eventActor;
+ let startTime = Date.parse(startedDateTime);
+
+ if (this.firstRequestStart == -1) {
+ this.firstRequestStart = startTime;
+ }
+
+ if (this.lastRequestEnd < startTime) {
+ this.lastRequestEnd = startTime;
+ }
+
+ let file = this.getFile(actor);
+ if (file) {
+ console.error("HarCollector.onNetworkEvent; ERROR " +
+ "existing file conflict!");
+ return;
+ }
+
+ file = {
+ startedDeltaMillis: startTime - this.firstRequestStart,
+ startedMillis: startTime,
+ method: method,
+ url: url,
+ isXHR: isXHR
+ };
+
+ this.files.set(actor, file);
+
+ // Mimic the Net panel data structure
+ this.items.push({
+ attachment: file
+ });
+ },
+
+ onNetworkEventUpdate: function (type, packet) {
+ let actor = packet.from;
+
+ // Skip events from unknown actors (not in the list).
+ // It can happen when there are zombie requests received after
+ // the target is closed or multiple tabs are attached through
+ // one connection (one DebuggerClient object).
+ let file = this.getFile(packet.from);
+ if (!file) {
+ return;
+ }
+
+ trace.log("HarCollector.onNetworkEventUpdate; " +
+ packet.updateType, packet);
+
+ let includeResponseBodies = Services.prefs.getBoolPref(
+ "devtools.netmonitor.har.includeResponseBodies");
+
+ let request;
+ switch (packet.updateType) {
+ case "requestHeaders":
+ request = this.getData(actor, "getRequestHeaders",
+ this.onRequestHeaders);
+ break;
+ case "requestCookies":
+ request = this.getData(actor, "getRequestCookies",
+ this.onRequestCookies);
+ break;
+ case "requestPostData":
+ request = this.getData(actor, "getRequestPostData",
+ this.onRequestPostData);
+ break;
+ case "responseHeaders":
+ request = this.getData(actor, "getResponseHeaders",
+ this.onResponseHeaders);
+ break;
+ case "responseCookies":
+ request = this.getData(actor, "getResponseCookies",
+ this.onResponseCookies);
+ break;
+ case "responseStart":
+ file.httpVersion = packet.response.httpVersion;
+ file.status = packet.response.status;
+ file.statusText = packet.response.statusText;
+ break;
+ case "responseContent":
+ file.contentSize = packet.contentSize;
+ file.mimeType = packet.mimeType;
+ file.transferredSize = packet.transferredSize;
+
+ if (includeResponseBodies) {
+ request = this.getData(actor, "getResponseContent",
+ this.onResponseContent);
+ }
+ break;
+ case "eventTimings":
+ request = this.getData(actor, "getEventTimings",
+ this.onEventTimings);
+ break;
+ }
+
+ if (request) {
+ this.requests.push(request);
+ }
+
+ this.resetPageLoadTimeout();
+ },
+
+ getData: function (actor, method, callback) {
+ let deferred = defer();
+
+ if (!this.webConsoleClient[method]) {
+ console.error("HarCollector.getData; ERROR " +
+ "Unknown method!");
+ return deferred.resolve();
+ }
+
+ let file = this.getFile(actor);
+
+ trace.log("HarCollector.getData; REQUEST " + method +
+ ", " + file.url, file);
+
+ this.webConsoleClient[method](actor, response => {
+ trace.log("HarCollector.getData; RESPONSE " + method +
+ ", " + file.url, response);
+
+ callback(response);
+ deferred.resolve(response);
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Handles additional information received for a "requestHeaders" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ onRequestHeaders: function (response) {
+ let file = this.getFile(response.from);
+ file.requestHeaders = response;
+
+ this.getLongHeaders(response.headers);
+ },
+
+ /**
+ * Handles additional information received for a "requestCookies" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ onRequestCookies: function (response) {
+ let file = this.getFile(response.from);
+ file.requestCookies = response;
+
+ this.getLongHeaders(response.cookies);
+ },
+
+ /**
+ * Handles additional information received for a "requestPostData" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ onRequestPostData: function (response) {
+ trace.log("HarCollector.onRequestPostData;", response);
+
+ let file = this.getFile(response.from);
+ file.requestPostData = response;
+
+ // Resolve long string
+ let text = response.postData.text;
+ if (typeof text == "object") {
+ this.getString(text).then(value => {
+ response.postData.text = value;
+ });
+ }
+ },
+
+ /**
+ * Handles additional information received for a "responseHeaders" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ onResponseHeaders: function (response) {
+ let file = this.getFile(response.from);
+ file.responseHeaders = response;
+
+ this.getLongHeaders(response.headers);
+ },
+
+ /**
+ * Handles additional information received for a "responseCookies" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ onResponseCookies: function (response) {
+ let file = this.getFile(response.from);
+ file.responseCookies = response;
+
+ this.getLongHeaders(response.cookies);
+ },
+
+ /**
+ * Handles additional information received for a "responseContent" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ onResponseContent: function (response) {
+ let file = this.getFile(response.from);
+ file.responseContent = response;
+
+ // Resolve long string
+ let text = response.content.text;
+ if (typeof text == "object") {
+ this.getString(text).then(value => {
+ response.content.text = value;
+ });
+ }
+ },
+
+ /**
+ * Handles additional information received for a "eventTimings" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ onEventTimings: function (response) {
+ let file = this.getFile(response.from);
+ file.eventTimings = response;
+
+ let totalTime = response.totalTime;
+ file.totalTime = totalTime;
+ file.endedMillis = file.startedMillis + totalTime;
+ },
+
+ // Helpers
+
+ getLongHeaders: makeInfallible(function (headers) {
+ for (let header of headers) {
+ if (typeof header.value == "object") {
+ this.getString(header.value).then(value => {
+ header.value = value;
+ });
+ }
+ }
+ }),
+
+ /**
+ * Fetches the full text of a string.
+ *
+ * @param object | string stringGrip
+ * The long string grip containing the corresponding actor.
+ * If you pass in a plain string (by accident or because you're lazy),
+ * then a promise of the same string is simply returned.
+ * @return object Promise
+ * A promise that is resolved when the full string contents
+ * are available, or rejected if something goes wrong.
+ */
+ getString: function (stringGrip) {
+ let promise = this.webConsoleClient.getString(stringGrip);
+ this.requests.push(promise);
+ return promise;
+ }
+};
+
+// Helpers
+
+/**
+ * Helper function that allows to wait for array of promises. It is
+ * possible to dynamically add new promises in the provided array.
+ * The function will wait even for the newly added promises.
+ * (this isn't possible with the default Promise.all);
+ */
+function waitForAll(promises) {
+ // Remove all from the original array and get clone of it.
+ let clone = promises.splice(0, promises.length);
+
+ // Wait for all promises in the given array.
+ return all(clone).then(() => {
+ // If there are new promises (in the original array)
+ // to wait for - chain them!
+ if (promises.length) {
+ return waitForAll(promises);
+ }
+ return undefined;
+ });
+}
+
+// Exports from this module
+exports.HarCollector = HarCollector;
diff --git a/devtools/client/netmonitor/har/har-exporter.js b/devtools/client/netmonitor/har/har-exporter.js
new file mode 100644
index 000000000..972cf87dc
--- /dev/null
+++ b/devtools/client/netmonitor/har/har-exporter.js
@@ -0,0 +1,187 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+/* eslint-disable mozilla/reject-some-requires */
+const { Cc, Ci } = require("chrome");
+const Services = require("Services");
+/* eslint-disable mozilla/reject-some-requires */
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+const { resolve } = require("promise");
+const { HarUtils } = require("./har-utils.js");
+const { HarBuilder } = require("./har-builder.js");
+
+XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function () {
+ return Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper);
+});
+
+var uid = 1;
+
+// Helper tracer. Should be generic sharable by other modules (bug 1171927)
+const trace = {
+ log: function (...args) {
+ }
+};
+
+/**
+ * This object represents the main public API designed to access
+ * Network export logic. Clients, such as the Network panel itself,
+ * should use this API to export collected HTTP data from the panel.
+ */
+const HarExporter = {
+ // Public API
+
+ /**
+ * Save collected HTTP data from the Network panel into HAR file.
+ *
+ * @param Object options
+ * Configuration object
+ *
+ * The following options are supported:
+ *
+ * - includeResponseBodies {Boolean}: If set to true, HTTP response bodies
+ * are also included in the HAR file (can produce significantly bigger
+ * amount of data).
+ *
+ * - items {Array}: List of Network requests to be exported. It is possible
+ * to use directly: NetMonitorView.RequestsMenu.items
+ *
+ * - jsonp {Boolean}: If set to true the export format is HARP (support
+ * for JSONP syntax).
+ *
+ * - jsonpCallback {String}: Default name of JSONP callback (used for
+ * HARP format).
+ *
+ * - compress {Boolean}: If set to true the final HAR file is zipped.
+ * This represents great disk-space optimization.
+ *
+ * - defaultFileName {String}: Default name of the target HAR file.
+ * The default file name supports formatters, see:
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleFormat
+ *
+ * - defaultLogDir {String}: Default log directory for automated logs.
+ *
+ * - id {String}: ID of the page (used in the HAR file).
+ *
+ * - title {String}: Title of the page (used in the HAR file).
+ *
+ * - forceExport {Boolean}: The result HAR file is created even if
+ * there are no HTTP entries.
+ */
+ save: function (options) {
+ // Set default options related to save operation.
+ options.defaultFileName = Services.prefs.getCharPref(
+ "devtools.netmonitor.har.defaultFileName");
+ options.compress = Services.prefs.getBoolPref(
+ "devtools.netmonitor.har.compress");
+
+ // Get target file for exported data. Bail out, if the user
+ // presses cancel.
+ let file = HarUtils.getTargetFile(options.defaultFileName,
+ options.jsonp, options.compress);
+
+ if (!file) {
+ return resolve();
+ }
+
+ trace.log("HarExporter.save; " + options.defaultFileName, options);
+
+ return this.fetchHarData(options).then(jsonString => {
+ if (!HarUtils.saveToFile(file, jsonString, options.compress)) {
+ let msg = "Failed to save HAR file at: " + options.defaultFileName;
+ console.error(msg);
+ }
+ return jsonString;
+ });
+ },
+
+ /**
+ * Copy HAR string into the clipboard.
+ *
+ * @param Object options
+ * Configuration object, see save() for detailed description.
+ */
+ copy: function (options) {
+ return this.fetchHarData(options).then(jsonString => {
+ clipboardHelper.copyString(jsonString);
+ return jsonString;
+ });
+ },
+
+ // Helpers
+
+ fetchHarData: function (options) {
+ // Generate page ID
+ options.id = options.id || uid++;
+
+ // Set default generic HAR export options.
+ options.jsonp = options.jsonp ||
+ Services.prefs.getBoolPref("devtools.netmonitor.har.jsonp");
+ options.includeResponseBodies = options.includeResponseBodies ||
+ Services.prefs.getBoolPref(
+ "devtools.netmonitor.har.includeResponseBodies");
+ options.jsonpCallback = options.jsonpCallback ||
+ Services.prefs.getCharPref("devtools.netmonitor.har.jsonpCallback");
+ options.forceExport = options.forceExport ||
+ Services.prefs.getBoolPref("devtools.netmonitor.har.forceExport");
+
+ // Build HAR object.
+ return this.buildHarData(options).then(har => {
+ // Do not export an empty HAR file, unless the user
+ // explicitly says so (using the forceExport option).
+ if (!har.log.entries.length && !options.forceExport) {
+ return resolve();
+ }
+
+ let jsonString = this.stringify(har);
+ if (!jsonString) {
+ return resolve();
+ }
+
+ // If JSONP is wanted, wrap the string in a function call
+ if (options.jsonp) {
+ // This callback name is also used in HAR Viewer by default.
+ // http://www.softwareishard.com/har/viewer/
+ let callbackName = options.jsonpCallback || "onInputData";
+ jsonString = callbackName + "(" + jsonString + ");";
+ }
+
+ return jsonString;
+ }).then(null, function onError(err) {
+ console.error(err);
+ });
+ },
+
+ /**
+ * Build HAR data object. This object contains all HTTP data
+ * collected by the Network panel. The process is asynchronous
+ * since it can involve additional RDP communication (e.g. resolving
+ * long strings).
+ */
+ buildHarData: function (options) {
+ // Build HAR object from collected data.
+ let builder = new HarBuilder(options);
+ return builder.build();
+ },
+
+ /**
+ * Build JSON string from the HAR data object.
+ */
+ stringify: function (har) {
+ if (!har) {
+ return null;
+ }
+
+ try {
+ return JSON.stringify(har, null, " ");
+ } catch (err) {
+ console.error(err);
+ return undefined;
+ }
+ },
+};
+
+// Exports from this module
+exports.HarExporter = HarExporter;
diff --git a/devtools/client/netmonitor/har/har-utils.js b/devtools/client/netmonitor/har/har-utils.js
new file mode 100644
index 000000000..aa9bd3811
--- /dev/null
+++ b/devtools/client/netmonitor/har/har-utils.js
@@ -0,0 +1,189 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+/* eslint-disable mozilla/reject-some-requires */
+const { Ci, Cc, CC } = require("chrome");
+/* eslint-disable mozilla/reject-some-requires */
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "dirService", function () {
+ return Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties);
+});
+
+XPCOMUtils.defineLazyGetter(this, "ZipWriter", function () {
+ return CC("@mozilla.org/zipwriter;1", "nsIZipWriter");
+});
+
+XPCOMUtils.defineLazyGetter(this, "LocalFile", function () {
+ return new CC("@mozilla.org/file/local;1", "nsILocalFile", "initWithPath");
+});
+
+XPCOMUtils.defineLazyGetter(this, "getMostRecentBrowserWindow", function () {
+ return require("sdk/window/utils").getMostRecentBrowserWindow;
+});
+
+const nsIFilePicker = Ci.nsIFilePicker;
+
+const OPEN_FLAGS = {
+ RDONLY: parseInt("0x01", 16),
+ WRONLY: parseInt("0x02", 16),
+ CREATE_FILE: parseInt("0x08", 16),
+ APPEND: parseInt("0x10", 16),
+ TRUNCATE: parseInt("0x20", 16),
+ EXCL: parseInt("0x80", 16)
+};
+
+/**
+ * Helper API for HAR export features.
+ */
+var HarUtils = {
+ /**
+ * Open File Save As dialog and let the user pick the proper file
+ * location for generated HAR log.
+ */
+ getTargetFile: function (fileName, jsonp, compress) {
+ let browser = getMostRecentBrowserWindow();
+
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
+ fp.init(browser, null, nsIFilePicker.modeSave);
+ fp.appendFilter(
+ "HTTP Archive Files", "*.har; *.harp; *.json; *.jsonp; *.zip");
+ fp.appendFilters(nsIFilePicker.filterAll | nsIFilePicker.filterText);
+ fp.filterIndex = 1;
+
+ fp.defaultString = this.getHarFileName(fileName, jsonp, compress);
+
+ let rv = fp.show();
+ if (rv == nsIFilePicker.returnOK || rv == nsIFilePicker.returnReplace) {
+ return fp.file;
+ }
+
+ return null;
+ },
+
+ getHarFileName: function (defaultFileName, jsonp, compress) {
+ let extension = jsonp ? ".harp" : ".har";
+
+ // Read more about toLocaleFormat & format string.
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleFormat
+ let now = new Date();
+ let name = now.toLocaleFormat(defaultFileName);
+ name = name.replace(/\:/gm, "-", "");
+ name = name.replace(/\//gm, "_", "");
+
+ let fileName = name + extension;
+
+ // Default file extension is zip if compressing is on.
+ if (compress) {
+ fileName += ".zip";
+ }
+
+ return fileName;
+ },
+
+ /**
+ * Save HAR string into a given file. The file might be compressed
+ * if specified in the options.
+ *
+ * @param {File} file Target file where the HAR string (JSON)
+ * should be stored.
+ * @param {String} jsonString HAR data (JSON or JSONP)
+ * @param {Boolean} compress The result file is zipped if set to true.
+ */
+ saveToFile: function (file, jsonString, compress) {
+ let openFlags = OPEN_FLAGS.WRONLY | OPEN_FLAGS.CREATE_FILE |
+ OPEN_FLAGS.TRUNCATE;
+
+ try {
+ let foStream = Cc["@mozilla.org/network/file-output-stream;1"]
+ .createInstance(Ci.nsIFileOutputStream);
+
+ let permFlags = parseInt("0666", 8);
+ foStream.init(file, openFlags, permFlags, 0);
+
+ let convertor = Cc["@mozilla.org/intl/converter-output-stream;1"]
+ .createInstance(Ci.nsIConverterOutputStream);
+ convertor.init(foStream, "UTF-8", 0, 0);
+
+ // The entire jsonString can be huge so, write the data in chunks.
+ let chunkLength = 1024 * 1024;
+ for (let i = 0; i <= jsonString.length; i++) {
+ let data = jsonString.substr(i, chunkLength + 1);
+ if (data) {
+ convertor.writeString(data);
+ }
+
+ i = i + chunkLength;
+ }
+
+ // this closes foStream
+ convertor.close();
+ } catch (err) {
+ console.error(err);
+ return false;
+ }
+
+ // If no compressing then bail out.
+ if (!compress) {
+ return true;
+ }
+
+ // Remember name of the original file, it'll be replaced by a zip file.
+ let originalFilePath = file.path;
+ let originalFileName = file.leafName;
+
+ try {
+ // Rename using unique name (the file is going to be removed).
+ file.moveTo(null, "temp" + (new Date()).getTime() + "temphar");
+
+ // Create compressed file with the original file path name.
+ let zipFile = Cc["@mozilla.org/file/local;1"]
+ .createInstance(Ci.nsILocalFile);
+ zipFile.initWithPath(originalFilePath);
+
+ // The file within the zipped file doesn't use .zip extension.
+ let fileName = originalFileName;
+ if (fileName.indexOf(".zip") == fileName.length - 4) {
+ fileName = fileName.substr(0, fileName.indexOf(".zip"));
+ }
+
+ let zip = new ZipWriter();
+ zip.open(zipFile, openFlags);
+ zip.addEntryFile(fileName, Ci.nsIZipWriter.COMPRESSION_DEFAULT,
+ file, false);
+ zip.close();
+
+ // Remove the original file (now zipped).
+ file.remove(true);
+ return true;
+ } catch (err) {
+ console.error(err);
+
+ // Something went wrong (disk space?) rename the original file back.
+ file.moveTo(null, originalFileName);
+ }
+
+ return false;
+ },
+
+ getLocalDirectory: function (path) {
+ let dir;
+
+ if (!path) {
+ dir = dirService.get("ProfD", Ci.nsILocalFile);
+ dir.append("har");
+ dir.append("logs");
+ } else {
+ dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ dir.initWithPath(path);
+ }
+
+ return dir;
+ },
+};
+
+// Exports from this module
+exports.HarUtils = HarUtils;
diff --git a/devtools/client/netmonitor/har/moz.build b/devtools/client/netmonitor/har/moz.build
new file mode 100644
index 000000000..f6dd4aff8
--- /dev/null
+++ b/devtools/client/netmonitor/har/moz.build
@@ -0,0 +1,15 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ 'har-automation.js',
+ 'har-builder.js',
+ 'har-collector.js',
+ 'har-exporter.js',
+ 'har-utils.js',
+ 'toolbox-overlay.js',
+)
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
diff --git a/devtools/client/netmonitor/har/test/.eslintrc.js b/devtools/client/netmonitor/har/test/.eslintrc.js
new file mode 100644
index 000000000..698ae9181
--- /dev/null
+++ b/devtools/client/netmonitor/har/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/netmonitor/har/test/browser.ini b/devtools/client/netmonitor/har/test/browser.ini
new file mode 100644
index 000000000..14d4f846f
--- /dev/null
+++ b/devtools/client/netmonitor/har/test/browser.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+tags = devtools
+subsuite = clipboard
+support-files =
+ head.js
+ html_har_post-data-test-page.html
+ !/devtools/client/netmonitor/test/head.js
+ !/devtools/client/netmonitor/test/html_simple-test-page.html
+
+[browser_net_har_copy_all_as_har.js]
+[browser_net_har_post_data.js]
+[browser_net_har_throttle_upload.js]
diff --git a/devtools/client/netmonitor/har/test/browser_net_har_copy_all_as_har.js b/devtools/client/netmonitor/har/test/browser_net_har_copy_all_as_har.js
new file mode 100644
index 000000000..10df7aba6
--- /dev/null
+++ b/devtools/client/netmonitor/har/test/browser_net_har_copy_all_as_har.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Basic tests for exporting Network panel content into HAR format.
+ */
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
+
+ info("Starting test... ");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ tab.linkedBrowser.reload();
+ yield wait;
+
+ yield RequestsMenu.contextMenu.copyAllAsHar();
+
+ let jsonString = SpecialPowers.getClipboardData("text/unicode");
+ let har = JSON.parse(jsonString);
+
+ // Check out HAR log
+ isnot(har.log, null, "The HAR log must exist");
+ is(har.log.creator.name, "Firefox", "The creator field must be set");
+ is(har.log.browser.name, "Firefox", "The browser field must be set");
+ is(har.log.pages.length, 1, "There must be one page");
+ is(har.log.entries.length, 1, "There must be one request");
+
+ let entry = har.log.entries[0];
+ is(entry.request.method, "GET", "Check the method");
+ is(entry.request.url, SIMPLE_URL, "Check the URL");
+ is(entry.request.headers.length, 9, "Check number of request headers");
+ is(entry.response.status, 200, "Check response status");
+ is(entry.response.statusText, "OK", "Check response status text");
+ is(entry.response.headers.length, 6, "Check number of response headers");
+ is(entry.response.content.mimeType, // eslint-disable-line
+ "text/html", "Check response content type"); // eslint-disable-line
+ isnot(entry.response.content.text, undefined, // eslint-disable-line
+ "Check response body");
+ isnot(entry.timings, undefined, "Check timings");
+
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/har/test/browser_net_har_post_data.js b/devtools/client/netmonitor/har/test/browser_net_har_post_data.js
new file mode 100644
index 000000000..b3d611ca7
--- /dev/null
+++ b/devtools/client/netmonitor/har/test/browser_net_har_post_data.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests for exporting POST data into HAR format.
+ */
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(
+ HAR_EXAMPLE_URL + "html_har_post-data-test-page.html");
+
+ info("Starting test... ");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ // Execute one POST request on the page and wait till its done.
+ let wait = waitForNetworkEvents(monitor, 0, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.executeTest();
+ });
+ yield wait;
+
+ // Copy HAR into the clipboard (asynchronous).
+ let jsonString = yield RequestsMenu.contextMenu.copyAllAsHar();
+ let har = JSON.parse(jsonString);
+
+ // Check out the HAR log.
+ isnot(har.log, null, "The HAR log must exist");
+ is(har.log.pages.length, 1, "There must be one page");
+ is(har.log.entries.length, 1, "There must be one request");
+
+ let entry = har.log.entries[0];
+ is(entry.request.postData.mimeType, "application/json",
+ "Check post data content type");
+ is(entry.request.postData.text, "{'first': 'John', 'last': 'Doe'}",
+ "Check post data payload");
+
+ // Clean up
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/har/test/browser_net_har_throttle_upload.js b/devtools/client/netmonitor/har/test/browser_net_har_throttle_upload.js
new file mode 100644
index 000000000..c0e424172
--- /dev/null
+++ b/devtools/client/netmonitor/har/test/browser_net_har_throttle_upload.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test timing of upload when throttling.
+
+"use strict";
+
+add_task(function* () {
+ yield throttleUploadTest(true);
+ yield throttleUploadTest(false);
+});
+
+function* throttleUploadTest(actuallyThrottle) {
+ let { tab, monitor } = yield initNetMonitor(
+ HAR_EXAMPLE_URL + "html_har_post-data-test-page.html");
+
+ info("Starting test... (actuallyThrottle = " + actuallyThrottle + ")");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ const size = 4096;
+ const uploadSize = actuallyThrottle ? size / 3 : 0;
+
+ const request = {
+ "NetworkMonitor.throttleData": {
+ roundTripTimeMean: 0,
+ roundTripTimeMax: 0,
+ downloadBPSMean: 200000,
+ downloadBPSMax: 200000,
+ uploadBPSMean: uploadSize,
+ uploadBPSMax: uploadSize,
+ },
+ };
+ let client = monitor._controller.webConsoleClient;
+
+ info("sending throttle request");
+ let deferred = promise.defer();
+ client.setPreferences(request, response => {
+ deferred.resolve(response);
+ });
+ yield deferred.promise;
+
+ RequestsMenu.lazyUpdate = false;
+
+ // Execute one POST request on the page and wait till its done.
+ let wait = waitForNetworkEvents(monitor, 0, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, { size }, function* (args) {
+ content.wrappedJSObject.executeTest2(args.size);
+ });
+ yield wait;
+
+ // Copy HAR into the clipboard (asynchronous).
+ let jsonString = yield RequestsMenu.contextMenu.copyAllAsHar();
+ let har = JSON.parse(jsonString);
+
+ // Check out the HAR log.
+ isnot(har.log, null, "The HAR log must exist");
+ is(har.log.pages.length, 1, "There must be one page");
+ is(har.log.entries.length, 1, "There must be one request");
+
+ let entry = har.log.entries[0];
+ is(entry.request.postData.text, "x".repeat(size),
+ "Check post data payload");
+
+ const wasTwoSeconds = entry.timings.send >= 2000;
+ if (actuallyThrottle) {
+ ok(wasTwoSeconds, "upload should have taken more than 2 seconds");
+ } else {
+ ok(!wasTwoSeconds, "upload should not have taken more than 2 seconds");
+ }
+
+ // Clean up
+ yield teardown(monitor);
+}
diff --git a/devtools/client/netmonitor/har/test/head.js b/devtools/client/netmonitor/har/test/head.js
new file mode 100644
index 000000000..22eb87fe6
--- /dev/null
+++ b/devtools/client/netmonitor/har/test/head.js
@@ -0,0 +1,14 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
+/* import-globals-from ../../test/head.js */
+
+// Load the NetMonitor head.js file to share its API.
+var netMonitorHead = "chrome://mochitests/content/browser/devtools/client/netmonitor/test/head.js";
+Services.scriptloader.loadSubScript(netMonitorHead, this);
+
+// Directory with HAR related test files.
+const HAR_EXAMPLE_URL = "http://example.com/browser/devtools/client/netmonitor/har/test/";
diff --git a/devtools/client/netmonitor/har/test/html_har_post-data-test-page.html b/devtools/client/netmonitor/har/test/html_har_post-data-test-page.html
new file mode 100644
index 000000000..816dad08e
--- /dev/null
+++ b/devtools/client/netmonitor/har/test/html_har_post-data-test-page.html
@@ -0,0 +1,39 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor Test Page</title>
+ </head>
+
+ <body>
+ <p>HAR POST data test</p>
+
+ <script type="text/javascript">
+ function post(aAddress, aData) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", aAddress, true);
+ xhr.setRequestHeader("Content-Type", "application/json");
+ xhr.send(aData);
+ }
+
+ function executeTest() {
+ var url = "html_har_post-data-test-page.html";
+ var data = "{'first': 'John', 'last': 'Doe'}";
+ post(url, data);
+ }
+
+ function executeTest2(size) {
+ var url = "html_har_post-data-test-page.html";
+ var data = "x".repeat(size);
+ post(url, data);
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/har/toolbox-overlay.js b/devtools/client/netmonitor/har/toolbox-overlay.js
new file mode 100644
index 000000000..4ba5d08a9
--- /dev/null
+++ b/devtools/client/netmonitor/har/toolbox-overlay.js
@@ -0,0 +1,85 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const Services = require("Services");
+
+loader.lazyRequireGetter(this, "HarAutomation", "devtools/client/netmonitor/har/har-automation", true);
+
+// Map of all created overlays. There is always one instance of
+// an overlay per Toolbox instance (i.e. one per browser tab).
+const overlays = new WeakMap();
+
+/**
+ * This object is responsible for initialization and cleanup for HAR
+ * export feature. It represents an overlay for the Toolbox
+ * following the same life time by listening to its events.
+ *
+ * HAR APIs are designed for integration with tools (such as Selenium)
+ * that automates the browser. Primarily, it is for automating web apps
+ * and getting HAR file for every loaded page.
+ */
+function ToolboxOverlay(toolbox) {
+ this.toolbox = toolbox;
+
+ this.onInit = this.onInit.bind(this);
+ this.onDestroy = this.onDestroy.bind(this);
+
+ this.toolbox.on("ready", this.onInit);
+ this.toolbox.on("destroy", this.onDestroy);
+}
+
+ToolboxOverlay.prototype = {
+ /**
+ * Executed when the toolbox is ready.
+ */
+ onInit: function () {
+ let autoExport = Services.prefs.getBoolPref(
+ "devtools.netmonitor.har.enableAutoExportToFile");
+
+ if (!autoExport) {
+ return;
+ }
+
+ this.initAutomation();
+ },
+
+ /**
+ * Executed when the toolbox is destroyed.
+ */
+ onDestroy: function (eventId, toolbox) {
+ this.destroyAutomation();
+ },
+
+ // Automation
+
+ initAutomation: function () {
+ this.automation = new HarAutomation(this.toolbox);
+ },
+
+ destroyAutomation: function () {
+ if (this.automation) {
+ this.automation.destroy();
+ }
+ },
+};
+
+// Registration
+function register(toolbox) {
+ if (overlays.has(toolbox)) {
+ throw Error("There is an existing overlay for the toolbox");
+ }
+
+ // Instantiate an overlay for the toolbox.
+ let overlay = new ToolboxOverlay(toolbox);
+ overlays.set(toolbox, overlay);
+}
+
+function get(toolbox) {
+ return overlays.get(toolbox);
+}
+
+// Exports from this module
+exports.register = register;
+exports.get = get;
diff --git a/devtools/client/netmonitor/l10n.js b/devtools/client/netmonitor/l10n.js
new file mode 100644
index 000000000..3375483f0
--- /dev/null
+++ b/devtools/client/netmonitor/l10n.js
@@ -0,0 +1,9 @@
+"use strict";
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+
+const NET_STRINGS_URI = "devtools/client/locales/netmonitor.properties";
+const WEBCONSOLE_STRINGS_URI = "devtools/client/locales/webconsole.properties";
+
+exports.L10N = new LocalizationHelper(NET_STRINGS_URI);
+exports.WEBCONSOLE_L10N = new LocalizationHelper(WEBCONSOLE_STRINGS_URI);
diff --git a/devtools/client/netmonitor/moz.build b/devtools/client/netmonitor/moz.build
new file mode 100644
index 000000000..4b34b093b
--- /dev/null
+++ b/devtools/client/netmonitor/moz.build
@@ -0,0 +1,31 @@
+# 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',
+ 'har',
+ 'reducers',
+ 'selectors'
+]
+
+DevToolsModules(
+ 'constants.js',
+ 'custom-request-view.js',
+ 'events.js',
+ 'filter-predicates.js',
+ 'l10n.js',
+ 'panel.js',
+ 'performance-statistics-view.js',
+ 'prefs.js',
+ 'request-list-context-menu.js',
+ 'request-utils.js',
+ 'requests-menu-view.js',
+ 'sort-predicates.js',
+ 'store.js',
+ 'toolbar-view.js',
+)
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
diff --git a/devtools/client/netmonitor/netmonitor-controller.js b/devtools/client/netmonitor/netmonitor-controller.js
new file mode 100644
index 000000000..739e174fb
--- /dev/null
+++ b/devtools/client/netmonitor/netmonitor-controller.js
@@ -0,0 +1,816 @@
+/* -*- 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/. */
+/* globals window, document, NetMonitorView, gStore, Actions */
+/* exported loader */
+"use strict";
+
+var { utils: Cu } = Components;
+
+// Descriptions for what this frontend is currently doing.
+const ACTIVITY_TYPE = {
+ // Standing by and handling requests normally.
+ NONE: 0,
+
+ // Forcing the target to reload with cache enabled or disabled.
+ RELOAD: {
+ WITH_CACHE_ENABLED: 1,
+ WITH_CACHE_DISABLED: 2,
+ WITH_CACHE_DEFAULT: 3
+ },
+
+ // Enabling or disabling the cache without triggering a reload.
+ ENABLE_CACHE: 3,
+ DISABLE_CACHE: 4
+};
+
+var BrowserLoaderModule = {};
+Cu.import("resource://devtools/client/shared/browser-loader.js", BrowserLoaderModule);
+var { loader, require } = BrowserLoaderModule.BrowserLoader({
+ baseURI: "resource://devtools/client/netmonitor/",
+ window
+});
+
+const promise = require("promise");
+const Services = require("Services");
+/* eslint-disable mozilla/reject-some-requires */
+const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
+const EventEmitter = require("devtools/shared/event-emitter");
+const Editor = require("devtools/client/sourceeditor/editor");
+const {TimelineFront} = require("devtools/shared/fronts/timeline");
+const {Task} = require("devtools/shared/task");
+const {Prefs} = require("./prefs");
+const {EVENTS} = require("./events");
+const Actions = require("./actions/index");
+
+XPCOMUtils.defineConstant(this, "EVENTS", EVENTS);
+XPCOMUtils.defineConstant(this, "ACTIVITY_TYPE", ACTIVITY_TYPE);
+XPCOMUtils.defineConstant(this, "Editor", Editor);
+XPCOMUtils.defineConstant(this, "Prefs", Prefs);
+
+XPCOMUtils.defineLazyModuleGetter(this, "Chart",
+ "resource://devtools/client/shared/widgets/Chart.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper");
+
+Object.defineProperty(this, "NetworkHelper", {
+ get: function () {
+ return require("devtools/shared/webconsole/network-helper");
+ },
+ configurable: true,
+ enumerable: true
+});
+
+/**
+ * Object defining the network monitor controller components.
+ */
+var NetMonitorController = {
+ /**
+ * Initializes the view and connects the monitor client.
+ *
+ * @return object
+ * A promise that is resolved when the monitor finishes startup.
+ */
+ startupNetMonitor: Task.async(function* () {
+ if (this._startup) {
+ return this._startup.promise;
+ }
+ this._startup = promise.defer();
+ {
+ NetMonitorView.initialize();
+ yield this.connect();
+ }
+ this._startup.resolve();
+ return undefined;
+ }),
+
+ /**
+ * Destroys the view and disconnects the monitor client from the server.
+ *
+ * @return object
+ * A promise that is resolved when the monitor finishes shutdown.
+ */
+ shutdownNetMonitor: Task.async(function* () {
+ if (this._shutdown) {
+ return this._shutdown.promise;
+ }
+ this._shutdown = promise.defer();
+ {
+ NetMonitorView.destroy();
+ this.TargetEventsHandler.disconnect();
+ this.NetworkEventsHandler.disconnect();
+ yield this.disconnect();
+ }
+ this._shutdown.resolve();
+ return undefined;
+ }),
+
+ /**
+ * Initiates remote or chrome network monitoring based on the current target,
+ * wiring event handlers as necessary. Since the TabTarget will have already
+ * started listening to network requests by now, this is largely
+ * netmonitor-specific initialization.
+ *
+ * @return object
+ * A promise that is resolved when the monitor finishes connecting.
+ */
+ connect: Task.async(function* () {
+ if (this._connection) {
+ return this._connection.promise;
+ }
+ this._connection = promise.defer();
+
+ // Some actors like AddonActor or RootActor for chrome debugging
+ // aren't actual tabs.
+ if (this._target.isTabActor) {
+ this.tabClient = this._target.activeTab;
+ }
+
+ let connectTimeline = () => {
+ // Don't start up waiting for timeline markers if the server isn't
+ // recent enough to emit the markers we're interested in.
+ if (this._target.getTrait("documentLoadingMarkers")) {
+ this.timelineFront = new TimelineFront(this._target.client,
+ this._target.form);
+ return this.timelineFront.start({ withDocLoadingEvents: true });
+ }
+ return undefined;
+ };
+
+ this.webConsoleClient = this._target.activeConsole;
+ yield connectTimeline();
+
+ this.TargetEventsHandler.connect();
+ this.NetworkEventsHandler.connect();
+
+ window.emit(EVENTS.CONNECTED);
+
+ this._connection.resolve();
+ this._connected = true;
+ return undefined;
+ }),
+
+ /**
+ * Disconnects the debugger client and removes event handlers as necessary.
+ */
+ disconnect: Task.async(function* () {
+ if (this._disconnection) {
+ return this._disconnection.promise;
+ }
+ this._disconnection = promise.defer();
+
+ // Wait for the connection to finish first.
+ if (!this.isConnected()) {
+ yield this._connection.promise;
+ }
+
+ // When debugging local or a remote instance, the connection is closed by
+ // the RemoteTarget. The webconsole actor is stopped on disconnect.
+ this.tabClient = null;
+ this.webConsoleClient = null;
+
+ // The timeline front wasn't initialized and started if the server wasn't
+ // recent enough to emit the markers we were interested in.
+ if (this._target.getTrait("documentLoadingMarkers")) {
+ yield this.timelineFront.destroy();
+ this.timelineFront = null;
+ }
+
+ this._disconnection.resolve();
+ this._connected = false;
+ return undefined;
+ }),
+
+ /**
+ * Checks whether the netmonitor connection is active.
+ * @return boolean
+ */
+ isConnected: function () {
+ return !!this._connected;
+ },
+
+ /**
+ * Gets the activity currently performed by the frontend.
+ * @return number
+ */
+ getCurrentActivity: function () {
+ return this._currentActivity || ACTIVITY_TYPE.NONE;
+ },
+
+ /**
+ * Triggers a specific "activity" to be performed by the frontend.
+ * This can be, for example, triggering reloads or enabling/disabling cache.
+ *
+ * @param number type
+ * The activity type. See the ACTIVITY_TYPE const.
+ * @return object
+ * A promise resolved once the activity finishes and the frontend
+ * is back into "standby" mode.
+ */
+ triggerActivity: function (type) {
+ // Puts the frontend into "standby" (when there's no particular activity).
+ let standBy = () => {
+ this._currentActivity = ACTIVITY_TYPE.NONE;
+ };
+
+ // Waits for a series of "navigation start" and "navigation stop" events.
+ let waitForNavigation = () => {
+ let deferred = promise.defer();
+ this._target.once("will-navigate", () => {
+ this._target.once("navigate", () => {
+ deferred.resolve();
+ });
+ });
+ return deferred.promise;
+ };
+
+ // Reconfigures the tab, optionally triggering a reload.
+ let reconfigureTab = options => {
+ let deferred = promise.defer();
+ this._target.activeTab.reconfigure(options, deferred.resolve);
+ return deferred.promise;
+ };
+
+ // Reconfigures the tab and waits for the target to finish navigating.
+ let reconfigureTabAndWaitForNavigation = options => {
+ options.performReload = true;
+ let navigationFinished = waitForNavigation();
+ return reconfigureTab(options).then(() => navigationFinished);
+ };
+ if (type == ACTIVITY_TYPE.RELOAD.WITH_CACHE_DEFAULT) {
+ return reconfigureTabAndWaitForNavigation({}).then(standBy);
+ }
+ if (type == ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED) {
+ this._currentActivity = ACTIVITY_TYPE.ENABLE_CACHE;
+ this._target.once("will-navigate", () => {
+ this._currentActivity = type;
+ });
+ return reconfigureTabAndWaitForNavigation({
+ cacheDisabled: false,
+ performReload: true
+ }).then(standBy);
+ }
+ if (type == ACTIVITY_TYPE.RELOAD.WITH_CACHE_DISABLED) {
+ this._currentActivity = ACTIVITY_TYPE.DISABLE_CACHE;
+ this._target.once("will-navigate", () => {
+ this._currentActivity = type;
+ });
+ return reconfigureTabAndWaitForNavigation({
+ cacheDisabled: true,
+ performReload: true
+ }).then(standBy);
+ }
+ if (type == ACTIVITY_TYPE.ENABLE_CACHE) {
+ this._currentActivity = type;
+ return reconfigureTab({
+ cacheDisabled: false,
+ performReload: false
+ }).then(standBy);
+ }
+ if (type == ACTIVITY_TYPE.DISABLE_CACHE) {
+ this._currentActivity = type;
+ return reconfigureTab({
+ cacheDisabled: true,
+ performReload: false
+ }).then(standBy);
+ }
+ this._currentActivity = ACTIVITY_TYPE.NONE;
+ return promise.reject(new Error("Invalid activity type"));
+ },
+
+ /**
+ * Selects the specified request in the waterfall and opens the details view.
+ *
+ * @param string requestId
+ * The actor ID of the request to inspect.
+ * @return object
+ * A promise resolved once the task finishes.
+ */
+ inspectRequest: function (requestId) {
+ // Look for the request in the existing ones or wait for it to appear, if
+ // the network monitor is still loading.
+ let deferred = promise.defer();
+ let request = null;
+ let inspector = function () {
+ let predicate = i => i.value === requestId;
+ request = NetMonitorView.RequestsMenu.getItemForPredicate(predicate);
+ if (!request) {
+ // Reset filters so that the request is visible.
+ gStore.dispatch(Actions.toggleFilterType("all"));
+ request = NetMonitorView.RequestsMenu.getItemForPredicate(predicate);
+ }
+
+ // If the request was found, select it. Otherwise this function will be
+ // called again once new requests arrive.
+ if (request) {
+ window.off(EVENTS.REQUEST_ADDED, inspector);
+ NetMonitorView.RequestsMenu.selectedItem = request;
+ deferred.resolve();
+ }
+ };
+
+ inspector();
+ if (!request) {
+ window.on(EVENTS.REQUEST_ADDED, inspector);
+ }
+ return deferred.promise;
+ },
+
+ /**
+ * Getter that tells if the server supports sending custom network requests.
+ * @type boolean
+ */
+ get supportsCustomRequest() {
+ return this.webConsoleClient &&
+ (this.webConsoleClient.traits.customNetworkRequest ||
+ !this._target.isApp);
+ },
+
+ /**
+ * Getter that tells if the server includes the transferred (compressed /
+ * encoded) response size.
+ * @type boolean
+ */
+ get supportsTransferredResponseSize() {
+ return this.webConsoleClient &&
+ this.webConsoleClient.traits.transferredResponseSize;
+ },
+
+ /**
+ * Getter that tells if the server can do network performance statistics.
+ * @type boolean
+ */
+ get supportsPerfStats() {
+ return this.tabClient &&
+ (this.tabClient.traits.reconfigure || !this._target.isApp);
+ },
+
+ /**
+ * Open a given source in Debugger
+ */
+ viewSourceInDebugger(sourceURL, sourceLine) {
+ return this._toolbox.viewSourceInDebugger(sourceURL, sourceLine);
+ }
+};
+
+/**
+ * Functions handling target-related lifetime events.
+ */
+function TargetEventsHandler() {
+ this._onTabNavigated = this._onTabNavigated.bind(this);
+ this._onTabDetached = this._onTabDetached.bind(this);
+}
+
+TargetEventsHandler.prototype = {
+ get target() {
+ return NetMonitorController._target;
+ },
+
+ /**
+ * Listen for events emitted by the current tab target.
+ */
+ connect: function () {
+ dumpn("TargetEventsHandler is connecting...");
+ this.target.on("close", this._onTabDetached);
+ this.target.on("navigate", this._onTabNavigated);
+ this.target.on("will-navigate", this._onTabNavigated);
+ },
+
+ /**
+ * Remove events emitted by the current tab target.
+ */
+ disconnect: function () {
+ if (!this.target) {
+ return;
+ }
+ dumpn("TargetEventsHandler is disconnecting...");
+ this.target.off("close", this._onTabDetached);
+ this.target.off("navigate", this._onTabNavigated);
+ this.target.off("will-navigate", this._onTabNavigated);
+ },
+
+ /**
+ * Called for each location change in the monitored tab.
+ *
+ * @param string type
+ * Packet type.
+ * @param object packet
+ * Packet received from the server.
+ */
+ _onTabNavigated: function (type, packet) {
+ switch (type) {
+ case "will-navigate": {
+ // Reset UI.
+ if (!Services.prefs.getBoolPref("devtools.webconsole.persistlog")) {
+ NetMonitorView.RequestsMenu.reset();
+ NetMonitorView.Sidebar.toggle(false);
+ }
+ // Switch to the default network traffic inspector view.
+ if (NetMonitorController.getCurrentActivity() == ACTIVITY_TYPE.NONE) {
+ NetMonitorView.showNetworkInspectorView();
+ }
+ // Clear any accumulated markers.
+ NetMonitorController.NetworkEventsHandler.clearMarkers();
+
+ window.emit(EVENTS.TARGET_WILL_NAVIGATE);
+ break;
+ }
+ case "navigate": {
+ window.emit(EVENTS.TARGET_DID_NAVIGATE);
+ break;
+ }
+ }
+ },
+
+ /**
+ * Called when the monitored tab is closed.
+ */
+ _onTabDetached: function () {
+ NetMonitorController.shutdownNetMonitor();
+ }
+};
+
+/**
+ * Functions handling target network events.
+ */
+function NetworkEventsHandler() {
+ this._markers = [];
+
+ this._onNetworkEvent = this._onNetworkEvent.bind(this);
+ this._onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this);
+ this._onDocLoadingMarker = this._onDocLoadingMarker.bind(this);
+ this._onRequestHeaders = this._onRequestHeaders.bind(this);
+ this._onRequestCookies = this._onRequestCookies.bind(this);
+ this._onRequestPostData = this._onRequestPostData.bind(this);
+ this._onResponseHeaders = this._onResponseHeaders.bind(this);
+ this._onResponseCookies = this._onResponseCookies.bind(this);
+ this._onResponseContent = this._onResponseContent.bind(this);
+ this._onEventTimings = this._onEventTimings.bind(this);
+}
+
+NetworkEventsHandler.prototype = {
+ get client() {
+ return NetMonitorController._target.client;
+ },
+
+ get webConsoleClient() {
+ return NetMonitorController.webConsoleClient;
+ },
+
+ get timelineFront() {
+ return NetMonitorController.timelineFront;
+ },
+
+ get firstDocumentDOMContentLoadedTimestamp() {
+ let marker = this._markers.filter(e => {
+ return e.name == "document::DOMContentLoaded";
+ })[0];
+
+ return marker ? marker.unixTime / 1000 : -1;
+ },
+
+ get firstDocumentLoadTimestamp() {
+ let marker = this._markers.filter(e => e.name == "document::Load")[0];
+ return marker ? marker.unixTime / 1000 : -1;
+ },
+
+ /**
+ * Connect to the current target client.
+ */
+ connect: function () {
+ dumpn("NetworkEventsHandler is connecting...");
+ this.webConsoleClient.on("networkEvent", this._onNetworkEvent);
+ this.webConsoleClient.on("networkEventUpdate", this._onNetworkEventUpdate);
+
+ if (this.timelineFront) {
+ this.timelineFront.on("doc-loading", this._onDocLoadingMarker);
+ }
+
+ this._displayCachedEvents();
+ },
+
+ /**
+ * Disconnect from the client.
+ */
+ disconnect: function () {
+ if (!this.client) {
+ return;
+ }
+ dumpn("NetworkEventsHandler is disconnecting...");
+ this.webConsoleClient.off("networkEvent", this._onNetworkEvent);
+ this.webConsoleClient.off("networkEventUpdate", this._onNetworkEventUpdate);
+
+ if (this.timelineFront) {
+ this.timelineFront.off("doc-loading", this._onDocLoadingMarker);
+ }
+ },
+
+ /**
+ * Display any network events already in the cache.
+ */
+ _displayCachedEvents: function () {
+ for (let cachedEvent of this.webConsoleClient.getNetworkEvents()) {
+ // First add the request to the timeline.
+ this._onNetworkEvent("networkEvent", cachedEvent);
+ // Then replay any updates already received.
+ for (let update of cachedEvent.updates) {
+ this._onNetworkEventUpdate("networkEventUpdate", {
+ packet: {
+ updateType: update
+ },
+ networkInfo: cachedEvent
+ });
+ }
+ }
+ },
+
+ /**
+ * The "DOMContentLoaded" and "Load" events sent by the timeline actor.
+ * @param object marker
+ */
+ _onDocLoadingMarker: function (marker) {
+ window.emit(EVENTS.TIMELINE_EVENT, marker);
+ this._markers.push(marker);
+ },
+
+ /**
+ * The "networkEvent" message type handler.
+ *
+ * @param string type
+ * Message type.
+ * @param object networkInfo
+ * The network request information.
+ */
+ _onNetworkEvent: function (type, networkInfo) {
+ let { actor,
+ startedDateTime,
+ request: { method, url },
+ isXHR,
+ cause,
+ fromCache,
+ fromServiceWorker
+ } = networkInfo;
+
+ NetMonitorView.RequestsMenu.addRequest(
+ actor, startedDateTime, method, url, isXHR, cause, fromCache,
+ fromServiceWorker
+ );
+ window.emit(EVENTS.NETWORK_EVENT, actor);
+ },
+
+ /**
+ * The "networkEventUpdate" message type handler.
+ *
+ * @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 }) {
+ let { actor } = networkInfo;
+
+ switch (packet.updateType) {
+ case "requestHeaders":
+ this.webConsoleClient.getRequestHeaders(actor, this._onRequestHeaders);
+ window.emit(EVENTS.UPDATING_REQUEST_HEADERS, actor);
+ break;
+ case "requestCookies":
+ this.webConsoleClient.getRequestCookies(actor, this._onRequestCookies);
+ window.emit(EVENTS.UPDATING_REQUEST_COOKIES, actor);
+ break;
+ case "requestPostData":
+ this.webConsoleClient.getRequestPostData(actor,
+ this._onRequestPostData);
+ window.emit(EVENTS.UPDATING_REQUEST_POST_DATA, actor);
+ break;
+ case "securityInfo":
+ NetMonitorView.RequestsMenu.updateRequest(actor, {
+ securityState: networkInfo.securityInfo,
+ });
+ this.webConsoleClient.getSecurityInfo(actor, this._onSecurityInfo);
+ window.emit(EVENTS.UPDATING_SECURITY_INFO, actor);
+ break;
+ case "responseHeaders":
+ this.webConsoleClient.getResponseHeaders(actor,
+ this._onResponseHeaders);
+ window.emit(EVENTS.UPDATING_RESPONSE_HEADERS, actor);
+ break;
+ case "responseCookies":
+ this.webConsoleClient.getResponseCookies(actor,
+ this._onResponseCookies);
+ window.emit(EVENTS.UPDATING_RESPONSE_COOKIES, actor);
+ break;
+ case "responseStart":
+ NetMonitorView.RequestsMenu.updateRequest(actor, {
+ httpVersion: networkInfo.response.httpVersion,
+ remoteAddress: networkInfo.response.remoteAddress,
+ remotePort: networkInfo.response.remotePort,
+ status: networkInfo.response.status,
+ statusText: networkInfo.response.statusText,
+ headersSize: networkInfo.response.headersSize
+ });
+ window.emit(EVENTS.STARTED_RECEIVING_RESPONSE, actor);
+ break;
+ case "responseContent":
+ NetMonitorView.RequestsMenu.updateRequest(actor, {
+ contentSize: networkInfo.response.bodySize,
+ transferredSize: networkInfo.response.transferredSize,
+ mimeType: networkInfo.response.content.mimeType
+ });
+ this.webConsoleClient.getResponseContent(actor,
+ this._onResponseContent);
+ window.emit(EVENTS.UPDATING_RESPONSE_CONTENT, actor);
+ break;
+ case "eventTimings":
+ NetMonitorView.RequestsMenu.updateRequest(actor, {
+ totalTime: networkInfo.totalTime
+ });
+ this.webConsoleClient.getEventTimings(actor, this._onEventTimings);
+ window.emit(EVENTS.UPDATING_EVENT_TIMINGS, actor);
+ break;
+ }
+ },
+
+ /**
+ * Handles additional information received for a "requestHeaders" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ _onRequestHeaders: function (response) {
+ NetMonitorView.RequestsMenu.updateRequest(response.from, {
+ requestHeaders: response
+ }, () => {
+ window.emit(EVENTS.RECEIVED_REQUEST_HEADERS, response.from);
+ });
+ },
+
+ /**
+ * Handles additional information received for a "requestCookies" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ _onRequestCookies: function (response) {
+ NetMonitorView.RequestsMenu.updateRequest(response.from, {
+ requestCookies: response
+ }, () => {
+ window.emit(EVENTS.RECEIVED_REQUEST_COOKIES, response.from);
+ });
+ },
+
+ /**
+ * Handles additional information received for a "requestPostData" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ _onRequestPostData: function (response) {
+ NetMonitorView.RequestsMenu.updateRequest(response.from, {
+ requestPostData: response
+ }, () => {
+ window.emit(EVENTS.RECEIVED_REQUEST_POST_DATA, response.from);
+ });
+ },
+
+ /**
+ * Handles additional information received for a "securityInfo" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ _onSecurityInfo: function (response) {
+ NetMonitorView.RequestsMenu.updateRequest(response.from, {
+ securityInfo: response.securityInfo
+ }, () => {
+ window.emit(EVENTS.RECEIVED_SECURITY_INFO, response.from);
+ });
+ },
+
+ /**
+ * Handles additional information received for a "responseHeaders" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ _onResponseHeaders: function (response) {
+ NetMonitorView.RequestsMenu.updateRequest(response.from, {
+ responseHeaders: response
+ }, () => {
+ window.emit(EVENTS.RECEIVED_RESPONSE_HEADERS, response.from);
+ });
+ },
+
+ /**
+ * Handles additional information received for a "responseCookies" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ _onResponseCookies: function (response) {
+ NetMonitorView.RequestsMenu.updateRequest(response.from, {
+ responseCookies: response
+ }, () => {
+ window.emit(EVENTS.RECEIVED_RESPONSE_COOKIES, response.from);
+ });
+ },
+
+ /**
+ * Handles additional information received for a "responseContent" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ _onResponseContent: function (response) {
+ NetMonitorView.RequestsMenu.updateRequest(response.from, {
+ responseContent: response
+ }, () => {
+ window.emit(EVENTS.RECEIVED_RESPONSE_CONTENT, response.from);
+ });
+ },
+
+ /**
+ * Handles additional information received for a "eventTimings" packet.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ _onEventTimings: function (response) {
+ NetMonitorView.RequestsMenu.updateRequest(response.from, {
+ eventTimings: response
+ }, () => {
+ window.emit(EVENTS.RECEIVED_EVENT_TIMINGS, response.from);
+ });
+ },
+
+ /**
+ * Clears all accumulated markers.
+ */
+ clearMarkers: function () {
+ this._markers.length = 0;
+ },
+
+ /**
+ * Fetches the full text of a LongString.
+ *
+ * @param object | string stringGrip
+ * The long string grip containing the corresponding actor.
+ * If you pass in a plain string (by accident or because you're lazy),
+ * then a promise of the same string is simply returned.
+ * @return object Promise
+ * A promise that is resolved when the full string contents
+ * are available, or rejected if something goes wrong.
+ */
+ getString: function (stringGrip) {
+ return this.webConsoleClient.getString(stringGrip);
+ }
+};
+
+/**
+ * Returns true if this is document is in RTL mode.
+ * @return boolean
+ */
+XPCOMUtils.defineLazyGetter(window, "isRTL", function () {
+ return window.getComputedStyle(document.documentElement, null)
+ .direction == "rtl";
+});
+
+/**
+ * Convenient way of emitting events from the panel window.
+ */
+EventEmitter.decorate(this);
+
+/**
+ * Preliminary setup for the NetMonitorController object.
+ */
+NetMonitorController.TargetEventsHandler = new TargetEventsHandler();
+NetMonitorController.NetworkEventsHandler = new NetworkEventsHandler();
+
+/**
+ * Export some properties to the global scope for easier access.
+ */
+Object.defineProperties(window, {
+ "gNetwork": {
+ get: function () {
+ return NetMonitorController.NetworkEventsHandler;
+ },
+ configurable: true
+ }
+});
+
+/**
+ * Helper method for debugging.
+ * @param string
+ */
+function dumpn(str) {
+ if (wantLogging) {
+ dump("NET-FRONTEND: " + str + "\n");
+ }
+}
+
+var wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");
diff --git a/devtools/client/netmonitor/netmonitor-view.js b/devtools/client/netmonitor/netmonitor-view.js
new file mode 100644
index 000000000..68470f7a9
--- /dev/null
+++ b/devtools/client/netmonitor/netmonitor-view.js
@@ -0,0 +1,1230 @@
+/* -*- 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/. */
+/* import-globals-from ./netmonitor-controller.js */
+/* globals Prefs, gNetwork, setInterval, setTimeout, clearInterval, clearTimeout, btoa */
+/* exported $, $all */
+"use strict";
+
+XPCOMUtils.defineLazyGetter(this, "NetworkHelper", function () {
+ return require("devtools/shared/webconsole/network-helper");
+});
+
+/* eslint-disable mozilla/reject-some-requires */
+const {VariablesView} = require("resource://devtools/client/shared/widgets/VariablesView.jsm");
+/* eslint-disable mozilla/reject-some-requires */
+const {VariablesViewController} = require("resource://devtools/client/shared/widgets/VariablesViewController.jsm");
+const {ToolSidebar} = require("devtools/client/framework/sidebar");
+const {testing: isTesting} = require("devtools/shared/flags");
+const {ViewHelpers, Heritage} = require("devtools/client/shared/widgets/view-helpers");
+const {Filters} = require("./filter-predicates");
+const {getFormDataSections,
+ formDataURI,
+ getUriHostPort} = require("./request-utils");
+const {L10N} = require("./l10n");
+const {RequestsMenuView} = require("./requests-menu-view");
+const {CustomRequestView} = require("./custom-request-view");
+const {ToolbarView} = require("./toolbar-view");
+const {configureStore} = require("./store");
+const {PerformanceStatisticsView} = require("./performance-statistics-view");
+
+// Initialize the global redux variables
+var gStore = configureStore();
+
+// ms
+const WDA_DEFAULT_VERIFY_INTERVAL = 50;
+
+// Use longer timeout during testing as the tests need this process to succeed
+// and two seconds is quite short on slow debug builds. The timeout here should
+// be at least equal to the general mochitest timeout of 45 seconds so that this
+// never gets hit during testing.
+// ms
+const WDA_DEFAULT_GIVE_UP_TIMEOUT = isTesting ? 45000 : 2000;
+
+// 100 KB in bytes
+const SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 102400;
+const HEADERS_SIZE_DECIMALS = 3;
+const CONTENT_MIME_TYPE_MAPPINGS = {
+ "/ecmascript": Editor.modes.js,
+ "/javascript": Editor.modes.js,
+ "/x-javascript": Editor.modes.js,
+ "/html": Editor.modes.html,
+ "/xhtml": Editor.modes.html,
+ "/xml": Editor.modes.html,
+ "/atom": Editor.modes.html,
+ "/soap": Editor.modes.html,
+ "/vnd.mpeg.dash.mpd": Editor.modes.html,
+ "/rdf": Editor.modes.css,
+ "/rss": Editor.modes.css,
+ "/css": Editor.modes.css
+};
+
+const DEFAULT_EDITOR_CONFIG = {
+ mode: Editor.modes.text,
+ readOnly: true,
+ lineNumbers: true
+};
+const GENERIC_VARIABLES_VIEW_SETTINGS = {
+ lazyEmpty: true,
+ // ms
+ lazyEmptyDelay: 10,
+ searchEnabled: true,
+ editableValueTooltip: "",
+ editableNameTooltip: "",
+ preventDisableOnChange: true,
+ preventDescriptorModifiers: true,
+ eval: () => {}
+};
+
+/**
+ * Object defining the network monitor view components.
+ */
+var NetMonitorView = {
+ /**
+ * Initializes the network monitor view.
+ */
+ initialize: function () {
+ this._initializePanes();
+
+ this.Toolbar.initialize(gStore);
+ this.RequestsMenu.initialize(gStore);
+ this.NetworkDetails.initialize();
+ this.CustomRequest.initialize();
+ this.PerformanceStatistics.initialize(gStore);
+ },
+
+ /**
+ * Destroys the network monitor view.
+ */
+ destroy: function () {
+ this._isDestroyed = true;
+ this.Toolbar.destroy();
+ this.RequestsMenu.destroy();
+ this.NetworkDetails.destroy();
+ this.CustomRequest.destroy();
+
+ this._destroyPanes();
+ },
+
+ /**
+ * Initializes the UI for all the displayed panes.
+ */
+ _initializePanes: function () {
+ dumpn("Initializing the NetMonitorView panes");
+
+ this._body = $("#body");
+ this._detailsPane = $("#details-pane");
+
+ this._detailsPane.setAttribute("width", Prefs.networkDetailsWidth);
+ this._detailsPane.setAttribute("height", Prefs.networkDetailsHeight);
+ this.toggleDetailsPane({ visible: false });
+
+ // Disable the performance statistics mode.
+ if (!Prefs.statistics) {
+ $("#request-menu-context-perf").hidden = true;
+ $("#notice-perf-message").hidden = true;
+ $("#requests-menu-network-summary-button").hidden = true;
+ }
+ },
+
+ /**
+ * Destroys the UI for all the displayed panes.
+ */
+ _destroyPanes: Task.async(function* () {
+ dumpn("Destroying the NetMonitorView panes");
+
+ Prefs.networkDetailsWidth = this._detailsPane.getAttribute("width");
+ Prefs.networkDetailsHeight = this._detailsPane.getAttribute("height");
+
+ this._detailsPane = null;
+
+ for (let p of this._editorPromises.values()) {
+ let editor = yield p;
+ editor.destroy();
+ }
+ }),
+
+ /**
+ * Gets the visibility state of the network details pane.
+ * @return boolean
+ */
+ get detailsPaneHidden() {
+ return this._detailsPane.classList.contains("pane-collapsed");
+ },
+
+ /**
+ * Sets the network details pane hidden or visible.
+ *
+ * @param object flags
+ * An object containing some of the following properties:
+ * - visible: true if the pane should be shown, false to hide
+ * - animated: true to display an animation on toggle
+ * - delayed: true to wait a few cycles before toggle
+ * - callback: a function to invoke when the toggle finishes
+ * @param number tabIndex [optional]
+ * The index of the intended selected tab in the details pane.
+ */
+ toggleDetailsPane: function (flags, tabIndex) {
+ ViewHelpers.togglePane(flags, this._detailsPane);
+
+ if (flags.visible) {
+ this._body.classList.remove("pane-collapsed");
+ gStore.dispatch(Actions.showSidebar(true));
+ } else {
+ this._body.classList.add("pane-collapsed");
+ gStore.dispatch(Actions.showSidebar(false));
+ }
+
+ if (tabIndex !== undefined) {
+ $("#event-details-pane").selectedIndex = tabIndex;
+ }
+ },
+
+ /**
+ * Gets the current mode for this tool.
+ * @return string (e.g, "network-inspector-view" or "network-statistics-view")
+ */
+ get currentFrontendMode() {
+ // The getter may be called from a timeout after the panel is destroyed.
+ if (!this._body.selectedPanel) {
+ return null;
+ }
+ return this._body.selectedPanel.id;
+ },
+
+ /**
+ * Toggles between the frontend view modes ("Inspector" vs. "Statistics").
+ */
+ toggleFrontendMode: function () {
+ if (this.currentFrontendMode != "network-inspector-view") {
+ this.showNetworkInspectorView();
+ } else {
+ this.showNetworkStatisticsView();
+ }
+ },
+
+ /**
+ * Switches to the "Inspector" frontend view mode.
+ */
+ showNetworkInspectorView: function () {
+ this._body.selectedPanel = $("#network-inspector-view");
+ this.RequestsMenu._flushWaterfallViews(true);
+ },
+
+ /**
+ * Switches to the "Statistics" frontend view mode.
+ */
+ showNetworkStatisticsView: function () {
+ this._body.selectedPanel = $("#network-statistics-view");
+
+ let controller = NetMonitorController;
+ let requestsView = this.RequestsMenu;
+ let statisticsView = this.PerformanceStatistics;
+
+ Task.spawn(function* () {
+ statisticsView.displayPlaceholderCharts();
+ yield controller.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED);
+
+ try {
+ // • The response headers and status code are required for determining
+ // whether a response is "fresh" (cacheable).
+ // • The response content size and request total time are necessary for
+ // populating the statistics view.
+ // • The response mime type is used for categorization.
+ yield whenDataAvailable(requestsView, [
+ "responseHeaders", "status", "contentSize", "mimeType", "totalTime"
+ ]);
+ } catch (ex) {
+ // Timed out while waiting for data. Continue with what we have.
+ console.error(ex);
+ }
+
+ statisticsView.createPrimedCacheChart(requestsView.items);
+ statisticsView.createEmptyCacheChart(requestsView.items);
+ });
+ },
+
+ reloadPage: function () {
+ NetMonitorController.triggerActivity(
+ ACTIVITY_TYPE.RELOAD.WITH_CACHE_DEFAULT);
+ },
+
+ /**
+ * Lazily initializes and returns a promise for a Editor instance.
+ *
+ * @param string id
+ * The id of the editor placeholder node.
+ * @return object
+ * A promise that is resolved when the editor is available.
+ */
+ editor: function (id) {
+ dumpn("Getting a NetMonitorView editor: " + id);
+
+ if (this._editorPromises.has(id)) {
+ return this._editorPromises.get(id);
+ }
+
+ let deferred = promise.defer();
+ this._editorPromises.set(id, deferred.promise);
+
+ // Initialize the source editor and store the newly created instance
+ // in the ether of a resolved promise's value.
+ let editor = new Editor(DEFAULT_EDITOR_CONFIG);
+ editor.appendTo($(id)).then(() => deferred.resolve(editor));
+
+ return deferred.promise;
+ },
+
+ _body: null,
+ _detailsPane: null,
+ _editorPromises: new Map()
+};
+
+/**
+ * Functions handling the sidebar details view.
+ */
+function SidebarView() {
+ dumpn("SidebarView was instantiated");
+}
+
+SidebarView.prototype = {
+ /**
+ * Sets this view hidden or visible. It's visible by default.
+ *
+ * @param boolean visibleFlag
+ * Specifies the intended visibility.
+ */
+ toggle: function (visibleFlag) {
+ NetMonitorView.toggleDetailsPane({ visible: visibleFlag });
+ NetMonitorView.RequestsMenu._flushWaterfallViews(true);
+ },
+
+ /**
+ * Populates this view with the specified data.
+ *
+ * @param object data
+ * The data source (this should be the attachment of a request item).
+ * @return object
+ * Returns a promise that resolves upon population of the subview.
+ */
+ populate: Task.async(function* (data) {
+ let isCustom = data.isCustom;
+ let view = isCustom ?
+ NetMonitorView.CustomRequest :
+ NetMonitorView.NetworkDetails;
+
+ yield view.populate(data);
+ $("#details-pane").selectedIndex = isCustom ? 0 : 1;
+
+ window.emit(EVENTS.SIDEBAR_POPULATED);
+ })
+};
+
+/**
+ * Functions handling the requests details view.
+ */
+function NetworkDetailsView() {
+ dumpn("NetworkDetailsView was instantiated");
+
+ // The ToolSidebar requires the panel object to be able to emit events.
+ EventEmitter.decorate(this);
+
+ this._onTabSelect = this._onTabSelect.bind(this);
+}
+
+NetworkDetailsView.prototype = {
+ /**
+ * An object containing the state of tabs.
+ */
+ _viewState: {
+ // if updating[tab] is true a task is currently updating the given tab.
+ updating: [],
+ // if dirty[tab] is true, the tab needs to be repopulated once current
+ // update task finishes
+ dirty: [],
+ // the most recently received attachment data for the request
+ latestData: null,
+ },
+
+ /**
+ * Initialization function, called when the network monitor is started.
+ */
+ initialize: function () {
+ dumpn("Initializing the NetworkDetailsView");
+
+ this.widget = $("#event-details-pane");
+ this.sidebar = new ToolSidebar(this.widget, this, "netmonitor", {
+ disableTelemetry: true,
+ showAllTabsMenu: true
+ });
+
+ this._headers = new VariablesView($("#all-headers"),
+ Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
+ emptyText: L10N.getStr("headersEmptyText"),
+ searchPlaceholder: L10N.getStr("headersFilterText")
+ }));
+ this._cookies = new VariablesView($("#all-cookies"),
+ Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
+ emptyText: L10N.getStr("cookiesEmptyText"),
+ searchPlaceholder: L10N.getStr("cookiesFilterText")
+ }));
+ this._params = new VariablesView($("#request-params"),
+ Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
+ emptyText: L10N.getStr("paramsEmptyText"),
+ searchPlaceholder: L10N.getStr("paramsFilterText")
+ }));
+ this._json = new VariablesView($("#response-content-json"),
+ Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
+ onlyEnumVisible: true,
+ searchPlaceholder: L10N.getStr("jsonFilterText")
+ }));
+ VariablesViewController.attach(this._json);
+
+ this._paramsQueryString = L10N.getStr("paramsQueryString");
+ this._paramsFormData = L10N.getStr("paramsFormData");
+ this._paramsPostPayload = L10N.getStr("paramsPostPayload");
+ this._requestHeaders = L10N.getStr("requestHeaders");
+ this._requestHeadersFromUpload = L10N.getStr("requestHeadersFromUpload");
+ this._responseHeaders = L10N.getStr("responseHeaders");
+ this._requestCookies = L10N.getStr("requestCookies");
+ this._responseCookies = L10N.getStr("responseCookies");
+
+ $("tabpanels", this.widget).addEventListener("select", this._onTabSelect);
+ },
+
+ /**
+ * Destruction function, called when the network monitor is closed.
+ */
+ destroy: function () {
+ dumpn("Destroying the NetworkDetailsView");
+ this.sidebar.destroy();
+ $("tabpanels", this.widget).removeEventListener("select",
+ this._onTabSelect);
+ },
+
+ /**
+ * Populates this view with the specified data.
+ *
+ * @param object data
+ * The data source (this should be the attachment of a request item).
+ * @return object
+ * Returns a promise that resolves upon population the view.
+ */
+ populate: function (data) {
+ $("#request-params-box").setAttribute("flex", "1");
+ $("#request-params-box").hidden = false;
+ $("#request-post-data-textarea-box").hidden = true;
+ $("#response-content-info-header").hidden = true;
+ $("#response-content-json-box").hidden = true;
+ $("#response-content-textarea-box").hidden = true;
+ $("#raw-headers").hidden = true;
+ $("#response-content-image-box").hidden = true;
+
+ let isHtml = Filters.html(data);
+
+ // Show the "Preview" tabpanel only for plain HTML responses.
+ this.sidebar.toggleTab(isHtml, "preview-tab");
+
+ // Show the "Security" tab only for requests that
+ // 1) are https (state != insecure)
+ // 2) come from a target that provides security information.
+ let hasSecurityInfo = data.securityState &&
+ data.securityState !== "insecure";
+ this.sidebar.toggleTab(hasSecurityInfo, "security-tab");
+
+ // Switch to the "Headers" tabpanel if the "Preview" previously selected
+ // and this is not an HTML response or "Security" was selected but this
+ // request has no security information.
+
+ if (!isHtml && this.widget.selectedPanel === $("#preview-tabpanel") ||
+ !hasSecurityInfo && this.widget.selectedPanel ===
+ $("#security-tabpanel")) {
+ this.widget.selectedIndex = 0;
+ }
+
+ this._headers.empty();
+ this._cookies.empty();
+ this._params.empty();
+ this._json.empty();
+
+ this._dataSrc = { src: data, populated: [] };
+ this._onTabSelect();
+ window.emit(EVENTS.NETWORKDETAILSVIEW_POPULATED);
+
+ return promise.resolve();
+ },
+
+ /**
+ * Listener handling the tab selection event.
+ */
+ _onTabSelect: function () {
+ let { src, populated } = this._dataSrc || {};
+ let tab = this.widget.selectedIndex;
+ let view = this;
+
+ // Make sure the data source is valid and don't populate the same tab twice.
+ if (!src || populated[tab]) {
+ return;
+ }
+
+ let viewState = this._viewState;
+ if (viewState.updating[tab]) {
+ // A task is currently updating this tab. If we started another update
+ // task now it would result in a duplicated content as described in bugs
+ // 997065 and 984687. As there's no way to stop the current task mark the
+ // tab dirty and refresh the panel once the current task finishes.
+ viewState.dirty[tab] = true;
+ viewState.latestData = src;
+ return;
+ }
+
+ Task.spawn(function* () {
+ viewState.updating[tab] = true;
+ switch (tab) {
+ // "Headers"
+ case 0:
+ yield view._setSummary(src);
+ yield view._setResponseHeaders(src.responseHeaders);
+ yield view._setRequestHeaders(
+ src.requestHeaders,
+ src.requestHeadersFromUploadStream);
+ break;
+ // "Cookies"
+ case 1:
+ yield view._setResponseCookies(src.responseCookies);
+ yield view._setRequestCookies(src.requestCookies);
+ break;
+ // "Params"
+ case 2:
+ yield view._setRequestGetParams(src.url);
+ yield view._setRequestPostParams(
+ src.requestHeaders,
+ src.requestHeadersFromUploadStream,
+ src.requestPostData);
+ break;
+ // "Response"
+ case 3:
+ yield view._setResponseBody(src.url, src.responseContent);
+ break;
+ // "Timings"
+ case 4:
+ yield view._setTimingsInformation(src.eventTimings);
+ break;
+ // "Security"
+ case 5:
+ yield view._setSecurityInfo(src.securityInfo, src.url);
+ break;
+ // "Preview"
+ case 6:
+ yield view._setHtmlPreview(src.responseContent);
+ break;
+ }
+ viewState.updating[tab] = false;
+ }).then(() => {
+ if (tab == this.widget.selectedIndex) {
+ if (viewState.dirty[tab]) {
+ // The request information was updated while the task was running.
+ viewState.dirty[tab] = false;
+ view.populate(viewState.latestData);
+ } else {
+ // Tab is selected but not dirty. We're done here.
+ populated[tab] = true;
+ window.emit(EVENTS.TAB_UPDATED);
+
+ if (NetMonitorController.isConnected()) {
+ NetMonitorView.RequestsMenu.ensureSelectedItemIsVisible();
+ }
+ }
+ } else if (viewState.dirty[tab]) {
+ // Tab is dirty but no longer selected. Don't refresh it now, it'll be
+ // done if the tab is shown again.
+ viewState.dirty[tab] = false;
+ }
+ }, e => console.error(e));
+ },
+
+ /**
+ * Sets the network request summary shown in this view.
+ *
+ * @param object data
+ * The data source (this should be the attachment of a request item).
+ */
+ _setSummary: function (data) {
+ if (data.url) {
+ let unicodeUrl = NetworkHelper.convertToUnicode(unescape(data.url));
+ $("#headers-summary-url-value").setAttribute("value", unicodeUrl);
+ $("#headers-summary-url-value").setAttribute("tooltiptext", unicodeUrl);
+ $("#headers-summary-url").removeAttribute("hidden");
+ } else {
+ $("#headers-summary-url").setAttribute("hidden", "true");
+ }
+
+ if (data.method) {
+ $("#headers-summary-method-value").setAttribute("value", data.method);
+ $("#headers-summary-method").removeAttribute("hidden");
+ } else {
+ $("#headers-summary-method").setAttribute("hidden", "true");
+ }
+
+ if (data.remoteAddress) {
+ let address = data.remoteAddress;
+ if (address.indexOf(":") != -1) {
+ address = `[${address}]`;
+ }
+ if (data.remotePort) {
+ address += `:${data.remotePort}`;
+ }
+ $("#headers-summary-address-value").setAttribute("value", address);
+ $("#headers-summary-address-value").setAttribute("tooltiptext", address);
+ $("#headers-summary-address").removeAttribute("hidden");
+ } else {
+ $("#headers-summary-address").setAttribute("hidden", "true");
+ }
+
+ if (data.status) {
+ // "code" attribute is only used by css to determine the icon color
+ let code;
+ if (data.fromCache) {
+ code = "cached";
+ } else if (data.fromServiceWorker) {
+ code = "service worker";
+ } else {
+ code = data.status;
+ }
+ $("#headers-summary-status-circle").setAttribute("code", code);
+ $("#headers-summary-status-value").setAttribute("value",
+ data.status + " " + data.statusText);
+ $("#headers-summary-status").removeAttribute("hidden");
+ } else {
+ $("#headers-summary-status").setAttribute("hidden", "true");
+ }
+
+ if (data.httpVersion) {
+ $("#headers-summary-version-value").setAttribute("value",
+ data.httpVersion);
+ $("#headers-summary-version").removeAttribute("hidden");
+ } else {
+ $("#headers-summary-version").setAttribute("hidden", "true");
+ }
+ },
+
+ /**
+ * Sets the network request headers shown in this view.
+ *
+ * @param object headers
+ * The "requestHeaders" message received from the server.
+ * @param object uploadHeaders
+ * The "requestHeadersFromUploadStream" inferred from the POST payload.
+ * @return object
+ * A promise that resolves when request headers are set.
+ */
+ _setRequestHeaders: Task.async(function* (headers, uploadHeaders) {
+ if (headers && headers.headers.length) {
+ yield this._addHeaders(this._requestHeaders, headers);
+ }
+ if (uploadHeaders && uploadHeaders.headers.length) {
+ yield this._addHeaders(this._requestHeadersFromUpload, uploadHeaders);
+ }
+ }),
+
+ /**
+ * Sets the network response headers shown in this view.
+ *
+ * @param object response
+ * The message received from the server.
+ * @return object
+ * A promise that resolves when response headers are set.
+ */
+ _setResponseHeaders: Task.async(function* (response) {
+ if (response && response.headers.length) {
+ response.headers.sort((a, b) => a.name > b.name);
+ yield this._addHeaders(this._responseHeaders, response);
+ }
+ }),
+
+ /**
+ * Populates the headers container in this view with the specified data.
+ *
+ * @param string name
+ * The type of headers to populate (request or response).
+ * @param object response
+ * The message received from the server.
+ * @return object
+ * A promise that resolves when headers are added.
+ */
+ _addHeaders: Task.async(function* (name, response) {
+ let kb = response.headersSize / 1024;
+ let size = L10N.numberWithDecimals(kb, HEADERS_SIZE_DECIMALS);
+ let text = L10N.getFormatStr("networkMenu.sizeKB", size);
+
+ let headersScope = this._headers.addScope(name + " (" + text + ")");
+ headersScope.expanded = true;
+
+ for (let header of response.headers) {
+ let headerVar = headersScope.addItem(header.name, {}, {relaxed: true});
+ let headerValue = yield gNetwork.getString(header.value);
+ headerVar.setGrip(headerValue);
+ }
+ }),
+
+ /**
+ * Sets the network request cookies shown in this view.
+ *
+ * @param object response
+ * The message received from the server.
+ * @return object
+ * A promise that is resolved when the request cookies are set.
+ */
+ _setRequestCookies: Task.async(function* (response) {
+ if (response && response.cookies.length) {
+ response.cookies.sort((a, b) => a.name > b.name);
+ yield this._addCookies(this._requestCookies, response);
+ }
+ }),
+
+ /**
+ * Sets the network response cookies shown in this view.
+ *
+ * @param object response
+ * The message received from the server.
+ * @return object
+ * A promise that is resolved when the response cookies are set.
+ */
+ _setResponseCookies: Task.async(function* (response) {
+ if (response && response.cookies.length) {
+ yield this._addCookies(this._responseCookies, response);
+ }
+ }),
+
+ /**
+ * Populates the cookies container in this view with the specified data.
+ *
+ * @param string name
+ * The type of cookies to populate (request or response).
+ * @param object response
+ * The message received from the server.
+ * @return object
+ * Returns a promise that resolves upon the adding of cookies.
+ */
+ _addCookies: Task.async(function* (name, response) {
+ let cookiesScope = this._cookies.addScope(name);
+ cookiesScope.expanded = true;
+
+ for (let cookie of response.cookies) {
+ let cookieVar = cookiesScope.addItem(cookie.name, {}, {relaxed: true});
+ let cookieValue = yield gNetwork.getString(cookie.value);
+ cookieVar.setGrip(cookieValue);
+
+ // By default the cookie name and value are shown. If this is the only
+ // information available, then nothing else is to be displayed.
+ let cookieProps = Object.keys(cookie);
+ if (cookieProps.length == 2) {
+ continue;
+ }
+
+ // Display any other information other than the cookie name and value
+ // which may be available.
+ let rawObject = Object.create(null);
+ let otherProps = cookieProps.filter(e => e != "name" && e != "value");
+ for (let prop of otherProps) {
+ rawObject[prop] = cookie[prop];
+ }
+ cookieVar.populate(rawObject);
+ cookieVar.twisty = true;
+ cookieVar.expanded = true;
+ }
+ }),
+
+ /**
+ * Sets the network request get params shown in this view.
+ *
+ * @param string url
+ * The request's url.
+ */
+ _setRequestGetParams: function (url) {
+ let query = NetworkHelper.nsIURL(url).query;
+ if (query) {
+ this._addParams(this._paramsQueryString, query);
+ }
+ },
+
+ /**
+ * Sets the network request post params shown in this view.
+ *
+ * @param object headers
+ * The "requestHeaders" message received from the server.
+ * @param object uploadHeaders
+ * The "requestHeadersFromUploadStream" inferred from the POST payload.
+ * @param object postData
+ * The "requestPostData" message received from the server.
+ * @return object
+ * A promise that is resolved when the request post params are set.
+ */
+ _setRequestPostParams: Task.async(function* (headers, uploadHeaders,
+ postData) {
+ if (!headers || !uploadHeaders || !postData) {
+ return;
+ }
+
+ let formDataSections = yield getFormDataSections(
+ headers,
+ uploadHeaders,
+ postData,
+ gNetwork.getString.bind(gNetwork));
+
+ this._params.onlyEnumVisible = false;
+
+ // Handle urlencoded form data sections (e.g. "?foo=bar&baz=42").
+ if (formDataSections.length > 0) {
+ formDataSections.forEach(section => {
+ this._addParams(this._paramsFormData, section);
+ });
+ } else {
+ // Handle JSON and actual forms ("multipart/form-data" content type).
+ let postDataLongString = postData.postData.text;
+ let text = yield gNetwork.getString(postDataLongString);
+ let jsonVal = null;
+ try {
+ jsonVal = JSON.parse(text);
+ } catch (ex) { // eslint-disable-line
+ }
+
+ if (jsonVal) {
+ this._params.onlyEnumVisible = true;
+ let jsonScopeName = L10N.getStr("jsonScopeName");
+ let jsonScope = this._params.addScope(jsonScopeName);
+ jsonScope.expanded = true;
+ let jsonItem = jsonScope.addItem(undefined, { enumerable: true });
+ jsonItem.populate(jsonVal, { sorted: true });
+ } else {
+ // This is really awkward, but hey, it works. Let's show an empty
+ // scope in the params view and place the source editor containing
+ // the raw post data directly underneath.
+ $("#request-params-box").removeAttribute("flex");
+ let paramsScope = this._params.addScope(this._paramsPostPayload);
+ paramsScope.expanded = true;
+ paramsScope.locked = true;
+
+ $("#request-post-data-textarea-box").hidden = false;
+ let editor = yield NetMonitorView.editor("#request-post-data-textarea");
+ editor.setMode(Editor.modes.text);
+ editor.setText(text);
+ }
+ }
+
+ window.emit(EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+ }),
+
+ /**
+ * Populates the params container in this view with the specified data.
+ *
+ * @param string name
+ * The type of params to populate (get or post).
+ * @param string queryString
+ * A query string of params (e.g. "?foo=bar&baz=42").
+ */
+ _addParams: function (name, queryString) {
+ let paramsArray = NetworkHelper.parseQueryString(queryString);
+ if (!paramsArray) {
+ return;
+ }
+ let paramsScope = this._params.addScope(name);
+ paramsScope.expanded = true;
+
+ for (let param of paramsArray) {
+ let paramVar = paramsScope.addItem(param.name, {}, {relaxed: true});
+ paramVar.setGrip(param.value);
+ }
+ },
+
+ /**
+ * Sets the network response body shown in this view.
+ *
+ * @param string url
+ * The request's url.
+ * @param object response
+ * The message received from the server.
+ * @return object
+ * A promise that is resolved when the response body is set.
+ */
+ _setResponseBody: Task.async(function* (url, response) {
+ if (!response) {
+ return;
+ }
+ let { mimeType, text, encoding } = response.content;
+ let responseBody = yield gNetwork.getString(text);
+
+ // Handle json, which we tentatively identify by checking the MIME type
+ // for "json" after any word boundary. This works for the standard
+ // "application/json", and also for custom types like "x-bigcorp-json".
+ // Additionally, we also directly parse the response text content to
+ // verify whether it's json or not, to handle responses incorrectly
+ // labeled as text/plain instead.
+ let jsonMimeType, jsonObject, jsonObjectParseError;
+ try {
+ jsonMimeType = /\bjson/.test(mimeType);
+ jsonObject = JSON.parse(responseBody);
+ } catch (e) {
+ jsonObjectParseError = e;
+ }
+ if (jsonMimeType || jsonObject) {
+ // Extract the actual json substring in case this might be a "JSONP".
+ // This regex basically parses a function call and captures the
+ // function name and arguments in two separate groups.
+ let jsonpRegex = /^\s*([\w$]+)\s*\(\s*([^]*)\s*\)\s*;?\s*$/;
+ let [_, callbackPadding, jsonpString] = // eslint-disable-line
+ responseBody.match(jsonpRegex) || [];
+
+ // Make sure this is a valid JSON object first. If so, nicely display
+ // the parsing results in a variables view. Otherwise, simply show
+ // the contents as plain text.
+ if (callbackPadding && jsonpString) {
+ try {
+ jsonObject = JSON.parse(jsonpString);
+ } catch (e) {
+ jsonObjectParseError = e;
+ }
+ }
+
+ // Valid JSON or JSONP.
+ if (jsonObject) {
+ $("#response-content-json-box").hidden = false;
+ let jsonScopeName = callbackPadding
+ ? L10N.getFormatStr("jsonpScopeName", callbackPadding)
+ : L10N.getStr("jsonScopeName");
+
+ let jsonVar = { label: jsonScopeName, rawObject: jsonObject };
+ yield this._json.controller.setSingleVariable(jsonVar).expanded;
+ } else {
+ // Malformed JSON.
+ $("#response-content-textarea-box").hidden = false;
+ let infoHeader = $("#response-content-info-header");
+ infoHeader.setAttribute("value", jsonObjectParseError);
+ infoHeader.setAttribute("tooltiptext", jsonObjectParseError);
+ infoHeader.hidden = false;
+
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+ editor.setMode(Editor.modes.js);
+ editor.setText(responseBody);
+ }
+ } else if (mimeType.includes("image/")) {
+ // Handle images.
+ $("#response-content-image-box").setAttribute("align", "center");
+ $("#response-content-image-box").setAttribute("pack", "center");
+ $("#response-content-image-box").hidden = false;
+ $("#response-content-image").src = formDataURI(mimeType, encoding, responseBody);
+
+ // Immediately display additional information about the image:
+ // file name, mime type and encoding.
+ $("#response-content-image-name-value").setAttribute("value",
+ NetworkHelper.nsIURL(url).fileName);
+ $("#response-content-image-mime-value").setAttribute("value", mimeType);
+
+ // Wait for the image to load in order to display the width and height.
+ $("#response-content-image").onload = e => {
+ // XUL images are majestic so they don't bother storing their dimensions
+ // in width and height attributes like the rest of the folk. Hack around
+ // this by getting the bounding client rect and subtracting the margins.
+ let { width, height } = e.target.getBoundingClientRect();
+ let dimensions = (width - 2) + " \u00D7 " + (height - 2);
+ $("#response-content-image-dimensions-value").setAttribute("value",
+ dimensions);
+ };
+ } else {
+ $("#response-content-textarea-box").hidden = false;
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+ editor.setMode(Editor.modes.text);
+ editor.setText(responseBody);
+
+ // Maybe set a more appropriate mode in the Source Editor if possible,
+ // but avoid doing this for very large files.
+ if (responseBody.length < SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE) {
+ let mapping = Object.keys(CONTENT_MIME_TYPE_MAPPINGS).find(key => {
+ return mimeType.includes(key);
+ });
+
+ if (mapping) {
+ editor.setMode(CONTENT_MIME_TYPE_MAPPINGS[mapping]);
+ }
+ }
+ }
+
+ window.emit(EVENTS.RESPONSE_BODY_DISPLAYED);
+ }),
+
+ /**
+ * Sets the timings information shown in this view.
+ *
+ * @param object response
+ * The message received from the server.
+ */
+ _setTimingsInformation: function (response) {
+ if (!response) {
+ return;
+ }
+ let { blocked, dns, connect, send, wait, receive } = response.timings;
+
+ let tabboxWidth = $("#details-pane").getAttribute("width");
+
+ // Other nodes also take some space.
+ let availableWidth = tabboxWidth / 2;
+ let scale = (response.totalTime > 0 ?
+ Math.max(availableWidth / response.totalTime, 0) :
+ 0);
+
+ $("#timings-summary-blocked .requests-menu-timings-box")
+ .setAttribute("width", blocked * scale);
+ $("#timings-summary-blocked .requests-menu-timings-total")
+ .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", blocked));
+
+ $("#timings-summary-dns .requests-menu-timings-box")
+ .setAttribute("width", dns * scale);
+ $("#timings-summary-dns .requests-menu-timings-total")
+ .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", dns));
+
+ $("#timings-summary-connect .requests-menu-timings-box")
+ .setAttribute("width", connect * scale);
+ $("#timings-summary-connect .requests-menu-timings-total")
+ .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", connect));
+
+ $("#timings-summary-send .requests-menu-timings-box")
+ .setAttribute("width", send * scale);
+ $("#timings-summary-send .requests-menu-timings-total")
+ .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", send));
+
+ $("#timings-summary-wait .requests-menu-timings-box")
+ .setAttribute("width", wait * scale);
+ $("#timings-summary-wait .requests-menu-timings-total")
+ .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", wait));
+
+ $("#timings-summary-receive .requests-menu-timings-box")
+ .setAttribute("width", receive * scale);
+ $("#timings-summary-receive .requests-menu-timings-total")
+ .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", receive));
+
+ $("#timings-summary-dns .requests-menu-timings-box")
+ .style.transform = "translateX(" + (scale * blocked) + "px)";
+ $("#timings-summary-connect .requests-menu-timings-box")
+ .style.transform = "translateX(" + (scale * (blocked + dns)) + "px)";
+ $("#timings-summary-send .requests-menu-timings-box")
+ .style.transform =
+ "translateX(" + (scale * (blocked + dns + connect)) + "px)";
+ $("#timings-summary-wait .requests-menu-timings-box")
+ .style.transform =
+ "translateX(" + (scale * (blocked + dns + connect + send)) + "px)";
+ $("#timings-summary-receive .requests-menu-timings-box")
+ .style.transform =
+ "translateX(" + (scale * (blocked + dns + connect + send + wait)) +
+ "px)";
+
+ $("#timings-summary-dns .requests-menu-timings-total")
+ .style.transform = "translateX(" + (scale * blocked) + "px)";
+ $("#timings-summary-connect .requests-menu-timings-total")
+ .style.transform = "translateX(" + (scale * (blocked + dns)) + "px)";
+ $("#timings-summary-send .requests-menu-timings-total")
+ .style.transform =
+ "translateX(" + (scale * (blocked + dns + connect)) + "px)";
+ $("#timings-summary-wait .requests-menu-timings-total")
+ .style.transform =
+ "translateX(" + (scale * (blocked + dns + connect + send)) + "px)";
+ $("#timings-summary-receive .requests-menu-timings-total")
+ .style.transform =
+ "translateX(" + (scale * (blocked + dns + connect + send + wait)) +
+ "px)";
+ },
+
+ /**
+ * Sets the preview for HTML responses shown in this view.
+ *
+ * @param object response
+ * The message received from the server.
+ * @return object
+ * A promise that is resolved when the html preview is rendered.
+ */
+ _setHtmlPreview: Task.async(function* (response) {
+ if (!response) {
+ return promise.resolve();
+ }
+ let { text } = response.content;
+ let responseBody = yield gNetwork.getString(text);
+
+ // Always disable JS when previewing HTML responses.
+ let iframe = $("#response-preview");
+ iframe.contentDocument.docShell.allowJavascript = false;
+ iframe.contentDocument.documentElement.innerHTML = responseBody;
+
+ window.emit(EVENTS.RESPONSE_HTML_PREVIEW_DISPLAYED);
+ return undefined;
+ }),
+
+ /**
+ * Sets the security information shown in this view.
+ *
+ * @param object securityInfo
+ * The data received from server
+ * @param string url
+ * The URL of this request
+ * @return object
+ * A promise that is resolved when the security info is rendered.
+ */
+ _setSecurityInfo: Task.async(function* (securityInfo, url) {
+ if (!securityInfo) {
+ // We don't have security info. This could mean one of two things:
+ // 1) This connection is not secure and this tab is not visible and thus
+ // we shouldn't be here.
+ // 2) We have already received securityState and the tab is visible BUT
+ // the rest of the information is still on its way. Once it arrives
+ // this method is called again.
+ return;
+ }
+
+ /**
+ * A helper that sets value and tooltiptext attributes of an element to
+ * specified value.
+ *
+ * @param string selector
+ * A selector for the element.
+ * @param string value
+ * The value to set. If this evaluates to false a placeholder string
+ * <Not Available> is used instead.
+ */
+ function setValue(selector, value) {
+ let label = $(selector);
+ if (!value) {
+ label.setAttribute("value", L10N.getStr(
+ "netmonitor.security.notAvailable"));
+ label.setAttribute("tooltiptext", label.getAttribute("value"));
+ } else {
+ label.setAttribute("value", value);
+ label.setAttribute("tooltiptext", value);
+ }
+ }
+
+ let errorbox = $("#security-error");
+ let infobox = $("#security-information");
+
+ if (securityInfo.state === "secure" || securityInfo.state === "weak") {
+ infobox.hidden = false;
+ errorbox.hidden = true;
+
+ // Warning icons
+ let cipher = $("#security-warning-cipher");
+
+ if (securityInfo.state === "weak") {
+ cipher.hidden = securityInfo.weaknessReasons.indexOf("cipher") === -1;
+ } else {
+ cipher.hidden = true;
+ }
+
+ let enabledLabel = L10N.getStr("netmonitor.security.enabled");
+ let disabledLabel = L10N.getStr("netmonitor.security.disabled");
+
+ // Connection parameters
+ setValue("#security-protocol-version-value",
+ securityInfo.protocolVersion);
+ setValue("#security-ciphersuite-value", securityInfo.cipherSuite);
+
+ // Host header
+ let domain = getUriHostPort(url);
+ let hostHeader = L10N.getFormatStr("netmonitor.security.hostHeader",
+ domain);
+ setValue("#security-info-host-header", hostHeader);
+
+ // Parameters related to the domain
+ setValue("#security-http-strict-transport-security-value",
+ securityInfo.hsts ? enabledLabel : disabledLabel);
+
+ setValue("#security-public-key-pinning-value",
+ securityInfo.hpkp ? enabledLabel : disabledLabel);
+
+ // Certificate parameters
+ let cert = securityInfo.cert;
+ setValue("#security-cert-subject-cn", cert.subject.commonName);
+ setValue("#security-cert-subject-o", cert.subject.organization);
+ setValue("#security-cert-subject-ou", cert.subject.organizationalUnit);
+
+ setValue("#security-cert-issuer-cn", cert.issuer.commonName);
+ setValue("#security-cert-issuer-o", cert.issuer.organization);
+ setValue("#security-cert-issuer-ou", cert.issuer.organizationalUnit);
+
+ setValue("#security-cert-validity-begins", cert.validity.start);
+ setValue("#security-cert-validity-expires", cert.validity.end);
+
+ setValue("#security-cert-sha1-fingerprint", cert.fingerprint.sha1);
+ setValue("#security-cert-sha256-fingerprint", cert.fingerprint.sha256);
+ } else {
+ infobox.hidden = true;
+ errorbox.hidden = false;
+
+ // Strip any HTML from the message.
+ let plain = new DOMParser().parseFromString(securityInfo.errorMessage,
+ "text/html");
+ setValue("#security-error-message", plain.body.textContent);
+ }
+ }),
+
+ _dataSrc: null,
+ _headers: null,
+ _cookies: null,
+ _params: null,
+ _json: null,
+ _paramsQueryString: "",
+ _paramsFormData: "",
+ _paramsPostPayload: "",
+ _requestHeaders: "",
+ _responseHeaders: "",
+ _requestCookies: "",
+ _responseCookies: ""
+};
+
+/**
+ * DOM query helper.
+ * TODO: Move it into "dom-utils.js" module and "require" it when needed.
+ */
+var $ = (selector, target = document) => target.querySelector(selector);
+var $all = (selector, target = document) => target.querySelectorAll(selector);
+
+/**
+ * Makes sure certain properties are available on all objects in a data store.
+ *
+ * @param array dataStore
+ * The request view object from which to fetch the item list.
+ * @param array mandatoryFields
+ * A list of strings representing properties of objects in dataStore.
+ * @return object
+ * A promise resolved when all objects in dataStore contain the
+ * properties defined in mandatoryFields.
+ */
+function whenDataAvailable(requestsView, mandatoryFields) {
+ let deferred = promise.defer();
+
+ let interval = setInterval(() => {
+ const { attachments } = requestsView;
+ if (attachments.length > 0 && attachments.every(item => {
+ return mandatoryFields.every(field => field in item);
+ })) {
+ clearInterval(interval);
+ clearTimeout(timer);
+ deferred.resolve();
+ }
+ }, WDA_DEFAULT_VERIFY_INTERVAL);
+
+ let timer = setTimeout(() => {
+ clearInterval(interval);
+ deferred.reject(new Error("Timed out while waiting for data"));
+ }, WDA_DEFAULT_GIVE_UP_TIMEOUT);
+
+ return deferred.promise;
+}
+
+/**
+ * Preliminary setup for the NetMonitorView object.
+ */
+NetMonitorView.Toolbar = new ToolbarView();
+NetMonitorView.RequestsMenu = new RequestsMenuView();
+NetMonitorView.Sidebar = new SidebarView();
+NetMonitorView.CustomRequest = new CustomRequestView();
+NetMonitorView.NetworkDetails = new NetworkDetailsView();
+NetMonitorView.PerformanceStatistics = new PerformanceStatisticsView();
diff --git a/devtools/client/netmonitor/netmonitor.xul b/devtools/client/netmonitor/netmonitor.xul
new file mode 100644
index 000000000..bb580f7ad
--- /dev/null
+++ b/devtools/client/netmonitor/netmonitor.xul
@@ -0,0 +1,741 @@
+<?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/. -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/netmonitor.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+
+ <script type="application/javascript;version=1.8"
+ src="chrome://devtools/content/shared/theme-switching.js"/>
+ <script type="text/javascript" src="netmonitor-controller.js"/>
+ <script type="text/javascript" src="netmonitor-view.js"/>
+
+ <deck id="body"
+ class="theme-sidebar"
+ flex="1"
+ data-localization-bundle="devtools/client/locales/netmonitor.properties">
+
+ <vbox id="network-inspector-view" flex="1">
+ <hbox id="netmonitor-toolbar" class="devtools-toolbar">
+ <html:div xmlns="http://www.w3.org/1999/xhtml"
+ id="react-clear-button-hook"/>
+ <html:div xmlns="http://www.w3.org/1999/xhtml"
+ id="react-filter-buttons-hook"/>
+ <spacer id="requests-menu-spacer"
+ flex="1"/>
+ <toolbarbutton id="requests-menu-network-summary-button"
+ class="devtools-toolbarbutton icon-and-text"
+ data-localization="tooltiptext=netmonitor.toolbar.perf"/>
+ <html:div xmlns="http://www.w3.org/1999/xhtml"
+ id="react-search-box-hook"/>
+ <html:div xmlns="http://www.w3.org/1999/xhtml"
+ id="react-details-pane-toggle-hook"/>
+ </hbox>
+ <hbox id="network-table-and-sidebar"
+ class="devtools-responsive-container"
+ flex="1">
+ <vbox id="network-table" flex="1" class="devtools-main-content">
+ <toolbar id="requests-menu-toolbar"
+ class="devtools-toolbar"
+ align="center">
+ <hbox id="toolbar-labels" flex="1">
+ <hbox id="requests-menu-status-header-box"
+ class="requests-menu-header requests-menu-status"
+ align="center">
+ <button id="requests-menu-status-button"
+ class="requests-menu-header-button requests-menu-status"
+ data-key="status"
+ data-localization="label=netmonitor.toolbar.status3"
+ flex="1">
+ </button>
+ </hbox>
+ <hbox id="requests-menu-method-header-box"
+ class="requests-menu-header requests-menu-method"
+ align="center">
+ <button id="requests-menu-method-button"
+ class="requests-menu-header-button requests-menu-method"
+ data-key="method"
+ data-localization="label=netmonitor.toolbar.method"
+ crop="end"
+ flex="1">
+ </button>
+ </hbox>
+ <hbox id="requests-menu-icon-and-file-header-box"
+ class="requests-menu-header requests-menu-icon-and-file"
+ align="center">
+ <button id="requests-menu-file-button"
+ class="requests-menu-header-button requests-menu-file"
+ data-key="file"
+ data-localization="label=netmonitor.toolbar.file"
+ crop="end"
+ flex="1">
+ </button>
+ </hbox>
+ <hbox id="requests-menu-domain-header-box"
+ class="requests-menu-header requests-menu-security-and-domain"
+ align="center">
+ <button id="requests-menu-domain-button"
+ class="requests-menu-header-button requests-menu-security-and-domain"
+ data-key="domain"
+ data-localization="label=netmonitor.toolbar.domain"
+ crop="end"
+ flex="1">
+ </button>
+ </hbox>
+ <hbox id="requests-menu-cause-header-box"
+ class="requests-menu-header requests-menu-cause"
+ align="center">
+ <button id="requests-menu-cause-button"
+ class="requests-menu-header-button requests-menu-cause"
+ data-key="cause"
+ data-localization="label=netmonitor.toolbar.cause"
+ crop="end"
+ flex="1">
+ </button>
+ </hbox>
+ <hbox id="requests-menu-type-header-box"
+ class="requests-menu-header requests-menu-type"
+ align="center">
+ <button id="requests-menu-type-button"
+ class="requests-menu-header-button requests-menu-type"
+ data-key="type"
+ data-localization="label=netmonitor.toolbar.type"
+ crop="end"
+ flex="1">
+ </button>
+ </hbox>
+ <hbox id="requests-menu-transferred-header-box"
+ class="requests-menu-header requests-menu-transferred"
+ align="center">
+ <button id="requests-menu-transferred-button"
+ class="requests-menu-header-button requests-menu-transferred"
+ data-key="transferred"
+ data-localization="label=netmonitor.toolbar.transferred"
+ crop="end"
+ flex="1">
+ </button>
+ </hbox>
+ <hbox id="requests-menu-size-header-box"
+ class="requests-menu-header requests-menu-size"
+ align="center">
+ <button id="requests-menu-size-button"
+ class="requests-menu-header-button requests-menu-size"
+ data-key="size"
+ data-localization="label=netmonitor.toolbar.size"
+ crop="end"
+ flex="1">
+ </button>
+ </hbox>
+ <hbox id="requests-menu-waterfall-header-box"
+ class="requests-menu-header requests-menu-waterfall"
+ align="center"
+ flex="1">
+ <button id="requests-menu-waterfall-button"
+ class="requests-menu-header-button requests-menu-waterfall"
+ data-key="waterfall"
+ pack="start"
+ data-localization="label=netmonitor.toolbar.waterfall"
+ flex="1">
+ <image id="requests-menu-waterfall-image"/>
+ <box id="requests-menu-waterfall-label-wrapper">
+ <label id="requests-menu-waterfall-label"
+ class="plain requests-menu-waterfall"
+ data-localization="value=netmonitor.toolbar.waterfall"/>
+ </box>
+ </button>
+ </hbox>
+ </hbox>
+ </toolbar>
+
+ <vbox id="requests-menu-empty-notice"
+ class="side-menu-widget-empty-text">
+ <hbox id="notice-reload-message" align="center">
+ <label data-localization="content=netmonitor.reloadNotice1"/>
+ <button id="requests-menu-reload-notice-button"
+ class="devtools-toolbarbutton"
+ standalone="true"
+ data-localization="label=netmonitor.reloadNotice2"/>
+ <label data-localization="content=netmonitor.reloadNotice3"/>
+ </hbox>
+ <hbox id="notice-perf-message" align="center">
+ <label data-localization="content=netmonitor.perfNotice1"/>
+ <button id="requests-menu-perf-notice-button"
+ class="devtools-toolbarbutton"
+ standalone="true"
+ data-localization="tooltiptext=netmonitor.perfNotice3"/>
+ <label data-localization="content=netmonitor.perfNotice2"/>
+ </hbox>
+ </vbox>
+
+ <vbox id="requests-menu-contents" flex="1">
+ <hbox id="requests-menu-item-template" hidden="true">
+ <hbox class="requests-menu-subitem requests-menu-status"
+ align="center">
+ <box class="requests-menu-status-icon"/>
+ <label class="plain requests-menu-status-code"
+ crop="end"/>
+ </hbox>
+ <hbox class="requests-menu-subitem requests-menu-method-box"
+ align="center">
+ <label class="plain requests-menu-method"
+ crop="end"
+ flex="1"/>
+ </hbox>
+ <hbox class="requests-menu-subitem requests-menu-icon-and-file"
+ align="center">
+ <image class="requests-menu-icon" hidden="true"/>
+ <label class="plain requests-menu-file"
+ crop="end"
+ flex="1"/>
+ </hbox>
+ <hbox class="requests-menu-subitem requests-menu-security-and-domain"
+ align="center">
+ <image class="requests-security-state-icon" />
+ <label class="plain requests-menu-domain"
+ crop="end"
+ flex="1"/>
+ </hbox>
+ <hbox class="requests-menu-subitem requests-menu-cause" align="center">
+ <label class="requests-menu-cause-stack" value="JS" hidden="true"/>
+ <label class="plain requests-menu-cause-label" flex="1" crop="end"/>
+ </hbox>
+ <label class="plain requests-menu-subitem requests-menu-type"
+ crop="end"/>
+ <label class="plain requests-menu-subitem requests-menu-transferred"
+ crop="end"/>
+ <label class="plain requests-menu-subitem requests-menu-size"
+ crop="end"/>
+ <hbox class="requests-menu-subitem requests-menu-waterfall"
+ align="center"
+ flex="1">
+ <hbox class="requests-menu-timings"
+ align="center">
+ <label class="plain requests-menu-timings-total"/>
+ </hbox>
+ </hbox>
+ </hbox>
+ </vbox>
+ </vbox>
+
+ <splitter id="network-inspector-view-splitter"
+ class="devtools-side-splitter"/>
+
+ <deck id="details-pane"
+ hidden="true">
+ <vbox id="custom-pane"
+ class="tabpanel-content">
+ <hbox align="baseline">
+ <label data-localization="content=netmonitor.custom.newRequest"
+ class="plain tabpanel-summary-label
+ custom-header"/>
+ <hbox flex="1" pack="end"
+ class="devtools-toolbarbutton-group">
+ <button id="custom-request-send-button"
+ class="devtools-toolbarbutton"
+ data-localization="label=netmonitor.custom.send"/>
+ <button id="custom-request-close-button"
+ class="devtools-toolbarbutton"
+ data-localization="label=netmonitor.custom.cancel"/>
+ </hbox>
+ </hbox>
+ <hbox id="custom-method-and-url"
+ class="tabpanel-summary-container"
+ align="center">
+ <textbox id="custom-method-value"
+ data-key="method"/>
+ <textbox id="custom-url-value"
+ flex="1"
+ data-key="url"/>
+ </hbox>
+ <vbox id="custom-query"
+ class="tabpanel-summary-container custom-section">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.custom.query"/>
+ <textbox id="custom-query-value"
+ class="tabpanel-summary-input"
+ multiline="true"
+ rows="4"
+ wrap="off"
+ data-key="query"/>
+ </vbox>
+ <vbox id="custom-headers"
+ class="tabpanel-summary-container custom-section">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.custom.headers"/>
+ <textbox id="custom-headers-value"
+ class="tabpanel-summary-input"
+ multiline="true"
+ rows="8"
+ wrap="off"
+ data-key="headers"/>
+ </vbox>
+ <vbox id="custom-postdata"
+ class="tabpanel-summary-container custom-section">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.custom.postData"/>
+ <textbox id="custom-postdata-value"
+ class="tabpanel-summary-input"
+ multiline="true"
+ rows="6"
+ wrap="off"
+ data-key="body"/>
+ </vbox>
+ </vbox>
+ <tabbox id="event-details-pane"
+ class="devtools-sidebar-tabs"
+ handleCtrlTab="false">
+ <tabs>
+ <tab id="headers-tab"
+ crop="end"
+ data-localization="label=netmonitor.tab.headers"/>
+ <tab id="cookies-tab"
+ crop="end"
+ data-localization="label=netmonitor.tab.cookies"/>
+ <tab id="params-tab"
+ crop="end"
+ data-localization="label=netmonitor.tab.params"/>
+ <tab id="response-tab"
+ crop="end"
+ data-localization="label=netmonitor.tab.response"/>
+ <tab id="timings-tab"
+ crop="end"
+ data-localization="label=netmonitor.tab.timings"/>
+ <tab id="security-tab"
+ crop="end"
+ data-localization="label=netmonitor.tab.security"/>
+ <tab id="preview-tab"
+ crop="end"
+ data-localization="label=netmonitor.tab.preview"/>
+ </tabs>
+ <tabpanels flex="1">
+ <tabpanel id="headers-tabpanel"
+ class="tabpanel-content">
+ <vbox flex="1">
+ <hbox id="headers-summary-url"
+ class="tabpanel-summary-container"
+ align="center">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.summary.url"/>
+ <textbox id="headers-summary-url-value"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ <hbox id="headers-summary-method"
+ class="tabpanel-summary-container"
+ align="center">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.summary.method"/>
+ <label id="headers-summary-method-value"
+ class="plain tabpanel-summary-value devtools-monospace"
+ crop="end"
+ flex="1"/>
+ </hbox>
+ <hbox id="headers-summary-address"
+ class="tabpanel-summary-container"
+ align="center">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.summary.address"/>
+ <textbox id="headers-summary-address-value"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ <hbox id="headers-summary-status"
+ class="tabpanel-summary-container"
+ align="center">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.summary.status"/>
+ <box id="headers-summary-status-circle"
+ class="requests-menu-status-icon"/>
+ <label id="headers-summary-status-value"
+ class="plain tabpanel-summary-value devtools-monospace"
+ crop="end"
+ flex="1"/>
+ <button id="headers-summary-resend"
+ class="devtools-toolbarbutton"
+ data-localization="label=netmonitor.summary.editAndResend"/>
+ <button id="toggle-raw-headers"
+ class="devtools-toolbarbutton"
+ data-localization="label=netmonitor.summary.rawHeaders"/>
+ </hbox>
+ <hbox id="headers-summary-version"
+ class="tabpanel-summary-container"
+ align="center">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.summary.version"/>
+ <label id="headers-summary-version-value"
+ class="plain tabpanel-summary-value devtools-monospace"
+ crop="end"
+ flex="1"/>
+ </hbox>
+ <hbox id="raw-headers"
+ class="tabpanel-summary-container"
+ align="center"
+ hidden="true">
+ <vbox id="raw-request-headers-textarea-box" flex="1" hidden="false">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.summary.rawHeaders.requestHeaders"/>
+ <textbox id="raw-request-headers-textarea"
+ class="raw-response-textarea"
+ flex="1" multiline="true" readonly="true"/>
+ </vbox>
+ <vbox id="raw-response-headers-textarea-box" flex="1" hidden="false">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.summary.rawHeaders.responseHeaders"/>
+ <textbox id="raw-response-headers-textarea"
+ class="raw-response-textarea"
+ flex="1" multiline="true" readonly="true"/>
+ </vbox>
+ </hbox>
+ <vbox id="all-headers" flex="1"/>
+ </vbox>
+ </tabpanel>
+ <tabpanel id="cookies-tabpanel"
+ class="tabpanel-content">
+ <vbox flex="1">
+ <vbox id="all-cookies" flex="1"/>
+ </vbox>
+ </tabpanel>
+ <tabpanel id="params-tabpanel"
+ class="tabpanel-content">
+ <vbox flex="1">
+ <vbox id="request-params-box" flex="1" hidden="true">
+ <vbox id="request-params" flex="1"/>
+ </vbox>
+ <vbox id="request-post-data-textarea-box" flex="1" hidden="true">
+ <vbox id="request-post-data-textarea" flex="1"/>
+ </vbox>
+ </vbox>
+ </tabpanel>
+ <tabpanel id="response-tabpanel"
+ class="tabpanel-content">
+ <vbox flex="1">
+ <label id="response-content-info-header"/>
+ <vbox id="response-content-json-box" flex="1" hidden="true">
+ <vbox id="response-content-json" flex="1" context="network-response-popup" />
+ </vbox>
+ <vbox id="response-content-textarea-box" flex="1" hidden="true">
+ <vbox id="response-content-textarea" flex="1"/>
+ </vbox>
+ <vbox id="response-content-image-box" flex="1" hidden="true">
+ <image id="response-content-image"/>
+ <hbox>
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.response.name"/>
+ <label id="response-content-image-name-value"
+ class="plain tabpanel-summary-value devtools-monospace"
+ crop="end"
+ flex="1"/>
+ </hbox>
+ <hbox>
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.response.dimensions"/>
+ <label id="response-content-image-dimensions-value"
+ class="plain tabpanel-summary-value devtools-monospace"
+ crop="end"
+ flex="1"/>
+ </hbox>
+ <hbox>
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.response.mime"/>
+ <label id="response-content-image-mime-value"
+ class="plain tabpanel-summary-value devtools-monospace"
+ crop="end"
+ flex="1"/>
+ </hbox>
+ </vbox>
+ </vbox>
+ </tabpanel>
+ <tabpanel id="timings-tabpanel"
+ class="tabpanel-content">
+ <vbox flex="1">
+ <hbox id="timings-summary-blocked"
+ class="tabpanel-summary-container"
+ align="center">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.timings.blocked"/>
+ <hbox class="requests-menu-timings-box blocked"/>
+ <label class="plain requests-menu-timings-total"/>
+ </hbox>
+ <hbox id="timings-summary-dns"
+ class="tabpanel-summary-container"
+ align="center">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.timings.dns"/>
+ <hbox class="requests-menu-timings-box dns"/>
+ <label class="plain requests-menu-timings-total"/>
+ </hbox>
+ <hbox id="timings-summary-connect"
+ class="tabpanel-summary-container"
+ align="center">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.timings.connect"/>
+ <hbox class="requests-menu-timings-box connect"/>
+ <label class="plain requests-menu-timings-total"/>
+ </hbox>
+ <hbox id="timings-summary-send"
+ class="tabpanel-summary-container"
+ align="center">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.timings.send"/>
+ <hbox class="requests-menu-timings-box send"/>
+ <label class="plain requests-menu-timings-total"/>
+ </hbox>
+ <hbox id="timings-summary-wait"
+ class="tabpanel-summary-container"
+ align="center">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.timings.wait"/>
+ <hbox class="requests-menu-timings-box wait"/>
+ <label class="plain requests-menu-timings-total"/>
+ </hbox>
+ <hbox id="timings-summary-receive"
+ class="tabpanel-summary-container"
+ align="center">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.timings.receive"/>
+ <hbox class="requests-menu-timings-box receive"/>
+ <label class="plain requests-menu-timings-total"/>
+ </hbox>
+ </vbox>
+ </tabpanel>
+ <tabpanel id="security-tabpanel"
+ class="tabpanel-content">
+ <vbox id="security-error"
+ class="tabpanel-summary-container"
+ flex="1">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.security.error"/>
+ <hbox class="security-info-section"
+ flex="1">
+ <textbox id="security-error-message"
+ class="plain"
+ flex="1"
+ multiline="true"
+ readonly="true"/>
+ </hbox>
+ </vbox>
+ <vbox id="security-information"
+ flex="1">
+ <vbox id="security-info-connection"
+ class="tabpanel-summary-container">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.security.connection"/>
+ <vbox class="security-info-section">
+ <hbox id="security-protocol-version"
+ class="tabpanel-summary-container"
+ align="baseline">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.security.protocolVersion"/>
+ <textbox id="security-protocol-version-value"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ <hbox id="security-ciphersuite"
+ class="tabpanel-summary-container"
+ align="baseline">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.security.cipherSuite"/>
+ <textbox id="security-ciphersuite-value"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ <image class="security-warning-icon"
+ id="security-warning-cipher"
+ data-localization="tooltiptext=netmonitor.security.warning.cipher" />
+ </hbox>
+ </vbox>
+ </vbox>
+ <vbox id="security-info-domain"
+ class="tabpanel-summary-container">
+ <label class="plain tabpanel-summary-label"
+ id="security-info-host-header"/>
+ <vbox class="security-info-section">
+ <hbox id="security-http-strict-transport-security"
+ class="tabpanel-summary-container"
+ align="baseline">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.security.hsts"/>
+ <textbox id="security-http-strict-transport-security-value"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ <hbox id="security-public-key-pinning"
+ class="tabpanel-summary-container"
+ align="baseline">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.security.hpkp"/>
+ <textbox id="security-public-key-pinning-value"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ </vbox>
+ </vbox>
+ <vbox id="security-info-certificate"
+ class="tabpanel-summary-container">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=netmonitor.security.certificate"/>
+ <vbox class="security-info-section">
+ <vbox class="tabpanel-summary-container">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=certmgr.subjectinfo.label" flex="1"/>
+ </vbox>
+ <vbox class="security-info-section">
+ <hbox class="tabpanel-summary-container"
+ align="baseline">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=certmgr.certdetail.cn"/>
+ <textbox id="security-cert-subject-cn"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ <hbox class="tabpanel-summary-container"
+ align="baseline">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=certmgr.certdetail.o"/>
+ <textbox id="security-cert-subject-o"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ <hbox class="tabpanel-summary-container"
+ align="baseline">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=certmgr.certdetail.ou"/>
+ <textbox id="security-cert-subject-ou"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ </vbox>
+ <vbox class="tabpanel-summary-container">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=certmgr.issuerinfo.label"
+ flex="1"/>
+ </vbox>
+ <vbox class="security-info-section">
+ <hbox class="tabpanel-summary-container"
+ align="baseline">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=certmgr.certdetail.cn"/>
+ <textbox id="security-cert-issuer-cn"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ <hbox class="tabpanel-summary-container"
+ align="baseline">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=certmgr.certdetail.o"/>
+ <textbox id="security-cert-issuer-o"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ <hbox class="tabpanel-summary-container"
+ align="baseline">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=certmgr.certdetail.ou"/>
+ <textbox id="security-cert-issuer-ou"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ </vbox>
+ <vbox class="tabpanel-summary-container">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=certmgr.periodofvalidity.label"
+ flex="1"/>
+ </vbox>
+ <vbox class="security-info-section">
+ <hbox class="tabpanel-summary-container"
+ align="baseline">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=certmgr.begins"/>
+ <textbox id="security-cert-validity-begins"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ <hbox class="tabpanel-summary-container"
+ align="baseline">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=certmgr.expires"/>
+ <textbox id="security-cert-validity-expires"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ </vbox>
+ <vbox class="tabpanel-summary-container">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=certmgr.fingerprints.label"
+ flex="1"/>
+ </vbox>
+ <vbox class="security-info-section">
+ <hbox class="tabpanel-summary-container"
+ align="baseline">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=certmgr.certdetail.sha256fingerprint"/>
+ <textbox id="security-cert-sha256-fingerprint"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ <hbox class="tabpanel-summary-container"
+ align="baseline">
+ <label class="plain tabpanel-summary-label"
+ data-localization="content=certmgr.certdetail.sha1fingerprint"/>
+ <textbox id="security-cert-sha1-fingerprint"
+ class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
+ flex="1"
+ readonly="true"/>
+ </hbox>
+ </vbox>
+ </vbox>
+ </vbox>
+ </vbox>
+ </tabpanel>
+ <tabpanel id="preview-tabpanel"
+ class="tabpanel-content">
+ <html:iframe id="response-preview"
+ frameborder="0"
+ sandbox=""/>
+ </tabpanel>
+ </tabpanels>
+ </tabbox>
+ </deck>
+ </hbox>
+
+ </vbox>
+
+ <box id="network-statistics-view">
+ <toolbar id="network-statistics-toolbar"
+ class="devtools-toolbar">
+ <button id="network-statistics-back-button"
+ class="devtools-toolbarbutton"
+ data-localization="label=netmonitor.backButton"/>
+ </toolbar>
+ <box id="network-statistics-charts"
+ class="devtools-responsive-container"
+ flex="1">
+ <vbox id="primed-cache-chart" pack="center" flex="1"/>
+ <splitter id="network-statistics-view-splitter"
+ class="devtools-side-splitter"/>
+ <vbox id="empty-cache-chart" pack="center" flex="1"/>
+ </box>
+ </box>
+
+ </deck>
+
+</window>
diff --git a/devtools/client/netmonitor/panel.js b/devtools/client/netmonitor/panel.js
new file mode 100644
index 000000000..5195e4178
--- /dev/null
+++ b/devtools/client/netmonitor/panel.js
@@ -0,0 +1,77 @@
+/* -*- 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");
+const EventEmitter = require("devtools/shared/event-emitter");
+const { Task } = require("devtools/shared/task");
+const { localizeMarkup } = require("devtools/shared/l10n");
+
+function NetMonitorPanel(iframeWindow, toolbox) {
+ this.panelWin = iframeWindow;
+ this.panelDoc = iframeWindow.document;
+ this._toolbox = toolbox;
+
+ this._view = this.panelWin.NetMonitorView;
+ this._controller = this.panelWin.NetMonitorController;
+ this._controller._target = this.target;
+ this._controller._toolbox = this._toolbox;
+
+ EventEmitter.decorate(this);
+}
+
+exports.NetMonitorPanel = NetMonitorPanel;
+
+NetMonitorPanel.prototype = {
+ /**
+ * Open is effectively an asynchronous constructor.
+ *
+ * @return object
+ * A promise that is resolved when the NetMonitor completes opening.
+ */
+ open: Task.async(function* () {
+ if (this._opening) {
+ return this._opening;
+ }
+ // Localize all the nodes containing a data-localization attribute.
+ localizeMarkup(this.panelDoc);
+
+ let deferred = promise.defer();
+ this._opening = deferred.promise;
+
+ // Local monitoring needs to make the target remote.
+ if (!this.target.isRemote) {
+ yield this.target.makeRemote();
+ }
+
+ yield this._controller.startupNetMonitor();
+ this.isReady = true;
+ this.emit("ready");
+
+ deferred.resolve(this);
+ return this._opening;
+ }),
+
+ // DevToolPanel API
+
+ get target() {
+ return this._toolbox.target;
+ },
+
+ destroy: Task.async(function* () {
+ if (this._destroying) {
+ return this._destroying;
+ }
+ let deferred = promise.defer();
+ this._destroying = deferred.promise;
+
+ yield this._controller.shutdownNetMonitor();
+ this.emit("destroyed");
+
+ deferred.resolve();
+ return this._destroying;
+ })
+};
diff --git a/devtools/client/netmonitor/performance-statistics-view.js b/devtools/client/netmonitor/performance-statistics-view.js
new file mode 100644
index 000000000..c712c083d
--- /dev/null
+++ b/devtools/client/netmonitor/performance-statistics-view.js
@@ -0,0 +1,265 @@
+/* -*- 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/. */
+/* import-globals-from ./netmonitor-controller.js */
+/* globals $ */
+"use strict";
+
+const {PluralForm} = require("devtools/shared/plural-form");
+const {Filters} = require("./filter-predicates");
+const {L10N} = require("./l10n");
+const Actions = require("./actions/index");
+
+const REQUEST_TIME_DECIMALS = 2;
+const CONTENT_SIZE_DECIMALS = 2;
+
+// px
+const NETWORK_ANALYSIS_PIE_CHART_DIAMETER = 200;
+
+/**
+ * Functions handling the performance statistics view.
+ */
+function PerformanceStatisticsView() {
+}
+
+PerformanceStatisticsView.prototype = {
+ /**
+ * Initialization function, called when the debugger is started.
+ */
+ initialize: function (store) {
+ this.store = store;
+ },
+
+ /**
+ * Initializes and displays empty charts in this container.
+ */
+ displayPlaceholderCharts: function () {
+ this._createChart({
+ id: "#primed-cache-chart",
+ title: "charts.cacheEnabled"
+ });
+ this._createChart({
+ id: "#empty-cache-chart",
+ title: "charts.cacheDisabled"
+ });
+ window.emit(EVENTS.PLACEHOLDER_CHARTS_DISPLAYED);
+ },
+
+ /**
+ * Populates and displays the primed cache chart in this container.
+ *
+ * @param array items
+ * @see this._sanitizeChartDataSource
+ */
+ createPrimedCacheChart: function (items) {
+ this._createChart({
+ id: "#primed-cache-chart",
+ title: "charts.cacheEnabled",
+ data: this._sanitizeChartDataSource(items),
+ strings: this._commonChartStrings,
+ totals: this._commonChartTotals,
+ sorted: true
+ });
+ window.emit(EVENTS.PRIMED_CACHE_CHART_DISPLAYED);
+ },
+
+ /**
+ * Populates and displays the empty cache chart in this container.
+ *
+ * @param array items
+ * @see this._sanitizeChartDataSource
+ */
+ createEmptyCacheChart: function (items) {
+ this._createChart({
+ id: "#empty-cache-chart",
+ title: "charts.cacheDisabled",
+ data: this._sanitizeChartDataSource(items, true),
+ strings: this._commonChartStrings,
+ totals: this._commonChartTotals,
+ sorted: true
+ });
+ window.emit(EVENTS.EMPTY_CACHE_CHART_DISPLAYED);
+ },
+
+ /**
+ * Common stringifier predicates used for items and totals in both the
+ * "primed" and "empty" cache charts.
+ */
+ _commonChartStrings: {
+ size: value => {
+ let string = L10N.numberWithDecimals(value / 1024, CONTENT_SIZE_DECIMALS);
+ return L10N.getFormatStr("charts.sizeKB", string);
+ },
+ time: value => {
+ let string = L10N.numberWithDecimals(value / 1000, REQUEST_TIME_DECIMALS);
+ return L10N.getFormatStr("charts.totalS", string);
+ }
+ },
+ _commonChartTotals: {
+ size: total => {
+ let string = L10N.numberWithDecimals(total / 1024, CONTENT_SIZE_DECIMALS);
+ return L10N.getFormatStr("charts.totalSize", string);
+ },
+ time: total => {
+ let seconds = total / 1000;
+ let string = L10N.numberWithDecimals(seconds, REQUEST_TIME_DECIMALS);
+ return PluralForm.get(seconds,
+ L10N.getStr("charts.totalSeconds")).replace("#1", string);
+ },
+ cached: total => {
+ return L10N.getFormatStr("charts.totalCached", total);
+ },
+ count: total => {
+ return L10N.getFormatStr("charts.totalCount", total);
+ }
+ },
+
+ /**
+ * Adds a specific chart to this container.
+ *
+ * @param object
+ * An object containing all or some the following properties:
+ * - id: either "#primed-cache-chart" or "#empty-cache-chart"
+ * - title/data/strings/totals/sorted: @see Chart.jsm for details
+ */
+ _createChart: function ({ id, title, data, strings, totals, sorted }) {
+ let container = $(id);
+
+ // Nuke all existing charts of the specified type.
+ while (container.hasChildNodes()) {
+ container.firstChild.remove();
+ }
+
+ // Create a new chart.
+ let chart = Chart.PieTable(document, {
+ diameter: NETWORK_ANALYSIS_PIE_CHART_DIAMETER,
+ title: L10N.getStr(title),
+ data: data,
+ strings: strings,
+ totals: totals,
+ sorted: sorted
+ });
+
+ chart.on("click", (_, item) => {
+ // Reset FilterButtons and enable one filter exclusively
+ this.store.dispatch(Actions.enableFilterTypeOnly(item.label));
+ NetMonitorView.showNetworkInspectorView();
+ });
+
+ container.appendChild(chart.node);
+ },
+
+ /**
+ * Sanitizes the data source used for creating charts, to follow the
+ * data format spec defined in Chart.jsm.
+ *
+ * @param array items
+ * A collection of request items used as the data source for the chart.
+ * @param boolean emptyCache
+ * True if the cache is considered enabled, false for disabled.
+ */
+ _sanitizeChartDataSource: function (items, emptyCache) {
+ let data = [
+ "html", "css", "js", "xhr", "fonts", "images", "media", "flash", "ws", "other"
+ ].map(e => ({
+ cached: 0,
+ count: 0,
+ label: e,
+ size: 0,
+ time: 0
+ }));
+
+ for (let requestItem of items) {
+ let details = requestItem.attachment;
+ let type;
+
+ if (Filters.html(details)) {
+ // "html"
+ type = 0;
+ } else if (Filters.css(details)) {
+ // "css"
+ type = 1;
+ } else if (Filters.js(details)) {
+ // "js"
+ type = 2;
+ } else if (Filters.fonts(details)) {
+ // "fonts"
+ type = 4;
+ } else if (Filters.images(details)) {
+ // "images"
+ type = 5;
+ } else if (Filters.media(details)) {
+ // "media"
+ type = 6;
+ } else if (Filters.flash(details)) {
+ // "flash"
+ type = 7;
+ } else if (Filters.ws(details)) {
+ // "ws"
+ type = 8;
+ } else if (Filters.xhr(details)) {
+ // Verify XHR last, to categorize other mime types in their own blobs.
+ // "xhr"
+ type = 3;
+ } else {
+ // "other"
+ type = 9;
+ }
+
+ if (emptyCache || !responseIsFresh(details)) {
+ data[type].time += details.totalTime || 0;
+ data[type].size += details.contentSize || 0;
+ } else {
+ data[type].cached++;
+ }
+ data[type].count++;
+ }
+
+ return data.filter(e => e.count > 0);
+ },
+};
+
+/**
+ * Checks if the "Expiration Calculations" defined in section 13.2.4 of the
+ * "HTTP/1.1: Caching in HTTP" spec holds true for a collection of headers.
+ *
+ * @param object
+ * An object containing the { responseHeaders, status } properties.
+ * @return boolean
+ * True if the response is fresh and loaded from cache.
+ */
+function responseIsFresh({ responseHeaders, status }) {
+ // Check for a "304 Not Modified" status and response headers availability.
+ if (status != 304 || !responseHeaders) {
+ return false;
+ }
+
+ let list = responseHeaders.headers;
+ let cacheControl = list.filter(e => {
+ return e.name.toLowerCase() == "cache-control";
+ })[0];
+
+ let expires = list.filter(e => e.name.toLowerCase() == "expires")[0];
+
+ // Check the "Cache-Control" header for a maximum age value.
+ if (cacheControl) {
+ let maxAgeMatch =
+ cacheControl.value.match(/s-maxage\s*=\s*(\d+)/) ||
+ cacheControl.value.match(/max-age\s*=\s*(\d+)/);
+
+ if (maxAgeMatch && maxAgeMatch.pop() > 0) {
+ return true;
+ }
+ }
+
+ // Check the "Expires" header for a valid date.
+ if (expires && Date.parse(expires.value)) {
+ return true;
+ }
+
+ return false;
+}
+
+exports.PerformanceStatisticsView = PerformanceStatisticsView;
diff --git a/devtools/client/netmonitor/prefs.js b/devtools/client/netmonitor/prefs.js
new file mode 100644
index 000000000..6d4909d7c
--- /dev/null
+++ b/devtools/client/netmonitor/prefs.js
@@ -0,0 +1,14 @@
+"use strict";
+
+const {PrefsHelper} = require("devtools/client/shared/prefs");
+
+/**
+ * Shortcuts for accessing various network monitor preferences.
+ */
+
+exports.Prefs = new PrefsHelper("devtools.netmonitor", {
+ networkDetailsWidth: ["Int", "panes-network-details-width"],
+ networkDetailsHeight: ["Int", "panes-network-details-height"],
+ statistics: ["Bool", "statistics"],
+ filters: ["Json", "filters"]
+});
diff --git a/devtools/client/netmonitor/reducers/filters.js b/devtools/client/netmonitor/reducers/filters.js
new file mode 100644
index 000000000..cc81370d8
--- /dev/null
+++ b/devtools/client/netmonitor/reducers/filters.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 I = require("devtools/client/shared/vendor/immutable");
+const {
+ TOGGLE_FILTER_TYPE,
+ ENABLE_FILTER_TYPE_ONLY,
+ SET_FILTER_TEXT,
+} = require("../constants");
+
+const FilterTypes = I.Record({
+ all: false,
+ html: false,
+ css: false,
+ js: false,
+ xhr: false,
+ fonts: false,
+ images: false,
+ media: false,
+ flash: false,
+ ws: false,
+ other: false,
+});
+
+const Filters = I.Record({
+ types: new FilterTypes({ all: true }),
+ url: "",
+});
+
+function toggleFilterType(state, action) {
+ let { filter } = action;
+ let newState;
+
+ // Ignore unknown filter type
+ if (!state.has(filter)) {
+ return state;
+ }
+ if (filter === "all") {
+ return new FilterTypes({ all: true });
+ }
+
+ newState = state.withMutations(types => {
+ types.set("all", false);
+ types.set(filter, !state.get(filter));
+ });
+
+ if (!newState.includes(true)) {
+ newState = new FilterTypes({ all: true });
+ }
+
+ return newState;
+}
+
+function enableFilterTypeOnly(state, action) {
+ let { filter } = action;
+
+ // Ignore unknown filter type
+ if (!state.has(filter)) {
+ return state;
+ }
+
+ return new FilterTypes({ [filter]: true });
+}
+
+function filters(state = new Filters(), action) {
+ switch (action.type) {
+ case TOGGLE_FILTER_TYPE:
+ return state.set("types", toggleFilterType(state.types, action));
+ case ENABLE_FILTER_TYPE_ONLY:
+ return state.set("types", enableFilterTypeOnly(state.types, action));
+ case SET_FILTER_TEXT:
+ return state.set("url", action.url);
+ default:
+ return state;
+ }
+}
+
+module.exports = filters;
diff --git a/devtools/client/netmonitor/reducers/index.js b/devtools/client/netmonitor/reducers/index.js
new file mode 100644
index 000000000..58638a030
--- /dev/null
+++ b/devtools/client/netmonitor/reducers/index.js
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { combineReducers } = require("devtools/client/shared/vendor/redux");
+const filters = require("./filters");
+const sidebar = require("./sidebar");
+
+module.exports = combineReducers({
+ filters,
+ sidebar,
+});
diff --git a/devtools/client/netmonitor/reducers/moz.build b/devtools/client/netmonitor/reducers/moz.build
new file mode 100644
index 000000000..477cafb41
--- /dev/null
+++ b/devtools/client/netmonitor/reducers/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(
+ 'filters.js',
+ 'index.js',
+ 'sidebar.js',
+)
diff --git a/devtools/client/netmonitor/reducers/sidebar.js b/devtools/client/netmonitor/reducers/sidebar.js
new file mode 100644
index 000000000..eaa8b63df
--- /dev/null
+++ b/devtools/client/netmonitor/reducers/sidebar.js
@@ -0,0 +1,43 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const I = require("devtools/client/shared/vendor/immutable");
+const {
+ DISABLE_TOGGLE_BUTTON,
+ SHOW_SIDEBAR,
+ TOGGLE_SIDEBAR,
+} = require("../constants");
+
+const SidebarState = I.Record({
+ toggleButtonDisabled: true,
+ visible: false,
+});
+
+function disableToggleButton(state, action) {
+ return state.set("toggleButtonDisabled", action.disabled);
+}
+
+function showSidebar(state, action) {
+ return state.set("visible", action.visible);
+}
+
+function toggleSidebar(state, action) {
+ return state.set("visible", !state.visible);
+}
+
+function sidebar(state = new SidebarState(), action) {
+ switch (action.type) {
+ case DISABLE_TOGGLE_BUTTON:
+ return disableToggleButton(state, action);
+ case SHOW_SIDEBAR:
+ return showSidebar(state, action);
+ case TOGGLE_SIDEBAR:
+ return toggleSidebar(state, action);
+ default:
+ return state;
+ }
+}
+
+module.exports = sidebar;
diff --git a/devtools/client/netmonitor/request-list-context-menu.js b/devtools/client/netmonitor/request-list-context-menu.js
new file mode 100644
index 000000000..215296265
--- /dev/null
+++ b/devtools/client/netmonitor/request-list-context-menu.js
@@ -0,0 +1,357 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* globals NetMonitorController, NetMonitorView, gNetwork */
+
+"use strict";
+
+const Services = require("Services");
+const { Task } = require("devtools/shared/task");
+const { Curl } = require("devtools/client/shared/curl");
+const { gDevTools } = require("devtools/client/framework/devtools");
+const Menu = require("devtools/client/framework/menu");
+const MenuItem = require("devtools/client/framework/menu-item");
+const { L10N } = require("./l10n");
+const { formDataURI, getFormDataSections } = require("./request-utils");
+
+loader.lazyRequireGetter(this, "HarExporter",
+ "devtools/client/netmonitor/har/har-exporter", true);
+
+loader.lazyServiceGetter(this, "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper");
+
+loader.lazyRequireGetter(this, "NetworkHelper",
+ "devtools/shared/webconsole/network-helper");
+
+function RequestListContextMenu() {}
+
+RequestListContextMenu.prototype = {
+ get selectedItem() {
+ return NetMonitorView.RequestsMenu.selectedItem;
+ },
+
+ get items() {
+ return NetMonitorView.RequestsMenu.items;
+ },
+
+ /**
+ * Handle the context menu opening. Hide items if no request is selected.
+ * Since visible attribute only accept boolean value but the method call may
+ * return undefined, we use !! to force convert any object to boolean
+ */
+ open({ screenX = 0, screenY = 0 } = {}) {
+ let selectedItem = this.selectedItem;
+
+ let menu = new Menu();
+ menu.append(new MenuItem({
+ id: "request-menu-context-copy-url",
+ label: L10N.getStr("netmonitor.context.copyUrl"),
+ accesskey: L10N.getStr("netmonitor.context.copyUrl.accesskey"),
+ visible: !!selectedItem,
+ click: () => this.copyUrl(),
+ }));
+
+ menu.append(new MenuItem({
+ id: "request-menu-context-copy-url-params",
+ label: L10N.getStr("netmonitor.context.copyUrlParams"),
+ accesskey: L10N.getStr("netmonitor.context.copyUrlParams.accesskey"),
+ visible: !!(selectedItem &&
+ NetworkHelper.nsIURL(selectedItem.attachment.url).query),
+ click: () => this.copyUrlParams(),
+ }));
+
+ menu.append(new MenuItem({
+ id: "request-menu-context-copy-post-data",
+ label: L10N.getStr("netmonitor.context.copyPostData"),
+ accesskey: L10N.getStr("netmonitor.context.copyPostData.accesskey"),
+ visible: !!(selectedItem && selectedItem.attachment.requestPostData),
+ click: () => this.copyPostData(),
+ }));
+
+ menu.append(new MenuItem({
+ id: "request-menu-context-copy-as-curl",
+ label: L10N.getStr("netmonitor.context.copyAsCurl"),
+ accesskey: L10N.getStr("netmonitor.context.copyAsCurl.accesskey"),
+ visible: !!(selectedItem && selectedItem.attachment),
+ click: () => this.copyAsCurl(),
+ }));
+
+ menu.append(new MenuItem({
+ type: "separator",
+ visible: !!selectedItem,
+ }));
+
+ menu.append(new MenuItem({
+ id: "request-menu-context-copy-request-headers",
+ label: L10N.getStr("netmonitor.context.copyRequestHeaders"),
+ accesskey: L10N.getStr("netmonitor.context.copyRequestHeaders.accesskey"),
+ visible: !!(selectedItem && selectedItem.attachment.requestHeaders),
+ click: () => this.copyRequestHeaders(),
+ }));
+
+ menu.append(new MenuItem({
+ id: "response-menu-context-copy-response-headers",
+ label: L10N.getStr("netmonitor.context.copyResponseHeaders"),
+ accesskey: L10N.getStr("netmonitor.context.copyResponseHeaders.accesskey"),
+ visible: !!(selectedItem && selectedItem.attachment.responseHeaders),
+ click: () => this.copyResponseHeaders(),
+ }));
+
+ menu.append(new MenuItem({
+ id: "request-menu-context-copy-response",
+ label: L10N.getStr("netmonitor.context.copyResponse"),
+ accesskey: L10N.getStr("netmonitor.context.copyResponse.accesskey"),
+ visible: !!(selectedItem &&
+ selectedItem.attachment.responseContent &&
+ selectedItem.attachment.responseContent.content.text &&
+ selectedItem.attachment.responseContent.content.text.length !== 0),
+ click: () => this.copyResponse(),
+ }));
+
+ menu.append(new MenuItem({
+ id: "request-menu-context-copy-image-as-data-uri",
+ label: L10N.getStr("netmonitor.context.copyImageAsDataUri"),
+ accesskey: L10N.getStr("netmonitor.context.copyImageAsDataUri.accesskey"),
+ visible: !!(selectedItem &&
+ selectedItem.attachment.responseContent &&
+ selectedItem.attachment.responseContent.content
+ .mimeType.includes("image/")),
+ click: () => this.copyImageAsDataUri(),
+ }));
+
+ menu.append(new MenuItem({
+ type: "separator",
+ visible: !!selectedItem,
+ }));
+
+ menu.append(new MenuItem({
+ id: "request-menu-context-copy-all-as-har",
+ label: L10N.getStr("netmonitor.context.copyAllAsHar"),
+ accesskey: L10N.getStr("netmonitor.context.copyAllAsHar.accesskey"),
+ visible: !!this.items.length,
+ click: () => this.copyAllAsHar(),
+ }));
+
+ menu.append(new MenuItem({
+ id: "request-menu-context-save-all-as-har",
+ label: L10N.getStr("netmonitor.context.saveAllAsHar"),
+ accesskey: L10N.getStr("netmonitor.context.saveAllAsHar.accesskey"),
+ visible: !!this.items.length,
+ click: () => this.saveAllAsHar(),
+ }));
+
+ menu.append(new MenuItem({
+ type: "separator",
+ visible: !!selectedItem,
+ }));
+
+ menu.append(new MenuItem({
+ id: "request-menu-context-resend",
+ label: L10N.getStr("netmonitor.context.editAndResend"),
+ accesskey: L10N.getStr("netmonitor.context.editAndResend.accesskey"),
+ visible: !!(NetMonitorController.supportsCustomRequest &&
+ selectedItem &&
+ !selectedItem.attachment.isCustom),
+ click: () => NetMonitorView.RequestsMenu.cloneSelectedRequest(),
+ }));
+
+ menu.append(new MenuItem({
+ type: "separator",
+ visible: !!selectedItem,
+ }));
+
+ menu.append(new MenuItem({
+ id: "request-menu-context-newtab",
+ label: L10N.getStr("netmonitor.context.newTab"),
+ accesskey: L10N.getStr("netmonitor.context.newTab.accesskey"),
+ visible: !!selectedItem,
+ click: () => this.openRequestInTab()
+ }));
+
+ menu.append(new MenuItem({
+ id: "request-menu-context-perf",
+ label: L10N.getStr("netmonitor.context.perfTools"),
+ accesskey: L10N.getStr("netmonitor.context.perfTools.accesskey"),
+ visible: !!NetMonitorController.supportsPerfStats,
+ click: () => NetMonitorView.toggleFrontendMode()
+ }));
+
+ menu.popup(screenX, screenY, NetMonitorController._toolbox);
+ return menu;
+ },
+
+ /**
+ * Opens selected item in a new tab.
+ */
+ openRequestInTab() {
+ let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+ let { url } = this.selectedItem.attachment;
+ win.openUILinkIn(url, "tab", { relatedToCurrent: true });
+ },
+
+ /**
+ * Copy the request url from the currently selected item.
+ */
+ copyUrl() {
+ clipboardHelper.copyString(this.selectedItem.attachment.url);
+ },
+
+ /**
+ * Copy the request url query string parameters from the currently
+ * selected item.
+ */
+ copyUrlParams() {
+ let { url } = this.selectedItem.attachment;
+ let params = NetworkHelper.nsIURL(url).query.split("&");
+ let string = params.join(Services.appinfo.OS === "WINNT" ? "\r\n" : "\n");
+ clipboardHelper.copyString(string);
+ },
+
+ /**
+ * Copy the request form data parameters (or raw payload) from
+ * the currently selected item.
+ */
+ copyPostData: Task.async(function* () {
+ let selected = this.selectedItem.attachment;
+
+ // Try to extract any form data parameters.
+ let formDataSections = yield getFormDataSections(
+ selected.requestHeaders,
+ selected.requestHeadersFromUploadStream,
+ selected.requestPostData,
+ gNetwork.getString.bind(gNetwork));
+
+ let params = [];
+ formDataSections.forEach(section => {
+ let paramsArray = NetworkHelper.parseQueryString(section);
+ if (paramsArray) {
+ params = [...params, ...paramsArray];
+ }
+ });
+
+ let string = params
+ .map(param => param.name + (param.value ? "=" + param.value : ""))
+ .join(Services.appinfo.OS === "WINNT" ? "\r\n" : "\n");
+
+ // Fall back to raw payload.
+ if (!string) {
+ let postData = selected.requestPostData.postData.text;
+ string = yield gNetwork.getString(postData);
+ if (Services.appinfo.OS !== "WINNT") {
+ string = string.replace(/\r/g, "");
+ }
+ }
+
+ clipboardHelper.copyString(string);
+ }),
+
+ /**
+ * Copy a cURL command from the currently selected item.
+ */
+ copyAsCurl: Task.async(function* () {
+ let selected = this.selectedItem.attachment;
+
+ // Create a sanitized object for the Curl command generator.
+ let data = {
+ url: selected.url,
+ method: selected.method,
+ headers: [],
+ httpVersion: selected.httpVersion,
+ postDataText: null
+ };
+
+ // Fetch header values.
+ for (let { name, value } of selected.requestHeaders.headers) {
+ let text = yield gNetwork.getString(value);
+ data.headers.push({ name: name, value: text });
+ }
+
+ // Fetch the request payload.
+ if (selected.requestPostData) {
+ let postData = selected.requestPostData.postData.text;
+ data.postDataText = yield gNetwork.getString(postData);
+ }
+
+ clipboardHelper.copyString(Curl.generateCommand(data));
+ }),
+
+ /**
+ * Copy the raw request headers from the currently selected item.
+ */
+ copyRequestHeaders() {
+ let selected = this.selectedItem.attachment;
+ let rawHeaders = selected.requestHeaders.rawHeaders.trim();
+ if (Services.appinfo.OS !== "WINNT") {
+ rawHeaders = rawHeaders.replace(/\r/g, "");
+ }
+ clipboardHelper.copyString(rawHeaders);
+ },
+
+ /**
+ * Copy the raw response headers from the currently selected item.
+ */
+ copyResponseHeaders() {
+ let selected = this.selectedItem.attachment;
+ let rawHeaders = selected.responseHeaders.rawHeaders.trim();
+ if (Services.appinfo.OS !== "WINNT") {
+ rawHeaders = rawHeaders.replace(/\r/g, "");
+ }
+ clipboardHelper.copyString(rawHeaders);
+ },
+
+ /**
+ * Copy image as data uri.
+ */
+ copyImageAsDataUri() {
+ let selected = this.selectedItem.attachment;
+ let { mimeType, text, encoding } = selected.responseContent.content;
+
+ gNetwork.getString(text).then(string => {
+ let data = formDataURI(mimeType, encoding, string);
+ clipboardHelper.copyString(data);
+ });
+ },
+
+ /**
+ * Copy response data as a string.
+ */
+ copyResponse() {
+ let selected = this.selectedItem.attachment;
+ let text = selected.responseContent.content.text;
+
+ gNetwork.getString(text).then(string => {
+ clipboardHelper.copyString(string);
+ });
+ },
+
+ /**
+ * Copy HAR from the network panel content to the clipboard.
+ */
+ copyAllAsHar() {
+ let options = this.getDefaultHarOptions();
+ return HarExporter.copy(options);
+ },
+
+ /**
+ * Save HAR from the network panel content to a file.
+ */
+ saveAllAsHar() {
+ let options = this.getDefaultHarOptions();
+ return HarExporter.save(options);
+ },
+
+ getDefaultHarOptions() {
+ let form = NetMonitorController._target.form;
+ let title = form.title || form.url;
+
+ return {
+ getString: gNetwork.getString.bind(gNetwork),
+ view: NetMonitorView.RequestsMenu,
+ items: NetMonitorView.RequestsMenu.items,
+ title: title
+ };
+ }
+};
+
+module.exports = RequestListContextMenu;
diff --git a/devtools/client/netmonitor/request-utils.js b/devtools/client/netmonitor/request-utils.js
new file mode 100644
index 000000000..ba54efb4f
--- /dev/null
+++ b/devtools/client/netmonitor/request-utils.js
@@ -0,0 +1,185 @@
+"use strict";
+/* eslint-disable mozilla/reject-some-requires */
+const { Ci } = require("chrome");
+const { KeyCodes } = require("devtools/client/shared/keycodes");
+const { Task } = require("devtools/shared/task");
+const NetworkHelper = require("devtools/shared/webconsole/network-helper");
+
+/**
+ * Helper method to get a wrapped function which can be bound to as
+ * an event listener directly and is executed only when data-key is
+ * present in event.target.
+ *
+ * @param function callback
+ * Function to execute execute when data-key
+ * is present in event.target.
+ * @param bool onlySpaceOrReturn
+ * Flag to indicate if callback should only be called
+ when the space or return button is pressed
+ * @return function
+ * Wrapped function with the target data-key as the first argument
+ * and the event as the second argument.
+ */
+exports.getKeyWithEvent = function (callback, onlySpaceOrReturn) {
+ return function (event) {
+ let key = event.target.getAttribute("data-key");
+ let filterKeyboardEvent = !onlySpaceOrReturn ||
+ event.keyCode === KeyCodes.DOM_VK_SPACE ||
+ event.keyCode === KeyCodes.DOM_VK_RETURN;
+
+ if (key && filterKeyboardEvent) {
+ callback.call(null, key);
+ }
+ };
+};
+
+/**
+ * Extracts any urlencoded form data sections (e.g. "?foo=bar&baz=42") from a
+ * POST request.
+ *
+ * @param object headers
+ * The "requestHeaders".
+ * @param object uploadHeaders
+ * The "requestHeadersFromUploadStream".
+ * @param object postData
+ * The "requestPostData".
+ * @param object getString
+ Callback to retrieve a string from a LongStringGrip.
+ * @return array
+ * A promise that is resolved with the extracted form data.
+ */
+exports.getFormDataSections = Task.async(function* (headers, uploadHeaders, postData,
+ getString) {
+ let formDataSections = [];
+
+ let { headers: requestHeaders } = headers;
+ let { headers: payloadHeaders } = uploadHeaders;
+ let allHeaders = [...payloadHeaders, ...requestHeaders];
+
+ let contentTypeHeader = allHeaders.find(e => {
+ return e.name.toLowerCase() == "content-type";
+ });
+
+ let contentTypeLongString = contentTypeHeader ? contentTypeHeader.value : "";
+
+ let contentType = yield getString(contentTypeLongString);
+
+ if (contentType.includes("x-www-form-urlencoded")) {
+ let postDataLongString = postData.postData.text;
+ let text = yield getString(postDataLongString);
+
+ for (let section of text.split(/\r\n|\r|\n/)) {
+ // Before displaying it, make sure this section of the POST data
+ // isn't a line containing upload stream headers.
+ if (payloadHeaders.every(header => !section.startsWith(header.name))) {
+ formDataSections.push(section);
+ }
+ }
+ }
+
+ return formDataSections;
+});
+
+/**
+ * Form a data: URI given a mime type, encoding, and some text.
+ *
+ * @param {String} mimeType the mime type
+ * @param {String} encoding the encoding to use; if not set, the
+ * text will be base64-encoded.
+ * @param {String} text the text of the URI.
+ * @return {String} a data: URI
+ */
+exports.formDataURI = function (mimeType, encoding, text) {
+ if (!encoding) {
+ encoding = "base64";
+ text = btoa(text);
+ }
+ return "data:" + mimeType + ";" + encoding + "," + text;
+};
+
+/**
+ * Write out a list of headers into a chunk of text
+ *
+ * @param array headers
+ * Array of headers info {name, value}
+ * @return string text
+ * List of headers in text format
+ */
+exports.writeHeaderText = function (headers) {
+ return headers.map(({name, value}) => name + ": " + value).join("\n");
+};
+
+/**
+ * Helper for getting an abbreviated string for a mime type.
+ *
+ * @param string mimeType
+ * @return string
+ */
+exports.getAbbreviatedMimeType = function (mimeType) {
+ if (!mimeType) {
+ return "";
+ }
+ return (mimeType.split(";")[0].split("/")[1] || "").split("+")[0];
+};
+
+/**
+ * Helpers for getting details about an nsIURL.
+ *
+ * @param nsIURL | string url
+ * @return string
+ */
+exports.getUriNameWithQuery = function (url) {
+ if (!(url instanceof Ci.nsIURL)) {
+ url = NetworkHelper.nsIURL(url);
+ }
+
+ let name = NetworkHelper.convertToUnicode(
+ unescape(url.fileName || url.filePath || "/"));
+ let query = NetworkHelper.convertToUnicode(unescape(url.query));
+
+ return name + (query ? "?" + query : "");
+};
+
+exports.getUriHostPort = function (url) {
+ if (!(url instanceof Ci.nsIURL)) {
+ url = NetworkHelper.nsIURL(url);
+ }
+ return NetworkHelper.convertToUnicode(unescape(url.hostPort));
+};
+
+exports.getUriHost = function (url) {
+ return exports.getUriHostPort(url).replace(/:\d+$/, "");
+};
+
+/**
+ * Convert a nsIContentPolicy constant to a display string
+ */
+const LOAD_CAUSE_STRINGS = {
+ [Ci.nsIContentPolicy.TYPE_INVALID]: "invalid",
+ [Ci.nsIContentPolicy.TYPE_OTHER]: "other",
+ [Ci.nsIContentPolicy.TYPE_SCRIPT]: "script",
+ [Ci.nsIContentPolicy.TYPE_IMAGE]: "img",
+ [Ci.nsIContentPolicy.TYPE_STYLESHEET]: "stylesheet",
+ [Ci.nsIContentPolicy.TYPE_OBJECT]: "object",
+ [Ci.nsIContentPolicy.TYPE_DOCUMENT]: "document",
+ [Ci.nsIContentPolicy.TYPE_SUBDOCUMENT]: "subdocument",
+ [Ci.nsIContentPolicy.TYPE_REFRESH]: "refresh",
+ [Ci.nsIContentPolicy.TYPE_XBL]: "xbl",
+ [Ci.nsIContentPolicy.TYPE_PING]: "ping",
+ [Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST]: "xhr",
+ [Ci.nsIContentPolicy.TYPE_OBJECT_SUBREQUEST]: "objectSubdoc",
+ [Ci.nsIContentPolicy.TYPE_DTD]: "dtd",
+ [Ci.nsIContentPolicy.TYPE_FONT]: "font",
+ [Ci.nsIContentPolicy.TYPE_MEDIA]: "media",
+ [Ci.nsIContentPolicy.TYPE_WEBSOCKET]: "websocket",
+ [Ci.nsIContentPolicy.TYPE_CSP_REPORT]: "csp",
+ [Ci.nsIContentPolicy.TYPE_XSLT]: "xslt",
+ [Ci.nsIContentPolicy.TYPE_BEACON]: "beacon",
+ [Ci.nsIContentPolicy.TYPE_FETCH]: "fetch",
+ [Ci.nsIContentPolicy.TYPE_IMAGESET]: "imageset",
+ [Ci.nsIContentPolicy.TYPE_WEB_MANIFEST]: "webManifest"
+};
+
+exports.loadCauseString = function (causeType) {
+ return LOAD_CAUSE_STRINGS[causeType] || "unknown";
+};
diff --git a/devtools/client/netmonitor/requests-menu-view.js b/devtools/client/netmonitor/requests-menu-view.js
new file mode 100644
index 000000000..6ea6381ec
--- /dev/null
+++ b/devtools/client/netmonitor/requests-menu-view.js
@@ -0,0 +1,1649 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* globals document, window, dumpn, $, gNetwork, EVENTS, Prefs,
+ NetMonitorController, NetMonitorView */
+
+"use strict";
+
+/* eslint-disable mozilla/reject-some-requires */
+const { Cu } = require("chrome");
+const {Task} = require("devtools/shared/task");
+const {DeferredTask} = Cu.import("resource://gre/modules/DeferredTask.jsm", {});
+/* eslint-disable mozilla/reject-some-requires */
+const {SideMenuWidget} = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+const {setImageTooltip, getImageDimensions} =
+ require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper");
+const {Heritage, WidgetMethods, setNamedTimeout} =
+ require("devtools/client/shared/widgets/view-helpers");
+const {CurlUtils} = require("devtools/client/shared/curl");
+const {PluralForm} = require("devtools/shared/plural-form");
+const {Filters, isFreetextMatch} = require("./filter-predicates");
+const {Sorters} = require("./sort-predicates");
+const {L10N, WEBCONSOLE_L10N} = require("./l10n");
+const {formDataURI,
+ writeHeaderText,
+ getKeyWithEvent,
+ getAbbreviatedMimeType,
+ getUriNameWithQuery,
+ getUriHostPort,
+ getUriHost,
+ loadCauseString} = require("./request-utils");
+const Actions = require("./actions/index");
+const RequestListContextMenu = require("./request-list-context-menu");
+
+loader.lazyRequireGetter(this, "NetworkHelper",
+ "devtools/shared/webconsole/network-helper");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const EPSILON = 0.001;
+// ms
+const RESIZE_REFRESH_RATE = 50;
+// ms
+const REQUESTS_REFRESH_RATE = 50;
+// tooltip show/hide delay in ms
+const REQUESTS_TOOLTIP_TOGGLE_DELAY = 500;
+// px
+const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400;
+// px
+const REQUESTS_TOOLTIP_STACK_TRACE_WIDTH = 600;
+// px
+const REQUESTS_WATERFALL_SAFE_BOUNDS = 90;
+// ms
+const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5;
+// px
+const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60;
+// ms
+const REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5;
+const REQUESTS_WATERFALL_BACKGROUND_TICKS_SCALES = 3;
+// px
+const REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10;
+const REQUESTS_WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
+const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32;
+// byte
+const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32;
+const REQUESTS_WATERFALL_DOMCONTENTLOADED_TICKS_COLOR_RGBA = [255, 0, 0, 128];
+const REQUESTS_WATERFALL_LOAD_TICKS_COLOR_RGBA = [0, 0, 255, 128];
+
+// Constants for formatting bytes.
+const BYTES_IN_KB = 1024;
+const BYTES_IN_MB = Math.pow(BYTES_IN_KB, 2);
+const BYTES_IN_GB = Math.pow(BYTES_IN_KB, 3);
+const MAX_BYTES_SIZE = 1000;
+const MAX_KB_SIZE = 1000 * BYTES_IN_KB;
+const MAX_MB_SIZE = 1000 * BYTES_IN_MB;
+
+// TODO: duplicated from netmonitor-view.js. Move to a format-utils.js module.
+const REQUEST_TIME_DECIMALS = 2;
+const CONTENT_SIZE_DECIMALS = 2;
+
+const CONTENT_MIME_TYPE_ABBREVIATIONS = {
+ "ecmascript": "js",
+ "javascript": "js",
+ "x-javascript": "js"
+};
+
+// A smart store watcher to notify store changes as necessary
+function storeWatcher(initialValue, reduceValue, onChange) {
+ let currentValue = initialValue;
+
+ return () => {
+ const newValue = reduceValue(currentValue);
+ if (newValue !== currentValue) {
+ onChange(newValue, currentValue);
+ currentValue = newValue;
+ }
+ };
+}
+
+/**
+ * Functions handling the requests menu (containing details about each request,
+ * like status, method, file, domain, as well as a waterfall representing
+ * timing imformation).
+ */
+function RequestsMenuView() {
+ dumpn("RequestsMenuView was instantiated");
+
+ this._flushRequests = this._flushRequests.bind(this);
+ this._onHover = this._onHover.bind(this);
+ this._onSelect = this._onSelect.bind(this);
+ this._onSwap = this._onSwap.bind(this);
+ this._onResize = this._onResize.bind(this);
+ this._onScroll = this._onScroll.bind(this);
+ this._onSecurityIconClick = this._onSecurityIconClick.bind(this);
+}
+
+RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
+ /**
+ * Initialization function, called when the network monitor is started.
+ */
+ initialize: function (store) {
+ dumpn("Initializing the RequestsMenuView");
+
+ this.store = store;
+
+ this.contextMenu = new RequestListContextMenu();
+
+ let widgetParentEl = $("#requests-menu-contents");
+ this.widget = new SideMenuWidget(widgetParentEl);
+ this._splitter = $("#network-inspector-view-splitter");
+ this._summary = $("#requests-menu-network-summary-button");
+ this._summary.setAttribute("label", L10N.getStr("networkMenu.empty"));
+
+ // Create a tooltip for the newly appended network request item.
+ this.tooltip = new HTMLTooltip(NetMonitorController._toolbox.doc, { type: "arrow" });
+ this.tooltip.startTogglingOnHover(widgetParentEl, this._onHover, {
+ toggleDelay: REQUESTS_TOOLTIP_TOGGLE_DELAY,
+ interactive: true
+ });
+
+ this.sortContents((a, b) => Sorters.waterfall(a.attachment, b.attachment));
+
+ this.allowFocusOnRightClick = true;
+ this.maintainSelectionVisible = true;
+
+ this.widget.addEventListener("select", this._onSelect, false);
+ this.widget.addEventListener("swap", this._onSwap, false);
+ this._splitter.addEventListener("mousemove", this._onResize, false);
+ window.addEventListener("resize", this._onResize, false);
+
+ this.requestsMenuSortEvent = getKeyWithEvent(this.sortBy.bind(this));
+ this.requestsMenuSortKeyboardEvent = getKeyWithEvent(this.sortBy.bind(this), true);
+ this._onContextMenu = this._onContextMenu.bind(this);
+ this._onContextPerfCommand = () => NetMonitorView.toggleFrontendMode();
+ this._onReloadCommand = () => NetMonitorView.reloadPage();
+ this._flushRequestsTask = new DeferredTask(this._flushRequests,
+ REQUESTS_REFRESH_RATE);
+
+ this.sendCustomRequestEvent = this.sendCustomRequest.bind(this);
+ this.closeCustomRequestEvent = this.closeCustomRequest.bind(this);
+ this.cloneSelectedRequestEvent = this.cloneSelectedRequest.bind(this);
+ this.toggleRawHeadersEvent = this.toggleRawHeaders.bind(this);
+
+ this.reFilterRequests = this.reFilterRequests.bind(this);
+
+ $("#toolbar-labels").addEventListener("click",
+ this.requestsMenuSortEvent, false);
+ $("#toolbar-labels").addEventListener("keydown",
+ this.requestsMenuSortKeyboardEvent, false);
+ $("#toggle-raw-headers").addEventListener("click",
+ this.toggleRawHeadersEvent, false);
+ $("#requests-menu-contents").addEventListener("scroll", this._onScroll, true);
+ $("#requests-menu-contents").addEventListener("contextmenu", this._onContextMenu);
+
+ this.unsubscribeStore = store.subscribe(storeWatcher(
+ null,
+ () => store.getState().filters,
+ (newFilters) => {
+ this._activeFilters = newFilters.types
+ .toSeq()
+ .filter((checked, key) => checked)
+ .keySeq()
+ .toArray();
+ this._currentFreetextFilter = newFilters.url;
+ this.reFilterRequests();
+ }
+ ));
+
+ Prefs.filters.forEach(type =>
+ store.dispatch(Actions.toggleFilterType(type)));
+
+ window.once("connected", this._onConnect.bind(this));
+ },
+
+ _onConnect: function () {
+ $("#requests-menu-reload-notice-button").addEventListener("command",
+ this._onReloadCommand, false);
+
+ if (NetMonitorController.supportsCustomRequest) {
+ $("#custom-request-send-button").addEventListener("click",
+ this.sendCustomRequestEvent, false);
+ $("#custom-request-close-button").addEventListener("click",
+ this.closeCustomRequestEvent, false);
+ $("#headers-summary-resend").addEventListener("click",
+ this.cloneSelectedRequestEvent, false);
+ } else {
+ $("#headers-summary-resend").hidden = true;
+ }
+
+ if (NetMonitorController.supportsPerfStats) {
+ $("#requests-menu-perf-notice-button").addEventListener("command",
+ this._onContextPerfCommand, false);
+ $("#requests-menu-network-summary-button").addEventListener("command",
+ this._onContextPerfCommand, false);
+ $("#network-statistics-back-button").addEventListener("command",
+ this._onContextPerfCommand, false);
+ } else {
+ $("#notice-perf-message").hidden = true;
+ $("#requests-menu-network-summary-button").hidden = true;
+ }
+
+ if (!NetMonitorController.supportsTransferredResponseSize) {
+ $("#requests-menu-transferred-header-box").hidden = true;
+ $("#requests-menu-item-template .requests-menu-transferred")
+ .hidden = true;
+ }
+ },
+
+ /**
+ * Destruction function, called when the network monitor is closed.
+ */
+ destroy: function () {
+ dumpn("Destroying the RequestsMenuView");
+
+ Prefs.filters = this._activeFilters;
+
+ /* Destroy the tooltip */
+ this.tooltip.stopTogglingOnHover();
+ this.tooltip.destroy();
+ $("#requests-menu-contents").removeEventListener("scroll", this._onScroll, true);
+ $("#requests-menu-contents").removeEventListener("contextmenu", this._onContextMenu);
+
+ this.widget.removeEventListener("select", this._onSelect, false);
+ this.widget.removeEventListener("swap", this._onSwap, false);
+ this._splitter.removeEventListener("mousemove", this._onResize, false);
+ window.removeEventListener("resize", this._onResize, false);
+
+ $("#toolbar-labels").removeEventListener("click",
+ this.requestsMenuSortEvent, false);
+ $("#toolbar-labels").removeEventListener("keydown",
+ this.requestsMenuSortKeyboardEvent, false);
+
+ this._flushRequestsTask.disarm();
+
+ $("#requests-menu-reload-notice-button").removeEventListener("command",
+ this._onReloadCommand, false);
+ $("#requests-menu-perf-notice-button").removeEventListener("command",
+ this._onContextPerfCommand, false);
+ $("#requests-menu-network-summary-button").removeEventListener("command",
+ this._onContextPerfCommand, false);
+ $("#network-statistics-back-button").removeEventListener("command",
+ this._onContextPerfCommand, false);
+
+ $("#custom-request-send-button").removeEventListener("click",
+ this.sendCustomRequestEvent, false);
+ $("#custom-request-close-button").removeEventListener("click",
+ this.closeCustomRequestEvent, false);
+ $("#headers-summary-resend").removeEventListener("click",
+ this.cloneSelectedRequestEvent, false);
+ $("#toggle-raw-headers").removeEventListener("click",
+ this.toggleRawHeadersEvent, false);
+
+ this.unsubscribeStore();
+ },
+
+ /**
+ * Resets this container (removes all the networking information).
+ */
+ reset: function () {
+ this.empty();
+ this._addQueue = [];
+ this._updateQueue = [];
+ this._firstRequestStartedMillis = -1;
+ this._lastRequestEndedMillis = -1;
+ },
+
+ /**
+ * Specifies if this view may be updated lazily.
+ */
+ _lazyUpdate: true,
+
+ get lazyUpdate() {
+ return this._lazyUpdate;
+ },
+
+ set lazyUpdate(value) {
+ this._lazyUpdate = value;
+ if (!value) {
+ this._flushRequests();
+ }
+ },
+
+ /**
+ * Adds a network request to this container.
+ *
+ * @param string id
+ * An identifier coming from the network monitor controller.
+ * @param string startedDateTime
+ * A string representation of when the request was started, which
+ * can be parsed by Date (for example "2012-09-17T19:50:03.699Z").
+ * @param string method
+ * Specifies the request method (e.g. "GET", "POST", etc.)
+ * @param string url
+ * Specifies the request's url.
+ * @param boolean isXHR
+ * True if this request was initiated via XHR.
+ * @param object cause
+ * Specifies the request's cause. Has the following properties:
+ * - type: nsContentPolicyType constant
+ * - loadingDocumentUri: URI of the request origin
+ * - stacktrace: JS stacktrace of the request
+ * @param boolean fromCache
+ * Indicates if the result came from the browser cache
+ * @param boolean fromServiceWorker
+ * Indicates if the request has been intercepted by a Service Worker
+ */
+ addRequest: function (id, startedDateTime, method, url, isXHR, cause,
+ fromCache, fromServiceWorker) {
+ this._addQueue.push([id, startedDateTime, method, url, isXHR, cause,
+ fromCache, fromServiceWorker]);
+
+ // Lazy updating is disabled in some tests.
+ if (!this.lazyUpdate) {
+ return void this._flushRequests();
+ }
+
+ this._flushRequestsTask.arm();
+ return undefined;
+ },
+
+ /**
+ * Create a new custom request form populated with the data from
+ * the currently selected request.
+ */
+ cloneSelectedRequest: function () {
+ let selected = this.selectedItem.attachment;
+
+ // Create the element node for the network request item.
+ let menuView = this._createMenuView(selected.method, selected.url,
+ selected.cause);
+
+ // Append a network request item to this container.
+ let newItem = this.push([menuView], {
+ attachment: Object.create(selected, {
+ isCustom: { value: true }
+ })
+ });
+
+ // Immediately switch to new request pane.
+ this.selectedItem = newItem;
+ },
+
+ /**
+ * Send a new HTTP request using the data in the custom request form.
+ */
+ sendCustomRequest: function () {
+ let selected = this.selectedItem.attachment;
+
+ let data = {
+ url: selected.url,
+ method: selected.method,
+ httpVersion: selected.httpVersion,
+ };
+ if (selected.requestHeaders) {
+ data.headers = selected.requestHeaders.headers;
+ }
+ if (selected.requestPostData) {
+ data.body = selected.requestPostData.postData.text;
+ }
+
+ NetMonitorController.webConsoleClient.sendHTTPRequest(data, response => {
+ let id = response.eventActor.actor;
+ this._preferredItemId = id;
+ });
+
+ this.closeCustomRequest();
+ },
+
+ /**
+ * Remove the currently selected custom request.
+ */
+ closeCustomRequest: function () {
+ this.remove(this.selectedItem);
+ NetMonitorView.Sidebar.toggle(false);
+ },
+
+ /**
+ * Shows raw request/response headers in textboxes.
+ */
+ toggleRawHeaders: function () {
+ let requestTextarea = $("#raw-request-headers-textarea");
+ let responseTextare = $("#raw-response-headers-textarea");
+ let rawHeadersHidden = $("#raw-headers").getAttribute("hidden");
+
+ if (rawHeadersHidden) {
+ let selected = this.selectedItem.attachment;
+ let selectedRequestHeaders = selected.requestHeaders.headers;
+ let selectedResponseHeaders = selected.responseHeaders.headers;
+ requestTextarea.value = writeHeaderText(selectedRequestHeaders);
+ responseTextare.value = writeHeaderText(selectedResponseHeaders);
+ $("#raw-headers").hidden = false;
+ } else {
+ requestTextarea.value = null;
+ responseTextare.value = null;
+ $("#raw-headers").hidden = true;
+ }
+ },
+
+ /**
+ * Refreshes the view contents with the newly selected filters
+ */
+ reFilterRequests: function () {
+ this.filterContents(this._filterPredicate);
+ this.refreshSummary();
+ this.refreshZebra();
+ },
+
+ /**
+ * Returns a predicate that can be used to test if a request matches any of
+ * the active filters.
+ */
+ get _filterPredicate() {
+ let currentFreetextFilter = this._currentFreetextFilter;
+
+ return requestItem => {
+ const { attachment } = requestItem;
+ return this._activeFilters.some(filterName => Filters[filterName](attachment)) &&
+ isFreetextMatch(attachment, currentFreetextFilter);
+ };
+ },
+
+ /**
+ * Sorts all network requests in this container by a specified detail.
+ *
+ * @param string type
+ * Either "status", "method", "file", "domain", "type", "transferred",
+ * "size" or "waterfall".
+ */
+ sortBy: function (type = "waterfall") {
+ let target = $("#requests-menu-" + type + "-button");
+ let headers = document.querySelectorAll(".requests-menu-header-button");
+
+ for (let header of headers) {
+ if (header != target) {
+ header.removeAttribute("sorted");
+ header.removeAttribute("tooltiptext");
+ header.parentNode.removeAttribute("active");
+ }
+ }
+
+ let direction = "";
+ if (target) {
+ if (target.getAttribute("sorted") == "ascending") {
+ target.setAttribute("sorted", direction = "descending");
+ target.setAttribute("tooltiptext",
+ L10N.getStr("networkMenu.sortedDesc"));
+ } else {
+ target.setAttribute("sorted", direction = "ascending");
+ target.setAttribute("tooltiptext",
+ L10N.getStr("networkMenu.sortedAsc"));
+ }
+ // Used to style the next column.
+ target.parentNode.setAttribute("active", "true");
+ }
+
+ // Sort by whatever was requested.
+ switch (type) {
+ case "status":
+ if (direction == "ascending") {
+ this.sortContents((a, b) => Sorters.status(a.attachment, b.attachment));
+ } else {
+ this.sortContents((a, b) => -Sorters.status(a.attachment, b.attachment));
+ }
+ break;
+ case "method":
+ if (direction == "ascending") {
+ this.sortContents((a, b) => Sorters.method(a.attachment, b.attachment));
+ } else {
+ this.sortContents((a, b) => -Sorters.method(a.attachment, b.attachment));
+ }
+ break;
+ case "file":
+ if (direction == "ascending") {
+ this.sortContents((a, b) => Sorters.file(a.attachment, b.attachment));
+ } else {
+ this.sortContents((a, b) => -Sorters.file(a.attachment, b.attachment));
+ }
+ break;
+ case "domain":
+ if (direction == "ascending") {
+ this.sortContents((a, b) => Sorters.domain(a.attachment, b.attachment));
+ } else {
+ this.sortContents((a, b) => -Sorters.domain(a.attachment, b.attachment));
+ }
+ break;
+ case "cause":
+ if (direction == "ascending") {
+ this.sortContents((a, b) => Sorters.cause(a.attachment, b.attachment));
+ } else {
+ this.sortContents((a, b) => -Sorters.cause(a.attachment, b.attachment));
+ }
+ break;
+ case "type":
+ if (direction == "ascending") {
+ this.sortContents((a, b) => Sorters.type(a.attachment, b.attachment));
+ } else {
+ this.sortContents((a, b) => -Sorters.type(a.attachment, b.attachment));
+ }
+ break;
+ case "transferred":
+ if (direction == "ascending") {
+ this.sortContents((a, b) => Sorters.transferred(a.attachment, b.attachment));
+ } else {
+ this.sortContents((a, b) => -Sorters.transferred(a.attachment, b.attachment));
+ }
+ break;
+ case "size":
+ if (direction == "ascending") {
+ this.sortContents((a, b) => Sorters.size(a.attachment, b.attachment));
+ } else {
+ this.sortContents((a, b) => -Sorters.size(a.attachment, b.attachment));
+ }
+ break;
+ case "waterfall":
+ if (direction == "ascending") {
+ this.sortContents((a, b) => Sorters.waterfall(a.attachment, b.attachment));
+ } else {
+ this.sortContents((a, b) => -Sorters.waterfall(a.attachment, b.attachment));
+ }
+ break;
+ }
+
+ this.refreshSummary();
+ this.refreshZebra();
+ },
+
+ /**
+ * Removes all network requests and closes the sidebar if open.
+ */
+ clear: function () {
+ NetMonitorController.NetworkEventsHandler.clearMarkers();
+ NetMonitorView.Sidebar.toggle(false);
+
+ this.store.dispatch(Actions.disableToggleButton(true));
+ $("#requests-menu-empty-notice").hidden = false;
+
+ this.empty();
+ this.refreshSummary();
+ },
+
+ /**
+ * Refreshes the status displayed in this container's footer, providing
+ * concise information about all requests.
+ */
+ refreshSummary: function () {
+ let visibleItems = this.visibleItems;
+ let visibleRequestsCount = visibleItems.length;
+ if (!visibleRequestsCount) {
+ this._summary.setAttribute("label", L10N.getStr("networkMenu.empty"));
+ return;
+ }
+
+ let totalBytes = this._getTotalBytesOfRequests(visibleItems);
+ let totalMillis =
+ this._getNewestRequest(visibleItems).attachment.endedMillis -
+ this._getOldestRequest(visibleItems).attachment.startedMillis;
+
+ // https://developer.mozilla.org/en-US/docs/Localization_and_Plurals
+ let str = PluralForm.get(visibleRequestsCount,
+ L10N.getStr("networkMenu.summary"));
+
+ this._summary.setAttribute("label", str
+ .replace("#1", visibleRequestsCount)
+ .replace("#2", L10N.numberWithDecimals((totalBytes || 0) / 1024,
+ CONTENT_SIZE_DECIMALS))
+ .replace("#3", L10N.numberWithDecimals((totalMillis || 0) / 1000,
+ REQUEST_TIME_DECIMALS))
+ );
+ },
+
+ /**
+ * Adds odd/even attributes to all the visible items in this container.
+ */
+ refreshZebra: function () {
+ let visibleItems = this.visibleItems;
+
+ for (let i = 0, len = visibleItems.length; i < len; i++) {
+ let requestItem = visibleItems[i];
+ let requestTarget = requestItem.target;
+
+ if (i % 2 == 0) {
+ requestTarget.setAttribute("even", "");
+ requestTarget.removeAttribute("odd");
+ } else {
+ requestTarget.setAttribute("odd", "");
+ requestTarget.removeAttribute("even");
+ }
+ }
+ },
+
+ /**
+ * Attaches security icon click listener for the given request menu item.
+ *
+ * @param object item
+ * The network request item to attach the listener to.
+ */
+ attachSecurityIconClickListener: function ({ target }) {
+ let icon = $(".requests-security-state-icon", target);
+ icon.addEventListener("click", this._onSecurityIconClick);
+ },
+
+ /**
+ * Schedules adding additional information to a network request.
+ *
+ * @param string id
+ * An identifier coming from the network monitor controller.
+ * @param object data
+ * An object containing several { key: value } tuples of network info.
+ * Supported keys are "httpVersion", "status", "statusText" etc.
+ * @param function callback
+ * A function to call once the request has been updated in the view.
+ */
+ updateRequest: function (id, data, callback) {
+ this._updateQueue.push([id, data, callback]);
+
+ // Lazy updating is disabled in some tests.
+ if (!this.lazyUpdate) {
+ return void this._flushRequests();
+ }
+
+ this._flushRequestsTask.arm();
+ return undefined;
+ },
+
+ /**
+ * Starts adding all queued additional information about network requests.
+ */
+ _flushRequests: function () {
+ // Prevent displaying any updates received after the target closed.
+ if (NetMonitorView._isDestroyed) {
+ return;
+ }
+
+ let widget = NetMonitorView.RequestsMenu.widget;
+ let isScrolledToBottom = widget.isScrolledToBottom();
+
+ for (let [id, startedDateTime, method, url, isXHR, cause, fromCache,
+ fromServiceWorker] of this._addQueue) {
+ // Convert the received date/time string to a unix timestamp.
+ let unixTime = Date.parse(startedDateTime);
+
+ // Create the element node for the network request item.
+ let menuView = this._createMenuView(method, url, cause);
+
+ // Remember the first and last event boundaries.
+ this._registerFirstRequestStart(unixTime);
+ this._registerLastRequestEnd(unixTime);
+
+ // Append a network request item to this container.
+ let requestItem = this.push([menuView, id], {
+ attachment: {
+ startedDeltaMillis: unixTime - this._firstRequestStartedMillis,
+ startedMillis: unixTime,
+ method: method,
+ url: url,
+ isXHR: isXHR,
+ cause: cause,
+ fromCache: fromCache,
+ fromServiceWorker: fromServiceWorker
+ }
+ });
+
+ if (id == this._preferredItemId) {
+ this.selectedItem = requestItem;
+ }
+
+ window.emit(EVENTS.REQUEST_ADDED, id);
+ }
+
+ if (isScrolledToBottom && this._addQueue.length) {
+ widget.scrollToBottom();
+ }
+
+ // For each queued additional information packet, get the corresponding
+ // request item in the view and update it based on the specified data.
+ for (let [id, data, callback] of this._updateQueue) {
+ let requestItem = this.getItemByValue(id);
+ if (!requestItem) {
+ // Packet corresponds to a dead request item, target navigated.
+ continue;
+ }
+
+ // Each information packet may contain several { key: value } tuples of
+ // network info, so update the view based on each one.
+ for (let key in data) {
+ let val = data[key];
+ if (val === undefined) {
+ // The information in the packet is empty, it can be safely ignored.
+ continue;
+ }
+
+ switch (key) {
+ case "requestHeaders":
+ requestItem.attachment.requestHeaders = val;
+ break;
+ case "requestCookies":
+ requestItem.attachment.requestCookies = val;
+ break;
+ case "requestPostData":
+ // Search the POST data upload stream for request headers and add
+ // them to a separate store, different from the classic headers.
+ // XXX: Be really careful here! We're creating a function inside
+ // a loop, so remember the actual request item we want to modify.
+ let currentItem = requestItem;
+ let currentStore = { headers: [], headersSize: 0 };
+
+ Task.spawn(function* () {
+ let postData = yield gNetwork.getString(val.postData.text);
+ let payloadHeaders = CurlUtils.getHeadersFromMultipartText(
+ postData);
+
+ currentStore.headers = payloadHeaders;
+ currentStore.headersSize = payloadHeaders.reduce(
+ (acc, { name, value }) =>
+ acc + name.length + value.length + 2, 0);
+
+ // The `getString` promise is async, so we need to refresh the
+ // information displayed in the network details pane again here.
+ refreshNetworkDetailsPaneIfNecessary(currentItem);
+ });
+
+ requestItem.attachment.requestPostData = val;
+ requestItem.attachment.requestHeadersFromUploadStream =
+ currentStore;
+ break;
+ case "securityState":
+ requestItem.attachment.securityState = val;
+ this.updateMenuView(requestItem, key, val);
+ break;
+ case "securityInfo":
+ requestItem.attachment.securityInfo = val;
+ break;
+ case "responseHeaders":
+ requestItem.attachment.responseHeaders = val;
+ break;
+ case "responseCookies":
+ requestItem.attachment.responseCookies = val;
+ break;
+ case "httpVersion":
+ requestItem.attachment.httpVersion = val;
+ break;
+ case "remoteAddress":
+ requestItem.attachment.remoteAddress = val;
+ this.updateMenuView(requestItem, key, val);
+ break;
+ case "remotePort":
+ requestItem.attachment.remotePort = val;
+ break;
+ case "status":
+ requestItem.attachment.status = val;
+ this.updateMenuView(requestItem, key, {
+ status: val,
+ cached: requestItem.attachment.fromCache,
+ serviceWorker: requestItem.attachment.fromServiceWorker
+ });
+ break;
+ case "statusText":
+ requestItem.attachment.statusText = val;
+ let text = (requestItem.attachment.status + " " +
+ requestItem.attachment.statusText);
+ if (requestItem.attachment.fromCache) {
+ text += " (cached)";
+ } else if (requestItem.attachment.fromServiceWorker) {
+ text += " (service worker)";
+ }
+
+ this.updateMenuView(requestItem, key, text);
+ break;
+ case "headersSize":
+ requestItem.attachment.headersSize = val;
+ break;
+ case "contentSize":
+ requestItem.attachment.contentSize = val;
+ this.updateMenuView(requestItem, key, val);
+ break;
+ case "transferredSize":
+ if (requestItem.attachment.fromCache) {
+ requestItem.attachment.transferredSize = 0;
+ this.updateMenuView(requestItem, key, "cached");
+ } else if (requestItem.attachment.fromServiceWorker) {
+ requestItem.attachment.transferredSize = 0;
+ this.updateMenuView(requestItem, key, "service worker");
+ } else {
+ requestItem.attachment.transferredSize = val;
+ this.updateMenuView(requestItem, key, val);
+ }
+ break;
+ case "mimeType":
+ requestItem.attachment.mimeType = val;
+ this.updateMenuView(requestItem, key, val);
+ break;
+ case "responseContent":
+ // If there's no mime type available when the response content
+ // is received, assume text/plain as a fallback.
+ if (!requestItem.attachment.mimeType) {
+ requestItem.attachment.mimeType = "text/plain";
+ this.updateMenuView(requestItem, "mimeType", "text/plain");
+ }
+ requestItem.attachment.responseContent = val;
+ this.updateMenuView(requestItem, key, val);
+ break;
+ case "totalTime":
+ requestItem.attachment.totalTime = val;
+ requestItem.attachment.endedMillis =
+ requestItem.attachment.startedMillis + val;
+
+ this.updateMenuView(requestItem, key, val);
+ this._registerLastRequestEnd(requestItem.attachment.endedMillis);
+ break;
+ case "eventTimings":
+ requestItem.attachment.eventTimings = val;
+ this._createWaterfallView(
+ requestItem, val.timings,
+ requestItem.attachment.fromCache ||
+ requestItem.attachment.fromServiceWorker
+ );
+ break;
+ }
+ }
+ refreshNetworkDetailsPaneIfNecessary(requestItem);
+
+ if (callback) {
+ callback();
+ }
+ }
+
+ /**
+ * Refreshes the information displayed in the sidebar, in case this update
+ * may have additional information about a request which isn't shown yet
+ * in the network details pane.
+ *
+ * @param object requestItem
+ * The item to repopulate the sidebar with in case it's selected in
+ * this requests menu.
+ */
+ function refreshNetworkDetailsPaneIfNecessary(requestItem) {
+ let selectedItem = NetMonitorView.RequestsMenu.selectedItem;
+ if (selectedItem == requestItem) {
+ NetMonitorView.NetworkDetails.populate(selectedItem.attachment);
+ }
+ }
+
+ // We're done flushing all the requests, clear the update queue.
+ this._updateQueue = [];
+ this._addQueue = [];
+
+ this.store.dispatch(Actions.disableToggleButton(!this.itemCount));
+ $("#requests-menu-empty-notice").hidden = !!this.itemCount;
+
+ // Make sure all the requests are sorted and filtered.
+ // Freshly added requests may not yet contain all the information required
+ // for sorting and filtering predicates, so this is done each time the
+ // network requests table is flushed (don't worry, events are drained first
+ // so this doesn't happen once per network event update).
+ this.sortContents();
+ this.filterContents();
+ this.refreshSummary();
+ this.refreshZebra();
+
+ // Rescale all the waterfalls so that everything is visible at once.
+ this._flushWaterfallViews();
+ },
+
+ /**
+ * Customization function for creating an item's UI.
+ *
+ * @param string method
+ * Specifies the request method (e.g. "GET", "POST", etc.)
+ * @param string url
+ * Specifies the request's url.
+ * @param object cause
+ * Specifies the request's cause. Has two properties:
+ * - type: nsContentPolicyType constant
+ * - uri: URI of the request origin
+ * @return nsIDOMNode
+ * The network request view.
+ */
+ _createMenuView: function (method, url, cause) {
+ let template = $("#requests-menu-item-template");
+ let fragment = document.createDocumentFragment();
+
+ // Flatten the DOM by removing one redundant box (the template container).
+ for (let node of template.childNodes) {
+ fragment.appendChild(node.cloneNode(true));
+ }
+
+ this.updateMenuView(fragment, "method", method);
+ this.updateMenuView(fragment, "url", url);
+ this.updateMenuView(fragment, "cause", cause);
+
+ return fragment;
+ },
+
+ /**
+ * Get a human-readable string from a number of bytes, with the B, KB, MB, or
+ * GB value. Note that the transition between abbreviations is by 1000 rather
+ * than 1024 in order to keep the displayed digits smaller as "1016 KB" is
+ * more awkward than 0.99 MB"
+ */
+ getFormattedSize(bytes) {
+ if (bytes < MAX_BYTES_SIZE) {
+ return L10N.getFormatStr("networkMenu.sizeB", bytes);
+ } else if (bytes < MAX_KB_SIZE) {
+ let kb = bytes / BYTES_IN_KB;
+ let size = L10N.numberWithDecimals(kb, CONTENT_SIZE_DECIMALS);
+ return L10N.getFormatStr("networkMenu.sizeKB", size);
+ } else if (bytes < MAX_MB_SIZE) {
+ let mb = bytes / BYTES_IN_MB;
+ let size = L10N.numberWithDecimals(mb, CONTENT_SIZE_DECIMALS);
+ return L10N.getFormatStr("networkMenu.sizeMB", size);
+ }
+ let gb = bytes / BYTES_IN_GB;
+ let size = L10N.numberWithDecimals(gb, CONTENT_SIZE_DECIMALS);
+ return L10N.getFormatStr("networkMenu.sizeGB", size);
+ },
+
+ /**
+ * Updates the information displayed in a network request item view.
+ *
+ * @param object item
+ * The network request item in this container.
+ * @param string key
+ * The type of information that is to be updated.
+ * @param any value
+ * The new value to be shown.
+ * @return object
+ * A promise that is resolved once the information is displayed.
+ */
+ updateMenuView: Task.async(function* (item, key, value) {
+ let target = item.target || item;
+
+ switch (key) {
+ case "method": {
+ let node = $(".requests-menu-method", target);
+ node.setAttribute("value", value);
+ break;
+ }
+ case "url": {
+ let uri;
+ try {
+ uri = NetworkHelper.nsIURL(value);
+ } catch (e) {
+ // User input may not make a well-formed url yet.
+ break;
+ }
+ let nameWithQuery = getUriNameWithQuery(uri);
+ let hostPort = getUriHostPort(uri);
+ let host = getUriHost(uri);
+ let unicodeUrl = NetworkHelper.convertToUnicode(unescape(uri.spec));
+
+ let file = $(".requests-menu-file", target);
+ file.setAttribute("value", nameWithQuery);
+ file.setAttribute("tooltiptext", unicodeUrl);
+
+ let domain = $(".requests-menu-domain", target);
+ domain.setAttribute("value", hostPort);
+ domain.setAttribute("tooltiptext", hostPort);
+
+ // Mark local hosts specially, where "local" is as defined in the W3C
+ // spec for secure contexts.
+ // http://www.w3.org/TR/powerful-features/
+ //
+ // * If the name falls under 'localhost'
+ // * If the name is an IPv4 address within 127.0.0.0/8
+ // * If the name is an IPv6 address within ::1/128
+ //
+ // IPv6 parsing is a little sloppy; it assumes that the address has
+ // been validated before it gets here.
+ let icon = $(".requests-security-state-icon", target);
+ icon.classList.remove("security-state-local");
+ if (host.match(/(.+\.)?localhost$/) ||
+ host.match(/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}/) ||
+ host.match(/\[[0:]+1\]/)) {
+ let tooltip = L10N.getStr("netmonitor.security.state.secure");
+ icon.classList.add("security-state-local");
+ icon.setAttribute("tooltiptext", tooltip);
+ }
+
+ break;
+ }
+ case "remoteAddress":
+ let domain = $(".requests-menu-domain", target);
+ let tooltip = (domain.getAttribute("value") +
+ (value ? " (" + value + ")" : ""));
+ domain.setAttribute("tooltiptext", tooltip);
+ break;
+ case "securityState": {
+ let icon = $(".requests-security-state-icon", target);
+ this.attachSecurityIconClickListener(item);
+
+ // Security icon for local hosts is set in the "url" branch
+ if (icon.classList.contains("security-state-local")) {
+ break;
+ }
+
+ let tooltip2 = L10N.getStr("netmonitor.security.state." + value);
+ icon.classList.add("security-state-" + value);
+ icon.setAttribute("tooltiptext", tooltip2);
+ break;
+ }
+ case "status": {
+ let node = $(".requests-menu-status-icon", target);
+ // "code" attribute is only used by css to determine the icon color
+ let code;
+ if (value.cached) {
+ code = "cached";
+ } else if (value.serviceWorker) {
+ code = "service worker";
+ } else {
+ code = value.status;
+ }
+ node.setAttribute("code", code);
+ let codeNode = $(".requests-menu-status-code", target);
+ codeNode.setAttribute("value", value.status);
+ break;
+ }
+ case "statusText": {
+ let node = $(".requests-menu-status", target);
+ node.setAttribute("tooltiptext", value);
+ break;
+ }
+ case "cause": {
+ let labelNode = $(".requests-menu-cause-label", target);
+ labelNode.setAttribute("value", loadCauseString(value.type));
+ if (value.loadingDocumentUri) {
+ labelNode.setAttribute("tooltiptext", value.loadingDocumentUri);
+ }
+
+ let stackNode = $(".requests-menu-cause-stack", target);
+ if (value.stacktrace && value.stacktrace.length > 0) {
+ stackNode.removeAttribute("hidden");
+ }
+ break;
+ }
+ case "contentSize": {
+ let node = $(".requests-menu-size", target);
+
+ let text = this.getFormattedSize(value);
+
+ node.setAttribute("value", text);
+ node.setAttribute("tooltiptext", text);
+ break;
+ }
+ case "transferredSize": {
+ let node = $(".requests-menu-transferred", target);
+
+ let text;
+ if (value === null) {
+ text = L10N.getStr("networkMenu.sizeUnavailable");
+ } else if (value === "cached") {
+ text = L10N.getStr("networkMenu.sizeCached");
+ node.classList.add("theme-comment");
+ } else if (value === "service worker") {
+ text = L10N.getStr("networkMenu.sizeServiceWorker");
+ node.classList.add("theme-comment");
+ } else {
+ text = this.getFormattedSize(value);
+ }
+
+ node.setAttribute("value", text);
+ node.setAttribute("tooltiptext", text);
+ break;
+ }
+ case "mimeType": {
+ let type = getAbbreviatedMimeType(value);
+ let node = $(".requests-menu-type", target);
+ let text = CONTENT_MIME_TYPE_ABBREVIATIONS[type] || type;
+ node.setAttribute("value", text);
+ node.setAttribute("tooltiptext", value);
+ break;
+ }
+ case "responseContent": {
+ let { mimeType } = item.attachment;
+
+ if (mimeType.includes("image/")) {
+ let { text, encoding } = value.content;
+ let responseBody = yield gNetwork.getString(text);
+ let node = $(".requests-menu-icon", item.target);
+ node.src = formDataURI(mimeType, encoding, responseBody);
+ node.setAttribute("type", "thumbnail");
+ node.removeAttribute("hidden");
+
+ window.emit(EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED);
+ }
+ break;
+ }
+ case "totalTime": {
+ let node = $(".requests-menu-timings-total", target);
+
+ // integer
+ let text = L10N.getFormatStr("networkMenu.totalMS", value);
+ node.setAttribute("value", text);
+ node.setAttribute("tooltiptext", text);
+ break;
+ }
+ }
+ }),
+
+ /**
+ * Creates a waterfall representing timing information in a network
+ * request item view.
+ *
+ * @param object item
+ * The network request item in this container.
+ * @param object timings
+ * An object containing timing information.
+ * @param boolean fromCache
+ * Indicates if the result came from the browser cache or
+ * a service worker
+ */
+ _createWaterfallView: function (item, timings, fromCache) {
+ let { target } = item;
+ let sections = ["blocked", "dns", "connect", "send", "wait", "receive"];
+ // Skipping "blocked" because it doesn't work yet.
+
+ let timingsNode = $(".requests-menu-timings", target);
+ let timingsTotal = $(".requests-menu-timings-total", timingsNode);
+
+ if (fromCache) {
+ timingsTotal.style.display = "none";
+ return;
+ }
+
+ // Add a set of boxes representing timing information.
+ for (let key of sections) {
+ let width = timings[key];
+
+ // Don't render anything if it surely won't be visible.
+ // One millisecond == one unscaled pixel.
+ if (width > 0) {
+ let timingBox = document.createElement("hbox");
+ timingBox.className = "requests-menu-timings-box " + key;
+ timingBox.setAttribute("width", width);
+ timingsNode.insertBefore(timingBox, timingsTotal);
+ }
+ }
+ },
+
+ /**
+ * Rescales and redraws all the waterfall views in this container.
+ *
+ * @param boolean reset
+ * True if this container's width was changed.
+ */
+ _flushWaterfallViews: function (reset) {
+ // Don't paint things while the waterfall view isn't even visible,
+ // or there are no items added to this container.
+ if (NetMonitorView.currentFrontendMode !=
+ "network-inspector-view" || !this.itemCount) {
+ return;
+ }
+
+ // To avoid expensive operations like getBoundingClientRect() and
+ // rebuilding the waterfall background each time a new request comes in,
+ // stuff is cached. However, in certain scenarios like when the window
+ // is resized, this needs to be invalidated.
+ if (reset) {
+ this._cachedWaterfallWidth = 0;
+ }
+
+ // Determine the scaling to be applied to all the waterfalls so that
+ // everything is visible at once. One millisecond == one unscaled pixel.
+ let availableWidth = this._waterfallWidth - REQUESTS_WATERFALL_SAFE_BOUNDS;
+ let longestWidth = this._lastRequestEndedMillis -
+ this._firstRequestStartedMillis;
+ let scale = Math.min(Math.max(availableWidth / longestWidth, EPSILON), 1);
+
+ // Redraw and set the canvas background for each waterfall view.
+ this._showWaterfallDivisionLabels(scale);
+ this._drawWaterfallBackground(scale);
+
+ // Apply CSS transforms to each waterfall in this container totalTime
+ // accurately translate and resize as needed.
+ for (let { target, attachment } of this) {
+ let timingsNode = $(".requests-menu-timings", target);
+ let totalNode = $(".requests-menu-timings-total", target);
+ let direction = window.isRTL ? -1 : 1;
+
+ // Render the timing information at a specific horizontal translation
+ // based on the delta to the first monitored event network.
+ let translateX = "translateX(" + (direction *
+ attachment.startedDeltaMillis) + "px)";
+
+ // Based on the total time passed until the last request, rescale
+ // all the waterfalls to a reasonable size.
+ let scaleX = "scaleX(" + scale + ")";
+
+ // Certain nodes should not be scaled, even if they're children of
+ // another scaled node. In this case, apply a reversed transformation.
+ let revScaleX = "scaleX(" + (1 / scale) + ")";
+
+ timingsNode.style.transform = scaleX + " " + translateX;
+ totalNode.style.transform = revScaleX;
+ }
+ },
+
+ /**
+ * Creates the labels displayed on the waterfall header in this container.
+ *
+ * @param number scale
+ * The current waterfall scale.
+ */
+ _showWaterfallDivisionLabels: function (scale) {
+ let container = $("#requests-menu-waterfall-label-wrapper");
+ let availableWidth = this._waterfallWidth - REQUESTS_WATERFALL_SAFE_BOUNDS;
+
+ // Nuke all existing labels.
+ while (container.hasChildNodes()) {
+ container.firstChild.remove();
+ }
+
+ // Build new millisecond tick labels...
+ let timingStep = REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE;
+ let optimalTickIntervalFound = false;
+
+ while (!optimalTickIntervalFound) {
+ // Ignore any divisions that would end up being too close to each other.
+ let scaledStep = scale * timingStep;
+ if (scaledStep < REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN) {
+ timingStep <<= 1;
+ continue;
+ }
+ optimalTickIntervalFound = true;
+
+ // Insert one label for each division on the current scale.
+ let fragment = document.createDocumentFragment();
+ let direction = window.isRTL ? -1 : 1;
+
+ for (let x = 0; x < availableWidth; x += scaledStep) {
+ let translateX = "translateX(" + ((direction * x) | 0) + "px)";
+ let millisecondTime = x / scale;
+
+ let normalizedTime = millisecondTime;
+ let divisionScale = "millisecond";
+
+ // If the division is greater than 1 minute.
+ if (normalizedTime > 60000) {
+ normalizedTime /= 60000;
+ divisionScale = "minute";
+ } else if (normalizedTime > 1000) {
+ // If the division is greater than 1 second.
+ normalizedTime /= 1000;
+ divisionScale = "second";
+ }
+
+ // Showing too many decimals is bad UX.
+ if (divisionScale == "millisecond") {
+ normalizedTime |= 0;
+ } else {
+ normalizedTime = L10N.numberWithDecimals(normalizedTime,
+ REQUEST_TIME_DECIMALS);
+ }
+
+ let node = document.createElement("label");
+ let text = L10N.getFormatStr("networkMenu." +
+ divisionScale, normalizedTime);
+ node.className = "plain requests-menu-timings-division";
+ node.setAttribute("division-scale", divisionScale);
+ node.style.transform = translateX;
+
+ node.setAttribute("value", text);
+ fragment.appendChild(node);
+ }
+ container.appendChild(fragment);
+
+ container.className = "requests-menu-waterfall-visible";
+ }
+ },
+
+ /**
+ * Creates the background displayed on each waterfall view in this container.
+ *
+ * @param number scale
+ * The current waterfall scale.
+ */
+ _drawWaterfallBackground: function (scale) {
+ if (!this._canvas || !this._ctx) {
+ this._canvas = document.createElementNS(HTML_NS, "canvas");
+ this._ctx = this._canvas.getContext("2d");
+ }
+ let canvas = this._canvas;
+ let ctx = this._ctx;
+
+ // Nuke the context.
+ let canvasWidth = canvas.width = this._waterfallWidth;
+ // Awww yeah, 1px, repeats on Y axis.
+ let canvasHeight = canvas.height = 1;
+
+ // Start over.
+ let imageData = ctx.createImageData(canvasWidth, canvasHeight);
+ let pixelArray = imageData.data;
+
+ let buf = new ArrayBuffer(pixelArray.length);
+ let view8bit = new Uint8ClampedArray(buf);
+ let view32bit = new Uint32Array(buf);
+
+ // Build new millisecond tick lines...
+ let timingStep = REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE;
+ let [r, g, b] = REQUESTS_WATERFALL_BACKGROUND_TICKS_COLOR_RGB;
+ let alphaComponent = REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_MIN;
+ let optimalTickIntervalFound = false;
+
+ while (!optimalTickIntervalFound) {
+ // Ignore any divisions that would end up being too close to each other.
+ let scaledStep = scale * timingStep;
+ if (scaledStep < REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN) {
+ timingStep <<= 1;
+ continue;
+ }
+ optimalTickIntervalFound = true;
+
+ // Insert one pixel for each division on each scale.
+ for (let i = 1; i <= REQUESTS_WATERFALL_BACKGROUND_TICKS_SCALES; i++) {
+ let increment = scaledStep * Math.pow(2, i);
+ for (let x = 0; x < canvasWidth; x += increment) {
+ let position = (window.isRTL ? canvasWidth - x : x) | 0;
+ view32bit[position] =
+ (alphaComponent << 24) | (b << 16) | (g << 8) | r;
+ }
+ alphaComponent += REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD;
+ }
+ }
+
+ {
+ let t = NetMonitorController.NetworkEventsHandler
+ .firstDocumentDOMContentLoadedTimestamp;
+
+ let delta = Math.floor((t - this._firstRequestStartedMillis) * scale);
+ let [r1, g1, b1, a1] =
+ REQUESTS_WATERFALL_DOMCONTENTLOADED_TICKS_COLOR_RGBA;
+ view32bit[delta] = (a1 << 24) | (r1 << 16) | (g1 << 8) | b1;
+ }
+ {
+ let t = NetMonitorController.NetworkEventsHandler
+ .firstDocumentLoadTimestamp;
+
+ let delta = Math.floor((t - this._firstRequestStartedMillis) * scale);
+ let [r2, g2, b2, a2] = REQUESTS_WATERFALL_LOAD_TICKS_COLOR_RGBA;
+ view32bit[delta] = (a2 << 24) | (r2 << 16) | (g2 << 8) | b2;
+ }
+
+ // Flush the image data and cache the waterfall background.
+ pixelArray.set(view8bit);
+ ctx.putImageData(imageData, 0, 0);
+ document.mozSetImageElement("waterfall-background", canvas);
+ },
+
+ /**
+ * The selection listener for this container.
+ */
+ _onSelect: function ({ detail: item }) {
+ if (item) {
+ NetMonitorView.Sidebar.populate(item.attachment);
+ NetMonitorView.Sidebar.toggle(true);
+ } else {
+ NetMonitorView.Sidebar.toggle(false);
+ }
+ },
+
+ /**
+ * The swap listener for this container.
+ * Called when two items switch places, when the contents are sorted.
+ */
+ _onSwap: function ({ detail: [firstItem, secondItem] }) {
+ // Reattach click listener to the security icons
+ this.attachSecurityIconClickListener(firstItem);
+ this.attachSecurityIconClickListener(secondItem);
+ },
+
+ /**
+ * The predicate used when deciding whether a popup should be shown
+ * over a request item or not.
+ *
+ * @param nsIDOMNode target
+ * The element node currently being hovered.
+ * @param object tooltip
+ * The current tooltip instance.
+ * @return {Promise}
+ */
+ _onHover: Task.async(function* (target, tooltip) {
+ let requestItem = this.getItemForElement(target);
+ if (!requestItem) {
+ return false;
+ }
+
+ let hovered = requestItem.attachment;
+ if (hovered.responseContent && target.closest(".requests-menu-icon-and-file")) {
+ return this._setTooltipImageContent(tooltip, requestItem);
+ } else if (hovered.cause && target.closest(".requests-menu-cause-stack")) {
+ return this._setTooltipStackTraceContent(tooltip, requestItem);
+ }
+
+ return false;
+ }),
+
+ _setTooltipImageContent: Task.async(function* (tooltip, requestItem) {
+ let { mimeType, text, encoding } = requestItem.attachment.responseContent.content;
+
+ if (!mimeType || !mimeType.includes("image/")) {
+ return false;
+ }
+
+ let string = yield gNetwork.getString(text);
+ let src = formDataURI(mimeType, encoding, string);
+ let maxDim = REQUESTS_TOOLTIP_IMAGE_MAX_DIM;
+ let { naturalWidth, naturalHeight } = yield getImageDimensions(tooltip.doc, src);
+ let options = { maxDim, naturalWidth, naturalHeight };
+ setImageTooltip(tooltip, tooltip.doc, src, options);
+
+ return $(".requests-menu-icon", requestItem.target);
+ }),
+
+ _setTooltipStackTraceContent: Task.async(function* (tooltip, requestItem) {
+ let {stacktrace} = requestItem.attachment.cause;
+
+ if (!stacktrace || stacktrace.length == 0) {
+ return false;
+ }
+
+ let doc = tooltip.doc;
+ let el = doc.createElementNS(HTML_NS, "div");
+ el.className = "stack-trace-tooltip devtools-monospace";
+
+ for (let f of stacktrace) {
+ let { functionName, filename, lineNumber, columnNumber, asyncCause } = f;
+
+ if (asyncCause) {
+ // if there is asyncCause, append a "divider" row into the trace
+ let asyncFrameEl = doc.createElementNS(HTML_NS, "div");
+ asyncFrameEl.className = "stack-frame stack-frame-async";
+ asyncFrameEl.textContent =
+ WEBCONSOLE_L10N.getFormatStr("stacktrace.asyncStack", asyncCause);
+ el.appendChild(asyncFrameEl);
+ }
+
+ // Parse a source name in format "url -> url"
+ let sourceUrl = filename.split(" -> ").pop();
+
+ let frameEl = doc.createElementNS(HTML_NS, "div");
+ frameEl.className = "stack-frame stack-frame-call";
+
+ let funcEl = doc.createElementNS(HTML_NS, "span");
+ funcEl.className = "stack-frame-function-name";
+ funcEl.textContent =
+ functionName || WEBCONSOLE_L10N.getStr("stacktrace.anonymousFunction");
+ frameEl.appendChild(funcEl);
+
+ let sourceEl = doc.createElementNS(HTML_NS, "span");
+ sourceEl.className = "stack-frame-source-name";
+ frameEl.appendChild(sourceEl);
+
+ let sourceInnerEl = doc.createElementNS(HTML_NS, "span");
+ sourceInnerEl.className = "stack-frame-source-name-inner";
+ sourceEl.appendChild(sourceInnerEl);
+
+ sourceInnerEl.textContent = sourceUrl;
+ sourceInnerEl.title = sourceUrl;
+
+ let lineEl = doc.createElementNS(HTML_NS, "span");
+ lineEl.className = "stack-frame-line";
+ lineEl.textContent = `:${lineNumber}:${columnNumber}`;
+ sourceInnerEl.appendChild(lineEl);
+
+ frameEl.addEventListener("click", () => {
+ // hide the tooltip immediately, not after delay
+ tooltip.hide();
+ NetMonitorController.viewSourceInDebugger(filename, lineNumber);
+ }, false);
+
+ el.appendChild(frameEl);
+ }
+
+ tooltip.setContent(el, {width: REQUESTS_TOOLTIP_STACK_TRACE_WIDTH});
+
+ return true;
+ }),
+
+ /**
+ * A handler that opens the security tab in the details view if secure or
+ * broken security indicator is clicked.
+ */
+ _onSecurityIconClick: function (e) {
+ let state = this.selectedItem.attachment.securityState;
+ if (state !== "insecure") {
+ // Choose the security tab.
+ NetMonitorView.NetworkDetails.widget.selectedIndex = 5;
+ }
+ },
+
+ /**
+ * The resize listener for this container's window.
+ */
+ _onResize: function (e) {
+ // Allow requests to settle down first.
+ setNamedTimeout("resize-events",
+ RESIZE_REFRESH_RATE, () => this._flushWaterfallViews(true));
+ },
+
+ /**
+ * Scroll listener for the requests menu view.
+ */
+ _onScroll: function () {
+ this.tooltip.hide();
+ },
+
+ /**
+ * Open context menu
+ */
+ _onContextMenu: function (e) {
+ e.preventDefault();
+ this.contextMenu.open(e);
+ },
+
+ /**
+ * Checks if the specified unix time is the first one to be known of,
+ * and saves it if so.
+ *
+ * @param number unixTime
+ * The milliseconds to check and save.
+ */
+ _registerFirstRequestStart: function (unixTime) {
+ if (this._firstRequestStartedMillis == -1) {
+ this._firstRequestStartedMillis = unixTime;
+ }
+ },
+
+ /**
+ * Checks if the specified unix time is the last one to be known of,
+ * and saves it if so.
+ *
+ * @param number unixTime
+ * The milliseconds to check and save.
+ */
+ _registerLastRequestEnd: function (unixTime) {
+ if (this._lastRequestEndedMillis < unixTime) {
+ this._lastRequestEndedMillis = unixTime;
+ }
+ },
+
+ /**
+ * Gets the total number of bytes representing the cumulated content size of
+ * a set of requests. Returns 0 for an empty set.
+ *
+ * @param array itemsArray
+ * @return number
+ */
+ _getTotalBytesOfRequests: function (itemsArray) {
+ if (!itemsArray.length) {
+ return 0;
+ }
+
+ let result = 0;
+ itemsArray.forEach(item => {
+ let size = item.attachment.contentSize;
+ result += (typeof size == "number") ? size : 0;
+ });
+
+ return result;
+ },
+
+ /**
+ * Gets the oldest (first performed) request in a set. Returns null for an
+ * empty set.
+ *
+ * @param array itemsArray
+ * @return object
+ */
+ _getOldestRequest: function (itemsArray) {
+ if (!itemsArray.length) {
+ return null;
+ }
+ return itemsArray.reduce((prev, curr) =>
+ prev.attachment.startedMillis < curr.attachment.startedMillis ?
+ prev : curr);
+ },
+
+ /**
+ * Gets the newest (latest performed) request in a set. Returns null for an
+ * empty set.
+ *
+ * @param array itemsArray
+ * @return object
+ */
+ _getNewestRequest: function (itemsArray) {
+ if (!itemsArray.length) {
+ return null;
+ }
+ return itemsArray.reduce((prev, curr) =>
+ prev.attachment.startedMillis > curr.attachment.startedMillis ?
+ prev : curr);
+ },
+
+ /**
+ * Gets the available waterfall width in this container.
+ * @return number
+ */
+ get _waterfallWidth() {
+ if (this._cachedWaterfallWidth == 0) {
+ let container = $("#requests-menu-toolbar");
+ let waterfall = $("#requests-menu-waterfall-header-box");
+ let containerBounds = container.getBoundingClientRect();
+ let waterfallBounds = waterfall.getBoundingClientRect();
+ if (!window.isRTL) {
+ this._cachedWaterfallWidth = containerBounds.width -
+ waterfallBounds.left;
+ } else {
+ this._cachedWaterfallWidth = waterfallBounds.right;
+ }
+ }
+ return this._cachedWaterfallWidth;
+ },
+
+ _splitter: null,
+ _summary: null,
+ _canvas: null,
+ _ctx: null,
+ _cachedWaterfallWidth: 0,
+ _firstRequestStartedMillis: -1,
+ _lastRequestEndedMillis: -1,
+ _updateQueue: [],
+ _addQueue: [],
+ _updateTimeout: null,
+ _resizeTimeout: null,
+ _activeFilters: ["all"],
+ _currentFreetextFilter: ""
+});
+
+exports.RequestsMenuView = RequestsMenuView;
diff --git a/devtools/client/netmonitor/selectors/index.js b/devtools/client/netmonitor/selectors/index.js
new file mode 100644
index 000000000..f473149b5
--- /dev/null
+++ b/devtools/client/netmonitor/selectors/index.js
@@ -0,0 +1,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/. */
+"use strict";
+
+module.exports = {
+ // selectors...
+};
diff --git a/devtools/client/netmonitor/selectors/moz.build b/devtools/client/netmonitor/selectors/moz.build
new file mode 100644
index 000000000..b3975906e
--- /dev/null
+++ b/devtools/client/netmonitor/selectors/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(
+ 'index.js'
+)
diff --git a/devtools/client/netmonitor/sort-predicates.js b/devtools/client/netmonitor/sort-predicates.js
new file mode 100644
index 000000000..1ead67c22
--- /dev/null
+++ b/devtools/client/netmonitor/sort-predicates.js
@@ -0,0 +1,92 @@
+"use strict";
+
+const { getAbbreviatedMimeType,
+ getUriNameWithQuery,
+ getUriHostPort,
+ loadCauseString } = require("./request-utils");
+
+/**
+ * Predicates used when sorting items.
+ *
+ * @param object first
+ * The first item used in the comparison.
+ * @param object second
+ * The second item used in the comparison.
+ * @return number
+ * <0 to sort first to a lower index than second
+ * =0 to leave first and second unchanged with respect to each other
+ * >0 to sort second to a lower index than first
+ */
+
+function waterfall(first, second) {
+ return first.startedMillis - second.startedMillis;
+}
+
+function status(first, second) {
+ return first.status == second.status
+ ? first.startedMillis - second.startedMillis
+ : first.status - second.status;
+}
+
+function method(first, second) {
+ if (first.method == second.method) {
+ return first.startedMillis - second.startedMillis;
+ }
+ return first.method > second.method ? 1 : -1;
+}
+
+function file(first, second) {
+ let firstUrl = getUriNameWithQuery(first.url).toLowerCase();
+ let secondUrl = getUriNameWithQuery(second.url).toLowerCase();
+ if (firstUrl == secondUrl) {
+ return first.startedMillis - second.startedMillis;
+ }
+ return firstUrl > secondUrl ? 1 : -1;
+}
+
+function domain(first, second) {
+ let firstDomain = getUriHostPort(first.url).toLowerCase();
+ let secondDomain = getUriHostPort(second.url).toLowerCase();
+ if (firstDomain == secondDomain) {
+ return first.startedMillis - second.startedMillis;
+ }
+ return firstDomain > secondDomain ? 1 : -1;
+}
+
+function cause(first, second) {
+ let firstCause = loadCauseString(first.cause.type);
+ let secondCause = loadCauseString(second.cause.type);
+ if (firstCause == secondCause) {
+ return first.startedMillis - second.startedMillis;
+ }
+ return firstCause > secondCause ? 1 : -1;
+}
+
+function type(first, second) {
+ let firstType = getAbbreviatedMimeType(first.mimeType).toLowerCase();
+ let secondType = getAbbreviatedMimeType(second.mimeType).toLowerCase();
+ if (firstType == secondType) {
+ return first.startedMillis - second.startedMillis;
+ }
+ return firstType > secondType ? 1 : -1;
+}
+
+function transferred(first, second) {
+ return first.transferredSize - second.transferredSize;
+}
+
+function size(first, second) {
+ return first.contentSize - second.contentSize;
+}
+
+exports.Sorters = {
+ status,
+ method,
+ file,
+ domain,
+ cause,
+ type,
+ transferred,
+ size,
+ waterfall,
+};
diff --git a/devtools/client/netmonitor/store.js b/devtools/client/netmonitor/store.js
new file mode 100644
index 000000000..454b94711
--- /dev/null
+++ b/devtools/client/netmonitor/store.js
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const createStore = require("devtools/client/shared/redux/create-store");
+const reducers = require("./reducers/index");
+
+function configureStore() {
+ return createStore()(reducers);
+}
+
+exports.configureStore = configureStore;
diff --git a/devtools/client/netmonitor/test/.eslintrc.js b/devtools/client/netmonitor/test/.eslintrc.js
new file mode 100644
index 000000000..8d15a76d9
--- /dev/null
+++ b/devtools/client/netmonitor/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/netmonitor/test/browser.ini b/devtools/client/netmonitor/test/browser.ini
new file mode 100644
index 000000000..5dfe9012d
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser.ini
@@ -0,0 +1,156 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ dropmarker.svg
+ head.js
+ html_cause-test-page.html
+ html_content-type-test-page.html
+ html_content-type-without-cache-test-page.html
+ html_brotli-test-page.html
+ html_image-tooltip-test-page.html
+ html_cors-test-page.html
+ html_custom-get-page.html
+ html_cyrillic-test-page.html
+ html_frame-test-page.html
+ html_frame-subdocument.html
+ html_filter-test-page.html
+ html_infinite-get-page.html
+ html_json-custom-mime-test-page.html
+ html_json-long-test-page.html
+ html_json-malformed-test-page.html
+ html_json-text-mime-test-page.html
+ html_jsonp-test-page.html
+ html_navigate-test-page.html
+ html_params-test-page.html
+ html_post-data-test-page.html
+ html_post-json-test-page.html
+ html_post-raw-test-page.html
+ html_post-raw-with-headers-test-page.html
+ html_simple-test-page.html
+ html_single-get-page.html
+ html_send-beacon.html
+ html_sorting-test-page.html
+ html_statistics-test-page.html
+ html_status-codes-test-page.html
+ html_api-calls-test-page.html
+ html_copy-as-curl.html
+ html_curl-utils.html
+ sjs_content-type-test-server.sjs
+ sjs_cors-test-server.sjs
+ sjs_https-redirect-test-server.sjs
+ sjs_hsts-test-server.sjs
+ sjs_simple-test-server.sjs
+ sjs_sorting-test-server.sjs
+ sjs_status-codes-test-server.sjs
+ sjs_truncate-test-server.sjs
+ test-image.png
+ service-workers/status-codes.html
+ service-workers/status-codes-service-worker.js
+ !/devtools/client/framework/test/shared-head.js
+
+[browser_net_aaa_leaktest.js]
+[browser_net_accessibility-01.js]
+[browser_net_accessibility-02.js]
+skip-if = (toolkit == "cocoa" && e10s) # bug 1252254
+[browser_net_api-calls.js]
+[browser_net_autoscroll.js]
+skip-if = true # Bug 1309191 - replace with rewritten version in React
+[browser_net_cached-status.js]
+[browser_net_cause.js]
+[browser_net_cause_redirect.js]
+[browser_net_service-worker-status.js]
+[browser_net_charts-01.js]
+[browser_net_charts-02.js]
+[browser_net_charts-03.js]
+[browser_net_charts-04.js]
+[browser_net_charts-05.js]
+[browser_net_charts-06.js]
+[browser_net_charts-07.js]
+[browser_net_clear.js]
+[browser_net_complex-params.js]
+[browser_net_content-type.js]
+[browser_net_brotli.js]
+[browser_net_curl-utils.js]
+[browser_net_copy_image_as_data_uri.js]
+subsuite = clipboard
+[browser_net_copy_svg_image_as_data_uri.js]
+subsuite = clipboard
+[browser_net_copy_url.js]
+subsuite = clipboard
+[browser_net_copy_params.js]
+subsuite = clipboard
+[browser_net_copy_response.js]
+subsuite = clipboard
+[browser_net_copy_headers.js]
+subsuite = clipboard
+[browser_net_copy_as_curl.js]
+subsuite = clipboard
+[browser_net_cors_requests.js]
+[browser_net_cyrillic-01.js]
+[browser_net_cyrillic-02.js]
+[browser_net_details-no-duplicated-content.js]
+skip-if = (os == 'linux' && e10s && debug) # Bug 1242204
+[browser_net_frame.js]
+[browser_net_filter-01.js]
+[browser_net_filter-02.js]
+[browser_net_filter-03.js]
+[browser_net_filter-04.js]
+[browser_net_footer-summary.js]
+[browser_net_html-preview.js]
+[browser_net_icon-preview.js]
+[browser_net_image-tooltip.js]
+[browser_net_json-long.js]
+[browser_net_json-malformed.js]
+[browser_net_json_custom_mime.js]
+[browser_net_json_text_mime.js]
+[browser_net_jsonp.js]
+[browser_net_large-response.js]
+[browser_net_leak_on_tab_close.js]
+[browser_net_open_request_in_tab.js]
+[browser_net_page-nav.js]
+[browser_net_pane-collapse.js]
+[browser_net_pane-toggle.js]
+[browser_net_post-data-01.js]
+[browser_net_post-data-02.js]
+[browser_net_post-data-03.js]
+[browser_net_post-data-04.js]
+[browser_net_prefs-and-l10n.js]
+[browser_net_prefs-reload.js]
+[browser_net_raw_headers.js]
+[browser_net_reload-button.js]
+[browser_net_reload-markers.js]
+[browser_net_req-resp-bodies.js]
+[browser_net_resend_cors.js]
+[browser_net_resend_headers.js]
+[browser_net_resend.js]
+[browser_net_security-details.js]
+[browser_net_security-error.js]
+[browser_net_security-icon-click.js]
+[browser_net_security-redirect.js]
+[browser_net_security-state.js]
+[browser_net_security-tab-deselect.js]
+[browser_net_security-tab-visibility.js]
+[browser_net_security-warnings.js]
+[browser_net_send-beacon.js]
+[browser_net_send-beacon-other-tab.js]
+[browser_net_simple-init.js]
+[browser_net_simple-request-data.js]
+skip-if = true # Bug 1258809
+[browser_net_simple-request-details.js]
+skip-if = true # Bug 1258809
+[browser_net_simple-request.js]
+[browser_net_sort-01.js]
+skip-if = (e10s && debug && os == 'mac') # Bug 1253037
+[browser_net_sort-02.js]
+[browser_net_sort-03.js]
+[browser_net_statistics-01.js]
+[browser_net_statistics-02.js]
+[browser_net_statistics-03.js]
+[browser_net_status-codes.js]
+[browser_net_streaming-response.js]
+[browser_net_throttle.js]
+[browser_net_timeline_ticks.js]
+[browser_net_timing-division.js]
+[browser_net_truncate.js]
+[browser_net_persistent_logs.js]
diff --git a/devtools/client/netmonitor/test/browser_net_aaa_leaktest.js b/devtools/client/netmonitor/test/browser_net_aaa_leaktest.js
new file mode 100644
index 000000000..31c1e03ad
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_aaa_leaktest.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the network monitor leaks on initialization and sudden destruction.
+ * You can also use this initialization format as a template for other tests.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { document, NetMonitorView, NetMonitorController } = monitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+
+ ok(tab, "Should have a tab available.");
+ ok(monitor, "Should have a network monitor pane available.");
+
+ ok(document, "Should have a document available.");
+ ok(NetMonitorView, "Should have a NetMonitorView object available.");
+ ok(NetMonitorController, "Should have a NetMonitorController object available.");
+ ok(RequestsMenu, "Should have a RequestsMenu object available.");
+ ok(NetworkDetails, "Should have a NetworkDetails object available.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_accessibility-01.js b/devtools/client/netmonitor/test/browser_net_accessibility-01.js
new file mode 100644
index 000000000..c0832064f
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_accessibility-01.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if focus modifiers work for the SideMenuWidget.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ info("Starting test... ");
+
+ // It seems that this test may be slow on Ubuntu builds running on ec2.
+ requestLongerTimeout(2);
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let count = 0;
+ function check(selectedIndex, paneVisibility) {
+ info("Performing check " + (count++) + ".");
+
+ is(RequestsMenu.selectedIndex, selectedIndex,
+ "The selected item in the requests menu was incorrect.");
+ is(NetMonitorView.detailsPaneHidden, !paneVisibility,
+ "The network requests details pane visibility state was incorrect.");
+ }
+
+ let wait = waitForNetworkEvents(monitor, 2);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests(2);
+ });
+ yield wait;
+
+ check(-1, false);
+
+ RequestsMenu.focusLastVisibleItem();
+ check(1, true);
+ RequestsMenu.focusFirstVisibleItem();
+ check(0, true);
+
+ RequestsMenu.focusNextItem();
+ check(1, true);
+ RequestsMenu.focusPrevItem();
+ check(0, true);
+
+ RequestsMenu.focusItemAtDelta(+1);
+ check(1, true);
+ RequestsMenu.focusItemAtDelta(-1);
+ check(0, true);
+
+ RequestsMenu.focusItemAtDelta(+10);
+ check(1, true);
+ RequestsMenu.focusItemAtDelta(-10);
+ check(0, true);
+
+ wait = waitForNetworkEvents(monitor, 18);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests(18);
+ });
+ yield wait;
+
+ RequestsMenu.focusLastVisibleItem();
+ check(19, true);
+ RequestsMenu.focusFirstVisibleItem();
+ check(0, true);
+
+ RequestsMenu.focusNextItem();
+ check(1, true);
+ RequestsMenu.focusPrevItem();
+ check(0, true);
+
+ RequestsMenu.focusItemAtDelta(+10);
+ check(10, true);
+ RequestsMenu.focusItemAtDelta(-10);
+ check(0, true);
+
+ RequestsMenu.focusItemAtDelta(+100);
+ check(19, true);
+ RequestsMenu.focusItemAtDelta(-100);
+ check(0, true);
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_accessibility-02.js b/devtools/client/netmonitor/test/browser_net_accessibility-02.js
new file mode 100644
index 000000000..33420a440
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_accessibility-02.js
@@ -0,0 +1,130 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if keyboard and mouse navigation works in the network requests menu.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ info("Starting test... ");
+
+ // It seems that this test may be slow on Ubuntu builds running on ec2.
+ requestLongerTimeout(2);
+
+ let { window, $, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let count = 0;
+ function check(selectedIndex, paneVisibility) {
+ info("Performing check " + (count++) + ".");
+
+ is(RequestsMenu.selectedIndex, selectedIndex,
+ "The selected item in the requests menu was incorrect.");
+ is(NetMonitorView.detailsPaneHidden, !paneVisibility,
+ "The network requests details pane visibility state was incorrect.");
+ }
+
+ let wait = waitForNetworkEvents(monitor, 2);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests(2);
+ });
+ yield wait;
+
+ check(-1, false);
+
+ EventUtils.sendKey("DOWN", window);
+ check(0, true);
+ EventUtils.sendKey("UP", window);
+ check(0, true);
+
+ EventUtils.sendKey("PAGE_DOWN", window);
+ check(1, true);
+ EventUtils.sendKey("PAGE_UP", window);
+ check(0, true);
+
+ EventUtils.sendKey("END", window);
+ check(1, true);
+ EventUtils.sendKey("HOME", window);
+ check(0, true);
+
+ wait = waitForNetworkEvents(monitor, 18);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests(18);
+ });
+ yield wait;
+
+ EventUtils.sendKey("DOWN", window);
+ check(1, true);
+ EventUtils.sendKey("DOWN", window);
+ check(2, true);
+ EventUtils.sendKey("UP", window);
+ check(1, true);
+ EventUtils.sendKey("UP", window);
+ check(0, true);
+
+ EventUtils.sendKey("PAGE_DOWN", window);
+ check(4, true);
+ EventUtils.sendKey("PAGE_DOWN", window);
+ check(8, true);
+ EventUtils.sendKey("PAGE_UP", window);
+ check(4, true);
+ EventUtils.sendKey("PAGE_UP", window);
+ check(0, true);
+
+ EventUtils.sendKey("HOME", window);
+ check(0, true);
+ EventUtils.sendKey("HOME", window);
+ check(0, true);
+ EventUtils.sendKey("PAGE_UP", window);
+ check(0, true);
+ EventUtils.sendKey("HOME", window);
+ check(0, true);
+
+ EventUtils.sendKey("END", window);
+ check(19, true);
+ EventUtils.sendKey("END", window);
+ check(19, true);
+ EventUtils.sendKey("PAGE_DOWN", window);
+ check(19, true);
+ EventUtils.sendKey("END", window);
+ check(19, true);
+
+ EventUtils.sendKey("PAGE_UP", window);
+ check(15, true);
+ EventUtils.sendKey("PAGE_UP", window);
+ check(11, true);
+ EventUtils.sendKey("UP", window);
+ check(10, true);
+ EventUtils.sendKey("UP", window);
+ check(9, true);
+ EventUtils.sendKey("PAGE_DOWN", window);
+ check(13, true);
+ EventUtils.sendKey("PAGE_DOWN", window);
+ check(17, true);
+ EventUtils.sendKey("PAGE_DOWN", window);
+ check(19, true);
+ EventUtils.sendKey("PAGE_DOWN", window);
+ check(19, true);
+
+ EventUtils.sendKey("HOME", window);
+ check(0, true);
+ EventUtils.sendKey("DOWN", window);
+ check(1, true);
+ EventUtils.sendKey("END", window);
+ check(19, true);
+ EventUtils.sendKey("DOWN", window);
+ check(19, true);
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, $("#details-pane-toggle"));
+ check(-1, false);
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, $(".side-menu-widget-item"));
+ check(0, true);
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_api-calls.js b/devtools/client/netmonitor/test/browser_net_api-calls.js
new file mode 100644
index 000000000..994dc0354
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_api-calls.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests whether API call URLs (without a filename) are correctly displayed
+ * (including Unicode)
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(API_CALLS_URL);
+ info("Starting test... ");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ const REQUEST_URIS = [
+ "http://example.com/api/fileName.xml",
+ "http://example.com/api/file%E2%98%A2.xml",
+ "http://example.com/api/ascii/get/",
+ "http://example.com/api/unicode/%E2%98%A2/",
+ "http://example.com/api/search/?q=search%E2%98%A2"
+ ];
+
+ let wait = waitForNetworkEvents(monitor, 5);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ REQUEST_URIS.forEach(function (uri, index) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(index), "GET", uri);
+ });
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_autoscroll.js b/devtools/client/netmonitor/test/browser_net_autoscroll.js
new file mode 100644
index 000000000..9abb3fd17
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_autoscroll.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Bug 863102 - Automatically scroll down upon new network requests.
+ */
+add_task(function* () {
+ requestLongerTimeout(2);
+
+ let { monitor } = yield initNetMonitor(INFINITE_GET_URL);
+ let win = monitor.panelWin;
+ let topNode = win.document.getElementById("requests-menu-contents");
+ let requestsContainer = topNode.getElementsByTagName("scrollbox")[0];
+ ok(!!requestsContainer, "Container element exists as expected.");
+
+ // (1) Check that the scroll position is maintained at the bottom
+ // when the requests overflow the vertical size of the container.
+ yield waitForRequestsToOverflowContainer();
+ yield waitForScroll();
+ ok(scrolledToBottom(requestsContainer), "Scrolled to bottom on overflow.");
+
+ // (2) Now set the scroll position somewhere in the middle and check
+ // that additional requests do not change the scroll position.
+ let children = requestsContainer.childNodes;
+ let middleNode = children.item(children.length / 2);
+ middleNode.scrollIntoView();
+ ok(!scrolledToBottom(requestsContainer), "Not scrolled to bottom.");
+ // save for comparison later
+ let scrollTop = requestsContainer.scrollTop;
+ yield waitForNetworkEvents(monitor, 8);
+ yield waitSomeTime();
+ is(requestsContainer.scrollTop, scrollTop, "Did not scroll.");
+
+ // (3) Now set the scroll position back at the bottom and check that
+ // additional requests *do* cause the container to scroll down.
+ requestsContainer.scrollTop = requestsContainer.scrollHeight;
+ ok(scrolledToBottom(requestsContainer), "Set scroll position to bottom.");
+ yield waitForNetworkEvents(monitor, 8);
+ yield waitForScroll();
+ ok(scrolledToBottom(requestsContainer), "Still scrolled to bottom.");
+
+ // (4) Now select an item in the list and check that additional requests
+ // do not change the scroll position.
+ monitor.panelWin.NetMonitorView.RequestsMenu.selectedIndex = 0;
+ yield waitForNetworkEvents(monitor, 8);
+ yield waitSomeTime();
+ is(requestsContainer.scrollTop, 0, "Did not scroll.");
+
+ // Done: clean up.
+ yield teardown(monitor);
+
+ function* waitForRequestsToOverflowContainer() {
+ while (true) {
+ yield waitForNetworkEvents(monitor, 1);
+ if (requestsContainer.scrollHeight > requestsContainer.clientHeight) {
+ return;
+ }
+ }
+ }
+
+ function scrolledToBottom(element) {
+ return element.scrollTop + element.clientHeight >= element.scrollHeight;
+ }
+
+ function waitSomeTime() {
+ // Wait to make sure no scrolls happen
+ return wait(50);
+ }
+
+ function waitForScroll() {
+ return monitor._view.RequestsMenu.widget.once("scroll-to-bottom");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_brotli.js b/devtools/client/netmonitor/test/browser_net_brotli.js
new file mode 100644
index 000000000..cc6908d68
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_brotli.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const BROTLI_URL = HTTPS_EXAMPLE_URL + "html_brotli-test-page.html";
+const BROTLI_REQUESTS = 1;
+
+/**
+ * Test brotli encoded response is handled correctly on HTTPS.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(BROTLI_URL);
+ info("Starting test... ");
+
+ let { document, Editor, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, BROTLI_REQUESTS);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+ "GET", HTTPS_CONTENT_TYPE_SJS + "?fmt=br", {
+ status: 200,
+ statusText: "Connected",
+ type: "plain",
+ fullMimeType: "text/plain",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 10),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 64),
+ time: true
+ });
+
+ let onEvent = waitForResponseBodyDisplayed();
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+ yield onEvent;
+ yield testResponseTab("br");
+
+ yield teardown(monitor);
+
+ function* testResponseTab(type) {
+ let tabEl = document.querySelectorAll("#details-pane tab")[3];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[3];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The response tab in the network details pane should be selected.");
+
+ function checkVisibility(box) {
+ is(tabpanel.querySelector("#response-content-info-header")
+ .hasAttribute("hidden"), true,
+ "The response info header doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-json-box")
+ .hasAttribute("hidden"), box != "json",
+ "The response content json box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-textarea-box")
+ .hasAttribute("hidden"), box != "textarea",
+ "The response content textarea box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-image-box")
+ .hasAttribute("hidden"), box != "image",
+ "The response content image box doesn't have the intended visibility.");
+ }
+
+ switch (type) {
+ case "br": {
+ checkVisibility("textarea");
+
+ let expected = "X".repeat(64);
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+ is(editor.getText(), expected,
+ "The text shown in the source editor is incorrect for the brotli request.");
+ is(editor.getMode(), Editor.modes.text,
+ "The mode active in the source editor is incorrect for the brotli request.");
+ break;
+ }
+ }
+ }
+
+ function waitForResponseBodyDisplayed() {
+ return monitor.panelWin.once(monitor.panelWin.EVENTS.RESPONSE_BODY_DISPLAYED);
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_cached-status.js b/devtools/client/netmonitor/test/browser_net_cached-status.js
new file mode 100644
index 000000000..66b926bea
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_cached-status.js
@@ -0,0 +1,111 @@
+/* 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";
+
+/**
+ * Tests if cached requests have the correct status code
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(STATUS_CODES_URL, null, true);
+ info("Starting test... ");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+ NetworkDetails._params.lazyEmpty = false;
+
+ const REQUEST_DATA = [
+ {
+ method: "GET",
+ uri: STATUS_CODES_SJS + "?sts=ok&cached",
+ details: {
+ status: 200,
+ statusText: "OK",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8"
+ }
+ },
+ {
+ method: "GET",
+ uri: STATUS_CODES_SJS + "?sts=redirect&cached",
+ details: {
+ status: 301,
+ statusText: "Moved Permanently",
+ type: "html",
+ fullMimeType: "text/html; charset=utf-8"
+ }
+ },
+ {
+ method: "GET",
+ uri: "http://example.com/redirected",
+ details: {
+ status: 404,
+ statusText: "Not Found",
+ type: "html",
+ fullMimeType: "text/html; charset=utf-8"
+ }
+ },
+ {
+ method: "GET",
+ uri: STATUS_CODES_SJS + "?sts=ok&cached",
+ details: {
+ status: 200,
+ statusText: "OK (cached)",
+ displayedStatus: "cached",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8"
+ }
+ },
+ {
+ method: "GET",
+ uri: STATUS_CODES_SJS + "?sts=redirect&cached",
+ details: {
+ status: 301,
+ statusText: "Moved Permanently (cached)",
+ displayedStatus: "cached",
+ type: "html",
+ fullMimeType: "text/html; charset=utf-8"
+ }
+ },
+ {
+ method: "GET",
+ uri: "http://example.com/redirected",
+ details: {
+ status: 404,
+ statusText: "Not Found",
+ type: "html",
+ fullMimeType: "text/html; charset=utf-8"
+ }
+ }
+ ];
+
+ info("Performing requests #1...");
+ yield performRequestsAndWait();
+
+ info("Performing requests #2...");
+ yield performRequestsAndWait();
+
+ let index = 0;
+ for (let request of REQUEST_DATA) {
+ let item = RequestsMenu.getItemAtIndex(index);
+
+ info("Verifying request #" + index);
+ yield verifyRequestItemTarget(item, request.method, request.uri, request.details);
+
+ index++;
+ }
+
+ yield teardown(monitor);
+
+ function* performRequestsAndWait() {
+ let wait = waitForNetworkEvents(monitor, 3);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performCachedRequests();
+ });
+ yield wait;
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_cause.js b/devtools/client/netmonitor/test/browser_net_cause.js
new file mode 100644
index 000000000..2e73965d0
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_cause.js
@@ -0,0 +1,147 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if request cause is reported correctly.
+ */
+
+const CAUSE_FILE_NAME = "html_cause-test-page.html";
+const CAUSE_URL = EXAMPLE_URL + CAUSE_FILE_NAME;
+
+const EXPECTED_REQUESTS = [
+ {
+ method: "GET",
+ url: CAUSE_URL,
+ causeType: "document",
+ causeUri: "",
+ // The document load has internal privileged JS code on the stack
+ stack: true
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "stylesheet_request",
+ causeType: "stylesheet",
+ causeUri: CAUSE_URL,
+ stack: false
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "img_request",
+ causeType: "img",
+ causeUri: CAUSE_URL,
+ stack: false
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "xhr_request",
+ causeType: "xhr",
+ causeUri: CAUSE_URL,
+ stack: [{ fn: "performXhrRequest", file: CAUSE_FILE_NAME, line: 22 }]
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "fetch_request",
+ causeType: "fetch",
+ causeUri: CAUSE_URL,
+ stack: [{ fn: "performFetchRequest", file: CAUSE_FILE_NAME, line: 26 }]
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "promise_fetch_request",
+ causeType: "fetch",
+ causeUri: CAUSE_URL,
+ stack: [
+ { fn: "performPromiseFetchRequest", file: CAUSE_FILE_NAME, line: 38 },
+ { fn: null, file: CAUSE_FILE_NAME, line: 37, asyncCause: "promise callback" },
+ ]
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "timeout_fetch_request",
+ causeType: "fetch",
+ causeUri: CAUSE_URL,
+ stack: [
+ { fn: "performTimeoutFetchRequest", file: CAUSE_FILE_NAME, line: 40 },
+ { fn: "performPromiseFetchRequest", file: CAUSE_FILE_NAME, line: 39,
+ asyncCause: "setTimeout handler" },
+ ]
+ },
+ {
+ method: "POST",
+ url: EXAMPLE_URL + "beacon_request",
+ causeType: "beacon",
+ causeUri: CAUSE_URL,
+ stack: [{ fn: "performBeaconRequest", file: CAUSE_FILE_NAME, line: 30 }]
+ },
+];
+
+add_task(function* () {
+ // Async stacks aren't on by default in all builds
+ yield SpecialPowers.pushPrefEnv({ set: [["javascript.options.asyncstack", true]] });
+
+ // the initNetMonitor function clears the network request list after the
+ // page is loaded. That's why we first load a bogus page from SIMPLE_URL,
+ // and only then load the real thing from CAUSE_URL - we want to catch
+ // all the requests the page is making, not only the XHRs.
+ // We can't use about:blank here, because initNetMonitor checks that the
+ // page has actually made at least one request.
+ let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
+ let { $, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, EXPECTED_REQUESTS.length);
+ tab.linkedBrowser.loadURI(CAUSE_URL);
+ yield wait;
+
+ is(RequestsMenu.itemCount, EXPECTED_REQUESTS.length,
+ "All the page events should be recorded.");
+
+ EXPECTED_REQUESTS.forEach((spec, i) => {
+ let { method, url, causeType, causeUri, stack } = spec;
+
+ let requestItem = RequestsMenu.getItemAtIndex(i);
+ verifyRequestItemTarget(requestItem,
+ method, url, { cause: { type: causeType, loadingDocumentUri: causeUri } }
+ );
+
+ let { stacktrace } = requestItem.attachment.cause;
+ let stackLen = stacktrace ? stacktrace.length : 0;
+
+ if (stack) {
+ ok(stacktrace, `Request #${i} has a stacktrace`);
+ ok(stackLen > 0,
+ `Request #${i} (${causeType}) has a stacktrace with ${stackLen} items`);
+
+ // if "stack" is array, check the details about the top stack frames
+ if (Array.isArray(stack)) {
+ stack.forEach((frame, j) => {
+ is(stacktrace[j].functionName, frame.fn,
+ `Request #${i} has the correct function on JS stack frame #${j}`);
+ is(stacktrace[j].filename.split("/").pop(), frame.file,
+ `Request #${i} has the correct file on JS stack frame #${j}`);
+ is(stacktrace[j].lineNumber, frame.line,
+ `Request #${i} has the correct line number on JS stack frame #${j}`);
+ is(stacktrace[j].asyncCause, frame.asyncCause,
+ `Request #${i} has the correct async cause on JS stack frame #${j}`);
+ });
+ }
+ } else {
+ is(stackLen, 0, `Request #${i} (${causeType}) has an empty stacktrace`);
+ }
+ });
+
+ // Sort the requests by cause and check the order
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-cause-button"));
+ let expectedOrder = EXPECTED_REQUESTS.map(r => r.causeType).sort();
+ expectedOrder.forEach((expectedCause, i) => {
+ let { target } = RequestsMenu.getItemAtIndex(i);
+ let causeLabel = target.querySelector(".requests-menu-cause-label");
+ let cause = causeLabel.getAttribute("value");
+ is(cause, expectedCause, `The request #${i} has the expected cause after sorting`);
+ });
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_cause_redirect.js b/devtools/client/netmonitor/test/browser_net_cause_redirect.js
new file mode 100644
index 000000000..ace6390ab
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_cause_redirect.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if request JS stack is property reported if the request is internally
+ * redirected without hitting the network (HSTS is one of such cases)
+ */
+
+add_task(function* () {
+ const EXPECTED_REQUESTS = [
+ // Request to HTTP URL, redirects to HTTPS, has callstack
+ { status: 302, hasStack: true },
+ // Serves HTTPS, sets the Strict-Transport-Security header, no stack
+ { status: 200, hasStack: false },
+ // Second request to HTTP redirects to HTTPS internally
+ { status: 200, hasStack: true },
+ ];
+
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ let { RequestsMenu } = monitor.panelWin.NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, EXPECTED_REQUESTS.length);
+ yield performRequests(2, HSTS_SJS);
+ yield wait;
+
+ EXPECTED_REQUESTS.forEach(({status, hasStack}, i) => {
+ let { attachment } = RequestsMenu.getItemAtIndex(i);
+
+ is(attachment.status, status, `Request #${i} has the expected status`);
+
+ let { stacktrace } = attachment.cause;
+ let stackLen = stacktrace ? stacktrace.length : 0;
+
+ if (hasStack) {
+ ok(stacktrace, `Request #${i} has a stacktrace`);
+ ok(stackLen > 0, `Request #${i} has a stacktrace with ${stackLen} items`);
+ } else {
+ is(stackLen, 0, `Request #${i} has an empty stacktrace`);
+ }
+ });
+
+ // Send a request to reset the HSTS policy to state before the test
+ wait = waitForNetworkEvents(monitor, 1);
+ yield performRequests(1, HSTS_SJS + "?reset");
+ yield wait;
+
+ yield teardown(monitor);
+
+ function performRequests(count, url) {
+ return ContentTask.spawn(tab.linkedBrowser, { count, url }, function* (args) {
+ content.wrappedJSObject.performRequests(args.count, args.url);
+ });
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_charts-01.js b/devtools/client/netmonitor/test/browser_net_charts-01.js
new file mode 100644
index 000000000..987881836
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_charts-01.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Makes sure Pie Charts have the right internal structure.
+ */
+
+add_task(function* () {
+ let { monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { document, Chart } = monitor.panelWin;
+
+ let pie = Chart.Pie(document, {
+ width: 100,
+ height: 100,
+ data: [{
+ size: 1,
+ label: "foo"
+ }, {
+ size: 2,
+ label: "bar"
+ }, {
+ size: 3,
+ label: "baz"
+ }]
+ });
+
+ let node = pie.node;
+ let slices = node.querySelectorAll(".pie-chart-slice.chart-colored-blob");
+ let labels = node.querySelectorAll(".pie-chart-label");
+
+ ok(node.classList.contains("pie-chart-container") &&
+ node.classList.contains("generic-chart-container"),
+ "A pie chart container was created successfully.");
+
+ is(slices.length, 3,
+ "There should be 3 pie chart slices created.");
+ ok(slices[0].getAttribute("d").match(
+ /\s*M 50,50 L 49\.\d+,97\.\d+ A 47\.5,47\.5 0 0 1 49\.\d+,2\.5\d* Z/),
+ "The first slice has the correct data.");
+ ok(slices[1].getAttribute("d").match(
+ /\s*M 50,50 L 91\.\d+,26\.\d+ A 47\.5,47\.5 0 0 1 49\.\d+,97\.\d+ Z/),
+ "The second slice has the correct data.");
+ ok(slices[2].getAttribute("d").match(
+ /\s*M 50,50 L 50\.\d+,2\.5\d* A 47\.5,47\.5 0 0 1 91\.\d+,26\.\d+ Z/),
+ "The third slice has the correct data.");
+
+ ok(slices[0].hasAttribute("largest"),
+ "The first slice should be the largest one.");
+ ok(slices[2].hasAttribute("smallest"),
+ "The third slice should be the smallest one.");
+
+ ok(slices[0].getAttribute("name"), "baz",
+ "The first slice's name is correct.");
+ ok(slices[1].getAttribute("name"), "bar",
+ "The first slice's name is correct.");
+ ok(slices[2].getAttribute("name"), "foo",
+ "The first slice's name is correct.");
+
+ is(labels.length, 3,
+ "There should be 3 pie chart labels created.");
+ is(labels[0].textContent, "baz",
+ "The first label's text is correct.");
+ is(labels[1].textContent, "bar",
+ "The first label's text is correct.");
+ is(labels[2].textContent, "foo",
+ "The first label's text is correct.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_charts-02.js b/devtools/client/netmonitor/test/browser_net_charts-02.js
new file mode 100644
index 000000000..ae53147f0
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_charts-02.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Makes sure Pie Charts have the right internal structure when
+ * initialized with empty data.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { document, Chart } = monitor.panelWin;
+
+ let pie = Chart.Pie(document, {
+ data: null,
+ width: 100,
+ height: 100
+ });
+
+ let node = pie.node;
+ let slices = node.querySelectorAll(".pie-chart-slice.chart-colored-blob");
+ let labels = node.querySelectorAll(".pie-chart-label");
+
+ ok(node.classList.contains("pie-chart-container") &&
+ node.classList.contains("generic-chart-container"),
+ "A pie chart container was created successfully.");
+
+ is(slices.length, 1, "There should be 1 pie chart slice created.");
+ ok(slices[0].getAttribute("d").match(
+ /\s*M 50,50 L 50\.\d+,2\.5\d* A 47\.5,47\.5 0 1 1 49\.\d+,2\.5\d* Z/),
+ "The first slice has the correct data.");
+
+ ok(slices[0].hasAttribute("largest"),
+ "The first slice should be the largest one.");
+ ok(slices[0].hasAttribute("smallest"),
+ "The first slice should also be the smallest one.");
+ ok(slices[0].getAttribute("name"), L10N.getStr("pieChart.loading"),
+ "The first slice's name is correct.");
+
+ is(labels.length, 1, "There should be 1 pie chart label created.");
+ is(labels[0].textContent, "Loading", "The first label's text is correct.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_charts-03.js b/devtools/client/netmonitor/test/browser_net_charts-03.js
new file mode 100644
index 000000000..c7d9b0c1a
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_charts-03.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Makes sure Table Charts have the right internal structure.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { document, Chart } = monitor.panelWin;
+
+ let table = Chart.Table(document, {
+ title: "Table title",
+ data: [{
+ label1: 1,
+ label2: 11.1
+ }, {
+ label1: 2,
+ label2: 12.2
+ }, {
+ label1: 3,
+ label2: 13.3
+ }],
+ strings: {
+ label2: (value, index) => value + ["foo", "bar", "baz"][index]
+ },
+ totals: {
+ label1: value => "Hello " + L10N.numberWithDecimals(value, 2),
+ label2: value => "World " + L10N.numberWithDecimals(value, 2)
+ }
+ });
+
+ let node = table.node;
+ let title = node.querySelector(".table-chart-title");
+ let grid = node.querySelector(".table-chart-grid");
+ let totals = node.querySelector(".table-chart-totals");
+ let rows = grid.querySelectorAll(".table-chart-row");
+ let sums = node.querySelectorAll(".table-chart-summary-label");
+
+ ok(node.classList.contains("table-chart-container") &&
+ node.classList.contains("generic-chart-container"),
+ "A table chart container was created successfully.");
+
+ ok(title, "A title node was created successfully.");
+ is(title.getAttribute("value"), "Table title",
+ "The title node displays the correct text.");
+
+ is(rows.length, 3, "There should be 3 table chart rows created.");
+
+ ok(rows[0].querySelector(".table-chart-row-box.chart-colored-blob"),
+ "A colored blob exists for the firt row.");
+ is(rows[0].querySelectorAll("label")[0].getAttribute("name"), "label1",
+ "The first column of the first row exists.");
+ is(rows[0].querySelectorAll("label")[1].getAttribute("name"), "label2",
+ "The second column of the first row exists.");
+ is(rows[0].querySelectorAll("label")[0].getAttribute("value"), "1",
+ "The first column of the first row displays the correct text.");
+ is(rows[0].querySelectorAll("label")[1].getAttribute("value"), "11.1foo",
+ "The second column of the first row displays the correct text.");
+
+ ok(rows[1].querySelector(".table-chart-row-box.chart-colored-blob"),
+ "A colored blob exists for the second row.");
+ is(rows[1].querySelectorAll("label")[0].getAttribute("name"), "label1",
+ "The first column of the second row exists.");
+ is(rows[1].querySelectorAll("label")[1].getAttribute("name"), "label2",
+ "The second column of the second row exists.");
+ is(rows[1].querySelectorAll("label")[0].getAttribute("value"), "2",
+ "The first column of the second row displays the correct text.");
+ is(rows[1].querySelectorAll("label")[1].getAttribute("value"), "12.2bar",
+ "The second column of the first row displays the correct text.");
+
+ ok(rows[2].querySelector(".table-chart-row-box.chart-colored-blob"),
+ "A colored blob exists for the third row.");
+ is(rows[2].querySelectorAll("label")[0].getAttribute("name"), "label1",
+ "The first column of the third row exists.");
+ is(rows[2].querySelectorAll("label")[1].getAttribute("name"), "label2",
+ "The second column of the third row exists.");
+ is(rows[2].querySelectorAll("label")[0].getAttribute("value"), "3",
+ "The first column of the third row displays the correct text.");
+ is(rows[2].querySelectorAll("label")[1].getAttribute("value"), "13.3baz",
+ "The second column of the third row displays the correct text.");
+
+ is(sums.length, 2, "There should be 2 total summaries created.");
+
+ is(totals.querySelectorAll(".table-chart-summary-label")[0].getAttribute("name"),
+ "label1",
+ "The first sum's type is correct.");
+ is(totals.querySelectorAll(".table-chart-summary-label")[0].getAttribute("value"),
+ "Hello 6",
+ "The first sum's value is correct.");
+
+ is(totals.querySelectorAll(".table-chart-summary-label")[1].getAttribute("name"),
+ "label2",
+ "The second sum's type is correct.");
+ is(totals.querySelectorAll(".table-chart-summary-label")[1].getAttribute("value"),
+ "World 36.60",
+ "The second sum's value is correct.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_charts-04.js b/devtools/client/netmonitor/test/browser_net_charts-04.js
new file mode 100644
index 000000000..0d150c409
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_charts-04.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Makes sure Pie Charts have the right internal structure when
+ * initialized with empty data.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { document, Chart } = monitor.panelWin;
+
+ let table = Chart.Table(document, {
+ title: "Table title",
+ data: null,
+ totals: {
+ label1: value => "Hello " + L10N.numberWithDecimals(value, 2),
+ label2: value => "World " + L10N.numberWithDecimals(value, 2)
+ }
+ });
+
+ let node = table.node;
+ let title = node.querySelector(".table-chart-title");
+ let grid = node.querySelector(".table-chart-grid");
+ let totals = node.querySelector(".table-chart-totals");
+ let rows = grid.querySelectorAll(".table-chart-row");
+ let sums = node.querySelectorAll(".table-chart-summary-label");
+
+ ok(node.classList.contains("table-chart-container") &&
+ node.classList.contains("generic-chart-container"),
+ "A table chart container was created successfully.");
+
+ ok(title, "A title node was created successfully.");
+ is(title.getAttribute("value"), "Table title",
+ "The title node displays the correct text.");
+
+ is(rows.length, 1, "There should be 1 table chart row created.");
+
+ ok(rows[0].querySelector(".table-chart-row-box.chart-colored-blob"),
+ "A colored blob exists for the firt row.");
+ is(rows[0].querySelectorAll("label")[0].getAttribute("name"), "size",
+ "The first column of the first row exists.");
+ is(rows[0].querySelectorAll("label")[1].getAttribute("name"), "label",
+ "The second column of the first row exists.");
+ is(rows[0].querySelectorAll("label")[0].getAttribute("value"), "",
+ "The first column of the first row displays the correct text.");
+ is(rows[0].querySelectorAll("label")[1].getAttribute("value"),
+ L10N.getStr("tableChart.loading"),
+ "The second column of the first row displays the correct text.");
+
+ is(sums.length, 2,
+ "There should be 2 total summaries created.");
+
+ is(totals.querySelectorAll(".table-chart-summary-label")[0].getAttribute("name"),
+ "label1",
+ "The first sum's type is correct.");
+ is(totals.querySelectorAll(".table-chart-summary-label")[0].getAttribute("value"),
+ "Hello 0",
+ "The first sum's value is correct.");
+
+ is(totals.querySelectorAll(".table-chart-summary-label")[1].getAttribute("name"),
+ "label2",
+ "The second sum's type is correct.");
+ is(totals.querySelectorAll(".table-chart-summary-label")[1].getAttribute("value"),
+ "World 0",
+ "The second sum's value is correct.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_charts-05.js b/devtools/client/netmonitor/test/browser_net_charts-05.js
new file mode 100644
index 000000000..00445b132
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_charts-05.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Makes sure Pie+Table Charts have the right internal structure.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { document, Chart } = monitor.panelWin;
+
+ let chart = Chart.PieTable(document, {
+ title: "Table title",
+ data: [{
+ size: 1,
+ label: 11.1
+ }, {
+ size: 2,
+ label: 12.2
+ }, {
+ size: 3,
+ label: 13.3
+ }],
+ strings: {
+ label2: (value, index) => value + ["foo", "bar", "baz"][index]
+ },
+ totals: {
+ size: value => "Hello " + L10N.numberWithDecimals(value, 2),
+ label: value => "World " + L10N.numberWithDecimals(value, 2)
+ }
+ });
+
+ ok(chart.pie, "The pie chart proxy is accessible.");
+ ok(chart.table, "The table chart proxy is accessible.");
+
+ let node = chart.node;
+ let rows = node.querySelectorAll(".table-chart-row");
+ let sums = node.querySelectorAll(".table-chart-summary-label");
+
+ ok(node.classList.contains("pie-table-chart-container"),
+ "A pie+table chart container was created successfully.");
+
+ ok(node.querySelector(".table-chart-title"),
+ "A title node was created successfully.");
+ ok(node.querySelector(".pie-chart-container"),
+ "A pie chart was created successfully.");
+ ok(node.querySelector(".table-chart-container"),
+ "A table chart was created successfully.");
+
+ is(rows.length, 3, "There should be 3 pie chart slices created.");
+ is(rows.length, 3, "There should be 3 table chart rows created.");
+ is(sums.length, 2, "There should be 2 total summaries created.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_charts-06.js b/devtools/client/netmonitor/test/browser_net_charts-06.js
new file mode 100644
index 000000000..4bb70e53e
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_charts-06.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Makes sure Pie Charts correctly handle empty source data.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { document, Chart } = monitor.panelWin;
+
+ let pie = Chart.Pie(document, {
+ data: [],
+ width: 100,
+ height: 100
+ });
+
+ let node = pie.node;
+ let slices = node.querySelectorAll(".pie-chart-slice.chart-colored-blob");
+ let labels = node.querySelectorAll(".pie-chart-label");
+
+ is(slices.length, 1,
+ "There should be 1 pie chart slice created.");
+ ok(slices[0].getAttribute("d").match(
+ /\s*M 50,50 L 50\.\d+,2\.5\d* A 47\.5,47\.5 0 1 1 49\.\d+,2\.5\d* Z/),
+ "The slice has the correct data.");
+
+ ok(slices[0].hasAttribute("largest"),
+ "The slice should be the largest one.");
+ ok(slices[0].hasAttribute("smallest"),
+ "The slice should also be the smallest one.");
+ ok(slices[0].getAttribute("name"), L10N.getStr("pieChart.unavailable"),
+ "The slice's name is correct.");
+
+ is(labels.length, 1,
+ "There should be 1 pie chart label created.");
+ is(labels[0].textContent, "Empty",
+ "The label's text is correct.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_charts-07.js b/devtools/client/netmonitor/test/browser_net_charts-07.js
new file mode 100644
index 000000000..bb992e4eb
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_charts-07.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Makes sure Table Charts correctly handle empty source data.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { document, Chart } = monitor.panelWin;
+
+ let table = Chart.Table(document, {
+ data: [],
+ totals: {
+ label1: value => "Hello " + L10N.numberWithDecimals(value, 2),
+ label2: value => "World " + L10N.numberWithDecimals(value, 2)
+ }
+ });
+
+ let node = table.node;
+ let grid = node.querySelector(".table-chart-grid");
+ let totals = node.querySelector(".table-chart-totals");
+ let rows = grid.querySelectorAll(".table-chart-row");
+ let sums = node.querySelectorAll(".table-chart-summary-label");
+
+ is(rows.length, 1, "There should be 1 table chart row created.");
+
+ ok(rows[0].querySelector(".table-chart-row-box.chart-colored-blob"),
+ "A colored blob exists for the firt row.");
+ is(rows[0].querySelectorAll("label")[0].getAttribute("name"), "size",
+ "The first column of the first row exists.");
+ is(rows[0].querySelectorAll("label")[1].getAttribute("name"), "label",
+ "The second column of the first row exists.");
+ is(rows[0].querySelectorAll("label")[0].getAttribute("value"), "",
+ "The first column of the first row displays the correct text.");
+ is(rows[0].querySelectorAll("label")[1].getAttribute("value"),
+ L10N.getStr("tableChart.unavailable"),
+ "The second column of the first row displays the correct text.");
+
+ is(sums.length, 2, "There should be 2 total summaries created.");
+
+ is(totals.querySelectorAll(".table-chart-summary-label")[0].getAttribute("name"),
+ "label1",
+ "The first sum's type is correct.");
+ is(totals.querySelectorAll(".table-chart-summary-label")[0].getAttribute("value"),
+ "Hello 0",
+ "The first sum's value is correct.");
+
+ is(totals.querySelectorAll(".table-chart-summary-label")[1].getAttribute("name"),
+ "label2",
+ "The second sum's type is correct.");
+ is(totals.querySelectorAll(".table-chart-summary-label")[1].getAttribute("value"),
+ "World 0",
+ "The second sum's value is correct.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_clear.js b/devtools/client/netmonitor/test/browser_net_clear.js
new file mode 100644
index 000000000..94a60cd39
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_clear.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the clear button empties the request menu.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { $, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+ let detailsPane = $("#details-pane");
+ let detailsPaneToggleButton = $("#details-pane-toggle");
+ let clearButton = $("#requests-menu-clear-button");
+
+ RequestsMenu.lazyUpdate = false;
+
+ // Make sure we start in a sane state
+ assertNoRequestState(RequestsMenu, detailsPaneToggleButton);
+
+ // Load one request and assert it shows up in the list
+ let networkEvent = monitor.panelWin.once(monitor.panelWin.EVENTS.NETWORK_EVENT);
+ tab.linkedBrowser.reload();
+ yield networkEvent;
+
+ assertSingleRequestState();
+
+ // Click clear and make sure the requests are gone
+ EventUtils.sendMouseEvent({ type: "click" }, clearButton);
+ assertNoRequestState();
+
+ // Load a second request and make sure they still show up
+ networkEvent = monitor.panelWin.once(monitor.panelWin.EVENTS.NETWORK_EVENT);
+ tab.linkedBrowser.reload();
+ yield networkEvent;
+
+ assertSingleRequestState();
+
+ // Make sure we can now open the details pane
+ NetMonitorView.toggleDetailsPane({ visible: true, animated: false });
+ ok(!detailsPane.classList.contains("pane-collapsed") &&
+ !detailsPaneToggleButton.classList.contains("pane-collapsed"),
+ "The details pane should be visible after clicking the toggle button.");
+
+ // Click clear and make sure the details pane closes
+ EventUtils.sendMouseEvent({ type: "click" }, clearButton);
+ assertNoRequestState();
+ ok(detailsPane.classList.contains("pane-collapsed") &&
+ detailsPaneToggleButton.classList.contains("pane-collapsed"),
+ "The details pane should not be visible clicking 'clear'.");
+
+ return teardown(monitor);
+
+ /**
+ * Asserts the state of the network monitor when one request has loaded
+ */
+ function assertSingleRequestState() {
+ is(RequestsMenu.itemCount, 1,
+ "The request menu should have one item at this point.");
+ is(detailsPaneToggleButton.hasAttribute("disabled"), false,
+ "The pane toggle button should be enabled after a request is made.");
+ }
+
+ /**
+ * Asserts the state of the network monitor when no requests have loaded
+ */
+ function assertNoRequestState() {
+ is(RequestsMenu.itemCount, 0,
+ "The request menu should be empty at this point.");
+ is(detailsPaneToggleButton.hasAttribute("disabled"), true,
+ "The pane toggle button should be disabled when the request menu is cleared.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_complex-params.js b/devtools/client/netmonitor/test/browser_net_complex-params.js
new file mode 100644
index 000000000..103c644bb
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_complex-params.js
@@ -0,0 +1,195 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests whether complex request params and payload sent via POST are
+ * displayed correctly.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(PARAMS_URL);
+ info("Starting test... ");
+
+ let { document, EVENTS, Editor, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+ NetworkDetails._params.lazyEmpty = false;
+
+ let wait = waitForNetworkEvents(monitor, 1, 6);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ let onEvent = monitor.panelWin.once(EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[2]);
+ yield onEvent;
+ yield testParamsTab1("a", '""', '{ "foo": "bar" }', '""');
+
+ onEvent = monitor.panelWin.once(EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+ RequestsMenu.selectedIndex = 1;
+ yield onEvent;
+ yield testParamsTab1("a", '"b"', '{ "foo": "bar" }', '""');
+
+ onEvent = monitor.panelWin.once(EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+ RequestsMenu.selectedIndex = 2;
+ yield onEvent;
+ yield testParamsTab1("a", '"b"', "foo", '"bar"');
+
+ onEvent = monitor.panelWin.once(EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+ RequestsMenu.selectedIndex = 3;
+ yield onEvent;
+ yield testParamsTab2("a", '""', '{ "foo": "bar" }', "js");
+
+ onEvent = monitor.panelWin.once(EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+ RequestsMenu.selectedIndex = 4;
+ yield onEvent;
+ yield testParamsTab2("a", '"b"', '{ "foo": "bar" }', "js");
+
+ onEvent = monitor.panelWin.once(EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+ RequestsMenu.selectedIndex = 5;
+ yield onEvent;
+ yield testParamsTab2("a", '"b"', "?foo=bar", "text");
+
+ onEvent = monitor.panelWin.once(EVENTS.SIDEBAR_POPULATED);
+ RequestsMenu.selectedIndex = 6;
+ yield onEvent;
+ yield testParamsTab3("a", '"b"');
+
+ yield teardown(monitor);
+
+ function testParamsTab1(queryStringParamName, queryStringParamValue,
+ formDataParamName, formDataParamValue) {
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 2,
+ "The number of param scopes displayed in this tabpanel is incorrect.");
+ is(tabpanel.querySelectorAll(".variable-or-property").length, 2,
+ "The number of param values displayed in this tabpanel is incorrect.");
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+ "The empty notice should not be displayed in this tabpanel.");
+
+ is(tabpanel.querySelector("#request-params-box")
+ .hasAttribute("hidden"), false,
+ "The request params box should not be hidden.");
+ is(tabpanel.querySelector("#request-post-data-textarea-box")
+ .hasAttribute("hidden"), true,
+ "The request post data textarea box should be hidden.");
+
+ let paramsScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+ let formDataScope = tabpanel.querySelectorAll(".variables-view-scope")[1];
+
+ is(paramsScope.querySelector(".name").getAttribute("value"),
+ L10N.getStr("paramsQueryString"),
+ "The params scope doesn't have the correct title.");
+ is(formDataScope.querySelector(".name").getAttribute("value"),
+ L10N.getStr("paramsFormData"),
+ "The form data scope doesn't have the correct title.");
+
+ is(paramsScope.querySelectorAll(".variables-view-variable .name")[0]
+ .getAttribute("value"),
+ queryStringParamName,
+ "The first query string param name was incorrect.");
+ is(paramsScope.querySelectorAll(".variables-view-variable .value")[0]
+ .getAttribute("value"),
+ queryStringParamValue,
+ "The first query string param value was incorrect.");
+
+ is(formDataScope.querySelectorAll(".variables-view-variable .name")[0]
+ .getAttribute("value"),
+ formDataParamName,
+ "The first form data param name was incorrect.");
+ is(formDataScope.querySelectorAll(".variables-view-variable .value")[0]
+ .getAttribute("value"),
+ formDataParamValue,
+ "The first form data param value was incorrect.");
+ }
+
+ function* testParamsTab2(queryStringParamName, queryStringParamValue,
+ requestPayload, editorMode) {
+ let isJSON = editorMode == "js";
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 2,
+ "The number of param scopes displayed in this tabpanel is incorrect.");
+ is(tabpanel.querySelectorAll(".variable-or-property").length, isJSON ? 4 : 1,
+ "The number of param values displayed in this tabpanel is incorrect.");
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+ "The empty notice should not be displayed in this tabpanel.");
+
+ is(tabpanel.querySelector("#request-params-box")
+ .hasAttribute("hidden"), false,
+ "The request params box should not be hidden.");
+ is(tabpanel.querySelector("#request-post-data-textarea-box")
+ .hasAttribute("hidden"), isJSON,
+ "The request post data textarea box should be hidden.");
+
+ let paramsScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+ let payloadScope = tabpanel.querySelectorAll(".variables-view-scope")[1];
+
+ is(paramsScope.querySelector(".name").getAttribute("value"),
+ L10N.getStr("paramsQueryString"),
+ "The params scope doesn't have the correct title.");
+ is(payloadScope.querySelector(".name").getAttribute("value"),
+ isJSON ? L10N.getStr("jsonScopeName") : L10N.getStr("paramsPostPayload"),
+ "The request payload scope doesn't have the correct title.");
+
+ is(paramsScope.querySelectorAll(".variables-view-variable .name")[0]
+ .getAttribute("value"),
+ queryStringParamName,
+ "The first query string param name was incorrect.");
+ is(paramsScope.querySelectorAll(".variables-view-variable .value")[0]
+ .getAttribute("value"),
+ queryStringParamValue,
+ "The first query string param value was incorrect.");
+
+ if (isJSON) {
+ let requestPayloadObject = JSON.parse(requestPayload);
+ let requestPairs = Object.keys(requestPayloadObject)
+ .map(k => [k, requestPayloadObject[k]]);
+ let displayedNames = payloadScope.querySelectorAll(
+ ".variables-view-property.variable-or-property .name");
+ let displayedValues = payloadScope.querySelectorAll(
+ ".variables-view-property.variable-or-property .value");
+ for (let i = 0; i < requestPairs.length; i++) {
+ let [requestPayloadName, requestPayloadValue] = requestPairs[i];
+ is(requestPayloadName, displayedNames[i].getAttribute("value"),
+ "JSON property name " + i + " should be displayed correctly");
+ is('"' + requestPayloadValue + '"', displayedValues[i].getAttribute("value"),
+ "JSON property value " + i + " should be displayed correctly");
+ }
+ } else {
+ let editor = yield NetMonitorView.editor("#request-post-data-textarea");
+ is(editor.getText(), requestPayload,
+ "The text shown in the source editor is incorrect.");
+ is(editor.getMode(), Editor.modes[editorMode],
+ "The mode active in the source editor is incorrect.");
+ }
+ }
+
+ function testParamsTab3(queryStringParamName, queryStringParamValue) {
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 0,
+ "The number of param scopes displayed in this tabpanel is incorrect.");
+ is(tabpanel.querySelectorAll(".variable-or-property").length, 0,
+ "The number of param values displayed in this tabpanel is incorrect.");
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 1,
+ "The empty notice should be displayed in this tabpanel.");
+
+ is(tabpanel.querySelector("#request-params-box")
+ .hasAttribute("hidden"), false,
+ "The request params box should not be hidden.");
+ is(tabpanel.querySelector("#request-post-data-textarea-box")
+ .hasAttribute("hidden"), true,
+ "The request post data textarea box should be hidden.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_content-type.js b/devtools/client/netmonitor/test/browser_net_content-type.js
new file mode 100644
index 000000000..1951bc69d
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_content-type.js
@@ -0,0 +1,255 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if different response content types are handled correctly.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL);
+ info("Starting test... ");
+
+ let { document, Editor, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+ "GET", CONTENT_TYPE_SJS + "?fmt=xml", {
+ status: 200,
+ statusText: "OK",
+ type: "xml",
+ fullMimeType: "text/xml; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 42),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(1),
+ "GET", CONTENT_TYPE_SJS + "?fmt=css", {
+ status: 200,
+ statusText: "OK",
+ type: "css",
+ fullMimeType: "text/css; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 34),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(2),
+ "GET", CONTENT_TYPE_SJS + "?fmt=js", {
+ status: 200,
+ statusText: "OK",
+ type: "js",
+ fullMimeType: "application/javascript; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 34),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(3),
+ "GET", CONTENT_TYPE_SJS + "?fmt=json", {
+ status: 200,
+ statusText: "OK",
+ type: "json",
+ fullMimeType: "application/json; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 29),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(4),
+ "GET", CONTENT_TYPE_SJS + "?fmt=bogus", {
+ status: 404,
+ statusText: "Not Found",
+ type: "html",
+ fullMimeType: "text/html; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 24),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(5),
+ "GET", TEST_IMAGE, {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "png",
+ fullMimeType: "image/png",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 580),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(6),
+ "GET", CONTENT_TYPE_SJS + "?fmt=gzip", {
+ status: 200,
+ statusText: "OK",
+ type: "plain",
+ fullMimeType: "text/plain",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 73),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 10.73),
+ time: true
+ });
+
+ let onEvent = waitForResponseBodyDisplayed();
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+ yield onEvent;
+ yield testResponseTab("xml");
+
+ yield selectIndexAndWaitForTabUpdated(1);
+ yield testResponseTab("css");
+
+ yield selectIndexAndWaitForTabUpdated(2);
+ yield testResponseTab("js");
+
+ yield selectIndexAndWaitForTabUpdated(3);
+ yield testResponseTab("json");
+
+ yield selectIndexAndWaitForTabUpdated(4);
+ yield testResponseTab("html");
+
+ yield selectIndexAndWaitForTabUpdated(5);
+ yield testResponseTab("png");
+
+ yield selectIndexAndWaitForTabUpdated(6);
+ yield testResponseTab("gzip");
+
+ yield teardown(monitor);
+
+ function* testResponseTab(type) {
+ let tabEl = document.querySelectorAll("#details-pane tab")[3];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[3];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The response tab in the network details pane should be selected.");
+
+ function checkVisibility(box) {
+ is(tabpanel.querySelector("#response-content-info-header")
+ .hasAttribute("hidden"), true,
+ "The response info header doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-json-box")
+ .hasAttribute("hidden"), box != "json",
+ "The response content json box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-textarea-box")
+ .hasAttribute("hidden"), box != "textarea",
+ "The response content textarea box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-image-box")
+ .hasAttribute("hidden"), box != "image",
+ "The response content image box doesn't have the intended visibility.");
+ }
+
+ switch (type) {
+ case "xml": {
+ checkVisibility("textarea");
+
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+ is(editor.getText(), "<label value='greeting'>Hello XML!</label>",
+ "The text shown in the source editor is incorrect for the xml request.");
+ is(editor.getMode(), Editor.modes.html,
+ "The mode active in the source editor is incorrect for the xml request.");
+ break;
+ }
+ case "css": {
+ checkVisibility("textarea");
+
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+ is(editor.getText(), "body:pre { content: 'Hello CSS!' }",
+ "The text shown in the source editor is incorrect for the xml request.");
+ is(editor.getMode(), Editor.modes.css,
+ "The mode active in the source editor is incorrect for the xml request.");
+ break;
+ }
+ case "js": {
+ checkVisibility("textarea");
+
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+ is(editor.getText(), "function() { return 'Hello JS!'; }",
+ "The text shown in the source editor is incorrect for the xml request.");
+ is(editor.getMode(), Editor.modes.js,
+ "The mode active in the source editor is incorrect for the xml request.");
+ break;
+ }
+ case "json": {
+ checkVisibility("json");
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
+ "There should be 1 json scope displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-property").length, 2,
+ "There should be 2 json properties displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+ "The empty notice should not be displayed in this tabpanel.");
+
+ let jsonScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+
+ is(jsonScope.querySelector(".name").getAttribute("value"),
+ L10N.getStr("jsonScopeName"),
+ "The json scope doesn't have the correct title.");
+
+ is(jsonScope.querySelectorAll(".variables-view-property .name")[0]
+ .getAttribute("value"),
+ "greeting", "The first json property name was incorrect.");
+ is(jsonScope.querySelectorAll(".variables-view-property .value")[0]
+ .getAttribute("value"),
+ "\"Hello JSON!\"", "The first json property value was incorrect.");
+
+ is(jsonScope.querySelectorAll(".variables-view-property .name")[1]
+ .getAttribute("value"),
+ "__proto__", "The second json property name was incorrect.");
+ is(jsonScope.querySelectorAll(".variables-view-property .value")[1]
+ .getAttribute("value"),
+ "Object", "The second json property value was incorrect.");
+ break;
+ }
+ case "html": {
+ checkVisibility("textarea");
+
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+ is(editor.getText(), "<blink>Not Found</blink>",
+ "The text shown in the source editor is incorrect for the xml request.");
+ is(editor.getMode(), Editor.modes.html,
+ "The mode active in the source editor is incorrect for the xml request.");
+ break;
+ }
+ case "png": {
+ checkVisibility("image");
+
+ let imageNode = tabpanel.querySelector("#response-content-image");
+ yield once(imageNode, "load");
+
+ is(tabpanel.querySelector("#response-content-image-name-value")
+ .getAttribute("value"), "test-image.png",
+ "The image name info isn't correct.");
+ is(tabpanel.querySelector("#response-content-image-mime-value")
+ .getAttribute("value"), "image/png",
+ "The image mime info isn't correct.");
+ is(tabpanel.querySelector("#response-content-image-dimensions-value")
+ .getAttribute("value"), "16" + " \u00D7 " + "16",
+ "The image dimensions info isn't correct.");
+ break;
+ }
+ case "gzip": {
+ checkVisibility("textarea");
+
+ let expected = new Array(1000).join("Hello gzip!");
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+ is(editor.getText(), expected,
+ "The text shown in the source editor is incorrect for the gzip request.");
+ is(editor.getMode(), Editor.modes.text,
+ "The mode active in the source editor is incorrect for the gzip request.");
+ break;
+ }
+ }
+ }
+
+ function selectIndexAndWaitForTabUpdated(index) {
+ let onTabUpdated = monitor.panelWin.once(monitor.panelWin.EVENTS.TAB_UPDATED);
+ RequestsMenu.selectedIndex = index;
+ return onTabUpdated;
+ }
+
+ function waitForResponseBodyDisplayed() {
+ return monitor.panelWin.once(monitor.panelWin.EVENTS.RESPONSE_BODY_DISPLAYED);
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_copy_as_curl.js b/devtools/client/netmonitor/test/browser_net_copy_as_curl.js
new file mode 100644
index 000000000..9cf66aa4f
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_copy_as_curl.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if Copy as cURL works.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CURL_URL);
+ info("Starting test... ");
+
+ // Different quote chars are used for Windows and POSIX
+ const QUOTE = Services.appinfo.OS == "WINNT" ? "\"" : "'";
+
+ // Quote a string, escape the quotes inside the string
+ function quote(str) {
+ return QUOTE + str.replace(new RegExp(QUOTE, "g"), `\\${QUOTE}`) + QUOTE;
+ }
+
+ // Header param is formatted as -H "Header: value" or -H 'Header: value'
+ function header(h) {
+ return "-H " + quote(h);
+ }
+
+ // Construct the expected command
+ const EXPECTED_RESULT = [
+ "curl " + quote(SIMPLE_SJS),
+ "--compressed",
+ header("Host: example.com"),
+ header("User-Agent: " + navigator.userAgent),
+ header("Accept: */*"),
+ header("Accept-Language: " + navigator.language),
+ header("X-Custom-Header-1: Custom value"),
+ header("X-Custom-Header-2: 8.8.8.8"),
+ header("X-Custom-Header-3: Mon, 3 Mar 2014 11:11:11 GMT"),
+ header("Referer: " + CURL_URL),
+ header("Connection: keep-alive"),
+ header("Pragma: no-cache"),
+ header("Cache-Control: no-cache")
+ ];
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, SIMPLE_SJS, function* (url) {
+ content.wrappedJSObject.performRequest(url);
+ });
+ yield wait;
+
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+ RequestsMenu.selectedItem = requestItem;
+
+ yield waitForClipboardPromise(function setup() {
+ RequestsMenu.contextMenu.copyAsCurl();
+ }, function validate(result) {
+ if (typeof result !== "string") {
+ return false;
+ }
+
+ // Different setups may produce the same command, but with the
+ // parameters in a different order in the commandline (which is fine).
+ // Here we confirm that the commands are the same even in that case.
+
+ // This monster regexp parses the command line into an array of arguments,
+ // recognizing quoted args with matching quotes and escaped quotes inside:
+ // [ "curl 'url'", "--standalone-arg", "-arg-with-quoted-string 'value\'s'" ]
+ let matchRe = /[-A-Za-z1-9]+(?: ([\"'])(?:\\\1|.)*?\1)?/g;
+
+ let actual = result.match(matchRe);
+
+ // Must begin with the same "curl 'URL'" segment
+ if (!actual || EXPECTED_RESULT[0] != actual[0]) {
+ return false;
+ }
+
+ // Must match each of the params in the middle (headers and --compressed)
+ return EXPECTED_RESULT.length === actual.length &&
+ EXPECTED_RESULT.every(param => actual.includes(param));
+ });
+
+ info("Clipboard contains a cURL command for the currently selected item's url.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_copy_headers.js b/devtools/client/netmonitor/test/browser_net_copy_headers.js
new file mode 100644
index 000000000..36ce2fb34
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_copy_headers.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if copying a request's request/response headers works.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ tab.linkedBrowser.reload();
+ yield wait;
+
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+ RequestsMenu.selectedItem = requestItem;
+
+ let { method, httpVersion, status, statusText } = requestItem.attachment;
+
+ const EXPECTED_REQUEST_HEADERS = [
+ `${method} ${SIMPLE_URL} ${httpVersion}`,
+ "Host: example.com",
+ "User-Agent: " + navigator.userAgent + "",
+ "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
+ "Accept-Language: " + navigator.languages.join(",") + ";q=0.5",
+ "Accept-Encoding: gzip, deflate",
+ "Connection: keep-alive",
+ "Upgrade-Insecure-Requests: 1",
+ "Pragma: no-cache",
+ "Cache-Control: no-cache"
+ ].join("\n");
+
+ yield waitForClipboardPromise(function setup() {
+ RequestsMenu.contextMenu.copyRequestHeaders();
+ }, function validate(result) {
+ // Sometimes, a "Cookie" header is left over from other tests. Remove it:
+ result = String(result).replace(/Cookie: [^\n]+\n/, "");
+ return result === EXPECTED_REQUEST_HEADERS;
+ });
+ info("Clipboard contains the currently selected item's request headers.");
+
+ const EXPECTED_RESPONSE_HEADERS = [
+ `${httpVersion} ${status} ${statusText}`,
+ "Last-Modified: Sun, 3 May 2015 11:11:11 GMT",
+ "Content-Type: text/html",
+ "Content-Length: 465",
+ "Connection: close",
+ "Server: httpd.js",
+ "Date: Sun, 3 May 2015 11:11:11 GMT"
+ ].join("\n");
+
+ yield waitForClipboardPromise(function setup() {
+ RequestsMenu.contextMenu.copyResponseHeaders();
+ }, function validate(result) {
+ // Fake the "Last-Modified" and "Date" headers because they will vary:
+ result = String(result)
+ .replace(/Last-Modified: [^\n]+ GMT/, "Last-Modified: Sun, 3 May 2015 11:11:11 GMT")
+ .replace(/Date: [^\n]+ GMT/, "Date: Sun, 3 May 2015 11:11:11 GMT");
+ return result === EXPECTED_RESPONSE_HEADERS;
+ });
+ info("Clipboard contains the currently selected item's response headers.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_copy_image_as_data_uri.js b/devtools/client/netmonitor/test/browser_net_copy_image_as_data_uri.js
new file mode 100644
index 000000000..144ced80d
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_copy_image_as_data_uri.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if copying an image as data uri works.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL);
+ info("Starting test... ");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ let requestItem = RequestsMenu.getItemAtIndex(5);
+ RequestsMenu.selectedItem = requestItem;
+
+ yield waitForClipboardPromise(function setup() {
+ RequestsMenu.contextMenu.copyImageAsDataUri();
+ }, TEST_IMAGE_DATA_URI);
+
+ ok(true, "Clipboard contains the currently selected image as data uri.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_copy_params.js b/devtools/client/netmonitor/test/browser_net_copy_params.js
new file mode 100644
index 000000000..1cb6f6620
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_copy_params.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests whether copying a request item's parameters works.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(PARAMS_URL);
+ info("Starting test... ");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1, 6);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ RequestsMenu.selectedItem = RequestsMenu.getItemAtIndex(0);
+ yield testCopyUrlParamsHidden(false);
+ yield testCopyUrlParams("a");
+ yield testCopyPostDataHidden(false);
+ yield testCopyPostData("{ \"foo\": \"bar\" }");
+
+ RequestsMenu.selectedItem = RequestsMenu.getItemAtIndex(1);
+ yield testCopyUrlParamsHidden(false);
+ yield testCopyUrlParams("a=b");
+ yield testCopyPostDataHidden(false);
+ yield testCopyPostData("{ \"foo\": \"bar\" }");
+
+ RequestsMenu.selectedItem = RequestsMenu.getItemAtIndex(2);
+ yield testCopyUrlParamsHidden(false);
+ yield testCopyUrlParams("a=b");
+ yield testCopyPostDataHidden(false);
+ yield testCopyPostData("foo=bar");
+
+ RequestsMenu.selectedItem = RequestsMenu.getItemAtIndex(3);
+ yield testCopyUrlParamsHidden(false);
+ yield testCopyUrlParams("a");
+ yield testCopyPostDataHidden(false);
+ yield testCopyPostData("{ \"foo\": \"bar\" }");
+
+ RequestsMenu.selectedItem = RequestsMenu.getItemAtIndex(4);
+ yield testCopyUrlParamsHidden(false);
+ yield testCopyUrlParams("a=b");
+ yield testCopyPostDataHidden(false);
+ yield testCopyPostData("{ \"foo\": \"bar\" }");
+
+ RequestsMenu.selectedItem = RequestsMenu.getItemAtIndex(5);
+ yield testCopyUrlParamsHidden(false);
+ yield testCopyUrlParams("a=b");
+ yield testCopyPostDataHidden(false);
+ yield testCopyPostData("?foo=bar");
+
+ RequestsMenu.selectedItem = RequestsMenu.getItemAtIndex(6);
+ yield testCopyUrlParamsHidden(true);
+ yield testCopyPostDataHidden(true);
+
+ return teardown(monitor);
+
+ function testCopyUrlParamsHidden(hidden) {
+ let allMenuItems = openContextMenuAndGetAllItems(NetMonitorView);
+ let copyUrlParamsNode = allMenuItems.find(item =>
+ item.id === "request-menu-context-copy-url-params");
+ is(copyUrlParamsNode.visible, !hidden,
+ "The \"Copy URL Parameters\" context menu item should" + (hidden ? " " : " not ") +
+ "be hidden.");
+ }
+
+ function* testCopyUrlParams(queryString) {
+ yield waitForClipboardPromise(function setup() {
+ RequestsMenu.contextMenu.copyUrlParams();
+ }, queryString);
+ ok(true, "The url query string copied from the selected item is correct.");
+ }
+
+ function testCopyPostDataHidden(hidden) {
+ let allMenuItems = openContextMenuAndGetAllItems(NetMonitorView);
+ let copyPostDataNode = allMenuItems.find(item =>
+ item.id === "request-menu-context-copy-post-data");
+ is(copyPostDataNode.visible, !hidden,
+ "The \"Copy POST Data\" context menu item should" + (hidden ? " " : " not ") +
+ "be hidden.");
+ }
+
+ function* testCopyPostData(postData) {
+ yield waitForClipboardPromise(function setup() {
+ RequestsMenu.contextMenu.copyPostData();
+ }, postData);
+ ok(true, "The post data string copied from the selected item is correct.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_copy_response.js b/devtools/client/netmonitor/test/browser_net_copy_response.js
new file mode 100644
index 000000000..411fe5cf0
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_copy_response.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if copying a request's response works.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL);
+ info("Starting test... ");
+
+ const EXPECTED_RESULT = '{ "greeting": "Hello JSON!" }';
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ let requestItem = RequestsMenu.getItemAtIndex(3);
+ RequestsMenu.selectedItem = requestItem;
+
+ yield waitForClipboardPromise(function setup() {
+ RequestsMenu.contextMenu.copyResponse();
+ }, EXPECTED_RESULT);
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_copy_svg_image_as_data_uri.js b/devtools/client/netmonitor/test/browser_net_copy_svg_image_as_data_uri.js
new file mode 100644
index 000000000..252ce92bd
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_copy_svg_image_as_data_uri.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if copying an image as data uri works.
+ */
+
+const SVG_URL = EXAMPLE_URL + "dropmarker.svg";
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CURL_URL);
+ info("Starting test... ");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, SVG_URL, function* (url) {
+ content.wrappedJSObject.performRequest(url);
+ });
+ yield wait;
+
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+ RequestsMenu.selectedItem = requestItem;
+
+ yield waitForClipboardPromise(function setup() {
+ RequestsMenu.contextMenu.copyImageAsDataUri();
+ }, function check(text) {
+ return text.startsWith("data:") && !/undefined/.test(text);
+ });
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_copy_url.js b/devtools/client/netmonitor/test/browser_net_copy_url.js
new file mode 100644
index 000000000..660f5fe79
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_copy_url.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if copying a request's url works.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ info("Starting test... ");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests(1);
+ });
+ yield wait;
+
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+ RequestsMenu.selectedItem = requestItem;
+
+ yield waitForClipboardPromise(function setup() {
+ RequestsMenu.contextMenu.copyUrl();
+ }, requestItem.attachment.url);
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_cors_requests.js b/devtools/client/netmonitor/test/browser_net_cors_requests.js
new file mode 100644
index 000000000..d61b8e2f0
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_cors_requests.js
@@ -0,0 +1,33 @@
+/* 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 CORS preflight requests are displayed by network monitor
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CORS_URL);
+ let { RequestsMenu } = monitor.panelWin.NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1, 1);
+
+ info("Performing a CORS request");
+ let requestUrl = "http://test1.example.com" + CORS_SJS_PATH;
+ yield ContentTask.spawn(tab.linkedBrowser, requestUrl, function* (url) {
+ content.wrappedJSObject.performRequests(url, "triggering/preflight", "post-data");
+ });
+
+ info("Waiting until the requests appear in netmonitor");
+ yield wait;
+
+ info("Checking the preflight and flight methods");
+ ["OPTIONS", "POST"].forEach((method, i) => {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i), method, requestUrl);
+ });
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_curl-utils.js b/devtools/client/netmonitor/test/browser_net_curl-utils.js
new file mode 100644
index 000000000..7a5fc7926
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_curl-utils.js
@@ -0,0 +1,228 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests Curl Utils functionality.
+ */
+
+const { CurlUtils } = require("devtools/client/shared/curl");
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CURL_UTILS_URL);
+ info("Starting test... ");
+
+ let { NetMonitorView, gNetwork } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1, 3);
+ yield ContentTask.spawn(tab.linkedBrowser, SIMPLE_SJS, function* (url) {
+ content.wrappedJSObject.performRequests(url);
+ });
+ yield wait;
+
+ let requests = {
+ get: RequestsMenu.getItemAtIndex(0),
+ post: RequestsMenu.getItemAtIndex(1),
+ multipart: RequestsMenu.getItemAtIndex(2),
+ multipartForm: RequestsMenu.getItemAtIndex(3)
+ };
+
+ let data = yield createCurlData(requests.get.attachment, gNetwork);
+ testFindHeader(data);
+
+ data = yield createCurlData(requests.post.attachment, gNetwork);
+ testIsUrlEncodedRequest(data);
+ testWritePostDataTextParams(data);
+
+ data = yield createCurlData(requests.multipart.attachment, gNetwork);
+ testIsMultipartRequest(data);
+ testGetMultipartBoundary(data);
+ testRemoveBinaryDataFromMultipartText(data);
+
+ data = yield createCurlData(requests.multipartForm.attachment, gNetwork);
+ testGetHeadersFromMultipartText(data);
+
+ if (Services.appinfo.OS != "WINNT") {
+ testEscapeStringPosix();
+ } else {
+ testEscapeStringWin();
+ }
+
+ yield teardown(monitor);
+});
+
+function testIsUrlEncodedRequest(data) {
+ let isUrlEncoded = CurlUtils.isUrlEncodedRequest(data);
+ ok(isUrlEncoded, "Should return true for url encoded requests.");
+}
+
+function testIsMultipartRequest(data) {
+ let isMultipart = CurlUtils.isMultipartRequest(data);
+ ok(isMultipart, "Should return true for multipart/form-data requests.");
+}
+
+function testFindHeader(data) {
+ let headers = data.headers;
+ let hostName = CurlUtils.findHeader(headers, "Host");
+ let requestedWithLowerCased = CurlUtils.findHeader(headers, "x-requested-with");
+ let doesNotExist = CurlUtils.findHeader(headers, "X-Does-Not-Exist");
+
+ is(hostName, "example.com",
+ "Header with name 'Host' should be found in the request array.");
+ is(requestedWithLowerCased, "XMLHttpRequest",
+ "The search should be case insensitive.");
+ is(doesNotExist, null,
+ "Should return null when a header is not found.");
+}
+
+function testWritePostDataTextParams(data) {
+ let params = CurlUtils.writePostDataTextParams(data.postDataText);
+ is(params, "param1=value1&param2=value2&param3=value3",
+ "Should return a serialized representation of the request parameters");
+}
+
+function testGetMultipartBoundary(data) {
+ let boundary = CurlUtils.getMultipartBoundary(data);
+ ok(/-{3,}\w+/.test(boundary),
+ "A boundary string should be found in a multipart request.");
+}
+
+function testRemoveBinaryDataFromMultipartText(data) {
+ let generatedBoundary = CurlUtils.getMultipartBoundary(data);
+ let text = data.postDataText;
+ let binaryRemoved =
+ CurlUtils.removeBinaryDataFromMultipartText(text, generatedBoundary);
+ let boundary = "--" + generatedBoundary;
+
+ const EXPECTED_POSIX_RESULT = [
+ "$'",
+ boundary,
+ "\\r\\n\\r\\n",
+ "Content-Disposition: form-data; name=\"param1\"",
+ "\\r\\n\\r\\n",
+ "value1",
+ "\\r\\n",
+ boundary,
+ "\\r\\n\\r\\n",
+ "Content-Disposition: form-data; name=\"file\"; filename=\"filename.png\"",
+ "\\r\\n",
+ "Content-Type: image/png",
+ "\\r\\n\\r\\n",
+ boundary + "--",
+ "\\r\\n",
+ "'"
+ ].join("");
+
+ const EXPECTED_WIN_RESULT = [
+ '"' + boundary + '"^',
+ "\u000d\u000A\u000d\u000A",
+ '"Content-Disposition: form-data; name=""param1"""^',
+ "\u000d\u000A\u000d\u000A",
+ '"value1"^',
+ "\u000d\u000A",
+ '"' + boundary + '"^',
+ "\u000d\u000A\u000d\u000A",
+ '"Content-Disposition: form-data; name=""file""; filename=""filename.png"""^',
+ "\u000d\u000A",
+ '"Content-Type: image/png"^',
+ "\u000d\u000A\u000d\u000A",
+ '"' + boundary + '--"^',
+ "\u000d\u000A",
+ '""'
+ ].join("");
+
+ if (Services.appinfo.OS != "WINNT") {
+ is(CurlUtils.escapeStringPosix(binaryRemoved), EXPECTED_POSIX_RESULT,
+ "The mulitpart request payload should not contain binary data.");
+ } else {
+ is(CurlUtils.escapeStringWin(binaryRemoved), EXPECTED_WIN_RESULT,
+ "WinNT: The mulitpart request payload should not contain binary data.");
+ }
+}
+
+function testGetHeadersFromMultipartText(data) {
+ let headers = CurlUtils.getHeadersFromMultipartText(data.postDataText);
+
+ ok(Array.isArray(headers), "Should return an array.");
+ ok(headers.length > 0, "There should exist at least one request header.");
+ is(headers[0].name, "Content-Type", "The first header name should be 'Content-Type'.");
+}
+
+function testEscapeStringPosix() {
+ let surroundedWithQuotes = "A simple string";
+ is(CurlUtils.escapeStringPosix(surroundedWithQuotes), "'A simple string'",
+ "The string should be surrounded with single quotes.");
+
+ let singleQuotes = "It's unusual to put crickets in your coffee.";
+ is(CurlUtils.escapeStringPosix(singleQuotes),
+ "$'It\\'s unusual to put crickets in your coffee.'",
+ "Single quotes should be escaped.");
+
+ let newLines = "Line 1\r\nLine 2\u000d\u000ALine3";
+ is(CurlUtils.escapeStringPosix(newLines), "$'Line 1\\r\\nLine 2\\r\\nLine3'",
+ "Newlines should be escaped.");
+
+ let controlChars = "\u0007 \u0009 \u000C \u001B";
+ is(CurlUtils.escapeStringPosix(controlChars), "$'\\x07 \\x09 \\x0c \\x1b'",
+ "Control characters should be escaped.");
+
+ let extendedAsciiChars = "æ ø ü ß ö é";
+ is(CurlUtils.escapeStringPosix(extendedAsciiChars),
+ "$'\\xc3\\xa6 \\xc3\\xb8 \\xc3\\xbc \\xc3\\x9f \\xc3\\xb6 \\xc3\\xa9'",
+ "Character codes outside of the decimal range 32 - 126 should be escaped.");
+}
+
+function testEscapeStringWin() {
+ let surroundedWithDoubleQuotes = "A simple string";
+ is(CurlUtils.escapeStringWin(surroundedWithDoubleQuotes), '"A simple string"',
+ "The string should be surrounded with double quotes.");
+
+ let doubleQuotes = "Quote: \"Time is an illusion. Lunchtime doubly so.\"";
+ is(CurlUtils.escapeStringWin(doubleQuotes),
+ '"Quote: ""Time is an illusion. Lunchtime doubly so."""',
+ "Double quotes should be escaped.");
+
+ let percentSigns = "%AppData%";
+ is(CurlUtils.escapeStringWin(percentSigns), '""%"AppData"%""',
+ "Percent signs should be escaped.");
+
+ let backslashes = "\\A simple string\\";
+ is(CurlUtils.escapeStringWin(backslashes), '"\\\\A simple string\\\\"',
+ "Backslashes should be escaped.");
+
+ let newLines = "line1\r\nline2\r\nline3";
+ is(CurlUtils.escapeStringWin(newLines),
+ '"line1"^\u000d\u000A"line2"^\u000d\u000A"line3"',
+ "Newlines should be escaped.");
+}
+
+function* createCurlData(selected, network, controller) {
+ let { url, method, httpVersion } = selected;
+
+ // Create a sanitized object for the Curl command generator.
+ let data = {
+ url,
+ method,
+ headers: [],
+ httpVersion,
+ postDataText: null
+ };
+
+ // Fetch header values.
+ for (let { name, value } of selected.requestHeaders.headers) {
+ let text = yield network.getString(value);
+ data.headers.push({ name: name, value: text });
+ }
+
+ // Fetch the request payload.
+ if (selected.requestPostData) {
+ let postData = selected.requestPostData.postData.text;
+ data.postDataText = yield network.getString(postData);
+ }
+
+ return data;
+}
diff --git a/devtools/client/netmonitor/test/browser_net_cyrillic-01.js b/devtools/client/netmonitor/test/browser_net_cyrillic-01.js
new file mode 100644
index 000000000..43d6f522e
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_cyrillic-01.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if cyrillic text is rendered correctly in the source editor.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CYRILLIC_URL);
+ info("Starting test... ");
+
+ let { document, EVENTS, Editor, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+ "GET", CONTENT_TYPE_SJS + "?fmt=txt", {
+ status: 200,
+ statusText: "DA DA DA"
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+
+ yield monitor.panelWin.once(EVENTS.RESPONSE_BODY_DISPLAYED);
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+ // u044F = я
+ is(editor.getText().indexOf("\u044F"), 26,
+ "The text shown in the source editor is correct.");
+ is(editor.getMode(), Editor.modes.text,
+ "The mode active in the source editor is correct.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_cyrillic-02.js b/devtools/client/netmonitor/test/browser_net_cyrillic-02.js
new file mode 100644
index 000000000..cd6b2000e
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_cyrillic-02.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if cyrillic text is rendered correctly in the source editor
+ * when loaded directly from an HTML page.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CYRILLIC_URL);
+ info("Starting test... ");
+
+ let { document, EVENTS, Editor, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ tab.linkedBrowser.reload();
+ yield wait;
+
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+ "GET", CYRILLIC_URL, {
+ status: 200,
+ statusText: "OK"
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+
+ yield monitor.panelWin.once(EVENTS.RESPONSE_BODY_DISPLAYED);
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+ // u044F = я
+ is(editor.getText().indexOf("\u044F"), 486,
+ "The text shown in the source editor is correct.");
+ is(editor.getMode(), Editor.modes.html,
+ "The mode active in the source editor is correct.");
+
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_details-no-duplicated-content.js b/devtools/client/netmonitor/test/browser_net_details-no-duplicated-content.js
new file mode 100644
index 000000000..c3df51ced
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_details-no-duplicated-content.js
@@ -0,0 +1,172 @@
+/* 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";
+
+// A test to ensure that the content in details pane is not duplicated.
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ let panel = monitor.panelWin;
+ let { NetMonitorView, EVENTS } = panel;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+
+ const COOKIE_UNIQUE_PATH = "/do-not-use-in-other-tests-using-cookies";
+
+ let TEST_CASES = [
+ {
+ desc: "Test headers tab",
+ pageURI: CUSTOM_GET_URL,
+ requestURI: null,
+ isPost: false,
+ tabIndex: 0,
+ variablesView: NetworkDetails._headers,
+ expectedScopeLength: 2,
+ },
+ {
+ desc: "Test cookies tab",
+ pageURI: CUSTOM_GET_URL,
+ requestURI: COOKIE_UNIQUE_PATH,
+ isPost: false,
+ tabIndex: 1,
+ variablesView: NetworkDetails._cookies,
+ expectedScopeLength: 1,
+ },
+ {
+ desc: "Test params tab",
+ pageURI: POST_RAW_URL,
+ requestURI: null,
+ isPost: true,
+ tabIndex: 2,
+ variablesView: NetworkDetails._params,
+ expectedScopeLength: 1,
+ },
+ ];
+
+ info("Adding a cookie for the \"Cookie\" tab test");
+ yield setDocCookie("a=b; path=" + COOKIE_UNIQUE_PATH);
+
+ info("Running tests");
+ for (let spec of TEST_CASES) {
+ yield runTestCase(spec);
+ }
+
+ // Remove the cookie. If an error occurs the path of the cookie ensures it
+ // doesn't mess with the other tests.
+ info("Removing the added cookie.");
+ yield setDocCookie(
+ "a=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=" + COOKIE_UNIQUE_PATH);
+
+ yield teardown(monitor);
+
+ /**
+ * Set a content document cookie
+ */
+ function setDocCookie(cookie) {
+ return ContentTask.spawn(tab.linkedBrowser, cookie, function* (cookieArg) {
+ content.document.cookie = cookieArg;
+ });
+ }
+
+ /**
+ * A helper that handles the execution of each case.
+ */
+ function* runTestCase(spec) {
+ info("Running case: " + spec.desc);
+ let wait = waitForNetworkEvents(monitor, 1);
+ tab.linkedBrowser.loadURI(spec.pageURI);
+ yield wait;
+
+ RequestsMenu.clear();
+ yield waitForFinalDetailTabUpdate(spec.tabIndex, spec.isPost, spec.requestURI);
+
+ is(spec.variablesView._store.length, spec.expectedScopeLength,
+ "View contains " + spec.expectedScopeLength + " scope headers");
+ }
+
+ /**
+ * A helper that prepares the variables view for the actual testing. It
+ * - selects the correct tab
+ * - performs the specified request to specified URI
+ * - opens the details view
+ * - waits for the final update to happen
+ */
+ function* waitForFinalDetailTabUpdate(tabIndex, isPost, uri) {
+ let onNetworkEvent = panel.once(EVENTS.NETWORK_EVENT);
+ let onDetailsPopulated = panel.once(EVENTS.NETWORKDETAILSVIEW_POPULATED);
+ let onRequestFinished = isPost ?
+ waitForNetworkEvents(monitor, 0, 1) :
+ waitForNetworkEvents(monitor, 1);
+
+ info("Performing a request");
+ yield ContentTask.spawn(tab.linkedBrowser, uri, function* (url) {
+ content.wrappedJSObject.performRequests(1, url);
+ });
+
+ info("Waiting for NETWORK_EVENT");
+ yield onNetworkEvent;
+
+ if (!RequestsMenu.getItemAtIndex(0)) {
+ info("Waiting for the request to be added to the view");
+ yield monitor.panelWin.once(EVENTS.REQUEST_ADDED);
+ }
+
+ ok(true, "Received NETWORK_EVENT. Selecting the item.");
+ let item = RequestsMenu.getItemAtIndex(0);
+ RequestsMenu.selectedItem = item;
+
+ info("Item selected. Waiting for NETWORKDETAILSVIEW_POPULATED");
+ yield onDetailsPopulated;
+
+ info("Received populated event. Selecting tab at index " + tabIndex);
+ NetworkDetails.widget.selectedIndex = tabIndex;
+
+ info("Waiting for request to finish.");
+ yield onRequestFinished;
+
+ ok(true, "Request finished.");
+
+ /**
+ * Because this test uses lazy updates there's four scenarios to consider:
+ * #1: Everything is updated and test is ready to continue.
+ * #2: There's updates that are waiting to be flushed.
+ * #3: Updates are flushed but the tab update is still running.
+ * #4: There's pending updates and a tab update is still running.
+ *
+ * For case #1 there's not going to be a TAB_UPDATED event so don't wait for
+ * it (bug 1106181).
+ *
+ * For cases #2 and #3 it's enough to wait for one TAB_UPDATED event as for
+ * - case #2 the next flush will perform the final update and single
+ * TAB_UPDATED event is emitted.
+ * - case #3 the running update is the final update that'll emit one
+ * TAB_UPDATED event.
+ *
+ * For case #4 we must wait for the updates to be flushed before we can
+ * start waiting for TAB_UPDATED event or we'll continue the test right
+ * after the pending update finishes.
+ */
+ let hasQueuedUpdates = RequestsMenu._updateQueue.length !== 0;
+ let hasRunningTabUpdate = NetworkDetails._viewState.updating[tabIndex];
+
+ if (hasQueuedUpdates || hasRunningTabUpdate) {
+ info("There's pending updates - waiting for them to finish.");
+ info(" hasQueuedUpdates: " + hasQueuedUpdates);
+ info(" hasRunningTabUpdate: " + hasRunningTabUpdate);
+
+ if (hasQueuedUpdates && hasRunningTabUpdate) {
+ info("Waiting for updates to be flushed.");
+ // _flushRequests calls .populate which emits the following event
+ yield panel.once(EVENTS.NETWORKDETAILSVIEW_POPULATED);
+
+ info("Requests flushed.");
+ }
+
+ info("Waiting for final tab update.");
+ yield waitFor(panel, EVENTS.TAB_UPDATED);
+ }
+
+ info("All updates completed.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_filter-01.js b/devtools/client/netmonitor/test/browser_net_filter-01.js
new file mode 100644
index 000000000..b0d76c629
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_filter-01.js
@@ -0,0 +1,264 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test if filtering items in the network table works correctly.
+ */
+const BASIC_REQUESTS = [
+ { url: "sjs_content-type-test-server.sjs?fmt=html&res=undefined&text=Sample" },
+ { url: "sjs_content-type-test-server.sjs?fmt=css&text=sample" },
+ { url: "sjs_content-type-test-server.sjs?fmt=js&text=sample" },
+];
+
+const REQUESTS_WITH_MEDIA = BASIC_REQUESTS.concat([
+ { url: "sjs_content-type-test-server.sjs?fmt=font" },
+ { url: "sjs_content-type-test-server.sjs?fmt=image" },
+ { url: "sjs_content-type-test-server.sjs?fmt=audio" },
+ { url: "sjs_content-type-test-server.sjs?fmt=video" },
+]);
+
+const REQUESTS_WITH_MEDIA_AND_FLASH = REQUESTS_WITH_MEDIA.concat([
+ { url: "sjs_content-type-test-server.sjs?fmt=flash" },
+]);
+
+const REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS = REQUESTS_WITH_MEDIA_AND_FLASH.concat([
+ /* "Upgrade" is a reserved header and can not be set on XMLHttpRequest */
+ { url: "sjs_content-type-test-server.sjs?fmt=ws" },
+]);
+
+add_task(function* () {
+ let Actions = require("devtools/client/netmonitor/actions/index");
+ let { monitor } = yield initNetMonitor(FILTERING_URL);
+ let { gStore } = monitor.panelWin;
+
+ function setFreetextFilter(value) {
+ gStore.dispatch(Actions.setFilterText(value));
+ }
+
+ info("Starting test... ");
+
+ let { $, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 9);
+ loadCommonFrameScript();
+ yield performRequestsInContent(REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS);
+ yield wait;
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, $("#details-pane-toggle"));
+
+ isnot(RequestsMenu.selectedItem, null,
+ "There should be a selected item in the requests menu.");
+ is(RequestsMenu.selectedIndex, 0,
+ "The first item should be selected in the requests menu.");
+ is(NetMonitorView.detailsPaneHidden, false,
+ "The details pane should not be hidden after toggle button was pressed.");
+
+ // First test with single filters...
+ testFilterButtons(monitor, "all");
+ testContents([1, 1, 1, 1, 1, 1, 1, 1, 1]);
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-html-button"));
+ testFilterButtons(monitor, "html");
+ testContents([1, 0, 0, 0, 0, 0, 0, 0, 0]);
+
+ // Reset filters
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-css-button"));
+ testFilterButtons(monitor, "css");
+ testContents([0, 1, 0, 0, 0, 0, 0, 0, 0]);
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-js-button"));
+ testFilterButtons(monitor, "js");
+ testContents([0, 0, 1, 0, 0, 0, 0, 0, 0]);
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-xhr-button"));
+ testFilterButtons(monitor, "xhr");
+ testContents([1, 1, 1, 1, 1, 1, 1, 1, 0]);
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-fonts-button"));
+ testFilterButtons(monitor, "fonts");
+ testContents([0, 0, 0, 1, 0, 0, 0, 0, 0]);
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-images-button"));
+ testFilterButtons(monitor, "images");
+ testContents([0, 0, 0, 0, 1, 0, 0, 0, 0]);
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-media-button"));
+ testFilterButtons(monitor, "media");
+ testContents([0, 0, 0, 0, 0, 1, 1, 0, 0]);
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-flash-button"));
+ testFilterButtons(monitor, "flash");
+ testContents([0, 0, 0, 0, 0, 0, 0, 1, 0]);
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-ws-button"));
+ testFilterButtons(monitor, "ws");
+ testContents([0, 0, 0, 0, 0, 0, 0, 0, 1]);
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ testFilterButtons(monitor, "all");
+ testContents([1, 1, 1, 1, 1, 1, 1, 1, 1]);
+
+ // Text in filter box that matches nothing should hide all.
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ setFreetextFilter("foobar");
+ testContents([0, 0, 0, 0, 0, 0, 0, 0, 0]);
+
+ // Text in filter box that matches should filter out everything else.
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ setFreetextFilter("sample");
+ testContents([1, 1, 1, 0, 0, 0, 0, 0, 0]);
+
+ // Text in filter box that matches should filter out everything else.
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ setFreetextFilter("SAMPLE");
+ testContents([1, 1, 1, 0, 0, 0, 0, 0, 0]);
+
+ // Test negative filtering (only show unmatched items)
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ setFreetextFilter("-sample");
+ testContents([0, 0, 0, 1, 1, 1, 1, 1, 1]);
+
+ // ...then combine multiple filters together.
+
+ // Enable filtering for html and css; should show request of both type.
+ setFreetextFilter("");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-html-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-css-button"));
+ testFilterButtonsCustom(monitor, [0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]);
+ testContents([1, 1, 0, 0, 0, 0, 0, 0, 0]);
+
+ // Html and css filter enabled and text filter should show just the html and css match.
+ // Should not show both the items matching the button plus the items matching the text.
+ setFreetextFilter("sample");
+ testContents([1, 1, 0, 0, 0, 0, 0, 0, 0]);
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-flash-button"));
+ setFreetextFilter("");
+ testFilterButtonsCustom(monitor, [0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0]);
+ testContents([1, 1, 0, 0, 0, 0, 0, 1, 0]);
+
+ // Disable some filters. Only one left active.
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-css-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-flash-button"));
+ testFilterButtons(monitor, "html");
+ testContents([1, 0, 0, 0, 0, 0, 0, 0, 0]);
+
+ // Disable last active filter. Should toggle to all.
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-html-button"));
+ testFilterButtons(monitor, "all");
+ testContents([1, 1, 1, 1, 1, 1, 1, 1, 1]);
+
+ // Enable few filters and click on all. Only "all" should be checked.
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-html-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-css-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-ws-button"));
+ testFilterButtonsCustom(monitor, [0, 1, 1, 0, 0, 0, 0, 0, 0, 1]);
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ testFilterButtons(monitor, "all");
+ testContents([1, 1, 1, 1, 1, 1, 1, 1, 1]);
+
+ yield teardown(monitor);
+
+ function testContents(visibility) {
+ isnot(RequestsMenu.selectedItem, null,
+ "There should still be a selected item after filtering.");
+ is(RequestsMenu.selectedIndex, 0,
+ "The first item should be still selected after filtering.");
+ is(NetMonitorView.detailsPaneHidden, false,
+ "The details pane should still be visible after filtering.");
+
+ is(RequestsMenu.items.length, visibility.length,
+ "There should be a specific amount of items in the requests menu.");
+ is(RequestsMenu.visibleItems.length, visibility.filter(e => e).length,
+ "There should be a specific amount of visbile items in the requests menu.");
+
+ for (let i = 0; i < visibility.length; i++) {
+ is(RequestsMenu.getItemAtIndex(i).target.hidden, !visibility[i],
+ "The item at index " + i + " doesn't have the correct hidden state.");
+ }
+
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+ "GET", CONTENT_TYPE_SJS + "?fmt=html", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "html",
+ fullMimeType: "text/html; charset=utf-8"
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(1),
+ "GET", CONTENT_TYPE_SJS + "?fmt=css", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "css",
+ fullMimeType: "text/css; charset=utf-8"
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(2),
+ "GET", CONTENT_TYPE_SJS + "?fmt=js", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "js",
+ fullMimeType: "application/javascript; charset=utf-8"
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(3),
+ "GET", CONTENT_TYPE_SJS + "?fmt=font", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "woff",
+ fullMimeType: "font/woff"
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(4),
+ "GET", CONTENT_TYPE_SJS + "?fmt=image", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "png",
+ fullMimeType: "image/png"
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(5),
+ "GET", CONTENT_TYPE_SJS + "?fmt=audio", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "ogg",
+ fullMimeType: "audio/ogg"
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(6),
+ "GET", CONTENT_TYPE_SJS + "?fmt=video", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "webm",
+ fullMimeType: "video/webm"
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(7),
+ "GET", CONTENT_TYPE_SJS + "?fmt=flash", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "x-shockwave-flash",
+ fullMimeType: "application/x-shockwave-flash"
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(8),
+ "GET", CONTENT_TYPE_SJS + "?fmt=ws", {
+ fuzzyUrl: true,
+ status: 101,
+ statusText: "Switching Protocols",
+ });
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_filter-02.js b/devtools/client/netmonitor/test/browser_net_filter-02.js
new file mode 100644
index 000000000..70a051b6d
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_filter-02.js
@@ -0,0 +1,200 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test if filtering items in the network table works correctly with new requests.
+ */
+
+const BASIC_REQUESTS = [
+ { url: "sjs_content-type-test-server.sjs?fmt=html&res=undefined" },
+ { url: "sjs_content-type-test-server.sjs?fmt=css" },
+ { url: "sjs_content-type-test-server.sjs?fmt=js" },
+];
+
+const REQUESTS_WITH_MEDIA = BASIC_REQUESTS.concat([
+ { url: "sjs_content-type-test-server.sjs?fmt=font" },
+ { url: "sjs_content-type-test-server.sjs?fmt=image" },
+ { url: "sjs_content-type-test-server.sjs?fmt=audio" },
+ { url: "sjs_content-type-test-server.sjs?fmt=video" },
+]);
+
+const REQUESTS_WITH_MEDIA_AND_FLASH = REQUESTS_WITH_MEDIA.concat([
+ { url: "sjs_content-type-test-server.sjs?fmt=flash" },
+]);
+
+const REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS = REQUESTS_WITH_MEDIA_AND_FLASH.concat([
+ /* "Upgrade" is a reserved header and can not be set on XMLHttpRequest */
+ { url: "sjs_content-type-test-server.sjs?fmt=ws" },
+]);
+
+add_task(function* () {
+ let { monitor } = yield initNetMonitor(FILTERING_URL);
+ info("Starting test... ");
+
+ // It seems that this test may be slow on Ubuntu builds running on ec2.
+ requestLongerTimeout(2);
+
+ let { $, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 9);
+ loadCommonFrameScript();
+ yield performRequestsInContent(REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS);
+ yield wait;
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, $("#details-pane-toggle"));
+
+ isnot(RequestsMenu.selectedItem, null,
+ "There should be a selected item in the requests menu.");
+ is(RequestsMenu.selectedIndex, 0,
+ "The first item should be selected in the requests menu.");
+ is(NetMonitorView.detailsPaneHidden, false,
+ "The details pane should not be hidden after toggle button was pressed.");
+
+ testFilterButtons(monitor, "all");
+ testContents([1, 1, 1, 1, 1, 1, 1, 1, 1]);
+
+ info("Testing html filtering.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-html-button"));
+ testFilterButtons(monitor, "html");
+ testContents([1, 0, 0, 0, 0, 0, 0, 0, 0]);
+
+ info("Performing more requests.");
+ wait = waitForNetworkEvents(monitor, 9);
+ yield performRequestsInContent(REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS);
+ yield wait;
+
+ info("Testing html filtering again.");
+ testFilterButtons(monitor, "html");
+ testContents([1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]);
+
+ info("Performing more requests.");
+ wait = waitForNetworkEvents(monitor, 9);
+ yield performRequestsInContent(REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS);
+ yield wait;
+
+ info("Testing html filtering again.");
+ testFilterButtons(monitor, "html");
+ testContents([1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0,
+ 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]);
+
+ info("Resetting filters.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+ testFilterButtons(monitor, "all");
+ testContents([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]);
+
+ yield teardown(monitor);
+
+ function testContents(visibility) {
+ isnot(RequestsMenu.selectedItem, null,
+ "There should still be a selected item after filtering.");
+ is(RequestsMenu.selectedIndex, 0,
+ "The first item should be still selected after filtering.");
+ is(NetMonitorView.detailsPaneHidden, false,
+ "The details pane should still be visible after filtering.");
+
+ is(RequestsMenu.items.length, visibility.length,
+ "There should be a specific amount of items in the requests menu.");
+ is(RequestsMenu.visibleItems.length, visibility.filter(e => e).length,
+ "There should be a specific amount of visbile items in the requests menu.");
+
+ for (let i = 0; i < visibility.length; i++) {
+ is(RequestsMenu.getItemAtIndex(i).target.hidden, !visibility[i],
+ "The item at index " + i + " doesn't have the correct hidden state.");
+ }
+
+ for (let i = 0; i < visibility.length; i += 9) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
+ "GET", CONTENT_TYPE_SJS + "?fmt=html", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "html",
+ fullMimeType: "text/html; charset=utf-8"
+ });
+ }
+ for (let i = 1; i < visibility.length; i += 9) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
+ "GET", CONTENT_TYPE_SJS + "?fmt=css", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "css",
+ fullMimeType: "text/css; charset=utf-8"
+ });
+ }
+ for (let i = 2; i < visibility.length; i += 9) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
+ "GET", CONTENT_TYPE_SJS + "?fmt=js", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "js",
+ fullMimeType: "application/javascript; charset=utf-8"
+ });
+ }
+ for (let i = 3; i < visibility.length; i += 9) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
+ "GET", CONTENT_TYPE_SJS + "?fmt=font", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "woff",
+ fullMimeType: "font/woff"
+ });
+ }
+ for (let i = 4; i < visibility.length; i += 9) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
+ "GET", CONTENT_TYPE_SJS + "?fmt=image", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "png",
+ fullMimeType: "image/png"
+ });
+ }
+ for (let i = 5; i < visibility.length; i += 9) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
+ "GET", CONTENT_TYPE_SJS + "?fmt=audio", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "ogg",
+ fullMimeType: "audio/ogg"
+ });
+ }
+ for (let i = 6; i < visibility.length; i += 9) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
+ "GET", CONTENT_TYPE_SJS + "?fmt=video", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "webm",
+ fullMimeType: "video/webm"
+ });
+ }
+ for (let i = 7; i < visibility.length; i += 9) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
+ "GET", CONTENT_TYPE_SJS + "?fmt=flash", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "x-shockwave-flash",
+ fullMimeType: "application/x-shockwave-flash"
+ });
+ }
+ for (let i = 8; i < visibility.length; i += 9) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
+ "GET", CONTENT_TYPE_SJS + "?fmt=ws", {
+ fuzzyUrl: true,
+ status: 101,
+ statusText: "Switching Protocols"
+ });
+ }
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_filter-03.js b/devtools/client/netmonitor/test/browser_net_filter-03.js
new file mode 100644
index 000000000..2babdaab3
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_filter-03.js
@@ -0,0 +1,185 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test if filtering items in the network table works correctly with new requests
+ * and while sorting is enabled.
+ */
+const BASIC_REQUESTS = [
+ { url: "sjs_content-type-test-server.sjs?fmt=html&res=undefined" },
+ { url: "sjs_content-type-test-server.sjs?fmt=css" },
+ { url: "sjs_content-type-test-server.sjs?fmt=js" },
+];
+
+const REQUESTS_WITH_MEDIA = BASIC_REQUESTS.concat([
+ { url: "sjs_content-type-test-server.sjs?fmt=font" },
+ { url: "sjs_content-type-test-server.sjs?fmt=image" },
+ { url: "sjs_content-type-test-server.sjs?fmt=audio" },
+ { url: "sjs_content-type-test-server.sjs?fmt=video" },
+]);
+
+add_task(function* () {
+ let { monitor } = yield initNetMonitor(FILTERING_URL);
+ info("Starting test... ");
+
+ // It seems that this test may be slow on Ubuntu builds running on ec2.
+ requestLongerTimeout(2);
+
+ let { $, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ // The test assumes that the first HTML request here has a longer response
+ // body than the other HTML requests performed later during the test.
+ let requests = Cu.cloneInto(REQUESTS_WITH_MEDIA, {});
+ let newres = "res=<p>" + new Array(10).join(Math.random(10)) + "</p>";
+ requests[0].url = requests[0].url.replace("res=undefined", newres);
+
+ loadCommonFrameScript();
+
+ let wait = waitForNetworkEvents(monitor, 7);
+ yield performRequestsInContent(requests);
+ yield wait;
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, $("#details-pane-toggle"));
+
+ isnot(RequestsMenu.selectedItem, null,
+ "There should be a selected item in the requests menu.");
+ is(RequestsMenu.selectedIndex, 0,
+ "The first item should be selected in the requests menu.");
+ is(NetMonitorView.detailsPaneHidden, false,
+ "The details pane should not be hidden after toggle button was pressed.");
+
+ testFilterButtons(monitor, "all");
+ testContents([0, 1, 2, 3, 4, 5, 6], 7, 0);
+
+ info("Sorting by size, ascending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-size-button"));
+ testFilterButtons(monitor, "all");
+ testContents([6, 4, 5, 0, 1, 2, 3], 7, 6);
+
+ info("Testing html filtering.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-html-button"));
+ testFilterButtons(monitor, "html");
+ testContents([6, 4, 5, 0, 1, 2, 3], 1, 6);
+
+ info("Performing more requests.");
+ wait = waitForNetworkEvents(monitor, 7);
+ performRequestsInContent(REQUESTS_WITH_MEDIA);
+ yield wait;
+
+ info("Testing html filtering again.");
+ resetSorting();
+ testFilterButtons(monitor, "html");
+ testContents([8, 13, 9, 11, 10, 12, 0, 4, 1, 5, 2, 6, 3, 7], 2, 13);
+
+ info("Performing more requests.");
+ performRequestsInContent(REQUESTS_WITH_MEDIA);
+ yield waitForNetworkEvents(monitor, 7);
+
+ info("Testing html filtering again.");
+ resetSorting();
+ testFilterButtons(monitor, "html");
+ testContents([12, 13, 20, 14, 16, 18, 15, 17, 19, 0, 4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11],
+ 3, 20);
+
+ yield teardown(monitor);
+
+ function resetSorting() {
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-waterfall-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-size-button"));
+ }
+
+ function testContents(order, visible, selection) {
+ isnot(RequestsMenu.selectedItem, null,
+ "There should still be a selected item after filtering.");
+ is(RequestsMenu.selectedIndex, selection,
+ "The first item should be still selected after filtering.");
+ is(NetMonitorView.detailsPaneHidden, false,
+ "The details pane should still be visible after filtering.");
+
+ is(RequestsMenu.items.length, order.length,
+ "There should be a specific amount of items in the requests menu.");
+ is(RequestsMenu.visibleItems.length, visible,
+ "There should be a specific amount of visbile items in the requests menu.");
+
+ for (let i = 0; i < order.length; i++) {
+ is(RequestsMenu.getItemAtIndex(i), RequestsMenu.items[i],
+ "The requests menu items aren't ordered correctly. Misplaced item " + i + ".");
+ }
+
+ for (let i = 0, len = order.length / 7; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i]),
+ "GET", CONTENT_TYPE_SJS + "?fmt=html", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "html",
+ fullMimeType: "text/html; charset=utf-8"
+ });
+ }
+ for (let i = 0, len = order.length / 7; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len]),
+ "GET", CONTENT_TYPE_SJS + "?fmt=css", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "css",
+ fullMimeType: "text/css; charset=utf-8"
+ });
+ }
+ for (let i = 0, len = order.length / 7; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len * 2]),
+ "GET", CONTENT_TYPE_SJS + "?fmt=js", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "js",
+ fullMimeType: "application/javascript; charset=utf-8"
+ });
+ }
+ for (let i = 0, len = order.length / 7; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len * 3]),
+ "GET", CONTENT_TYPE_SJS + "?fmt=font", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "woff",
+ fullMimeType: "font/woff"
+ });
+ }
+ for (let i = 0, len = order.length / 7; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len * 4]),
+ "GET", CONTENT_TYPE_SJS + "?fmt=image", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "png",
+ fullMimeType: "image/png"
+ });
+ }
+ for (let i = 0, len = order.length / 7; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len * 5]),
+ "GET", CONTENT_TYPE_SJS + "?fmt=audio", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "ogg",
+ fullMimeType: "audio/ogg"
+ });
+ }
+ for (let i = 0, len = order.length / 7; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len * 6]),
+ "GET", CONTENT_TYPE_SJS + "?fmt=video", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "OK",
+ type: "webm",
+ fullMimeType: "video/webm"
+ });
+ }
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_filter-04.js b/devtools/client/netmonitor/test/browser_net_filter-04.js
new file mode 100644
index 000000000..e617dbaa9
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_filter-04.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if invalid filter types are sanitized when loaded from the preferences.
+ */
+
+const BASIC_REQUESTS = [
+ { url: "sjs_content-type-test-server.sjs?fmt=html&res=undefined" },
+ { url: "sjs_content-type-test-server.sjs?fmt=css" },
+ { url: "sjs_content-type-test-server.sjs?fmt=js" },
+];
+
+const REQUESTS_WITH_MEDIA = BASIC_REQUESTS.concat([
+ { url: "sjs_content-type-test-server.sjs?fmt=font" },
+ { url: "sjs_content-type-test-server.sjs?fmt=image" },
+ { url: "sjs_content-type-test-server.sjs?fmt=audio" },
+ { url: "sjs_content-type-test-server.sjs?fmt=video" },
+]);
+
+const REQUESTS_WITH_MEDIA_AND_FLASH = REQUESTS_WITH_MEDIA.concat([
+ { url: "sjs_content-type-test-server.sjs?fmt=flash" },
+]);
+
+const REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS = REQUESTS_WITH_MEDIA_AND_FLASH.concat([
+ /* "Upgrade" is a reserved header and can not be set on XMLHttpRequest */
+ { url: "sjs_content-type-test-server.sjs?fmt=ws" },
+]);
+
+add_task(function* () {
+ Services.prefs.setCharPref("devtools.netmonitor.filters", '["js", "bogus"]');
+
+ let { monitor } = yield initNetMonitor(FILTERING_URL);
+ info("Starting test... ");
+
+ let { Prefs, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ is(Prefs.filters.length, 2,
+ "All filter types were loaded as an array from the preferences.");
+ is(Prefs.filters[0], "js",
+ "The first filter type is correct.");
+ is(Prefs.filters[1], "bogus",
+ "The second filter type is invalid, but loaded anyway.");
+
+ let wait = waitForNetworkEvents(monitor, 9);
+ loadCommonFrameScript();
+ yield performRequestsInContent(REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS);
+ yield wait;
+
+ testFilterButtons(monitor, "js");
+ ok(true, "Only the correct filter type was taken into consideration.");
+
+ yield teardown(monitor);
+
+ let filters = Services.prefs.getCharPref("devtools.netmonitor.filters");
+ is(filters, '["js"]',
+ "The bogus filter type was ignored and removed from the preferences.");
+});
diff --git a/devtools/client/netmonitor/test/browser_net_footer-summary.js b/devtools/client/netmonitor/test/browser_net_footer-summary.js
new file mode 100644
index 000000000..e484b2097
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_footer-summary.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test if the summary text displayed in the network requests menu footer
+ * is correct.
+ */
+
+add_task(function* () {
+ requestLongerTimeout(2);
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+ let { PluralForm } = require("devtools/shared/plural-form");
+
+ let { tab, monitor } = yield initNetMonitor(FILTERING_URL);
+ info("Starting test... ");
+
+ let { $, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+ testStatus();
+
+ for (let i = 0; i < 2; i++) {
+ info(`Performing requests in batch #${i}`);
+ let wait = waitForNetworkEvents(monitor, 8);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests('{ "getMedia": true, "getFlash": true }');
+ });
+ yield wait;
+
+ testStatus();
+
+ let buttons = ["html", "css", "js", "xhr", "fonts", "images", "media", "flash"];
+ for (let button of buttons) {
+ let buttonEl = $(`#requests-menu-filter-${button}-button`);
+ EventUtils.sendMouseEvent({ type: "click" }, buttonEl);
+ testStatus();
+ }
+ }
+
+ yield teardown(monitor);
+
+ function testStatus() {
+ let summary = $("#requests-menu-network-summary-button");
+ let value = summary.getAttribute("label");
+ info("Current summary: " + value);
+
+ let visibleItems = RequestsMenu.visibleItems;
+ let visibleRequestsCount = visibleItems.length;
+ let totalRequestsCount = RequestsMenu.itemCount;
+ info("Current requests: " + visibleRequestsCount + " of " + totalRequestsCount + ".");
+
+ if (!totalRequestsCount || !visibleRequestsCount) {
+ is(value, L10N.getStr("networkMenu.empty"),
+ "The current summary text is incorrect, expected an 'empty' label.");
+ return;
+ }
+
+ let totalBytes = RequestsMenu._getTotalBytesOfRequests(visibleItems);
+ let totalMillis =
+ RequestsMenu._getNewestRequest(visibleItems).attachment.endedMillis -
+ RequestsMenu._getOldestRequest(visibleItems).attachment.startedMillis;
+
+ info("Computed total bytes: " + totalBytes);
+ info("Computed total millis: " + totalMillis);
+
+ is(value, PluralForm.get(visibleRequestsCount, L10N.getStr("networkMenu.summary"))
+ .replace("#1", visibleRequestsCount)
+ .replace("#2", L10N.numberWithDecimals((totalBytes || 0) / 1024, 2))
+ .replace("#3", L10N.numberWithDecimals((totalMillis || 0) / 1000, 2))
+ , "The current summary text is incorrect.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_frame.js b/devtools/client/netmonitor/test/browser_net_frame.js
new file mode 100644
index 000000000..eeded652b
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_frame.js
@@ -0,0 +1,221 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests for all expected requests when an iframe is loading a subdocument.
+ */
+
+const TOP_FILE_NAME = "html_frame-test-page.html";
+const SUB_FILE_NAME = "html_frame-subdocument.html";
+const TOP_URL = EXAMPLE_URL + TOP_FILE_NAME;
+const SUB_URL = EXAMPLE_URL + SUB_FILE_NAME;
+
+const EXPECTED_REQUESTS_TOP = [
+ {
+ method: "GET",
+ url: TOP_URL,
+ causeType: "document",
+ causeUri: "",
+ stack: true
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "stylesheet_request",
+ causeType: "stylesheet",
+ causeUri: TOP_URL,
+ stack: false
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "img_request",
+ causeType: "img",
+ causeUri: TOP_URL,
+ stack: false
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "xhr_request",
+ causeType: "xhr",
+ causeUri: TOP_URL,
+ stack: [{ fn: "performXhrRequest", file: TOP_FILE_NAME, line: 23 }]
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "fetch_request",
+ causeType: "fetch",
+ causeUri: TOP_URL,
+ stack: [{ fn: "performFetchRequest", file: TOP_FILE_NAME, line: 27 }]
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "promise_fetch_request",
+ causeType: "fetch",
+ causeUri: TOP_URL,
+ stack: [
+ { fn: "performPromiseFetchRequest", file: TOP_FILE_NAME, line: 39 },
+ { fn: null, file: TOP_FILE_NAME, line: 38, asyncCause: "promise callback" },
+ ]
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "timeout_fetch_request",
+ causeType: "fetch",
+ causeUri: TOP_URL,
+ stack: [
+ { fn: "performTimeoutFetchRequest", file: TOP_FILE_NAME, line: 41 },
+ { fn: "performPromiseFetchRequest", file: TOP_FILE_NAME, line: 40,
+ asyncCause: "setTimeout handler" },
+ ]
+ },
+ {
+ method: "POST",
+ url: EXAMPLE_URL + "beacon_request",
+ causeType: "beacon",
+ causeUri: TOP_URL,
+ stack: [{ fn: "performBeaconRequest", file: TOP_FILE_NAME, line: 31 }]
+ },
+];
+
+const EXPECTED_REQUESTS_SUB = [
+ {
+ method: "GET",
+ url: SUB_URL,
+ causeType: "subdocument",
+ causeUri: TOP_URL,
+ stack: false
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "stylesheet_request",
+ causeType: "stylesheet",
+ causeUri: SUB_URL,
+ stack: false
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "img_request",
+ causeType: "img",
+ causeUri: SUB_URL,
+ stack: false
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "xhr_request",
+ causeType: "xhr",
+ causeUri: SUB_URL,
+ stack: [{ fn: "performXhrRequest", file: SUB_FILE_NAME, line: 22 }]
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "fetch_request",
+ causeType: "fetch",
+ causeUri: SUB_URL,
+ stack: [{ fn: "performFetchRequest", file: SUB_FILE_NAME, line: 26 }]
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "promise_fetch_request",
+ causeType: "fetch",
+ causeUri: SUB_URL,
+ stack: [
+ { fn: "performPromiseFetchRequest", file: SUB_FILE_NAME, line: 38 },
+ { fn: null, file: SUB_FILE_NAME, line: 37, asyncCause: "promise callback" },
+ ]
+ },
+ {
+ method: "GET",
+ url: EXAMPLE_URL + "timeout_fetch_request",
+ causeType: "fetch",
+ causeUri: SUB_URL,
+ stack: [
+ { fn: "performTimeoutFetchRequest", file: SUB_FILE_NAME, line: 40 },
+ { fn: "performPromiseFetchRequest", file: SUB_FILE_NAME, line: 39,
+ asyncCause: "setTimeout handler" },
+ ]
+ },
+ {
+ method: "POST",
+ url: EXAMPLE_URL + "beacon_request",
+ causeType: "beacon",
+ causeUri: SUB_URL,
+ stack: [{ fn: "performBeaconRequest", file: SUB_FILE_NAME, line: 30 }]
+ },
+];
+
+const REQUEST_COUNT = EXPECTED_REQUESTS_TOP.length + EXPECTED_REQUESTS_SUB.length;
+
+add_task(function* () {
+ // Async stacks aren't on by default in all builds
+ yield SpecialPowers.pushPrefEnv({ set: [["javascript.options.asyncstack", true]] });
+
+ // the initNetMonitor function clears the network request list after the
+ // page is loaded. That's why we first load a bogus page from SIMPLE_URL,
+ // and only then load the real thing from TOP_URL - we want to catch
+ // all the requests the page is making, not only the XHRs.
+ // We can't use about:blank here, because initNetMonitor checks that the
+ // page has actually made at least one request.
+ let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ tab.linkedBrowser.loadURI(TOP_URL, null, null);
+
+ yield waitForNetworkEvents(monitor, REQUEST_COUNT);
+
+ is(RequestsMenu.itemCount, REQUEST_COUNT,
+ "All the page events should be recorded.");
+
+ // While there is a defined order for requests in each document separately, the requests
+ // from different documents may interleave in various ways that change per test run, so
+ // there is not a single order when considering all the requests together.
+ let currentTop = 0;
+ let currentSub = 0;
+ for (let i = 0; i < REQUEST_COUNT; i++) {
+ let requestItem = RequestsMenu.getItemAtIndex(i);
+
+ let itemUrl = requestItem.attachment.url;
+ let itemCauseUri = requestItem.target.querySelector(".requests-menu-cause-label")
+ .getAttribute("tooltiptext");
+ let spec;
+ if (itemUrl == SUB_URL || itemCauseUri == SUB_URL) {
+ spec = EXPECTED_REQUESTS_SUB[currentSub++];
+ } else {
+ spec = EXPECTED_REQUESTS_TOP[currentTop++];
+ }
+ let { method, url, causeType, causeUri, stack } = spec;
+
+ verifyRequestItemTarget(requestItem,
+ method, url, { cause: { type: causeType, loadingDocumentUri: causeUri } }
+ );
+
+ let { stacktrace } = requestItem.attachment.cause;
+ let stackLen = stacktrace ? stacktrace.length : 0;
+
+ if (stack) {
+ ok(stacktrace, `Request #${i} has a stacktrace`);
+ ok(stackLen > 0,
+ `Request #${i} (${causeType}) has a stacktrace with ${stackLen} items`);
+
+ // if "stack" is array, check the details about the top stack frames
+ if (Array.isArray(stack)) {
+ stack.forEach((frame, j) => {
+ is(stacktrace[j].functionName, frame.fn,
+ `Request #${i} has the correct function on JS stack frame #${j}`);
+ is(stacktrace[j].filename.split("/").pop(), frame.file,
+ `Request #${i} has the correct file on JS stack frame #${j}`);
+ is(stacktrace[j].lineNumber, frame.line,
+ `Request #${i} has the correct line number on JS stack frame #${j}`);
+ is(stacktrace[j].asyncCause, frame.asyncCause,
+ `Request #${i} has the correct async cause on JS stack frame #${j}`);
+ });
+ }
+ } else {
+ is(stackLen, 0, `Request #${i} (${causeType}) has an empty stacktrace`);
+ }
+ }
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_html-preview.js b/devtools/client/netmonitor/test/browser_net_html-preview.js
new file mode 100644
index 000000000..351009de5
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_html-preview.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if html responses show and properly populate a "Preview" tab.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CONTENT_TYPE_URL);
+ info("Starting test... ");
+
+ let { $, document, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 6);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+
+ is($("#event-details-pane").selectedIndex, 0,
+ "The first tab in the details pane should be selected.");
+ is($("#preview-tab").hidden, true,
+ "The preview tab should be hidden for non html responses.");
+ is($("#preview-tabpanel").hidden, false,
+ "The preview tabpanel is not hidden for non html responses.");
+
+ RequestsMenu.selectedIndex = 4;
+ NetMonitorView.toggleDetailsPane({ visible: true, animated: false }, 6);
+
+ is($("#event-details-pane").selectedIndex, 6,
+ "The sixth tab in the details pane should be selected.");
+ is($("#preview-tab").hidden, false,
+ "The preview tab should be visible now.");
+
+ yield monitor.panelWin.once(EVENTS.RESPONSE_HTML_PREVIEW_DISPLAYED);
+ let iframe = $("#response-preview");
+ ok(iframe,
+ "There should be a response preview iframe available.");
+ ok(iframe.contentDocument,
+ "The iframe's content document should be available.");
+ is(iframe.contentDocument.querySelector("blink").textContent, "Not Found",
+ "The iframe's content document should be loaded and correct.");
+
+ RequestsMenu.selectedIndex = 5;
+
+ is($("#event-details-pane").selectedIndex, 0,
+ "The first tab in the details pane should be selected again.");
+ is($("#preview-tab").hidden, true,
+ "The preview tab should be hidden again for non html responses.");
+ is($("#preview-tabpanel").hidden, false,
+ "The preview tabpanel is not hidden again for non html responses.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_icon-preview.js b/devtools/client/netmonitor/test/browser_net_icon-preview.js
new file mode 100644
index 000000000..e3c5bde4e
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_icon-preview.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if image responses show a thumbnail in the requests menu.
+ */
+
+add_task(function* () {
+ let Actions = require("devtools/client/netmonitor/actions/index");
+
+ let { tab, monitor } = yield initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL);
+ info("Starting test... ");
+
+ let { $, $all, EVENTS, ACTIVITY_TYPE, NetMonitorView, NetMonitorController,
+ gStore } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ let wait = waitForEvents();
+ yield performRequests();
+ yield wait;
+
+ info("Checking the image thumbnail when all items are shown.");
+ checkImageThumbnail();
+
+ RequestsMenu.sortBy("size");
+ info("Checking the image thumbnail when all items are sorted.");
+ checkImageThumbnail();
+
+ gStore.dispatch(Actions.toggleFilterType("images"));
+ info("Checking the image thumbnail when only images are shown.");
+ checkImageThumbnail();
+
+ info("Reloading the debuggee and performing all requests again...");
+ wait = waitForEvents();
+ yield reloadAndPerformRequests();
+ yield wait;
+
+ info("Checking the image thumbnail after a reload.");
+ checkImageThumbnail();
+
+ yield teardown(monitor);
+
+ function waitForEvents() {
+ return promise.all([
+ waitForNetworkEvents(monitor, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS),
+ monitor.panelWin.once(EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED)
+ ]);
+ }
+
+ function performRequests() {
+ return ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ }
+
+ function* reloadAndPerformRequests() {
+ yield NetMonitorController.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED);
+ yield performRequests();
+ }
+
+ function checkImageThumbnail() {
+ is($all(".requests-menu-icon[type=thumbnail]").length, 1,
+ "There should be only one image request with a thumbnail displayed.");
+ is($(".requests-menu-icon[type=thumbnail]").src, TEST_IMAGE_DATA_URI,
+ "The image requests-menu-icon thumbnail is displayed correctly.");
+ is($(".requests-menu-icon[type=thumbnail]").hidden, false,
+ "The image requests-menu-icon thumbnail should not be hidden.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_image-tooltip.js b/devtools/client/netmonitor/test/browser_net_image-tooltip.js
new file mode 100644
index 000000000..04cd26959
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_image-tooltip.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const IMAGE_TOOLTIP_URL = EXAMPLE_URL + "html_image-tooltip-test-page.html";
+const IMAGE_TOOLTIP_REQUESTS = 1;
+
+/**
+ * Tests if image responses show a popup in the requests menu when hovered.
+ */
+add_task(function* test() {
+ let { tab, monitor } = yield initNetMonitor(IMAGE_TOOLTIP_URL);
+ info("Starting test... ");
+
+ let { $, EVENTS, ACTIVITY_TYPE, NetMonitorView, NetMonitorController } =
+ monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+ RequestsMenu.lazyUpdate = true;
+
+ let onEvents = waitForNetworkEvents(monitor, IMAGE_TOOLTIP_REQUESTS);
+ let onThumbnail = monitor.panelWin.once(EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED);
+
+ yield performRequests();
+ yield onEvents;
+ yield onThumbnail;
+
+ info("Checking the image thumbnail after a few requests were made...");
+ yield showTooltipAndVerify(RequestsMenu.tooltip, RequestsMenu.items[0]);
+
+ // Hide tooltip before next test, to avoid the situation that tooltip covers
+ // the icon for the request of the next test.
+ info("Checking the image thumbnail gets hidden...");
+ yield hideTooltipAndVerify(RequestsMenu.tooltip, RequestsMenu.items[0]);
+
+ // +1 extra document reload
+ onEvents = waitForNetworkEvents(monitor, IMAGE_TOOLTIP_REQUESTS + 1);
+ onThumbnail = monitor.panelWin.once(EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED);
+
+ info("Reloading the debuggee and performing all requests again...");
+ yield NetMonitorController.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED);
+ yield performRequests();
+ yield onEvents;
+ yield onThumbnail;
+
+ info("Checking the image thumbnail after a reload.");
+ yield showTooltipAndVerify(RequestsMenu.tooltip, RequestsMenu.items[1]);
+
+ info("Checking if the image thumbnail is hidden when mouse leaves the menu widget");
+ let requestsMenuEl = $("#requests-menu-contents");
+ let onHidden = RequestsMenu.tooltip.once("hidden");
+ EventUtils.synthesizeMouse(requestsMenuEl, 0, 0, {type: "mouseout"}, monitor.panelWin);
+ yield onHidden;
+
+ yield teardown(monitor);
+
+ function performRequests() {
+ return ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ }
+
+ /**
+ * Show a tooltip on the {requestItem} and verify that it was displayed
+ * with the expected content.
+ */
+ function* showTooltipAndVerify(tooltip, requestItem) {
+ let anchor = $(".requests-menu-file", requestItem.target);
+ yield showTooltipOn(tooltip, anchor);
+
+ info("Tooltip was successfully opened for the image request.");
+ is(tooltip.panel.querySelector("img").src, TEST_IMAGE_DATA_URI,
+ "The tooltip's image content is displayed correctly.");
+ }
+
+ /**
+ * Trigger a tooltip over an element by sending mousemove event.
+ * @return a promise that resolves when the tooltip is shown
+ */
+ function showTooltipOn(tooltip, element) {
+ let onShown = tooltip.once("shown");
+ let win = element.ownerDocument.defaultView;
+ EventUtils.synthesizeMouseAtCenter(element, {type: "mousemove"}, win);
+ return onShown;
+ }
+
+ /**
+ * Hide a tooltip on the {requestItem} and verify that it was closed.
+ */
+ function* hideTooltipAndVerify(tooltip, requestItem) {
+ // Hovering method hides tooltip.
+ let anchor = $(".requests-menu-method", requestItem.target);
+
+ let onHidden = tooltip.once("hidden");
+ let win = anchor.ownerDocument.defaultView;
+ EventUtils.synthesizeMouseAtCenter(anchor, {type: "mousemove"}, win);
+ yield onHidden;
+
+ info("Tooltip was successfully closed.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_json-long.js b/devtools/client/netmonitor/test/browser_net_json-long.js
new file mode 100644
index 000000000..2347d26c4
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_json-long.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if very long JSON responses are handled correctly.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(JSON_LONG_URL);
+ info("Starting test... ");
+
+ // This is receiving over 80 KB of json and will populate over 6000 items
+ // in a variables view instance. Debug builds are slow.
+ requestLongerTimeout(4);
+
+ let { document, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+ "GET", CONTENT_TYPE_SJS + "?fmt=json-long", {
+ status: 200,
+ statusText: "OK",
+ type: "json",
+ fullMimeType: "text/json; charset=utf-8",
+ size: L10N.getFormatStr("networkMenu.sizeKB",
+ L10N.numberWithDecimals(85975 / 1024, 2)),
+ time: true
+ });
+
+ let onEvent = monitor.panelWin.once(EVENTS.RESPONSE_BODY_DISPLAYED);
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+ yield onEvent;
+
+ testResponseTab();
+
+ yield teardown(monitor);
+
+ function testResponseTab() {
+ let tabEl = document.querySelectorAll("#details-pane tab")[3];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[3];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The response tab in the network details pane should be selected.");
+
+ is(tabpanel.querySelector("#response-content-info-header")
+ .hasAttribute("hidden"), true,
+ "The response info header doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-json-box")
+ .hasAttribute("hidden"), false,
+ "The response content json box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-textarea-box")
+ .hasAttribute("hidden"), true,
+ "The response content textarea box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-image-box")
+ .hasAttribute("hidden"), true,
+ "The response content image box doesn't have the intended visibility.");
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
+ "There should be 1 json scope displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-property").length, 6143,
+ "There should be 6143 json properties displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+ "The empty notice should not be displayed in this tabpanel.");
+
+ let jsonScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+ let names = ".variables-view-property > .title > .name";
+ let values = ".variables-view-property > .title > .value";
+
+ is(jsonScope.querySelector(".name").getAttribute("value"),
+ L10N.getStr("jsonScopeName"),
+ "The json scope doesn't have the correct title.");
+
+ is(jsonScope.querySelectorAll(names)[0].getAttribute("value"),
+ "0", "The first json property name was incorrect.");
+ is(jsonScope.querySelectorAll(values)[0].getAttribute("value"),
+ "Object", "The first json property value was incorrect.");
+
+ is(jsonScope.querySelectorAll(names)[1].getAttribute("value"),
+ "greeting", "The second json property name was incorrect.");
+ is(jsonScope.querySelectorAll(values)[1].getAttribute("value"),
+ "\"Hello long string JSON!\"", "The second json property value was incorrect.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_json-malformed.js b/devtools/client/netmonitor/test/browser_net_json-malformed.js
new file mode 100644
index 000000000..6bed60480
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_json-malformed.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if malformed JSON responses are handled correctly.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(JSON_MALFORMED_URL);
+ info("Starting test... ");
+
+ let { document, EVENTS, Editor, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+ "GET", CONTENT_TYPE_SJS + "?fmt=json-malformed", {
+ status: 200,
+ statusText: "OK",
+ type: "json",
+ fullMimeType: "text/json; charset=utf-8"
+ });
+
+ let onEvent = monitor.panelWin.once(EVENTS.RESPONSE_BODY_DISPLAYED);
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+ yield onEvent;
+
+ let tabEl = document.querySelectorAll("#details-pane tab")[3];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[3];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The response tab in the network details pane should be selected.");
+
+ is(tabpanel.querySelector("#response-content-info-header")
+ .hasAttribute("hidden"), false,
+ "The response info header doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-info-header")
+ .getAttribute("value"),
+ "SyntaxError: JSON.parse: unexpected non-whitespace character after JSON data" +
+ " at line 1 column 40 of the JSON data",
+ "The response info header doesn't have the intended value attribute.");
+ is(tabpanel.querySelector("#response-content-info-header")
+ .getAttribute("tooltiptext"),
+ "SyntaxError: JSON.parse: unexpected non-whitespace character after JSON data" +
+ " at line 1 column 40 of the JSON data",
+ "The response info header doesn't have the intended tooltiptext attribute.");
+
+ is(tabpanel.querySelector("#response-content-json-box")
+ .hasAttribute("hidden"), true,
+ "The response content json box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-textarea-box")
+ .hasAttribute("hidden"), false,
+ "The response content textarea box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-image-box")
+ .hasAttribute("hidden"), true,
+ "The response content image box doesn't have the intended visibility.");
+
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+ is(editor.getText(), "{ \"greeting\": \"Hello malformed JSON!\" },",
+ "The text shown in the source editor is incorrect.");
+ is(editor.getMode(), Editor.modes.js,
+ "The mode active in the source editor is incorrect.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_json_custom_mime.js b/devtools/client/netmonitor/test/browser_net_json_custom_mime.js
new file mode 100644
index 000000000..210ffbbe8
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_json_custom_mime.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if JSON responses with unusal/custom MIME types are handled correctly.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(JSON_CUSTOM_MIME_URL);
+ info("Starting test... ");
+
+ let { document, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+ "GET", CONTENT_TYPE_SJS + "?fmt=json-custom-mime", {
+ status: 200,
+ statusText: "OK",
+ type: "x-bigcorp-json",
+ fullMimeType: "text/x-bigcorp-json; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 41),
+ time: true
+ });
+
+ let onEvent = monitor.panelWin.once(EVENTS.RESPONSE_BODY_DISPLAYED);
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+ yield onEvent;
+
+ testResponseTab();
+
+ yield teardown(monitor);
+
+ function testResponseTab() {
+ let tabEl = document.querySelectorAll("#details-pane tab")[3];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[3];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The response tab in the network details pane should be selected.");
+
+ is(tabpanel.querySelector("#response-content-info-header")
+ .hasAttribute("hidden"), true,
+ "The response info header doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-json-box")
+ .hasAttribute("hidden"), false,
+ "The response content json box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-textarea-box")
+ .hasAttribute("hidden"), true,
+ "The response content textarea box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-image-box")
+ .hasAttribute("hidden"), true,
+ "The response content image box doesn't have the intended visibility.");
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
+ "There should be 1 json scope displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-property").length, 2,
+ "There should be 2 json properties displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+ "The empty notice should not be displayed in this tabpanel.");
+
+ let jsonScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+ is(jsonScope.querySelectorAll(".variables-view-property .name")[0]
+ .getAttribute("value"),
+ "greeting", "The first json property name was incorrect.");
+ is(jsonScope.querySelectorAll(".variables-view-property .value")[0]
+ .getAttribute("value"),
+ "\"Hello oddly-named JSON!\"", "The first json property value was incorrect.");
+
+ is(jsonScope.querySelectorAll(".variables-view-property .name")[1]
+ .getAttribute("value"),
+ "__proto__", "The second json property name was incorrect.");
+ is(jsonScope.querySelectorAll(".variables-view-property .value")[1]
+ .getAttribute("value"),
+ "Object", "The second json property value was incorrect.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_json_text_mime.js b/devtools/client/netmonitor/test/browser_net_json_text_mime.js
new file mode 100644
index 000000000..edc98a5c9
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_json_text_mime.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if JSON responses with unusal/custom MIME types are handled correctly.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(JSON_TEXT_MIME_URL);
+ info("Starting test... ");
+
+ let { document, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+ "GET", CONTENT_TYPE_SJS + "?fmt=json-text-mime", {
+ status: 200,
+ statusText: "OK",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 41),
+ time: true
+ });
+
+ let onEvent = monitor.panelWin.once(EVENTS.RESPONSE_BODY_DISPLAYED);
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+ yield onEvent;
+
+ testResponseTab();
+
+ yield teardown(monitor);
+
+ function testResponseTab() {
+ let tabEl = document.querySelectorAll("#details-pane tab")[3];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[3];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The response tab in the network details pane should be selected.");
+
+ is(tabpanel.querySelector("#response-content-info-header")
+ .hasAttribute("hidden"), true,
+ "The response info header doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-json-box")
+ .hasAttribute("hidden"), false,
+ "The response content json box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-textarea-box")
+ .hasAttribute("hidden"), true,
+ "The response content textarea box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-image-box")
+ .hasAttribute("hidden"), true,
+ "The response content image box doesn't have the intended visibility.");
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
+ "There should be 1 json scope displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-property").length, 2,
+ "There should be 2 json properties displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+ "The empty notice should not be displayed in this tabpanel.");
+
+ let jsonScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+ is(jsonScope.querySelectorAll(".variables-view-property .name")[0]
+ .getAttribute("value"),
+ "greeting", "The first json property name was incorrect.");
+ is(jsonScope.querySelectorAll(".variables-view-property .value")[0]
+ .getAttribute("value"),
+ "\"Hello third-party JSON!\"", "The first json property value was incorrect.");
+
+ is(jsonScope.querySelectorAll(".variables-view-property .name")[1]
+ .getAttribute("value"),
+ "__proto__", "The second json property name was incorrect.");
+ is(jsonScope.querySelectorAll(".variables-view-property .value")[1]
+ .getAttribute("value"),
+ "Object", "The second json property value was incorrect.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_jsonp.js b/devtools/client/netmonitor/test/browser_net_jsonp.js
new file mode 100644
index 000000000..3007d8c4d
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_jsonp.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if JSONP responses are handled correctly.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(JSONP_URL);
+ info("Starting test... ");
+
+ let { document, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+ NetworkDetails._json.lazyEmpty = false;
+
+ let wait = waitForNetworkEvents(monitor, 2);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+ "GET", CONTENT_TYPE_SJS + "?fmt=jsonp&jsonp=$_0123Fun", {
+ status: 200,
+ statusText: "OK",
+ type: "json",
+ fullMimeType: "text/json; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 41),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(1),
+ "GET", CONTENT_TYPE_SJS + "?fmt=jsonp2&jsonp=$_4567Sad", {
+ status: 200,
+ statusText: "OK",
+ type: "json",
+ fullMimeType: "text/json; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 54),
+ time: true
+ });
+
+ let onEvent = monitor.panelWin.once(EVENTS.RESPONSE_BODY_DISPLAYED);
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+ yield onEvent;
+
+ testResponseTab("$_0123Fun", "\"Hello JSONP!\"");
+
+ onEvent = monitor.panelWin.once(EVENTS.RESPONSE_BODY_DISPLAYED);
+ RequestsMenu.selectedIndex = 1;
+ yield onEvent;
+
+ testResponseTab("$_4567Sad", "\"Hello weird JSONP!\"");
+
+ yield teardown(monitor);
+
+ function testResponseTab(func, greeting) {
+ let tabEl = document.querySelectorAll("#details-pane tab")[3];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[3];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The response tab in the network details pane should be selected.");
+
+ is(tabpanel.querySelector("#response-content-info-header")
+ .hasAttribute("hidden"), true,
+ "The response info header doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-json-box")
+ .hasAttribute("hidden"), false,
+ "The response content json box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-textarea-box")
+ .hasAttribute("hidden"), true,
+ "The response content textarea box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#response-content-image-box")
+ .hasAttribute("hidden"), true,
+ "The response content image box doesn't have the intended visibility.");
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
+ "There should be 1 json scope displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-property").length, 2,
+ "There should be 2 json properties displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+ "The empty notice should not be displayed in this tabpanel.");
+
+ let jsonScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+
+ is(jsonScope.querySelector(".name").getAttribute("value"),
+ L10N.getFormatStr("jsonpScopeName", func),
+ "The json scope doesn't have the correct title.");
+
+ is(jsonScope.querySelectorAll(".variables-view-property .name")[0]
+ .getAttribute("value"),
+ "greeting", "The first json property name was incorrect.");
+ is(jsonScope.querySelectorAll(".variables-view-property .value")[0]
+ .getAttribute("value"),
+ greeting, "The first json property value was incorrect.");
+
+ is(jsonScope.querySelectorAll(".variables-view-property .name")[1]
+ .getAttribute("value"),
+ "__proto__", "The second json property name was incorrect.");
+ is(jsonScope.querySelectorAll(".variables-view-property .value")[1]
+ .getAttribute("value"),
+ "Object", "The second json property value was incorrect.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_large-response.js b/devtools/client/netmonitor/test/browser_net_large-response.js
new file mode 100644
index 000000000..98d67b46d
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_large-response.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if very large response contents are just displayed as plain text.
+ */
+
+const HTML_LONG_URL = CONTENT_TYPE_SJS + "?fmt=html-long";
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ info("Starting test... ");
+
+ // This test could potentially be slow because over 100 KB of stuff
+ // is going to be requested and displayed in the source editor.
+ requestLongerTimeout(2);
+
+ let { document, EVENTS, Editor, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, HTML_LONG_URL, function* (url) {
+ content.wrappedJSObject.performRequests(1, url);
+ });
+ yield wait;
+
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+ "GET", CONTENT_TYPE_SJS + "?fmt=html-long", {
+ status: 200,
+ statusText: "OK"
+ });
+
+ let onEvent = monitor.panelWin.once(EVENTS.RESPONSE_BODY_DISPLAYED);
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+ yield onEvent;
+
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+ ok(editor.getText().match(/^<p>/),
+ "The text shown in the source editor is incorrect.");
+ is(editor.getMode(), Editor.modes.text,
+ "The mode active in the source editor is incorrect.");
+
+ yield teardown(monitor);
+
+ // This test uses a lot of memory, so force a GC to help fragmentation.
+ info("Forcing GC after netmonitor test.");
+ Cu.forceGC();
+});
diff --git a/devtools/client/netmonitor/test/browser_net_leak_on_tab_close.js b/devtools/client/netmonitor/test/browser_net_leak_on_tab_close.js
new file mode 100644
index 000000000..9e788f36c
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_leak_on_tab_close.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that netmonitor doesn't leak windows on parent-side pages (bug 1285638)
+ */
+
+add_task(function* () {
+ // Tell initNetMonitor to enable cache. Otherwise it will assert that there were more
+ // than zero network requests during the page load. But when loading about:config,
+ // there are none.
+ let { monitor } = yield initNetMonitor("about:config", null, true);
+ ok(monitor, "The network monitor was opened");
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_open_request_in_tab.js b/devtools/client/netmonitor/test/browser_net_open_request_in_tab.js
new file mode 100644
index 000000000..8e7ffcc84
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_open_request_in_tab.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if Open in new tab works.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ info("Starting test...");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests(1);
+ });
+ yield wait;
+
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+ RequestsMenu.selectedItem = requestItem;
+
+ let onTabOpen = once(gBrowser.tabContainer, "TabOpen", false);
+ RequestsMenu.contextMenu.openRequestInTab();
+ yield onTabOpen;
+
+ ok(true, "A new tab has been opened");
+
+ yield teardown(monitor);
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/netmonitor/test/browser_net_page-nav.js b/devtools/client/netmonitor/test/browser_net_page-nav.js
new file mode 100644
index 000000000..6ac18297c
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_page-nav.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if page navigation ("close", "navigate", etc.) triggers an appropriate
+ * action in the network monitor.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { EVENTS } = monitor.panelWin;
+
+ yield testNavigate();
+ yield testNavigateBack();
+ yield testClose();
+
+ function* testNavigate() {
+ info("Navigating forward...");
+
+ let onWillNav = monitor.panelWin.once(EVENTS.TARGET_WILL_NAVIGATE);
+ let onDidNav = monitor.panelWin.once(EVENTS.TARGET_DID_NAVIGATE);
+
+ tab.linkedBrowser.loadURI(NAVIGATE_URL);
+ yield onWillNav;
+
+ is(tab.linkedBrowser.currentURI.spec, SIMPLE_URL,
+ "Target started navigating to the correct location.");
+
+ yield onDidNav;
+ is(tab.linkedBrowser.currentURI.spec, NAVIGATE_URL,
+ "Target finished navigating to the correct location.");
+ }
+
+ function* testNavigateBack() {
+ info("Navigating backward...");
+
+ let onWillNav = monitor.panelWin.once(EVENTS.TARGET_WILL_NAVIGATE);
+ let onDidNav = monitor.panelWin.once(EVENTS.TARGET_DID_NAVIGATE);
+
+ tab.linkedBrowser.loadURI(SIMPLE_URL);
+ yield onWillNav;
+
+ is(tab.linkedBrowser.currentURI.spec, NAVIGATE_URL,
+ "Target started navigating back to the previous location.");
+
+ yield onDidNav;
+ is(tab.linkedBrowser.currentURI.spec, SIMPLE_URL,
+ "Target finished navigating back to the previous location.");
+ }
+
+ function* testClose() {
+ info("Closing...");
+
+ let onDestroyed = monitor.once("destroyed");
+ removeTab(tab);
+ yield onDestroyed;
+
+ ok(!monitor._controller.client,
+ "There shouldn't be a client available after destruction.");
+ ok(!monitor._controller.tabClient,
+ "There shouldn't be a tabClient available after destruction.");
+ ok(!monitor._controller.webConsoleClient,
+ "There shouldn't be a webConsoleClient available after destruction.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_pane-collapse.js b/devtools/client/netmonitor/test/browser_net_pane-collapse.js
new file mode 100644
index 000000000..2760ec000
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_pane-collapse.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the network monitor panes collapse properly.
+ */
+
+add_task(function* () {
+ let { monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { document, Prefs, NetMonitorView } = monitor.panelWin;
+ let detailsPane = document.getElementById("details-pane");
+ let detailsPaneToggleButton = document.getElementById("details-pane-toggle");
+
+ ok(detailsPane.classList.contains("pane-collapsed") &&
+ detailsPaneToggleButton.classList.contains("pane-collapsed"),
+ "The details pane should initially be hidden.");
+
+ NetMonitorView.toggleDetailsPane({ visible: true, animated: false });
+
+ let width = ~~(detailsPane.getAttribute("width"));
+ is(width, Prefs.networkDetailsWidth,
+ "The details pane has an incorrect width.");
+ is(detailsPane.style.marginLeft, "0px",
+ "The details pane has an incorrect left margin.");
+ is(detailsPane.style.marginRight, "0px",
+ "The details pane has an incorrect right margin.");
+ ok(!detailsPane.hasAttribute("animated"),
+ "The details pane has an incorrect animated attribute.");
+ ok(!detailsPane.classList.contains("pane-collapsed") &&
+ !detailsPaneToggleButton.classList.contains("pane-collapsed"),
+ "The details pane should at this point be visible.");
+
+ // Trigger reflow to make sure the UI is in required state.
+ document.documentElement.getBoundingClientRect();
+
+ NetMonitorView.toggleDetailsPane({ visible: false, animated: true });
+
+ yield once(detailsPane, "transitionend");
+
+ let margin = -(width + 1) + "px";
+ is(width, Prefs.networkDetailsWidth,
+ "The details pane has an incorrect width after collapsing.");
+ is(detailsPane.style.marginLeft, margin,
+ "The details pane has an incorrect left margin after collapsing.");
+ is(detailsPane.style.marginRight, margin,
+ "The details pane has an incorrect right margin after collapsing.");
+ ok(!detailsPane.hasAttribute("animated"),
+ "The details pane has an incorrect attribute after an animated collapsing.");
+ ok(detailsPane.classList.contains("pane-collapsed") &&
+ detailsPaneToggleButton.classList.contains("pane-collapsed"),
+ "The details pane should not be visible after collapsing.");
+
+ NetMonitorView.toggleDetailsPane({ visible: true, animated: false });
+
+ is(width, Prefs.networkDetailsWidth,
+ "The details pane has an incorrect width after uncollapsing.");
+ is(detailsPane.style.marginLeft, "0px",
+ "The details pane has an incorrect left margin after uncollapsing.");
+ is(detailsPane.style.marginRight, "0px",
+ "The details pane has an incorrect right margin after uncollapsing.");
+ ok(!detailsPane.hasAttribute("animated"),
+ "The details pane has an incorrect attribute after an unanimated uncollapsing.");
+ ok(!detailsPane.classList.contains("pane-collapsed") &&
+ !detailsPaneToggleButton.classList.contains("pane-collapsed"),
+ "The details pane should be visible again after uncollapsing.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_pane-toggle.js b/devtools/client/netmonitor/test/browser_net_pane-toggle.js
new file mode 100644
index 000000000..87b71019c
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_pane-toggle.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if toggling the details pane works as expected.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { $, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+ let { NETWORK_EVENT, TAB_UPDATED } = monitor.panelWin.EVENTS;
+ RequestsMenu.lazyUpdate = false;
+
+ let toggleButton = $("#details-pane-toggle");
+
+ is(toggleButton.hasAttribute("disabled"), true,
+ "The pane toggle button should be disabled when the frontend is opened.");
+ is(toggleButton.classList.contains("pane-collapsed"), true,
+ "The pane toggle button should indicate that the details pane is " +
+ "collapsed when the frontend is opened.");
+ is(NetMonitorView.detailsPaneHidden, true,
+ "The details pane should be hidden when the frontend is opened.");
+ is(RequestsMenu.selectedItem, null,
+ "There should be no selected item in the requests menu.");
+
+ let networkEvent = monitor.panelWin.once(NETWORK_EVENT);
+ tab.linkedBrowser.reload();
+ yield networkEvent;
+
+ is(toggleButton.hasAttribute("disabled"), false,
+ "The pane toggle button should be enabled after the first request.");
+ is(toggleButton.classList.contains("pane-collapsed"), true,
+ "The pane toggle button should still indicate that the details pane is " +
+ "collapsed after the first request.");
+ is(NetMonitorView.detailsPaneHidden, true,
+ "The details pane should still be hidden after the first request.");
+ is(RequestsMenu.selectedItem, null,
+ "There should still be no selected item in the requests menu.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, toggleButton);
+
+ yield monitor.panelWin.once(TAB_UPDATED);
+
+ is(toggleButton.hasAttribute("disabled"), false,
+ "The pane toggle button should still be enabled after being pressed.");
+ is(toggleButton.classList.contains("pane-collapsed"), false,
+ "The pane toggle button should now indicate that the details pane is " +
+ "not collapsed anymore after being pressed.");
+ is(NetMonitorView.detailsPaneHidden, false,
+ "The details pane should not be hidden after toggle button was pressed.");
+ isnot(RequestsMenu.selectedItem, null,
+ "There should be a selected item in the requests menu.");
+ is(RequestsMenu.selectedIndex, 0,
+ "The first item should be selected in the requests menu.");
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, toggleButton);
+
+ is(toggleButton.hasAttribute("disabled"), false,
+ "The pane toggle button should still be enabled after being pressed again.");
+ is(toggleButton.classList.contains("pane-collapsed"), true,
+ "The pane toggle button should now indicate that the details pane is " +
+ "collapsed after being pressed again.");
+ is(NetMonitorView.detailsPaneHidden, true,
+ "The details pane should now be hidden after the toggle button was pressed again.");
+ is(RequestsMenu.selectedItem, null,
+ "There should now be no selected item in the requests menu.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_persistent_logs.js b/devtools/client/netmonitor/test/browser_net_persistent_logs.js
new file mode 100644
index 000000000..ac2371e1f
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_persistent_logs.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the network monitor leaks on initialization and sudden destruction.
+ * You can also use this initialization format as a template for other tests.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(SINGLE_GET_URL);
+ info("Starting test... ");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ Services.prefs.setBoolPref("devtools.webconsole.persistlog", false);
+
+ yield reloadAndWait();
+
+ is(RequestsMenu.itemCount, 2,
+ "The request menu should have two items at this point.");
+
+ yield reloadAndWait();
+
+ // Since the reload clears the log, we still expect two requests in the log
+ is(RequestsMenu.itemCount, 2,
+ "The request menu should still have two items at this point.");
+
+ // Now we toggle the persistence logs on
+ Services.prefs.setBoolPref("devtools.webconsole.persistlog", true);
+
+ yield reloadAndWait();
+
+ // Since we togged the persistence logs, we expect four items after the reload
+ is(RequestsMenu.itemCount, 4,
+ "The request menu should now have four items at this point.");
+
+ Services.prefs.setBoolPref("devtools.webconsole.persistlog", false);
+ return teardown(monitor);
+
+ /**
+ * Reload the page and wait for 2 GET requests. Race-free.
+ */
+ function reloadAndWait() {
+ let wait = waitForNetworkEvents(monitor, 2);
+ tab.linkedBrowser.reload();
+ return wait;
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_post-data-01.js b/devtools/client/netmonitor/test/browser_net_post-data-01.js
new file mode 100644
index 000000000..6d5f8dc1b
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_post-data-01.js
@@ -0,0 +1,166 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the POST requests display the correct information in the UI.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(POST_DATA_URL);
+ info("Starting test... ");
+
+ let { document, EVENTS, Editor, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+ NetworkDetails._params.lazyEmpty = false;
+
+ let wait = waitForNetworkEvents(monitor, 0, 2);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+ "POST", SIMPLE_SJS + "?foo=bar&baz=42&type=urlencoded", {
+ status: 200,
+ statusText: "Och Aye",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(1),
+ "POST", SIMPLE_SJS + "?foo=bar&baz=42&type=multipart", {
+ status: 200,
+ statusText: "Och Aye",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12),
+ time: true
+ });
+
+ let onEvent = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[2]);
+ yield onEvent;
+ yield testParamsTab("urlencoded");
+
+ onEvent = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+ RequestsMenu.selectedIndex = 1;
+ yield onEvent;
+ yield testParamsTab("multipart");
+
+ return teardown(monitor);
+
+ function* testParamsTab(type) {
+ let tabEl = document.querySelectorAll("#details-pane tab")[2];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The params tab in the network details pane should be selected.");
+
+ function checkVisibility(box) {
+ is(tabpanel.querySelector("#request-params-box")
+ .hasAttribute("hidden"), !box.includes("params"),
+ "The request params box doesn't have the indended visibility.");
+ is(tabpanel.querySelector("#request-post-data-textarea-box")
+ .hasAttribute("hidden"), !box.includes("textarea"),
+ "The request post data textarea box doesn't have the indended visibility.");
+ }
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 2,
+ "There should be 2 param scopes displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+ "The empty notice should not be displayed in this tabpanel.");
+
+ let queryScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+ let postScope = tabpanel.querySelectorAll(".variables-view-scope")[1];
+
+ is(queryScope.querySelector(".name").getAttribute("value"),
+ L10N.getStr("paramsQueryString"),
+ "The query scope doesn't have the correct title.");
+
+ is(postScope.querySelector(".name").getAttribute("value"),
+ L10N.getStr(type == "urlencoded" ? "paramsFormData" : "paramsPostPayload"),
+ "The post scope doesn't have the correct title.");
+
+ is(queryScope.querySelectorAll(".variables-view-variable .name")[0]
+ .getAttribute("value"),
+ "foo", "The first query param name was incorrect.");
+ is(queryScope.querySelectorAll(".variables-view-variable .value")[0]
+ .getAttribute("value"),
+ "\"bar\"", "The first query param value was incorrect.");
+ is(queryScope.querySelectorAll(".variables-view-variable .name")[1]
+ .getAttribute("value"),
+ "baz", "The second query param name was incorrect.");
+ is(queryScope.querySelectorAll(".variables-view-variable .value")[1]
+ .getAttribute("value"),
+ "\"42\"", "The second query param value was incorrect.");
+ is(queryScope.querySelectorAll(".variables-view-variable .name")[2]
+ .getAttribute("value"),
+ "type", "The third query param name was incorrect.");
+ is(queryScope.querySelectorAll(".variables-view-variable .value")[2]
+ .getAttribute("value"),
+ "\"" + type + "\"", "The third query param value was incorrect.");
+
+ if (type == "urlencoded") {
+ checkVisibility("params");
+
+ is(tabpanel.querySelectorAll(".variables-view-variable").length, 5,
+ "There should be 5 param values displayed in this tabpanel.");
+ is(queryScope.querySelectorAll(".variables-view-variable").length, 3,
+ "There should be 3 param values displayed in the query scope.");
+ is(postScope.querySelectorAll(".variables-view-variable").length, 2,
+ "There should be 2 param values displayed in the post scope.");
+
+ is(postScope.querySelectorAll(".variables-view-variable .name")[0]
+ .getAttribute("value"),
+ "foo", "The first post param name was incorrect.");
+ is(postScope.querySelectorAll(".variables-view-variable .value")[0]
+ .getAttribute("value"),
+ "\"bar\"", "The first post param value was incorrect.");
+ is(postScope.querySelectorAll(".variables-view-variable .name")[1]
+ .getAttribute("value"),
+ "baz", "The second post param name was incorrect.");
+ is(postScope.querySelectorAll(".variables-view-variable .value")[1]
+ .getAttribute("value"),
+ "\"123\"", "The second post param value was incorrect.");
+ } else {
+ checkVisibility("params textarea");
+
+ is(tabpanel.querySelectorAll(".variables-view-variable").length, 3,
+ "There should be 3 param values displayed in this tabpanel.");
+ is(queryScope.querySelectorAll(".variables-view-variable").length, 3,
+ "There should be 3 param values displayed in the query scope.");
+ is(postScope.querySelectorAll(".variables-view-variable").length, 0,
+ "There should be 0 param values displayed in the post scope.");
+
+ let editor = yield NetMonitorView.editor("#request-post-data-textarea");
+ let text = editor.getText();
+
+ ok(text.includes("Content-Disposition: form-data; name=\"text\""),
+ "The text shown in the source editor is incorrect (1.1).");
+ ok(text.includes("Content-Disposition: form-data; name=\"email\""),
+ "The text shown in the source editor is incorrect (2.1).");
+ ok(text.includes("Content-Disposition: form-data; name=\"range\""),
+ "The text shown in the source editor is incorrect (3.1).");
+ ok(text.includes("Content-Disposition: form-data; name=\"Custom field\""),
+ "The text shown in the source editor is incorrect (4.1).");
+ ok(text.includes("Some text..."),
+ "The text shown in the source editor is incorrect (2.2).");
+ ok(text.includes("42"),
+ "The text shown in the source editor is incorrect (3.2).");
+ ok(text.includes("Extra data"),
+ "The text shown in the source editor is incorrect (4.2).");
+ is(editor.getMode(), Editor.modes.text,
+ "The mode active in the source editor is incorrect.");
+ }
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_post-data-02.js b/devtools/client/netmonitor/test/browser_net_post-data-02.js
new file mode 100644
index 000000000..3cdd2f14a
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_post-data-02.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the POST requests display the correct information in the UI,
+ * for raw payloads with attached content-type headers.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(POST_RAW_URL);
+ info("Starting test... ");
+
+ let { document, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+ NetworkDetails._params.lazyEmpty = false;
+
+ let wait = waitForNetworkEvents(monitor, 0, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ let onEvent = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+ NetMonitorView.toggleDetailsPane({ visible: true }, 2);
+ RequestsMenu.selectedIndex = 0;
+ yield onEvent;
+
+ let tabEl = document.querySelectorAll("#event-details-pane tab")[2];
+ let tabpanel = document.querySelectorAll("#event-details-pane tabpanel")[2];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The params tab in the network details pane should be selected.");
+
+ is(tabpanel.querySelector("#request-params-box")
+ .hasAttribute("hidden"), false,
+ "The request params box doesn't have the indended visibility.");
+ is(tabpanel.querySelector("#request-post-data-textarea-box")
+ .hasAttribute("hidden"), true,
+ "The request post data textarea box doesn't have the indended visibility.");
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
+ "There should be 1 param scopes displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+ "The empty notice should not be displayed in this tabpanel.");
+
+ let postScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+ is(postScope.querySelector(".name").getAttribute("value"),
+ L10N.getStr("paramsFormData"),
+ "The post scope doesn't have the correct title.");
+
+ is(postScope.querySelectorAll(".variables-view-variable").length, 2,
+ "There should be 2 param values displayed in the post scope.");
+ is(postScope.querySelectorAll(".variables-view-variable .name")[0]
+ .getAttribute("value"),
+ "foo", "The first query param name was incorrect.");
+ is(postScope.querySelectorAll(".variables-view-variable .value")[0]
+ .getAttribute("value"),
+ "\"bar\"", "The first query param value was incorrect.");
+ is(postScope.querySelectorAll(".variables-view-variable .name")[1]
+ .getAttribute("value"),
+ "baz", "The second query param name was incorrect.");
+ is(postScope.querySelectorAll(".variables-view-variable .value")[1]
+ .getAttribute("value"),
+ "\"123\"", "The second query param value was incorrect.");
+
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_post-data-03.js b/devtools/client/netmonitor/test/browser_net_post-data-03.js
new file mode 100644
index 000000000..3433f89ce
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_post-data-03.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the POST requests display the correct information in the UI,
+ * for raw payloads with content-type headers attached to the upload stream.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(POST_RAW_WITH_HEADERS_URL);
+ info("Starting test... ");
+
+ let { document, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 0, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ let onEvent = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+ NetMonitorView.toggleDetailsPane({ visible: true });
+ RequestsMenu.selectedIndex = 0;
+ yield onEvent;
+
+ let tabEl = document.querySelectorAll("#details-pane tab")[0];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[0];
+ let requestFromUploadScope = tabpanel.querySelectorAll(".variables-view-scope")[2];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The headers tab in the network details pane should be selected.");
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 3,
+ "There should be 3 header scopes displayed in this tabpanel.");
+
+ is(requestFromUploadScope.querySelector(".name").getAttribute("value"),
+ L10N.getStr("requestHeadersFromUpload") + " (" +
+ L10N.getFormatStr("networkMenu.sizeKB", L10N.numberWithDecimals(74 / 1024, 3)) + ")",
+ "The request headers from upload scope doesn't have the correct title.");
+
+ is(requestFromUploadScope.querySelectorAll(".variables-view-variable").length, 2,
+ "There should be 2 headers displayed in the request headers from upload scope.");
+
+ is(requestFromUploadScope.querySelectorAll(".variables-view-variable .name")[0]
+ .getAttribute("value"),
+ "content-type", "The first request header name was incorrect.");
+ is(requestFromUploadScope.querySelectorAll(".variables-view-variable .value")[0]
+ .getAttribute("value"), "\"application/x-www-form-urlencoded\"",
+ "The first request header value was incorrect.");
+ is(requestFromUploadScope.querySelectorAll(".variables-view-variable .name")[1]
+ .getAttribute("value"),
+ "custom-header", "The second request header name was incorrect.");
+ is(requestFromUploadScope.querySelectorAll(".variables-view-variable .value")[1]
+ .getAttribute("value"),
+ "\"hello world!\"", "The second request header value was incorrect.");
+
+ onEvent = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[2]);
+ yield onEvent;
+
+ tabEl = document.querySelectorAll("#details-pane tab")[2];
+ tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
+ let formDataScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+
+ is(tab.getAttribute("selected"), "true",
+ "The response tab in the network details pane should be selected.");
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
+ "There should be 1 header scope displayed in this tabpanel.");
+
+ is(formDataScope.querySelector(".name").getAttribute("value"),
+ L10N.getStr("paramsFormData"),
+ "The form data scope doesn't have the correct title.");
+
+ is(formDataScope.querySelectorAll(".variables-view-variable").length, 2,
+ "There should be 2 payload values displayed in the form data scope.");
+
+ is(formDataScope.querySelectorAll(".variables-view-variable .name")[0]
+ .getAttribute("value"),
+ "foo", "The first payload param name was incorrect.");
+ is(formDataScope.querySelectorAll(".variables-view-variable .value")[0]
+ .getAttribute("value"),
+ "\"bar\"", "The first payload param value was incorrect.");
+ is(formDataScope.querySelectorAll(".variables-view-variable .name")[1]
+ .getAttribute("value"),
+ "baz", "The second payload param name was incorrect.");
+ is(formDataScope.querySelectorAll(".variables-view-variable .value")[1]
+ .getAttribute("value"),
+ "\"123\"", "The second payload param value was incorrect.");
+
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_post-data-04.js b/devtools/client/netmonitor/test/browser_net_post-data-04.js
new file mode 100644
index 000000000..565792287
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_post-data-04.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the POST requests display the correct information in the UI,
+ * for JSON payloads.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(POST_JSON_URL);
+ info("Starting test... ");
+
+ let { document, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+ NetworkDetails._params.lazyEmpty = false;
+
+ let wait = waitForNetworkEvents(monitor, 0, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ let onEvent = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+ NetMonitorView.toggleDetailsPane({ visible: true }, 2);
+ RequestsMenu.selectedIndex = 0;
+ yield onEvent;
+
+ let tabEl = document.querySelectorAll("#event-details-pane tab")[2];
+ let tabpanel = document.querySelectorAll("#event-details-pane tabpanel")[2];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The params tab in the network details pane should be selected.");
+
+ is(tabpanel.querySelector("#request-params-box")
+ .hasAttribute("hidden"), false,
+ "The request params box doesn't have the intended visibility.");
+ is(tabpanel.querySelector("#request-post-data-textarea-box")
+ .hasAttribute("hidden"), true,
+ "The request post data textarea box doesn't have the intended visibility.");
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
+ "There should be 1 param scopes displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+ "The empty notice should not be displayed in this tabpanel.");
+
+ let jsonScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+ is(jsonScope.querySelector(".name").getAttribute("value"),
+ L10N.getStr("jsonScopeName"),
+ "The JSON scope doesn't have the correct title.");
+
+ let valueScope = tabpanel.querySelector(
+ ".variables-view-scope > .variables-view-element-details");
+
+ is(valueScope.querySelectorAll(".variables-view-variable").length, 1,
+ "There should be 1 value displayed in the JSON scope.");
+ is(valueScope.querySelector(".variables-view-property .name")
+ .getAttribute("value"),
+ "a", "The JSON var name was incorrect.");
+ is(valueScope.querySelector(".variables-view-property .value")
+ .getAttribute("value"),
+ "1", "The JSON var value was incorrect.");
+
+ let detailsParent = valueScope.querySelector(".variables-view-property .name")
+ .closest(".variables-view-element-details");
+ is(detailsParent.hasAttribute("open"), true, "The JSON value must be visible");
+
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_prefs-and-l10n.js b/devtools/client/netmonitor/test/browser_net_prefs-and-l10n.js
new file mode 100644
index 000000000..e73f94d6d
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_prefs-and-l10n.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the preferences and localization objects work correctly.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ ok(monitor.panelWin.Prefs,
+ "Should have a preferences object available on the panel window.");
+
+ testL10N();
+ testPrefs();
+
+ return teardown(monitor);
+
+ function testL10N() {
+ is(typeof L10N.getStr("netmonitor.security.enabled"), "string",
+ "The getStr() method didn't return a valid string.");
+ is(typeof L10N.getFormatStr("networkMenu.totalMS", "foo"), "string",
+ "The getFormatStr() method didn't return a valid string.");
+ }
+
+ function testPrefs() {
+ let { Prefs } = monitor.panelWin;
+
+ is(Prefs.networkDetailsWidth,
+ Services.prefs.getIntPref("devtools.netmonitor.panes-network-details-width"),
+ "Getting a pref should work correctly.");
+
+ let previousValue = Prefs.networkDetailsWidth;
+ let bogusValue = ~~(Math.random() * 100);
+ Prefs.networkDetailsWidth = bogusValue;
+ is(Prefs.networkDetailsWidth,
+ Services.prefs.getIntPref("devtools.netmonitor.panes-network-details-width"),
+ "Getting a pref after it has been modified should work correctly.");
+ is(Prefs.networkDetailsWidth, bogusValue,
+ "The pref wasn't updated correctly in the preferences object.");
+
+ Prefs.networkDetailsWidth = previousValue;
+ is(Prefs.networkDetailsWidth,
+ Services.prefs.getIntPref("devtools.netmonitor.panes-network-details-width"),
+ "Getting a pref after it has been modified again should work correctly.");
+ is(Prefs.networkDetailsWidth, previousValue,
+ "The pref wasn't updated correctly again in the preferences object.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_prefs-reload.js b/devtools/client/netmonitor/test/browser_net_prefs-reload.js
new file mode 100644
index 000000000..ee56ee446
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_prefs-reload.js
@@ -0,0 +1,215 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the prefs that should survive across tool reloads work.
+ */
+
+add_task(function* () {
+ let Actions = require("devtools/client/netmonitor/actions/index");
+ let { monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ // This test reopens the network monitor a bunch of times, for different
+ // hosts (bottom, side, window). This seems to be slow on debug builds.
+ requestLongerTimeout(3);
+
+ // Use these getters instead of caching instances inside the panel win,
+ // since the tool is reopened a bunch of times during this test
+ // and the instances will differ.
+ let getView = () => monitor.panelWin.NetMonitorView;
+ let getStore = () => monitor.panelWin.gStore;
+
+ let prefsToCheck = {
+ filters: {
+ // A custom new value to be used for the verified preference.
+ newValue: ["html", "css"],
+ // Getter used to retrieve the current value from the frontend, in order
+ // to verify that the pref was applied properly.
+ validateValue: ($) => getView().RequestsMenu._activeFilters,
+ // Predicate used to modify the frontend when setting the new pref value,
+ // before trying to validate the changes.
+ modifyFrontend: ($, value) => value.forEach(e =>
+ getStore().dispatch(Actions.toggleFilterType(e)))
+ },
+ networkDetailsWidth: {
+ newValue: ~~(Math.random() * 200 + 100),
+ validateValue: ($) => ~~$("#details-pane").getAttribute("width"),
+ modifyFrontend: ($, value) => $("#details-pane").setAttribute("width", value)
+ },
+ networkDetailsHeight: {
+ newValue: ~~(Math.random() * 300 + 100),
+ validateValue: ($) => ~~$("#details-pane").getAttribute("height"),
+ modifyFrontend: ($, value) => $("#details-pane").setAttribute("height", value)
+ }
+ /* add more prefs here... */
+ };
+
+ yield testBottom();
+ yield testSide();
+ yield testWindow();
+
+ info("Moving toolbox back to the bottom...");
+ yield monitor._toolbox.switchHost(Toolbox.HostType.BOTTOM);
+ return teardown(monitor);
+
+ function storeFirstPrefValues() {
+ info("Caching initial pref values.");
+
+ for (let name in prefsToCheck) {
+ let currentValue = monitor.panelWin.Prefs[name];
+ prefsToCheck[name].firstValue = currentValue;
+ }
+ }
+
+ function validateFirstPrefValues() {
+ info("Validating current pref values to the UI elements.");
+
+ for (let name in prefsToCheck) {
+ let currentValue = monitor.panelWin.Prefs[name];
+ let firstValue = prefsToCheck[name].firstValue;
+ let validateValue = prefsToCheck[name].validateValue;
+
+ is(currentValue.toSource(), firstValue.toSource(),
+ "Pref " + name + " should be equal to first value: " + firstValue);
+ is(currentValue.toSource(), validateValue(monitor.panelWin.$).toSource(),
+ "Pref " + name + " should validate: " + currentValue);
+ }
+ }
+
+ function modifyFrontend() {
+ info("Modifying UI elements to the specified new values.");
+
+ for (let name in prefsToCheck) {
+ let currentValue = monitor.panelWin.Prefs[name];
+ let firstValue = prefsToCheck[name].firstValue;
+ let newValue = prefsToCheck[name].newValue;
+ let validateValue = prefsToCheck[name].validateValue;
+ let modFrontend = prefsToCheck[name].modifyFrontend;
+
+ modFrontend(monitor.panelWin.$, newValue);
+ info("Modified UI element affecting " + name + " to: " + newValue);
+
+ is(currentValue.toSource(), firstValue.toSource(),
+ "Pref " + name + " should still be equal to first value: " + firstValue);
+ isnot(currentValue.toSource(), newValue.toSource(),
+ "Pref " + name + " should't yet be equal to second value: " + newValue);
+ is(newValue.toSource(), validateValue(monitor.panelWin.$).toSource(),
+ "The UI element affecting " + name + " should validate: " + newValue);
+ }
+ }
+
+ function validateNewPrefValues() {
+ info("Invalidating old pref values to the modified UI elements.");
+
+ for (let name in prefsToCheck) {
+ let currentValue = monitor.panelWin.Prefs[name];
+ let firstValue = prefsToCheck[name].firstValue;
+ let newValue = prefsToCheck[name].newValue;
+ let validateValue = prefsToCheck[name].validateValue;
+
+ isnot(currentValue.toSource(), firstValue.toSource(),
+ "Pref " + name + " should't be equal to first value: " + firstValue);
+ is(currentValue.toSource(), newValue.toSource(),
+ "Pref " + name + " should now be equal to second value: " + newValue);
+ is(newValue.toSource(), validateValue(monitor.panelWin.$).toSource(),
+ "The UI element affecting " + name + " should validate: " + newValue);
+ }
+ }
+
+ function resetFrontend() {
+ info("Resetting UI elements to the cached initial pref values.");
+
+ for (let name in prefsToCheck) {
+ let currentValue = monitor.panelWin.Prefs[name];
+ let firstValue = prefsToCheck[name].firstValue;
+ let newValue = prefsToCheck[name].newValue;
+ let validateValue = prefsToCheck[name].validateValue;
+ let modFrontend = prefsToCheck[name].modifyFrontend;
+
+ modFrontend(monitor.panelWin.$, firstValue);
+ info("Modified UI element affecting " + name + " to: " + firstValue);
+
+ isnot(currentValue.toSource(), firstValue.toSource(),
+ "Pref " + name + " should't yet be equal to first value: " + firstValue);
+ is(currentValue.toSource(), newValue.toSource(),
+ "Pref " + name + " should still be equal to second value: " + newValue);
+ is(firstValue.toSource(), validateValue(monitor.panelWin.$).toSource(),
+ "The UI element affecting " + name + " should validate: " + firstValue);
+ }
+ }
+
+ function* testBottom() {
+ info("Testing prefs reload for a bottom host.");
+ storeFirstPrefValues();
+
+ // Validate and modify while toolbox is on the bottom.
+ validateFirstPrefValues();
+ modifyFrontend();
+
+ let newMonitor = yield restartNetMonitor(monitor);
+ monitor = newMonitor.monitor;
+
+ // Revalidate and reset frontend while toolbox is on the bottom.
+ validateNewPrefValues();
+ resetFrontend();
+
+ newMonitor = yield restartNetMonitor(monitor);
+ monitor = newMonitor.monitor;
+
+ // Revalidate.
+ validateFirstPrefValues();
+ }
+
+ function* testSide() {
+ info("Moving toolbox to the side...");
+
+ yield monitor._toolbox.switchHost(Toolbox.HostType.SIDE);
+ info("Testing prefs reload for a side host.");
+ storeFirstPrefValues();
+
+ // Validate and modify frontend while toolbox is on the side.
+ validateFirstPrefValues();
+ modifyFrontend();
+
+ let newMonitor = yield restartNetMonitor(monitor);
+ monitor = newMonitor.monitor;
+
+ // Revalidate and reset frontend while toolbox is on the side.
+ validateNewPrefValues();
+ resetFrontend();
+
+ newMonitor = yield restartNetMonitor(monitor);
+ monitor = newMonitor.monitor;
+
+ // Revalidate.
+ validateFirstPrefValues();
+ }
+
+ function* testWindow() {
+ info("Moving toolbox into a window...");
+
+ yield monitor._toolbox.switchHost(Toolbox.HostType.WINDOW);
+ info("Testing prefs reload for a window host.");
+ storeFirstPrefValues();
+
+ // Validate and modify frontend while toolbox is in a window.
+ validateFirstPrefValues();
+ modifyFrontend();
+
+ let newMonitor = yield restartNetMonitor(monitor);
+ monitor = newMonitor.monitor;
+
+ // Revalidate and reset frontend while toolbox is in a window.
+ validateNewPrefValues();
+ resetFrontend();
+
+ newMonitor = yield restartNetMonitor(monitor);
+ monitor = newMonitor.monitor;
+
+ // Revalidate.
+ validateFirstPrefValues();
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_raw_headers.js b/devtools/client/netmonitor/test/browser_net_raw_headers.js
new file mode 100644
index 000000000..2cb734745
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_raw_headers.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if showing raw headers works.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(POST_DATA_URL);
+ info("Starting test... ");
+
+ let { document, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 0, 2);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ let origItem = RequestsMenu.getItemAtIndex(0);
+
+ let onTabEvent = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+ RequestsMenu.selectedItem = origItem;
+ yield onTabEvent;
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ document.getElementById("toggle-raw-headers"));
+
+ testShowRawHeaders(origItem.attachment);
+
+ EventUtils.sendMouseEvent({ type: "click" },
+ document.getElementById("toggle-raw-headers"));
+
+ testHideRawHeaders(document);
+
+ return teardown(monitor);
+
+ /*
+ * Tests that raw headers were displayed correctly
+ */
+ function testShowRawHeaders(data) {
+ let requestHeaders = document.getElementById("raw-request-headers-textarea").value;
+ for (let header of data.requestHeaders.headers) {
+ ok(requestHeaders.indexOf(header.name + ": " + header.value) >= 0,
+ "textarea contains request headers");
+ }
+ let responseHeaders = document.getElementById("raw-response-headers-textarea").value;
+ for (let header of data.responseHeaders.headers) {
+ ok(responseHeaders.indexOf(header.name + ": " + header.value) >= 0,
+ "textarea contains response headers");
+ }
+ }
+
+ /*
+ * Tests that raw headers textareas are hidden and empty
+ */
+ function testHideRawHeaders() {
+ let rawHeadersHidden = document.getElementById("raw-headers").getAttribute("hidden");
+ let requestTextarea = document.getElementById("raw-request-headers-textarea");
+ let responseTextarea = document.getElementById("raw-response-headers-textarea");
+ ok(rawHeadersHidden, "raw headers textareas are hidden");
+ ok(requestTextarea.value == "", "raw request headers textarea is empty");
+ ok(responseTextarea.value == "", "raw response headers textarea is empty");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_reload-button.js b/devtools/client/netmonitor/test/browser_net_reload-button.js
new file mode 100644
index 000000000..e91de8302
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_reload-button.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the empty-requests reload button works.
+ */
+
+add_task(function* () {
+ let { monitor } = yield initNetMonitor(SINGLE_GET_URL);
+ info("Starting test... ");
+
+ let { document, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ let wait = waitForNetworkEvents(monitor, 2);
+ let button = document.querySelector("#requests-menu-reload-notice-button");
+ button.click();
+ yield wait;
+
+ is(RequestsMenu.itemCount, 2, "The request menu should have two items after reloading");
+
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_reload-markers.js b/devtools/client/netmonitor/test/browser_net_reload-markers.js
new file mode 100644
index 000000000..26866830f
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_reload-markers.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the empty-requests reload button works.
+ */
+
+add_task(function* () {
+ let { monitor } = yield initNetMonitor(SINGLE_GET_URL);
+ info("Starting test... ");
+
+ let { document, EVENTS } = monitor.panelWin;
+ let button = document.querySelector("#requests-menu-reload-notice-button");
+ button.click();
+
+ let markers = [];
+
+ monitor.panelWin.on(EVENTS.TIMELINE_EVENT, (_, marker) => {
+ markers.push(marker);
+ });
+
+ yield waitForNetworkEvents(monitor, 2);
+ yield waitUntil(() => markers.length == 2);
+
+ ok(true, "Reloading finished");
+
+ is(markers[0].name, "document::DOMContentLoaded",
+ "The first received marker is correct.");
+ is(markers[1].name, "document::Load",
+ "The second received marker is correct.");
+
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_req-resp-bodies.js b/devtools/client/netmonitor/test/browser_net_req-resp-bodies.js
new file mode 100644
index 000000000..71a913501
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_req-resp-bodies.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test if request and response body logging stays on after opening the console.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(JSON_LONG_URL);
+ info("Starting test... ");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ // Perform first batch of requests.
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ verifyRequest(0);
+
+ // Switch to the webconsole.
+ let onWebConsole = monitor._toolbox.once("webconsole-selected");
+ monitor._toolbox.selectTool("webconsole");
+ yield onWebConsole;
+
+ // Switch back to the netmonitor.
+ let onNetMonitor = monitor._toolbox.once("netmonitor-selected");
+ monitor._toolbox.selectTool("netmonitor");
+ yield onNetMonitor;
+
+ // Reload debugee.
+ wait = waitForNetworkEvents(monitor, 1);
+ tab.linkedBrowser.reload();
+ yield wait;
+
+ // Perform another batch of requests.
+ wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ verifyRequest(1);
+
+ return teardown(monitor);
+
+ function verifyRequest(offset) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(offset),
+ "GET", CONTENT_TYPE_SJS + "?fmt=json-long", {
+ status: 200,
+ statusText: "OK",
+ type: "json",
+ fullMimeType: "text/json; charset=utf-8",
+ size: L10N.getFormatStr("networkMenu.sizeKB",
+ L10N.numberWithDecimals(85975 / 1024, 2)),
+ time: true
+ });
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_resend.js b/devtools/client/netmonitor/test/browser_net_resend.js
new file mode 100644
index 000000000..7b540ec50
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_resend.js
@@ -0,0 +1,174 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if resending a request works.
+ */
+
+const ADD_QUERY = "t1=t2";
+const ADD_HEADER = "Test-header: true";
+const ADD_UA_HEADER = "User-Agent: Custom-Agent";
+const ADD_POSTDATA = "&t3=t4";
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(POST_DATA_URL);
+ info("Starting test... ");
+
+ let { panelWin } = monitor;
+ let { document, EVENTS, NetMonitorView } = panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 0, 2);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ let origItem = RequestsMenu.getItemAtIndex(0);
+
+ let onTabUpdated = panelWin.once(EVENTS.TAB_UPDATED);
+ RequestsMenu.selectedItem = origItem;
+ yield onTabUpdated;
+
+ // add a new custom request cloned from selected request
+ let onPopulated = panelWin.once(EVENTS.CUSTOMREQUESTVIEW_POPULATED);
+ RequestsMenu.cloneSelectedRequest();
+ yield onPopulated;
+
+ testCustomForm(origItem.attachment);
+
+ let customItem = RequestsMenu.selectedItem;
+ testCustomItem(customItem, origItem);
+
+ // edit the custom request
+ yield editCustomForm();
+ testCustomItemChanged(customItem, origItem);
+
+ // send the new request
+ wait = waitForNetworkEvents(monitor, 0, 1);
+ RequestsMenu.sendCustomRequest();
+ yield wait;
+
+ let sentItem = RequestsMenu.selectedItem;
+ testSentRequest(sentItem.attachment, origItem.attachment);
+
+ return teardown(monitor);
+
+ function testCustomItem(item, orig) {
+ let method = item.target.querySelector(".requests-menu-method").value;
+ let origMethod = orig.target.querySelector(".requests-menu-method").value;
+ is(method, origMethod, "menu item is showing the same method as original request");
+
+ let file = item.target.querySelector(".requests-menu-file").value;
+ let origFile = orig.target.querySelector(".requests-menu-file").value;
+ is(file, origFile, "menu item is showing the same file name as original request");
+
+ let domain = item.target.querySelector(".requests-menu-domain").value;
+ let origDomain = orig.target.querySelector(".requests-menu-domain").value;
+ is(domain, origDomain, "menu item is showing the same domain as original request");
+ }
+
+ function testCustomItemChanged(item, orig) {
+ let file = item.target.querySelector(".requests-menu-file").value;
+ let expectedFile = orig.target.querySelector(".requests-menu-file").value +
+ "&" + ADD_QUERY;
+
+ is(file, expectedFile, "menu item is updated to reflect url entered in form");
+ }
+
+ /*
+ * Test that the New Request form was populated correctly
+ */
+ function testCustomForm(data) {
+ is(document.getElementById("custom-method-value").value, data.method,
+ "new request form showing correct method");
+
+ is(document.getElementById("custom-url-value").value, data.url,
+ "new request form showing correct url");
+
+ let query = document.getElementById("custom-query-value");
+ is(query.value, "foo=bar\nbaz=42\ntype=urlencoded",
+ "new request form showing correct query string");
+
+ let headers = document.getElementById("custom-headers-value").value.split("\n");
+ for (let {name, value} of data.requestHeaders.headers) {
+ ok(headers.indexOf(name + ": " + value) >= 0, "form contains header from request");
+ }
+
+ let postData = document.getElementById("custom-postdata-value");
+ is(postData.value, data.requestPostData.postData.text,
+ "new request form showing correct post data");
+ }
+
+ /*
+ * Add some params and headers to the request form
+ */
+ function* editCustomForm() {
+ panelWin.focus();
+
+ let query = document.getElementById("custom-query-value");
+ let queryFocus = once(query, "focus", false);
+ // Bug 1195825: Due to some unexplained dark-matter with promise,
+ // focus only works if delayed by one tick.
+ query.setSelectionRange(query.value.length, query.value.length);
+ executeSoon(() => query.focus());
+ yield queryFocus;
+
+ // add params to url query string field
+ type(["VK_RETURN"]);
+ type(ADD_QUERY);
+
+ let headers = document.getElementById("custom-headers-value");
+ let headersFocus = once(headers, "focus", false);
+ headers.setSelectionRange(headers.value.length, headers.value.length);
+ headers.focus();
+ yield headersFocus;
+
+ // add a header
+ type(["VK_RETURN"]);
+ type(ADD_HEADER);
+
+ // add a User-Agent header, to check if default headers can be modified
+ // (there will be two of them, first gets overwritten by the second)
+ type(["VK_RETURN"]);
+ type(ADD_UA_HEADER);
+
+ let postData = document.getElementById("custom-postdata-value");
+ let postFocus = once(postData, "focus", false);
+ postData.setSelectionRange(postData.value.length, postData.value.length);
+ postData.focus();
+ yield postFocus;
+
+ // add to POST data
+ type(ADD_POSTDATA);
+ }
+
+ /*
+ * Make sure newly created event matches expected request
+ */
+ function testSentRequest(data, origData) {
+ is(data.method, origData.method, "correct method in sent request");
+ is(data.url, origData.url + "&" + ADD_QUERY, "correct url in sent request");
+
+ let { headers } = data.requestHeaders;
+ let hasHeader = headers.some(h => `${h.name}: ${h.value}` == ADD_HEADER);
+ ok(hasHeader, "new header added to sent request");
+
+ let hasUAHeader = headers.some(h => `${h.name}: ${h.value}` == ADD_UA_HEADER);
+ ok(hasUAHeader, "User-Agent header added to sent request");
+
+ is(data.requestPostData.postData.text,
+ origData.requestPostData.postData.text + ADD_POSTDATA,
+ "post data added to sent request");
+ }
+
+ function type(string) {
+ for (let ch of string) {
+ EventUtils.synthesizeKey(ch, {}, panelWin);
+ }
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_resend_cors.js b/devtools/client/netmonitor/test/browser_net_resend_cors.js
new file mode 100644
index 000000000..d63c3b54e
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_resend_cors.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if resending a CORS request avoids the security checks and doesn't send
+ * a preflight OPTIONS request (bug 1270096 and friends)
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CORS_URL);
+ info("Starting test... ");
+
+ let { EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let requestUrl = "http://test1.example.com" + CORS_SJS_PATH;
+
+ info("Waiting for OPTIONS, then POST");
+ let wait = waitForNetworkEvents(monitor, 1, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, requestUrl, function* (url) {
+ content.wrappedJSObject.performRequests(url, "triggering/preflight", "post-data");
+ });
+ yield wait;
+
+ const METHODS = ["OPTIONS", "POST"];
+
+ // Check the requests that were sent
+ for (let [i, method] of METHODS.entries()) {
+ let { attachment } = RequestsMenu.getItemAtIndex(i);
+ is(attachment.method, method, `The ${method} request has the right method`);
+ is(attachment.url, requestUrl, `The ${method} request has the right URL`);
+ }
+
+ // Resend both requests without modification. Wait for resent OPTIONS, then POST.
+ // POST is supposed to have no preflight OPTIONS request this time (CORS is disabled)
+ let onRequests = waitForNetworkEvents(monitor, 1, 1);
+ for (let [i, method] of METHODS.entries()) {
+ let item = RequestsMenu.getItemAtIndex(i);
+
+ info(`Selecting the ${method} request (at index ${i})`);
+ let onUpdate = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+ RequestsMenu.selectedItem = item;
+ yield onUpdate;
+
+ info("Cloning the selected request into a custom clone");
+ let onPopulate = monitor.panelWin.once(EVENTS.CUSTOMREQUESTVIEW_POPULATED);
+ RequestsMenu.cloneSelectedRequest();
+ yield onPopulate;
+
+ info("Sending the cloned request (without change)");
+ RequestsMenu.sendCustomRequest();
+ }
+
+ info("Waiting for both resent requests");
+ yield onRequests;
+
+ // Check the resent requests
+ for (let [i, method] of METHODS.entries()) {
+ let index = i + 2;
+ let item = RequestsMenu.getItemAtIndex(index).attachment;
+ is(item.method, method, `The ${method} request has the right method`);
+ is(item.url, requestUrl, `The ${method} request has the right URL`);
+ is(item.status, 200, `The ${method} response has the right status`);
+
+ if (method === "POST") {
+ is(item.requestPostData.postData.text, "post-data",
+ "The POST request has the right POST data");
+ // eslint-disable-next-line mozilla/no-cpows-in-tests
+ is(item.responseContent.content.text, "Access-Control-Allow-Origin: *",
+ "The POST response has the right content");
+ }
+ }
+
+ info("Finishing the test");
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_resend_headers.js b/devtools/client/netmonitor/test/browser_net_resend_headers.js
new file mode 100644
index 000000000..0503817e3
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_resend_headers.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test if custom request headers are not ignored (bug 1270096 and friends)
+ */
+
+add_task(function* () {
+ let { monitor } = yield initNetMonitor(SIMPLE_SJS);
+ info("Starting test... ");
+
+ let { NetMonitorView, NetMonitorController } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let requestUrl = SIMPLE_SJS;
+ let requestHeaders = [
+ { name: "Host", value: "fakehost.example.com" },
+ { name: "User-Agent", value: "Testzilla" },
+ { name: "Referer", value: "http://example.com/referrer" },
+ { name: "Accept", value: "application/jarda"},
+ { name: "Accept-Encoding", value: "compress, identity, funcoding" },
+ { name: "Accept-Language", value: "cs-CZ" }
+ ];
+
+ let wait = waitForNetworkEvents(monitor, 0, 1);
+ NetMonitorController.webConsoleClient.sendHTTPRequest({
+ url: requestUrl,
+ method: "POST",
+ headers: requestHeaders,
+ body: "Hello"
+ });
+ yield wait;
+
+ let { attachment } = RequestsMenu.getItemAtIndex(0);
+ is(attachment.method, "POST", "The request has the right method");
+ is(attachment.url, requestUrl, "The request has the right URL");
+
+ for (let { name, value } of attachment.requestHeaders.headers) {
+ info(`Request header: ${name}: ${value}`);
+ }
+
+ function hasRequestHeader(name, value) {
+ let { headers } = attachment.requestHeaders;
+ return headers.some(h => h.name === name && h.value === value);
+ }
+
+ function hasNotRequestHeader(name) {
+ let { headers } = attachment.requestHeaders;
+ return headers.every(h => h.name !== name);
+ }
+
+ for (let { name, value } of requestHeaders) {
+ ok(hasRequestHeader(name, value), `The ${name} header has the right value`);
+ }
+
+ // Check that the Cookie header was not added silently (i.e., that the request is
+ // anonymous.
+ for (let name of ["Cookie"]) {
+ ok(hasNotRequestHeader(name), `The ${name} header is not present`);
+ }
+
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_security-details.js b/devtools/client/netmonitor/test/browser_net_security-details.js
new file mode 100644
index 000000000..0a83b3ed9
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_security-details.js
@@ -0,0 +1,102 @@
+/* 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 Security details tab contains the expected data.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ let { $, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ info("Performing a secure request.");
+ const REQUESTS_URL = "https://example.com" + CORS_SJS_PATH;
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, REQUESTS_URL, function* (url) {
+ content.wrappedJSObject.performRequests(1, url);
+ });
+ yield wait;
+
+ info("Selecting the request.");
+ RequestsMenu.selectedIndex = 0;
+
+ info("Waiting for details pane to be updated.");
+ yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
+
+ info("Selecting security tab.");
+ NetworkDetails.widget.selectedIndex = 5;
+
+ info("Waiting for security tab to be updated.");
+ yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
+
+ let errorbox = $("#security-error");
+ let infobox = $("#security-information");
+
+ is(errorbox.hidden, true, "Error box is hidden.");
+ is(infobox.hidden, false, "Information box visible.");
+
+ // Connection
+
+ // The protocol will be TLS but the exact version depends on which protocol
+ // the test server example.com supports.
+ let protocol = $("#security-protocol-version-value").value;
+ ok(protocol.startsWith("TLS"), "The protocol " + protocol + " seems valid.");
+
+ // The cipher suite used by the test server example.com might change at any
+ // moment but all of them should start with "TLS_".
+ // http://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml
+ let suite = $("#security-ciphersuite-value").value;
+ ok(suite.startsWith("TLS_"), "The suite " + suite + " seems valid.");
+
+ // Host
+ checkLabel("#security-info-host-header", "Host example.com:");
+ checkLabel("#security-http-strict-transport-security-value", "Disabled");
+ checkLabel("#security-public-key-pinning-value", "Disabled");
+
+ // Cert
+ checkLabel("#security-cert-subject-cn", "example.com");
+ checkLabel("#security-cert-subject-o", "<Not Available>");
+ checkLabel("#security-cert-subject-ou", "<Not Available>");
+
+ checkLabel("#security-cert-issuer-cn", "Temporary Certificate Authority");
+ checkLabel("#security-cert-issuer-o", "Mozilla Testing");
+ checkLabel("#security-cert-issuer-ou", "<Not Available>");
+
+ // Locale sensitive and varies between timezones. Cant't compare equality or
+ // the test fails depending on which part of the world the test is executed.
+ checkLabelNotEmpty("#security-cert-validity-begins");
+ checkLabelNotEmpty("#security-cert-validity-expires");
+
+ checkLabelNotEmpty("#security-cert-sha1-fingerprint");
+ checkLabelNotEmpty("#security-cert-sha256-fingerprint");
+ yield teardown(monitor);
+
+ /**
+ * A helper that compares value attribute of a label with given selector to the
+ * expected value.
+ */
+ function checkLabel(selector, expected) {
+ info("Checking label " + selector);
+
+ let element = $(selector);
+
+ ok(element, "Selector matched an element.");
+ is(element.value, expected, "Label has the expected value.");
+ }
+
+ /**
+ * A helper that checks the label with given selector is not an empty string.
+ */
+ function checkLabelNotEmpty(selector) {
+ info("Checking that label " + selector + " is non-empty.");
+
+ let element = $(selector);
+
+ ok(element, "Selector matched an element.");
+ isnot(element.value, "", "Label was not empty.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_security-error.js b/devtools/client/netmonitor/test/browser_net_security-error.js
new file mode 100644
index 000000000..f6b8b34f3
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_security-error.js
@@ -0,0 +1,70 @@
+/* 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 Security details tab shows an error message with broken connections.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ let { $, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ info("Requesting a resource that has a certificate problem.");
+
+ let wait = waitForSecurityBrokenNetworkEvent();
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests(1, "https://nocert.example.com");
+ });
+ yield wait;
+
+ info("Selecting the request.");
+ RequestsMenu.selectedIndex = 0;
+
+ info("Waiting for details pane to be updated.");
+ yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
+
+ info("Selecting security tab.");
+ NetworkDetails.widget.selectedIndex = 5;
+
+ info("Waiting for security tab to be updated.");
+ yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
+
+ let errorbox = $("#security-error");
+ let errormsg = $("#security-error-message");
+ let infobox = $("#security-information");
+
+ is(errorbox.hidden, false, "Error box is visble.");
+ is(infobox.hidden, true, "Information box is hidden.");
+
+ isnot(errormsg.value, "", "Error message is not empty.");
+
+ return teardown(monitor);
+
+ /**
+ * Returns a promise that's resolved once a request with security issues is
+ * completed.
+ */
+ function waitForSecurityBrokenNetworkEvent() {
+ let awaitedEvents = [
+ "UPDATING_REQUEST_HEADERS",
+ "RECEIVED_REQUEST_HEADERS",
+ "UPDATING_REQUEST_COOKIES",
+ "RECEIVED_REQUEST_COOKIES",
+ "STARTED_RECEIVING_RESPONSE",
+ "UPDATING_RESPONSE_CONTENT",
+ "RECEIVED_RESPONSE_CONTENT",
+ "UPDATING_EVENT_TIMINGS",
+ "RECEIVED_EVENT_TIMINGS",
+ ];
+
+ let promises = awaitedEvents.map((event) => {
+ return monitor.panelWin.once(EVENTS[event]);
+ });
+
+ return Promise.all(promises);
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_security-icon-click.js b/devtools/client/netmonitor/test/browser_net_security-icon-click.js
new file mode 100644
index 000000000..2385b11aa
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_security-icon-click.js
@@ -0,0 +1,57 @@
+/* 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 clicking on the security indicator opens the security details tab.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ let { $, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ info("Requesting a resource over HTTPS.");
+ yield performRequestAndWait("https://example.com" + CORS_SJS_PATH + "?request_2");
+ yield performRequestAndWait("https://example.com" + CORS_SJS_PATH + "?request_1");
+
+ is(RequestsMenu.itemCount, 2, "Two events event logged.");
+
+ yield clickAndTestSecurityIcon();
+
+ info("Selecting headers panel again.");
+ NetworkDetails.widget.selectedIndex = 0;
+ yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
+
+ info("Sorting the items by filename.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-file-button"));
+
+ info("Testing that security icon can be clicked after the items were sorted.");
+ yield clickAndTestSecurityIcon();
+
+ return teardown(monitor);
+
+ function* performRequestAndWait(url) {
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, { url }, function* (args) {
+ content.wrappedJSObject.performRequests(1, args.url);
+ });
+ return wait;
+ }
+
+ function* clickAndTestSecurityIcon() {
+ let item = RequestsMenu.items[0];
+ let icon = $(".requests-security-state-icon", item.target);
+
+ info("Clicking security icon of the first request and waiting for the " +
+ "panel to update.");
+
+ icon.click();
+ yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
+
+ is(NetworkDetails.widget.selectedPanel, $("#security-tabpanel"),
+ "Security tab is selected.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_security-redirect.js b/devtools/client/netmonitor/test/browser_net_security-redirect.js
new file mode 100644
index 000000000..5f2956dbb
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_security-redirect.js
@@ -0,0 +1,38 @@
+/* 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 a http -> https redirect shows secure icon only for redirected https
+ * request.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ let { $, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 2);
+ yield ContentTask.spawn(tab.linkedBrowser, HTTPS_REDIRECT_SJS, function* (url) {
+ content.wrappedJSObject.performRequests(1, url);
+ });
+ yield wait;
+
+ is(RequestsMenu.itemCount, 2, "There were two requests due to redirect.");
+
+ let initial = RequestsMenu.items[0];
+ let redirect = RequestsMenu.items[1];
+
+ let initialSecurityIcon = $(".requests-security-state-icon", initial.target);
+ let redirectSecurityIcon = $(".requests-security-state-icon", redirect.target);
+
+ ok(initialSecurityIcon.classList.contains("security-state-insecure"),
+ "Initial request was marked insecure.");
+
+ ok(redirectSecurityIcon.classList.contains("security-state-secure"),
+ "Redirected request was marked secure.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_security-state.js b/devtools/client/netmonitor/test/browser_net_security-state.js
new file mode 100644
index 000000000..054e7c969
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_security-state.js
@@ -0,0 +1,119 @@
+/* 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 correct security state indicator appears depending on the security
+ * state.
+ */
+
+add_task(function* () {
+ const EXPECTED_SECURITY_STATES = {
+ "test1.example.com": "security-state-insecure",
+ "example.com": "security-state-secure",
+ "nocert.example.com": "security-state-broken",
+ "localhost": "security-state-local",
+ };
+
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ let { $, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ yield performRequests();
+
+ for (let item of RequestsMenu.items) {
+ let domain = $(".requests-menu-domain", item.target).value;
+
+ info("Found a request to " + domain);
+ ok(domain in EXPECTED_SECURITY_STATES, "Domain " + domain + " was expected.");
+
+ let classes = $(".requests-security-state-icon", item.target).classList;
+ let expectedClass = EXPECTED_SECURITY_STATES[domain];
+
+ info("Classes of security state icon are: " + classes);
+ info("Security state icon is expected to contain class: " + expectedClass);
+ ok(classes.contains(expectedClass), "Icon contained the correct class name.");
+ }
+
+ return teardown(monitor);
+
+ /**
+ * A helper that performs requests to
+ * - https://nocert.example.com (broken)
+ * - https://example.com (secure)
+ * - http://test1.example.com (insecure)
+ * - http://localhost (local)
+ * and waits until NetworkMonitor has handled all packets sent by the server.
+ */
+ function* performRequests() {
+ function executeRequests(count, url) {
+ return ContentTask.spawn(tab.linkedBrowser, {count, url}, function* (args) {
+ content.wrappedJSObject.performRequests(args.count, args.url);
+ });
+ }
+
+ // waitForNetworkEvents does not work for requests with security errors as
+ // those only emit 9/13 events of a successful request.
+ let done = waitForSecurityBrokenNetworkEvent();
+
+ info("Requesting a resource that has a certificate problem.");
+ yield executeRequests(1, "https://nocert.example.com");
+
+ // Wait for the request to complete before firing another request. Otherwise
+ // the request with security issues interfere with waitForNetworkEvents.
+ info("Waiting for request to complete.");
+ yield done;
+
+ // Next perform a request over HTTP. If done the other way around the latter
+ // occasionally hangs waiting for event timings that don't seem to appear...
+ done = waitForNetworkEvents(monitor, 1);
+ info("Requesting a resource over HTTP.");
+ yield executeRequests(1, "http://test1.example.com" + CORS_SJS_PATH);
+ yield done;
+
+ done = waitForNetworkEvents(monitor, 1);
+ info("Requesting a resource over HTTPS.");
+ yield executeRequests(1, "https://example.com" + CORS_SJS_PATH);
+ yield done;
+
+ done = waitForSecurityBrokenNetworkEvent(true);
+ info("Requesting a resource over HTTP to localhost.");
+ yield executeRequests(1, "http://localhost" + CORS_SJS_PATH);
+ yield done;
+
+ const expectedCount = Object.keys(EXPECTED_SECURITY_STATES).length;
+ is(RequestsMenu.itemCount, expectedCount, expectedCount + " events logged.");
+ }
+
+ /**
+ * Returns a promise that's resolved once a request with security issues is
+ * completed.
+ */
+ function waitForSecurityBrokenNetworkEvent(networkError) {
+ let awaitedEvents = [
+ "UPDATING_REQUEST_HEADERS",
+ "RECEIVED_REQUEST_HEADERS",
+ "UPDATING_REQUEST_COOKIES",
+ "RECEIVED_REQUEST_COOKIES",
+ "STARTED_RECEIVING_RESPONSE",
+ "UPDATING_RESPONSE_CONTENT",
+ "RECEIVED_RESPONSE_CONTENT",
+ "UPDATING_EVENT_TIMINGS",
+ "RECEIVED_EVENT_TIMINGS",
+ ];
+
+ // If the reason for breakage is a network error, then the
+ // STARTED_RECEIVING_RESPONSE event does not fire.
+ if (networkError) {
+ awaitedEvents = awaitedEvents.filter(e => e !== "STARTED_RECEIVING_RESPONSE");
+ }
+
+ let promises = awaitedEvents.map((event) => {
+ return monitor.panelWin.once(EVENTS[event]);
+ });
+
+ return Promise.all(promises);
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_security-tab-deselect.js b/devtools/client/netmonitor/test/browser_net_security-tab-deselect.js
new file mode 100644
index 000000000..4a2dd0885
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_security-tab-deselect.js
@@ -0,0 +1,46 @@
+/* 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 security details tab is no longer selected if an insecure request
+ * is selected.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ let { EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ info("Performing requests.");
+ let wait = waitForNetworkEvents(monitor, 2);
+ const REQUEST_URLS = [
+ "https://example.com" + CORS_SJS_PATH,
+ "http://example.com" + CORS_SJS_PATH,
+ ];
+ yield ContentTask.spawn(tab.linkedBrowser, REQUEST_URLS, function* (urls) {
+ for (let url of urls) {
+ content.wrappedJSObject.performRequests(1, url);
+ }
+ });
+ yield wait;
+
+ info("Selecting secure request.");
+ RequestsMenu.selectedIndex = 0;
+
+ info("Selecting security tab.");
+ NetworkDetails.widget.selectedIndex = 5;
+
+ info("Selecting insecure request.");
+ RequestsMenu.selectedIndex = 1;
+
+ info("Waiting for security tab to be updated.");
+ yield monitor.panelWin.once(EVENTS.NETWORKDETAILSVIEW_POPULATED);
+
+ is(NetworkDetails.widget.selectedIndex, 0,
+ "Selected tab was reset when selected security tab was hidden.");
+
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_security-tab-visibility.js b/devtools/client/netmonitor/test/browser_net_security-tab-visibility.js
new file mode 100644
index 000000000..b6685d7fe
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_security-tab-visibility.js
@@ -0,0 +1,121 @@
+/* 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 security details tab is visible only when it should.
+ */
+
+add_task(function* () {
+ const TEST_DATA = [
+ {
+ desc: "http request",
+ uri: "http://example.com" + CORS_SJS_PATH,
+ visibleOnNewEvent: false,
+ visibleOnSecurityInfo: false,
+ visibleOnceComplete: false,
+ }, {
+ desc: "working https request",
+ uri: "https://example.com" + CORS_SJS_PATH,
+ visibleOnNewEvent: false,
+ visibleOnSecurityInfo: true,
+ visibleOnceComplete: true,
+ }, {
+ desc: "broken https request",
+ uri: "https://nocert.example.com",
+ isBroken: true,
+ visibleOnNewEvent: false,
+ visibleOnSecurityInfo: true,
+ visibleOnceComplete: true,
+ }
+ ];
+
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ let { $, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ for (let testcase of TEST_DATA) {
+ info("Testing Security tab visibility for " + testcase.desc);
+ let onNewItem = monitor.panelWin.once(EVENTS.NETWORK_EVENT);
+ let onSecurityInfo = monitor.panelWin.once(EVENTS.RECEIVED_SECURITY_INFO);
+ let onComplete = testcase.isBroken ?
+ waitForSecurityBrokenNetworkEvent() :
+ waitForNetworkEvents(monitor, 1);
+
+ let tabEl = $("#security-tab");
+ let tabpanel = $("#security-tabpanel");
+
+ info("Performing a request to " + testcase.uri);
+ yield ContentTask.spawn(tab.linkedBrowser, testcase.uri, function* (url) {
+ content.wrappedJSObject.performRequests(1, url);
+ });
+
+ info("Waiting for new network event.");
+ yield onNewItem;
+
+ info("Selecting the request.");
+ RequestsMenu.selectedIndex = 0;
+
+ is(RequestsMenu.selectedItem.attachment.securityState, undefined,
+ "Security state has not yet arrived.");
+ is(tabEl.hidden, !testcase.visibleOnNewEvent,
+ "Security tab is " +
+ (testcase.visibleOnNewEvent ? "visible" : "hidden") +
+ " after new request was added to the menu.");
+ is(tabpanel.hidden, false,
+ "#security-tabpanel is visible after new request was added to the menu.");
+
+ info("Waiting for security information to arrive.");
+ yield onSecurityInfo;
+
+ ok(RequestsMenu.selectedItem.attachment.securityState,
+ "Security state arrived.");
+ is(tabEl.hidden, !testcase.visibleOnSecurityInfo,
+ "Security tab is " +
+ (testcase.visibleOnSecurityInfo ? "visible" : "hidden") +
+ " after security information arrived.");
+ is(tabpanel.hidden, false,
+ "#security-tabpanel is visible after security information arrived.");
+
+ info("Waiting for request to complete.");
+ yield onComplete;
+
+ is(tabEl.hidden, !testcase.visibleOnceComplete,
+ "Security tab is " +
+ (testcase.visibleOnceComplete ? "visible" : "hidden") +
+ " after request has been completed.");
+ is(tabpanel.hidden, false,
+ "#security-tabpanel is visible after request is complete.");
+
+ info("Clearing requests.");
+ RequestsMenu.clear();
+ }
+
+ return teardown(monitor);
+
+ /**
+ * Returns a promise that's resolved once a request with security issues is
+ * completed.
+ */
+ function waitForSecurityBrokenNetworkEvent() {
+ let awaitedEvents = [
+ "UPDATING_REQUEST_HEADERS",
+ "RECEIVED_REQUEST_HEADERS",
+ "UPDATING_REQUEST_COOKIES",
+ "RECEIVED_REQUEST_COOKIES",
+ "STARTED_RECEIVING_RESPONSE",
+ "UPDATING_RESPONSE_CONTENT",
+ "RECEIVED_RESPONSE_CONTENT",
+ "UPDATING_EVENT_TIMINGS",
+ "RECEIVED_EVENT_TIMINGS",
+ ];
+
+ let promises = awaitedEvents.map((event) => {
+ return monitor.panelWin.once(EVENTS[event]);
+ });
+
+ return Promise.all(promises);
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_security-warnings.js b/devtools/client/netmonitor/test/browser_net_security-warnings.js
new file mode 100644
index 000000000..cdfee70a1
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_security-warnings.js
@@ -0,0 +1,56 @@
+/* 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 warning indicators are shown when appropriate.
+ */
+
+const TEST_CASES = [
+ {
+ desc: "no warnings",
+ uri: "https://example.com" + CORS_SJS_PATH,
+ warnCipher: false,
+ },
+];
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ let { $, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ let cipher = $("#security-warning-cipher");
+
+ for (let test of TEST_CASES) {
+ info("Testing site with " + test.desc);
+
+ info("Performing request to " + test.uri);
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, test.uri, function* (url) {
+ content.wrappedJSObject.performRequests(1, url);
+ });
+ yield wait;
+
+ info("Selecting the request.");
+ RequestsMenu.selectedIndex = 0;
+
+ info("Waiting for details pane to be updated.");
+ yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
+
+ if (NetworkDetails.widget.selectedIndex !== 5) {
+ info("Selecting security tab.");
+ NetworkDetails.widget.selectedIndex = 5;
+
+ info("Waiting for details pane to be updated.");
+ yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
+ }
+
+ is(cipher.hidden, !test.warnCipher, "Cipher suite warning is hidden.");
+
+ RequestsMenu.clear();
+ }
+
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_send-beacon-other-tab.js b/devtools/client/netmonitor/test/browser_net_send-beacon-other-tab.js
new file mode 100644
index 000000000..b425ad5ca
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_send-beacon-other-tab.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if beacons from other tabs are properly ignored.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
+ let { RequestsMenu } = monitor.panelWin.NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ let beaconTab = yield addTab(SEND_BEACON_URL);
+ info("Beacon tab added successfully.");
+
+ is(RequestsMenu.itemCount, 0, "The requests menu should be empty.");
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(beaconTab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequest();
+ });
+ tab.linkedBrowser.reload();
+ yield wait;
+
+ is(RequestsMenu.itemCount, 1, "Only the reload should be recorded.");
+ let request = RequestsMenu.getItemAtIndex(0);
+ is(request.attachment.method, "GET", "The method is correct.");
+ is(request.attachment.status, "200", "The status is correct.");
+
+ yield removeTab(beaconTab);
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_send-beacon.js b/devtools/client/netmonitor/test/browser_net_send-beacon.js
new file mode 100644
index 000000000..bdc30a960
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_send-beacon.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if beacons are handled correctly.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(SEND_BEACON_URL);
+ let { RequestsMenu } = monitor.panelWin.NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ is(RequestsMenu.itemCount, 0, "The requests menu should be empty.");
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequest();
+ });
+ yield wait;
+
+ is(RequestsMenu.itemCount, 1, "The beacon should be recorded.");
+ let request = RequestsMenu.getItemAtIndex(0);
+ is(request.attachment.method, "POST", "The method is correct.");
+ ok(request.attachment.url.endsWith("beacon_request"), "The URL is correct.");
+ is(request.attachment.status, "404", "The status is correct.");
+
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_service-worker-status.js b/devtools/client/netmonitor/test/browser_net_service-worker-status.js
new file mode 100644
index 000000000..d7ada1645
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_service-worker-status.js
@@ -0,0 +1,87 @@
+/* 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";
+
+/**
+ * Tests if requests intercepted by service workers have the correct status code
+ */
+
+// Service workers only work on https
+const URL = EXAMPLE_URL.replace("http:", "https:");
+
+const TEST_URL = URL + "service-workers/status-codes.html";
+
+add_task(function* () {
+ yield new Promise(done => {
+ let options = { "set": [
+ // Accept workers from mochitest's http.
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.openWindow.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]};
+ SpecialPowers.pushPrefEnv(options, done);
+ });
+
+ let { tab, monitor } = yield initNetMonitor(TEST_URL, null, true);
+ info("Starting test... ");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ const REQUEST_DATA = [
+ {
+ method: "GET",
+ uri: URL + "service-workers/test/200",
+ details: {
+ status: 200,
+ statusText: "OK (service worker)",
+ displayedStatus: "service worker",
+ type: "plain",
+ fullMimeType: "text/plain; charset=UTF-8"
+ },
+ stackFunctions: ["doXHR", "performRequests"]
+ },
+ ];
+
+ info("Registering the service worker...");
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ yield content.wrappedJSObject.registerServiceWorker();
+ });
+
+ info("Performing requests...");
+ let wait = waitForNetworkEvents(monitor, REQUEST_DATA.length);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ let index = 0;
+ for (let request of REQUEST_DATA) {
+ let item = RequestsMenu.getItemAtIndex(index);
+
+ info(`Verifying request #${index}`);
+ yield verifyRequestItemTarget(item, request.method, request.uri, request.details);
+
+ let { stacktrace } = item.attachment.cause;
+ let stackLen = stacktrace ? stacktrace.length : 0;
+
+ ok(stacktrace, `Request #${index} has a stacktrace`);
+ ok(stackLen >= request.stackFunctions.length,
+ `Request #${index} has a stacktrace with enough (${stackLen}) items`);
+
+ request.stackFunctions.forEach((functionName, j) => {
+ is(stacktrace[j].functionName, functionName,
+ `Request #${index} has the correct function at position #${j} on the stack`);
+ });
+
+ index++;
+ }
+
+ info("Unregistering the service worker...");
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ yield content.wrappedJSObject.unregisterServiceWorker();
+ });
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_simple-init.js b/devtools/client/netmonitor/test/browser_net_simple-init.js
new file mode 100644
index 000000000..19d05811c
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_simple-init.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Simple check if the network monitor starts up and shuts down properly.
+ */
+
+function test() {
+ // These test suite functions are removed from the global scope inside a
+ // cleanup function. However, we still need them.
+ let gInfo = info;
+ let gOk = ok;
+
+ initNetMonitor(SIMPLE_URL).then(({ tab, monitor }) => {
+ info("Starting test... ");
+
+ is(tab.linkedBrowser.currentURI.spec, SIMPLE_URL,
+ "The current tab's location is the correct one.");
+
+ function checkIfInitialized(tag) {
+ info(`Checking if initialization is ok (${tag}).`);
+
+ ok(monitor._view,
+ `The network monitor view object exists (${tag}).`);
+ ok(monitor._controller,
+ `The network monitor controller object exists (${tag}).`);
+ ok(monitor._controller._startup,
+ `The network monitor controller object exists and is initialized (${tag}).`);
+
+ ok(monitor.isReady,
+ `The network monitor panel appears to be ready (${tag}).`);
+
+ ok(monitor._controller.tabClient,
+ `There should be a tabClient available at this point (${tag}).`);
+ ok(monitor._controller.webConsoleClient,
+ `There should be a webConsoleClient available at this point (${tag}).`);
+ ok(monitor._controller.timelineFront,
+ `There should be a timelineFront available at this point (${tag}).`);
+ }
+
+ function checkIfDestroyed(tag) {
+ gInfo("Checking if destruction is ok.");
+
+ gOk(monitor._view,
+ `The network monitor view object still exists (${tag}).`);
+ gOk(monitor._controller,
+ `The network monitor controller object still exists (${tag}).`);
+ gOk(monitor._controller._shutdown,
+ `The network monitor controller object still exists and is destroyed (${tag}).`);
+
+ gOk(!monitor._controller.tabClient,
+ `There shouldn't be a tabClient available after destruction (${tag}).`);
+ gOk(!monitor._controller.webConsoleClient,
+ `There shouldn't be a webConsoleClient available after destruction (${tag}).`);
+ gOk(!monitor._controller.timelineFront,
+ `There shouldn't be a timelineFront available after destruction (${tag}).`);
+ }
+
+ executeSoon(() => {
+ checkIfInitialized(1);
+
+ monitor._controller.startupNetMonitor()
+ .then(() => {
+ info("Starting up again shouldn't do anything special.");
+ checkIfInitialized(2);
+ return monitor._controller.connect();
+ })
+ .then(() => {
+ info("Connecting again shouldn't do anything special.");
+ checkIfInitialized(3);
+ return teardown(monitor);
+ })
+ .then(finish);
+ });
+
+ registerCleanupFunction(() => {
+ checkIfDestroyed(1);
+
+ monitor._controller.shutdownNetMonitor()
+ .then(() => {
+ gInfo("Shutting down again shouldn't do anything special.");
+ checkIfDestroyed(2);
+ return monitor._controller.disconnect();
+ })
+ .then(() => {
+ gInfo("Disconnecting again shouldn't do anything special.");
+ checkIfDestroyed(3);
+ });
+ });
+ });
+}
diff --git a/devtools/client/netmonitor/test/browser_net_simple-request-data.js b/devtools/client/netmonitor/test/browser_net_simple-request-data.js
new file mode 100644
index 000000000..1b952bd71
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_simple-request-data.js
@@ -0,0 +1,247 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if requests render correct information in the menu UI.
+ */
+
+function test() {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ initNetMonitor(SIMPLE_SJS).then(({ tab, monitor }) => {
+ info("Starting test... ");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ waitForNetworkEvents(monitor, 1)
+ .then(() => teardown(monitor))
+ .then(finish);
+
+ monitor.panelWin.once(monitor.panelWin.EVENTS.NETWORK_EVENT, () => {
+ is(RequestsMenu.selectedItem, null,
+ "There shouldn't be any selected item in the requests menu.");
+ is(RequestsMenu.itemCount, 1,
+ "The requests menu should not be empty after the first request.");
+ is(NetMonitorView.detailsPaneHidden, true,
+ "The details pane should still be hidden after the first request.");
+
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+
+ is(typeof requestItem.value, "string",
+ "The attached request id is incorrect.");
+ isnot(requestItem.value, "",
+ "The attached request id should not be empty.");
+
+ is(typeof requestItem.attachment.startedDeltaMillis, "number",
+ "The attached startedDeltaMillis is incorrect.");
+ is(requestItem.attachment.startedDeltaMillis, 0,
+ "The attached startedDeltaMillis should be zero.");
+
+ is(typeof requestItem.attachment.startedMillis, "number",
+ "The attached startedMillis is incorrect.");
+ isnot(requestItem.attachment.startedMillis, 0,
+ "The attached startedMillis should not be zero.");
+
+ is(requestItem.attachment.requestHeaders, undefined,
+ "The requestHeaders should not yet be set.");
+ is(requestItem.attachment.requestCookies, undefined,
+ "The requestCookies should not yet be set.");
+ is(requestItem.attachment.requestPostData, undefined,
+ "The requestPostData should not yet be set.");
+
+ is(requestItem.attachment.responseHeaders, undefined,
+ "The responseHeaders should not yet be set.");
+ is(requestItem.attachment.responseCookies, undefined,
+ "The responseCookies should not yet be set.");
+
+ is(requestItem.attachment.httpVersion, undefined,
+ "The httpVersion should not yet be set.");
+ is(requestItem.attachment.status, undefined,
+ "The status should not yet be set.");
+ is(requestItem.attachment.statusText, undefined,
+ "The statusText should not yet be set.");
+
+ is(requestItem.attachment.headersSize, undefined,
+ "The headersSize should not yet be set.");
+ is(requestItem.attachment.transferredSize, undefined,
+ "The transferredSize should not yet be set.");
+ is(requestItem.attachment.contentSize, undefined,
+ "The contentSize should not yet be set.");
+
+ is(requestItem.attachment.mimeType, undefined,
+ "The mimeType should not yet be set.");
+ is(requestItem.attachment.responseContent, undefined,
+ "The responseContent should not yet be set.");
+
+ is(requestItem.attachment.totalTime, undefined,
+ "The totalTime should not yet be set.");
+ is(requestItem.attachment.eventTimings, undefined,
+ "The eventTimings should not yet be set.");
+
+ verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
+ });
+
+ monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_REQUEST_HEADERS, () => {
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+
+ ok(requestItem.attachment.requestHeaders,
+ "There should be a requestHeaders attachment available.");
+ is(requestItem.attachment.requestHeaders.headers.length, 9,
+ "The requestHeaders attachment has an incorrect |headers| property.");
+ isnot(requestItem.attachment.requestHeaders.headersSize, 0,
+ "The requestHeaders attachment has an incorrect |headersSize| property.");
+ // Can't test for the exact request headers size because the value may
+ // vary across platforms ("User-Agent" header differs).
+
+ verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
+ });
+
+ monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_REQUEST_COOKIES, () => {
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+
+ ok(requestItem.attachment.requestCookies,
+ "There should be a requestCookies attachment available.");
+ is(requestItem.attachment.requestCookies.cookies.length, 2,
+ "The requestCookies attachment has an incorrect |cookies| property.");
+
+ verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
+ });
+
+ monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_REQUEST_POST_DATA, () => {
+ ok(false, "Trap listener: this request doesn't have any post data.");
+ });
+
+ monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_RESPONSE_HEADERS, () => {
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+
+ ok(requestItem.attachment.responseHeaders,
+ "There should be a responseHeaders attachment available.");
+ is(requestItem.attachment.responseHeaders.headers.length, 10,
+ "The responseHeaders attachment has an incorrect |headers| property.");
+ is(requestItem.attachment.responseHeaders.headersSize, 330,
+ "The responseHeaders attachment has an incorrect |headersSize| property.");
+
+ verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
+ });
+
+ monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_RESPONSE_COOKIES, () => {
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+
+ ok(requestItem.attachment.responseCookies,
+ "There should be a responseCookies attachment available.");
+ is(requestItem.attachment.responseCookies.cookies.length, 2,
+ "The responseCookies attachment has an incorrect |cookies| property.");
+
+ verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
+ });
+
+ monitor.panelWin.once(monitor.panelWin.EVENTS.STARTED_RECEIVING_RESPONSE, () => {
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+
+ is(requestItem.attachment.httpVersion, "HTTP/1.1",
+ "The httpVersion attachment has an incorrect value.");
+ is(requestItem.attachment.status, "200",
+ "The status attachment has an incorrect value.");
+ is(requestItem.attachment.statusText, "Och Aye",
+ "The statusText attachment has an incorrect value.");
+ is(requestItem.attachment.headersSize, 330,
+ "The headersSize attachment has an incorrect value.");
+
+ verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS, {
+ status: "200",
+ statusText: "Och Aye"
+ });
+ });
+
+ monitor.panelWin.once(monitor.panelWin.EVENTS.UPDATING_RESPONSE_CONTENT, () => {
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+
+ is(requestItem.attachment.transferredSize, "12",
+ "The transferredSize attachment has an incorrect value.");
+ is(requestItem.attachment.contentSize, "12",
+ "The contentSize attachment has an incorrect value.");
+ is(requestItem.attachment.mimeType, "text/plain; charset=utf-8",
+ "The mimeType attachment has an incorrect value.");
+
+ verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS, {
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01),
+ });
+ });
+
+ monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_RESPONSE_CONTENT, () => {
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+
+ ok(requestItem.attachment.responseContent,
+ "There should be a responseContent attachment available.");
+ is(requestItem.attachment.responseContent.content.mimeType,
+ "text/plain; charset=utf-8",
+ "The responseContent attachment has an incorrect |content.mimeType| property.");
+ is(requestItem.attachment.responseContent.content.text,
+ "Hello world!",
+ "The responseContent attachment has an incorrect |content.text| property.");
+ is(requestItem.attachment.responseContent.content.size,
+ 12,
+ "The responseContent attachment has an incorrect |content.size| property.");
+
+ verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS, {
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01),
+ });
+ });
+
+ monitor.panelWin.once(monitor.panelWin.EVENTS.UPDATING_EVENT_TIMINGS, () => {
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+
+ is(typeof requestItem.attachment.totalTime, "number",
+ "The attached totalTime is incorrect.");
+ ok(requestItem.attachment.totalTime >= 0,
+ "The attached totalTime should be positive.");
+
+ is(typeof requestItem.attachment.endedMillis, "number",
+ "The attached endedMillis is incorrect.");
+ ok(requestItem.attachment.endedMillis >= 0,
+ "The attached endedMillis should be positive.");
+
+ verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS, {
+ time: true
+ });
+ });
+
+ monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_EVENT_TIMINGS, () => {
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+
+ ok(requestItem.attachment.eventTimings,
+ "There should be a eventTimings attachment available.");
+ is(typeof requestItem.attachment.eventTimings.timings.blocked, "number",
+ "The eventTimings attachment has an incorrect |timings.blocked| property.");
+ is(typeof requestItem.attachment.eventTimings.timings.dns, "number",
+ "The eventTimings attachment has an incorrect |timings.dns| property.");
+ is(typeof requestItem.attachment.eventTimings.timings.connect, "number",
+ "The eventTimings attachment has an incorrect |timings.connect| property.");
+ is(typeof requestItem.attachment.eventTimings.timings.send, "number",
+ "The eventTimings attachment has an incorrect |timings.send| property.");
+ is(typeof requestItem.attachment.eventTimings.timings.wait, "number",
+ "The eventTimings attachment has an incorrect |timings.wait| property.");
+ is(typeof requestItem.attachment.eventTimings.timings.receive, "number",
+ "The eventTimings attachment has an incorrect |timings.receive| property.");
+ is(typeof requestItem.attachment.eventTimings.totalTime, "number",
+ "The eventTimings attachment has an incorrect |totalTime| property.");
+
+ verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS, {
+ time: true
+ });
+ });
+
+ tab.linkedBrowser.reload();
+ });
+}
diff --git a/devtools/client/netmonitor/test/browser_net_simple-request-details.js b/devtools/client/netmonitor/test/browser_net_simple-request-details.js
new file mode 100644
index 000000000..6be634e68
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_simple-request-details.js
@@ -0,0 +1,261 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if requests render correct information in the details UI.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(SIMPLE_SJS);
+ info("Starting test... ");
+
+ let { document, EVENTS, Editor, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ tab.linkedBrowser.reload();
+ yield wait;
+
+ is(RequestsMenu.selectedItem, null,
+ "There shouldn't be any selected item in the requests menu.");
+ is(RequestsMenu.itemCount, 1,
+ "The requests menu should not be empty after the first request.");
+ is(NetMonitorView.detailsPaneHidden, true,
+ "The details pane should still be hidden after the first request.");
+
+ let onTabUpdated = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ yield onTabUpdated;
+
+ isnot(RequestsMenu.selectedItem, null,
+ "There should be a selected item in the requests menu.");
+ is(RequestsMenu.selectedIndex, 0,
+ "The first item should be selected in the requests menu.");
+ is(NetMonitorView.detailsPaneHidden, false,
+ "The details pane should not be hidden after toggle button was pressed.");
+
+ testHeadersTab();
+ yield testCookiesTab();
+ testParamsTab();
+ yield testResponseTab();
+ testTimingsTab();
+ return teardown(monitor);
+
+ function testHeadersTab() {
+ let tabEl = document.querySelectorAll("#details-pane tab")[0];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[0];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The headers tab in the network details pane should be selected.");
+
+ is(tabpanel.querySelector("#headers-summary-url-value").getAttribute("value"),
+ SIMPLE_SJS, "The url summary value is incorrect.");
+ is(tabpanel.querySelector("#headers-summary-url-value").getAttribute("tooltiptext"),
+ SIMPLE_SJS, "The url summary tooltiptext is incorrect.");
+ is(tabpanel.querySelector("#headers-summary-method-value").getAttribute("value"),
+ "GET", "The method summary value is incorrect.");
+ is(tabpanel.querySelector("#headers-summary-address-value").getAttribute("value"),
+ "127.0.0.1:8888", "The remote address summary value is incorrect.");
+ is(tabpanel.querySelector("#headers-summary-status-circle").getAttribute("code"),
+ "200", "The status summary code is incorrect.");
+ is(tabpanel.querySelector("#headers-summary-status-value").getAttribute("value"),
+ "200 Och Aye", "The status summary value is incorrect.");
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 2,
+ "There should be 2 header scopes displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variable-or-property").length, 19,
+ "There should be 19 header values displayed in this tabpanel.");
+
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+ "The empty notice should not be displayed in this tabpanel.");
+
+ let responseScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+ let requestScope = tabpanel.querySelectorAll(".variables-view-scope")[1];
+
+ is(responseScope.querySelector(".name").getAttribute("value"),
+ L10N.getStr("responseHeaders") + " (" +
+ L10N.getFormatStr("networkMenu.sizeKB",
+ L10N.numberWithDecimals(330 / 1024, 3)) + ")",
+ "The response headers scope doesn't have the correct title.");
+
+ ok(requestScope.querySelector(".name").getAttribute("value").includes(
+ L10N.getStr("requestHeaders") + " (0"),
+ "The request headers scope doesn't have the correct title.");
+ // Can't test for full request headers title because the size may
+ // vary across platforms ("User-Agent" header differs). We're pretty
+ // sure it's smaller than 1 MB though, so it starts with a 0.
+
+ is(responseScope.querySelectorAll(".variables-view-variable .name")[0]
+ .getAttribute("value"),
+ "Cache-Control", "The first response header name was incorrect.");
+ is(responseScope.querySelectorAll(".variables-view-variable .value")[0]
+ .getAttribute("value"),
+ "\"no-cache, no-store, must-revalidate\"",
+ "The first response header value was incorrect.");
+ is(responseScope.querySelectorAll(".variables-view-variable .name")[1]
+ .getAttribute("value"),
+ "Connection", "The second response header name was incorrect.");
+ is(responseScope.querySelectorAll(".variables-view-variable .value")[1]
+ .getAttribute("value"),
+ "\"close\"", "The second response header value was incorrect.");
+ is(responseScope.querySelectorAll(".variables-view-variable .name")[2]
+ .getAttribute("value"),
+ "Content-Length", "The third response header name was incorrect.");
+ is(responseScope.querySelectorAll(".variables-view-variable .value")[2]
+ .getAttribute("value"),
+ "\"12\"", "The third response header value was incorrect.");
+ is(responseScope.querySelectorAll(".variables-view-variable .name")[3]
+ .getAttribute("value"),
+ "Content-Type", "The fourth response header name was incorrect.");
+ is(responseScope.querySelectorAll(".variables-view-variable .value")[3]
+ .getAttribute("value"),
+ "\"text/plain; charset=utf-8\"", "The fourth response header value was incorrect.");
+ is(responseScope.querySelectorAll(".variables-view-variable .name")[9]
+ .getAttribute("value"),
+ "foo-bar", "The last response header name was incorrect.");
+ is(responseScope.querySelectorAll(".variables-view-variable .value")[9]
+ .getAttribute("value"),
+ "\"baz\"", "The last response header value was incorrect.");
+
+ is(requestScope.querySelectorAll(".variables-view-variable .name")[0]
+ .getAttribute("value"),
+ "Host", "The first request header name was incorrect.");
+ is(requestScope.querySelectorAll(".variables-view-variable .value")[0]
+ .getAttribute("value"),
+ "\"example.com\"", "The first request header value was incorrect.");
+ is(requestScope.querySelectorAll(".variables-view-variable .name")[6]
+ .getAttribute("value"),
+ "Connection", "The ante-penultimate request header name was incorrect.");
+ is(requestScope.querySelectorAll(".variables-view-variable .value")[6]
+ .getAttribute("value"),
+ "\"keep-alive\"", "The ante-penultimate request header value was incorrect.");
+ is(requestScope.querySelectorAll(".variables-view-variable .name")[7]
+ .getAttribute("value"),
+ "Pragma", "The penultimate request header name was incorrect.");
+ is(requestScope.querySelectorAll(".variables-view-variable .value")[7]
+ .getAttribute("value"),
+ "\"no-cache\"", "The penultimate request header value was incorrect.");
+ is(requestScope.querySelectorAll(".variables-view-variable .name")[8]
+ .getAttribute("value"),
+ "Cache-Control", "The last request header name was incorrect.");
+ is(requestScope.querySelectorAll(".variables-view-variable .value")[8]
+ .getAttribute("value"),
+ "\"no-cache\"", "The last request header value was incorrect.");
+ }
+
+ function* testCookiesTab() {
+ let onEvent = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[1]);
+ yield onEvent;
+
+ let tabEl = document.querySelectorAll("#details-pane tab")[1];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[1];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The cookies tab in the network details pane should be selected.");
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 2,
+ "There should be 2 cookie scopes displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variable-or-property").length, 6,
+ "There should be 6 cookie values displayed in this tabpanel.");
+ }
+
+ function testParamsTab() {
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[2]);
+
+ let tabEl = document.querySelectorAll("#details-pane tab")[2];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The params tab in the network details pane should be selected.");
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 0,
+ "There should be no param scopes displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variable-or-property").length, 0,
+ "There should be no param values displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 1,
+ "The empty notice should be displayed in this tabpanel.");
+
+ is(tabpanel.querySelector("#request-params-box")
+ .hasAttribute("hidden"), false,
+ "The request params box should not be hidden.");
+ is(tabpanel.querySelector("#request-post-data-textarea-box")
+ .hasAttribute("hidden"), true,
+ "The request post data textarea box should be hidden.");
+ }
+
+ function* testResponseTab() {
+ let onEvent = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+ yield onEvent;
+
+ let tabEl = document.querySelectorAll("#details-pane tab")[3];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[3];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The response tab in the network details pane should be selected.");
+
+ is(tabpanel.querySelector("#response-content-info-header")
+ .hasAttribute("hidden"), true,
+ "The response info header should be hidden.");
+ is(tabpanel.querySelector("#response-content-json-box")
+ .hasAttribute("hidden"), true,
+ "The response content json box should be hidden.");
+ is(tabpanel.querySelector("#response-content-textarea-box")
+ .hasAttribute("hidden"), false,
+ "The response content textarea box should not be hidden.");
+ is(tabpanel.querySelector("#response-content-image-box")
+ .hasAttribute("hidden"), true,
+ "The response content image box should be hidden.");
+
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+ is(editor.getText(), "Hello world!",
+ "The text shown in the source editor is incorrect.");
+ is(editor.getMode(), Editor.modes.text,
+ "The mode active in the source editor is incorrect.");
+ }
+
+ function testTimingsTab() {
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[4]);
+
+ let tabEl = document.querySelectorAll("#details-pane tab")[4];
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[4];
+
+ is(tabEl.getAttribute("selected"), "true",
+ "The timings tab in the network details pane should be selected.");
+
+ ok(tabpanel.querySelector("#timings-summary-blocked .requests-menu-timings-total")
+ .getAttribute("value").match(/[0-9]+/),
+ "The blocked timing info does not appear to be correct.");
+
+ ok(tabpanel.querySelector("#timings-summary-dns .requests-menu-timings-total")
+ .getAttribute("value").match(/[0-9]+/),
+ "The dns timing info does not appear to be correct.");
+
+ ok(tabpanel.querySelector("#timings-summary-connect .requests-menu-timings-total")
+ .getAttribute("value").match(/[0-9]+/),
+ "The connect timing info does not appear to be correct.");
+
+ ok(tabpanel.querySelector("#timings-summary-send .requests-menu-timings-total")
+ .getAttribute("value").match(/[0-9]+/),
+ "The send timing info does not appear to be correct.");
+
+ ok(tabpanel.querySelector("#timings-summary-wait .requests-menu-timings-total")
+ .getAttribute("value").match(/[0-9]+/),
+ "The wait timing info does not appear to be correct.");
+
+ ok(tabpanel.querySelector("#timings-summary-receive .requests-menu-timings-total")
+ .getAttribute("value").match(/[0-9]+/),
+ "The receive timing info does not appear to be correct.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_simple-request.js b/devtools/client/netmonitor/test/browser_net_simple-request.js
new file mode 100644
index 000000000..898cb3710
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_simple-request.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Test whether the UI state properly reflects existence of requests
+ * displayed in the Net panel. The following parts of the UI are
+ * tested:
+ * 1) Side panel visibility
+ * 2) Side panel toggle button
+ * 3) Empty user message visibility
+ * 4) Number of requests displayed
+ */
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { document, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ is(document.querySelector("#details-pane-toggle").hasAttribute("disabled"), true,
+ "The pane toggle button should be disabled when the frontend is opened.");
+ is(document.querySelector("#requests-menu-empty-notice").hasAttribute("hidden"), false,
+ "An empty notice should be displayed when the frontend is opened.");
+ is(RequestsMenu.itemCount, 0,
+ "The requests menu should be empty when the frontend is opened.");
+ is(NetMonitorView.detailsPaneHidden, true,
+ "The details pane should be hidden when the frontend is opened.");
+
+ yield reloadAndWait();
+
+ is(document.querySelector("#details-pane-toggle").hasAttribute("disabled"), false,
+ "The pane toggle button should be enabled after the first request.");
+ is(document.querySelector("#requests-menu-empty-notice").hasAttribute("hidden"), true,
+ "The empty notice should be hidden after the first request.");
+ is(RequestsMenu.itemCount, 1,
+ "The requests menu should not be empty after the first request.");
+ is(NetMonitorView.detailsPaneHidden, true,
+ "The details pane should still be hidden after the first request.");
+
+ yield reloadAndWait();
+
+ is(document.querySelector("#details-pane-toggle").hasAttribute("disabled"), false,
+ "The pane toggle button should be still be enabled after a reload.");
+ is(document.querySelector("#requests-menu-empty-notice").hasAttribute("hidden"), true,
+ "The empty notice should be still hidden after a reload.");
+ is(RequestsMenu.itemCount, 1,
+ "The requests menu should not be empty after a reload.");
+ is(NetMonitorView.detailsPaneHidden, true,
+ "The details pane should still be hidden after a reload.");
+
+ RequestsMenu.clear();
+
+ is(document.querySelector("#details-pane-toggle").hasAttribute("disabled"), true,
+ "The pane toggle button should be disabled when after clear.");
+ is(document.querySelector("#requests-menu-empty-notice").hasAttribute("hidden"), false,
+ "An empty notice should be displayed again after clear.");
+ is(RequestsMenu.itemCount, 0,
+ "The requests menu should be empty after clear.");
+ is(NetMonitorView.detailsPaneHidden, true,
+ "The details pane should be hidden after clear.");
+
+ return teardown(monitor);
+
+ function* reloadAndWait() {
+ let wait = waitForNetworkEvents(monitor, 1);
+ tab.linkedBrowser.reload();
+ return wait;
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_sort-01.js b/devtools/client/netmonitor/test/browser_net_sort-01.js
new file mode 100644
index 000000000..2c4e718dc
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_sort-01.js
@@ -0,0 +1,230 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test if the sorting mechanism works correctly.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(STATUS_CODES_URL);
+ info("Starting test... ");
+
+ let { $all, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 5);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ testContents([0, 1, 2, 3, 4]);
+
+ info("Testing swap(0, 0)");
+ RequestsMenu.swapItemsAtIndices(0, 0);
+ RequestsMenu.refreshZebra();
+ testContents([0, 1, 2, 3, 4]);
+
+ info("Testing swap(0, 1)");
+ RequestsMenu.swapItemsAtIndices(0, 1);
+ RequestsMenu.refreshZebra();
+ testContents([1, 0, 2, 3, 4]);
+
+ info("Testing swap(0, 2)");
+ RequestsMenu.swapItemsAtIndices(0, 2);
+ RequestsMenu.refreshZebra();
+ testContents([1, 2, 0, 3, 4]);
+
+ info("Testing swap(0, 3)");
+ RequestsMenu.swapItemsAtIndices(0, 3);
+ RequestsMenu.refreshZebra();
+ testContents([1, 2, 3, 0, 4]);
+
+ info("Testing swap(0, 4)");
+ RequestsMenu.swapItemsAtIndices(0, 4);
+ RequestsMenu.refreshZebra();
+ testContents([1, 2, 3, 4, 0]);
+
+ info("Testing swap(1, 0)");
+ RequestsMenu.swapItemsAtIndices(1, 0);
+ RequestsMenu.refreshZebra();
+ testContents([0, 2, 3, 4, 1]);
+
+ info("Testing swap(1, 1)");
+ RequestsMenu.swapItemsAtIndices(1, 1);
+ RequestsMenu.refreshZebra();
+ testContents([0, 2, 3, 4, 1]);
+
+ info("Testing swap(1, 2)");
+ RequestsMenu.swapItemsAtIndices(1, 2);
+ RequestsMenu.refreshZebra();
+ testContents([0, 1, 3, 4, 2]);
+
+ info("Testing swap(1, 3)");
+ RequestsMenu.swapItemsAtIndices(1, 3);
+ RequestsMenu.refreshZebra();
+ testContents([0, 3, 1, 4, 2]);
+
+ info("Testing swap(1, 4)");
+ RequestsMenu.swapItemsAtIndices(1, 4);
+ RequestsMenu.refreshZebra();
+ testContents([0, 3, 4, 1, 2]);
+
+ info("Testing swap(2, 0)");
+ RequestsMenu.swapItemsAtIndices(2, 0);
+ RequestsMenu.refreshZebra();
+ testContents([2, 3, 4, 1, 0]);
+
+ info("Testing swap(2, 1)");
+ RequestsMenu.swapItemsAtIndices(2, 1);
+ RequestsMenu.refreshZebra();
+ testContents([1, 3, 4, 2, 0]);
+
+ info("Testing swap(2, 2)");
+ RequestsMenu.swapItemsAtIndices(2, 2);
+ RequestsMenu.refreshZebra();
+ testContents([1, 3, 4, 2, 0]);
+
+ info("Testing swap(2, 3)");
+ RequestsMenu.swapItemsAtIndices(2, 3);
+ RequestsMenu.refreshZebra();
+ testContents([1, 2, 4, 3, 0]);
+
+ info("Testing swap(2, 4)");
+ RequestsMenu.swapItemsAtIndices(2, 4);
+ RequestsMenu.refreshZebra();
+ testContents([1, 4, 2, 3, 0]);
+
+ info("Testing swap(3, 0)");
+ RequestsMenu.swapItemsAtIndices(3, 0);
+ RequestsMenu.refreshZebra();
+ testContents([1, 4, 2, 0, 3]);
+
+ info("Testing swap(3, 1)");
+ RequestsMenu.swapItemsAtIndices(3, 1);
+ RequestsMenu.refreshZebra();
+ testContents([3, 4, 2, 0, 1]);
+
+ info("Testing swap(3, 2)");
+ RequestsMenu.swapItemsAtIndices(3, 2);
+ RequestsMenu.refreshZebra();
+ testContents([2, 4, 3, 0, 1]);
+
+ info("Testing swap(3, 3)");
+ RequestsMenu.swapItemsAtIndices(3, 3);
+ RequestsMenu.refreshZebra();
+ testContents([2, 4, 3, 0, 1]);
+
+ info("Testing swap(3, 4)");
+ RequestsMenu.swapItemsAtIndices(3, 4);
+ RequestsMenu.refreshZebra();
+ testContents([2, 3, 4, 0, 1]);
+
+ info("Testing swap(4, 0)");
+ RequestsMenu.swapItemsAtIndices(4, 0);
+ RequestsMenu.refreshZebra();
+ testContents([2, 3, 0, 4, 1]);
+
+ info("Testing swap(4, 1)");
+ RequestsMenu.swapItemsAtIndices(4, 1);
+ RequestsMenu.refreshZebra();
+ testContents([2, 3, 0, 1, 4]);
+
+ info("Testing swap(4, 2)");
+ RequestsMenu.swapItemsAtIndices(4, 2);
+ RequestsMenu.refreshZebra();
+ testContents([4, 3, 0, 1, 2]);
+
+ info("Testing swap(4, 3)");
+ RequestsMenu.swapItemsAtIndices(4, 3);
+ RequestsMenu.refreshZebra();
+ testContents([3, 4, 0, 1, 2]);
+
+ info("Testing swap(4, 4)");
+ RequestsMenu.swapItemsAtIndices(4, 4);
+ RequestsMenu.refreshZebra();
+ testContents([3, 4, 0, 1, 2]);
+
+ info("Clearing sort.");
+ RequestsMenu.sortBy();
+ testContents([0, 1, 2, 3, 4]);
+
+ return teardown(monitor);
+
+ function testContents([a, b, c, d, e]) {
+ is(RequestsMenu.items.length, 5,
+ "There should be a total of 5 items in the requests menu.");
+ is(RequestsMenu.visibleItems.length, 5,
+ "There should be a total of 5 visbile items in the requests menu.");
+ is($all(".side-menu-widget-item").length, 5,
+ "The visible items in the requests menu are, in fact, visible!");
+
+ is(RequestsMenu.getItemAtIndex(0), RequestsMenu.items[0],
+ "The requests menu items aren't ordered correctly. First item is misplaced.");
+ is(RequestsMenu.getItemAtIndex(1), RequestsMenu.items[1],
+ "The requests menu items aren't ordered correctly. Second item is misplaced.");
+ is(RequestsMenu.getItemAtIndex(2), RequestsMenu.items[2],
+ "The requests menu items aren't ordered correctly. Third item is misplaced.");
+ is(RequestsMenu.getItemAtIndex(3), RequestsMenu.items[3],
+ "The requests menu items aren't ordered correctly. Fourth item is misplaced.");
+ is(RequestsMenu.getItemAtIndex(4), RequestsMenu.items[4],
+ "The requests menu items aren't ordered correctly. Fifth item is misplaced.");
+
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(a),
+ "GET", STATUS_CODES_SJS + "?sts=100", {
+ status: 101,
+ statusText: "Switching Protocols",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ transferred: L10N.getStr("networkMenu.sizeUnavailable"),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 0),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(b),
+ "GET", STATUS_CODES_SJS + "?sts=200", {
+ status: 202,
+ statusText: "Created",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(c),
+ "GET", STATUS_CODES_SJS + "?sts=300", {
+ status: 303,
+ statusText: "See Other",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(d),
+ "GET", STATUS_CODES_SJS + "?sts=400", {
+ status: 404,
+ statusText: "Not Found",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(e),
+ "GET", STATUS_CODES_SJS + "?sts=500", {
+ status: 501,
+ statusText: "Not Implemented",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+ time: true
+ });
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_sort-02.js b/devtools/client/netmonitor/test/browser_net_sort-02.js
new file mode 100644
index 000000000..ce8c69e45
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_sort-02.js
@@ -0,0 +1,272 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test if sorting columns in the network table works correctly.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { monitor } = yield initNetMonitor(SORTING_URL);
+ info("Starting test... ");
+
+ // It seems that this test may be slow on debug builds. This could be because
+ // of the heavy dom manipulation associated with sorting.
+ requestLongerTimeout(2);
+
+ let { $, $all, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ // Loading the frame script and preparing the xhr request URLs so we can
+ // generate some requests later.
+ loadCommonFrameScript();
+ let requests = [{
+ url: "sjs_sorting-test-server.sjs?index=1&" + Math.random(),
+ method: "GET1"
+ }, {
+ url: "sjs_sorting-test-server.sjs?index=5&" + Math.random(),
+ method: "GET5"
+ }, {
+ url: "sjs_sorting-test-server.sjs?index=2&" + Math.random(),
+ method: "GET2"
+ }, {
+ url: "sjs_sorting-test-server.sjs?index=4&" + Math.random(),
+ method: "GET4"
+ }, {
+ url: "sjs_sorting-test-server.sjs?index=3&" + Math.random(),
+ method: "GET3"
+ }];
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 5);
+ yield performRequestsInContent(requests);
+ yield wait;
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, $("#details-pane-toggle"));
+
+ isnot(RequestsMenu.selectedItem, null,
+ "There should be a selected item in the requests menu.");
+ is(RequestsMenu.selectedIndex, 0,
+ "The first item should be selected in the requests menu.");
+ is(NetMonitorView.detailsPaneHidden, false,
+ "The details pane should not be hidden after toggle button was pressed.");
+
+ testHeaders();
+ testContents([0, 2, 4, 3, 1]);
+
+ info("Testing status sort, ascending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-status-button"));
+ testHeaders("status", "ascending");
+ testContents([0, 1, 2, 3, 4]);
+
+ info("Testing status sort, descending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-status-button"));
+ testHeaders("status", "descending");
+ testContents([4, 3, 2, 1, 0]);
+
+ info("Testing status sort, ascending. Checking sort loops correctly.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-status-button"));
+ testHeaders("status", "ascending");
+ testContents([0, 1, 2, 3, 4]);
+
+ info("Testing method sort, ascending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-method-button"));
+ testHeaders("method", "ascending");
+ testContents([0, 1, 2, 3, 4]);
+
+ info("Testing method sort, descending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-method-button"));
+ testHeaders("method", "descending");
+ testContents([4, 3, 2, 1, 0]);
+
+ info("Testing method sort, ascending. Checking sort loops correctly.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-method-button"));
+ testHeaders("method", "ascending");
+ testContents([0, 1, 2, 3, 4]);
+
+ info("Testing file sort, ascending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-file-button"));
+ testHeaders("file", "ascending");
+ testContents([0, 1, 2, 3, 4]);
+
+ info("Testing file sort, descending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-file-button"));
+ testHeaders("file", "descending");
+ testContents([4, 3, 2, 1, 0]);
+
+ info("Testing file sort, ascending. Checking sort loops correctly.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-file-button"));
+ testHeaders("file", "ascending");
+ testContents([0, 1, 2, 3, 4]);
+
+ info("Testing type sort, ascending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-type-button"));
+ testHeaders("type", "ascending");
+ testContents([0, 1, 2, 3, 4]);
+
+ info("Testing type sort, descending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-type-button"));
+ testHeaders("type", "descending");
+ testContents([4, 3, 2, 1, 0]);
+
+ info("Testing type sort, ascending. Checking sort loops correctly.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-type-button"));
+ testHeaders("type", "ascending");
+ testContents([0, 1, 2, 3, 4]);
+
+ info("Testing transferred sort, ascending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-transferred-button"));
+ testHeaders("transferred", "ascending");
+ testContents([0, 1, 2, 3, 4]);
+
+ info("Testing transferred sort, descending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-transferred-button"));
+ testHeaders("transferred", "descending");
+ testContents([4, 3, 2, 1, 0]);
+
+ info("Testing transferred sort, ascending. Checking sort loops correctly.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-transferred-button"));
+ testHeaders("transferred", "ascending");
+ testContents([0, 1, 2, 3, 4]);
+
+ info("Testing size sort, ascending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-size-button"));
+ testHeaders("size", "ascending");
+ testContents([0, 1, 2, 3, 4]);
+
+ info("Testing size sort, descending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-size-button"));
+ testHeaders("size", "descending");
+ testContents([4, 3, 2, 1, 0]);
+
+ info("Testing size sort, ascending. Checking sort loops correctly.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-size-button"));
+ testHeaders("size", "ascending");
+ testContents([0, 1, 2, 3, 4]);
+
+ info("Testing waterfall sort, ascending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-waterfall-button"));
+ testHeaders("waterfall", "ascending");
+ testContents([0, 2, 4, 3, 1]);
+
+ info("Testing waterfall sort, descending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-waterfall-button"));
+ testHeaders("waterfall", "descending");
+ testContents([4, 2, 0, 1, 3]);
+
+ info("Testing waterfall sort, ascending. Checking sort loops correctly.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-waterfall-button"));
+ testHeaders("waterfall", "ascending");
+ testContents([0, 2, 4, 3, 1]);
+
+ return teardown(monitor);
+
+ function testHeaders(sortType, direction) {
+ let doc = monitor.panelWin.document;
+ let target = doc.querySelector("#requests-menu-" + sortType + "-button");
+ let headers = doc.querySelectorAll(".requests-menu-header-button");
+
+ for (let header of headers) {
+ if (header != target) {
+ is(header.hasAttribute("sorted"), false,
+ "The " + header.id + " header should not have a 'sorted' attribute.");
+ is(header.hasAttribute("tooltiptext"), false,
+ "The " + header.id + " header should not have a 'tooltiptext' attribute.");
+ } else {
+ is(header.getAttribute("sorted"), direction,
+ "The " + header.id + " header has an incorrect 'sorted' attribute.");
+ is(header.getAttribute("tooltiptext"), direction == "ascending"
+ ? L10N.getStr("networkMenu.sortedAsc")
+ : L10N.getStr("networkMenu.sortedDesc"),
+ "The " + header.id + " has an incorrect 'tooltiptext' attribute.");
+ }
+ }
+ }
+
+ function testContents([a, b, c, d, e]) {
+ isnot(RequestsMenu.selectedItem, null,
+ "There should still be a selected item after sorting.");
+ is(RequestsMenu.selectedIndex, a,
+ "The first item should be still selected after sorting.");
+ is(NetMonitorView.detailsPaneHidden, false,
+ "The details pane should still be visible after sorting.");
+
+ is(RequestsMenu.items.length, 5,
+ "There should be a total of 5 items in the requests menu.");
+ is(RequestsMenu.visibleItems.length, 5,
+ "There should be a total of 5 visbile items in the requests menu.");
+ is($all(".side-menu-widget-item").length, 5,
+ "The visible items in the requests menu are, in fact, visible!");
+
+ is(RequestsMenu.getItemAtIndex(0), RequestsMenu.items[0],
+ "The requests menu items aren't ordered correctly. First item is misplaced.");
+ is(RequestsMenu.getItemAtIndex(1), RequestsMenu.items[1],
+ "The requests menu items aren't ordered correctly. Second item is misplaced.");
+ is(RequestsMenu.getItemAtIndex(2), RequestsMenu.items[2],
+ "The requests menu items aren't ordered correctly. Third item is misplaced.");
+ is(RequestsMenu.getItemAtIndex(3), RequestsMenu.items[3],
+ "The requests menu items aren't ordered correctly. Fourth item is misplaced.");
+ is(RequestsMenu.getItemAtIndex(4), RequestsMenu.items[4],
+ "The requests menu items aren't ordered correctly. Fifth item is misplaced.");
+
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(a),
+ "GET1", SORTING_SJS + "?index=1", {
+ fuzzyUrl: true,
+ status: 101,
+ statusText: "Meh",
+ type: "1",
+ fullMimeType: "text/1",
+ transferred: L10N.getStr("networkMenu.sizeUnavailable"),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 0),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(b),
+ "GET2", SORTING_SJS + "?index=2", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "Meh",
+ type: "2",
+ fullMimeType: "text/2",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 19),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 19),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(c),
+ "GET3", SORTING_SJS + "?index=3", {
+ fuzzyUrl: true,
+ status: 300,
+ statusText: "Meh",
+ type: "3",
+ fullMimeType: "text/3",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 29),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 29),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(d),
+ "GET4", SORTING_SJS + "?index=4", {
+ fuzzyUrl: true,
+ status: 400,
+ statusText: "Meh",
+ type: "4",
+ fullMimeType: "text/4",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 39),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 39),
+ time: true
+ });
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(e),
+ "GET5", SORTING_SJS + "?index=5", {
+ fuzzyUrl: true,
+ status: 500,
+ statusText: "Meh",
+ type: "5",
+ fullMimeType: "text/5",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 49),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 49),
+ time: true
+ });
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_sort-03.js b/devtools/client/netmonitor/test/browser_net_sort-03.js
new file mode 100644
index 000000000..ada0872a8
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_sort-03.js
@@ -0,0 +1,209 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test if sorting columns in the network table works correctly with new requests.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { monitor } = yield initNetMonitor(SORTING_URL);
+ info("Starting test... ");
+
+ // It seems that this test may be slow on debug builds. This could be because
+ // of the heavy dom manipulation associated with sorting.
+ requestLongerTimeout(2);
+
+ let { $, $all, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ // Loading the frame script and preparing the xhr request URLs so we can
+ // generate some requests later.
+ loadCommonFrameScript();
+ let requests = [{
+ url: "sjs_sorting-test-server.sjs?index=1&" + Math.random(),
+ method: "GET1"
+ }, {
+ url: "sjs_sorting-test-server.sjs?index=5&" + Math.random(),
+ method: "GET5"
+ }, {
+ url: "sjs_sorting-test-server.sjs?index=2&" + Math.random(),
+ method: "GET2"
+ }, {
+ url: "sjs_sorting-test-server.sjs?index=4&" + Math.random(),
+ method: "GET4"
+ }, {
+ url: "sjs_sorting-test-server.sjs?index=3&" + Math.random(),
+ method: "GET3"
+ }];
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 5);
+ yield performRequestsInContent(requests);
+ yield wait;
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, $("#details-pane-toggle"));
+
+ isnot(RequestsMenu.selectedItem, null,
+ "There should be a selected item in the requests menu.");
+ is(RequestsMenu.selectedIndex, 0,
+ "The first item should be selected in the requests menu.");
+ is(NetMonitorView.detailsPaneHidden, false,
+ "The details pane should not be hidden after toggle button was pressed.");
+
+ testHeaders();
+ testContents([0, 2, 4, 3, 1], 0);
+
+ info("Testing status sort, ascending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-status-button"));
+ testHeaders("status", "ascending");
+ testContents([0, 1, 2, 3, 4], 0);
+
+ info("Performing more requests.");
+ wait = waitForNetworkEvents(monitor, 5);
+ yield performRequestsInContent(requests);
+ yield wait;
+
+ info("Testing status sort again, ascending.");
+ testHeaders("status", "ascending");
+ testContents([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 0);
+
+ info("Testing status sort, descending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-status-button"));
+ testHeaders("status", "descending");
+ testContents([9, 8, 7, 6, 5, 4, 3, 2, 1, 0], 9);
+
+ info("Performing more requests.");
+ wait = waitForNetworkEvents(monitor, 5);
+ yield performRequestsInContent(requests);
+ yield wait;
+
+ info("Testing status sort again, descending.");
+ testHeaders("status", "descending");
+ testContents([14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0], 14);
+
+ info("Testing status sort yet again, ascending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-status-button"));
+ testHeaders("status", "ascending");
+ testContents([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], 0);
+
+ info("Testing status sort yet again, descending.");
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-status-button"));
+ testHeaders("status", "descending");
+ testContents([14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0], 14);
+
+ return teardown(monitor);
+
+ function testHeaders(sortType, direction) {
+ let doc = monitor.panelWin.document;
+ let target = doc.querySelector("#requests-menu-" + sortType + "-button");
+ let headers = doc.querySelectorAll(".requests-menu-header-button");
+
+ for (let header of headers) {
+ if (header != target) {
+ is(header.hasAttribute("sorted"), false,
+ "The " + header.id + " header should not have a 'sorted' attribute.");
+ is(header.hasAttribute("tooltiptext"), false,
+ "The " + header.id + " header should not have a 'tooltiptext' attribute.");
+ } else {
+ is(header.getAttribute("sorted"), direction,
+ "The " + header.id + " header has an incorrect 'sorted' attribute.");
+ is(header.getAttribute("tooltiptext"), direction == "ascending"
+ ? L10N.getStr("networkMenu.sortedAsc")
+ : L10N.getStr("networkMenu.sortedDesc"),
+ "The " + header.id + " has an incorrect 'tooltiptext' attribute.");
+ }
+ }
+ }
+
+ function testContents(order, selection) {
+ isnot(RequestsMenu.selectedItem, null,
+ "There should still be a selected item after sorting.");
+ is(RequestsMenu.selectedIndex, selection,
+ "The first item should be still selected after sorting.");
+ is(NetMonitorView.detailsPaneHidden, false,
+ "The details pane should still be visible after sorting.");
+
+ is(RequestsMenu.items.length, order.length,
+ "There should be a specific number of items in the requests menu.");
+ is(RequestsMenu.visibleItems.length, order.length,
+ "There should be a specific number of visbile items in the requests menu.");
+ is($all(".side-menu-widget-item").length, order.length,
+ "The visible items in the requests menu are, in fact, visible!");
+
+ for (let i = 0; i < order.length; i++) {
+ is(RequestsMenu.getItemAtIndex(i), RequestsMenu.items[i],
+ "The requests menu items aren't ordered correctly. Misplaced item " + i + ".");
+ }
+
+ for (let i = 0, len = order.length / 5; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i]),
+ "GET1", SORTING_SJS + "?index=1", {
+ fuzzyUrl: true,
+ status: 101,
+ statusText: "Meh",
+ type: "1",
+ fullMimeType: "text/1",
+ transferred: L10N.getStr("networkMenu.sizeUnavailable"),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 0),
+ time: true
+ });
+ }
+ for (let i = 0, len = order.length / 5; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len]),
+ "GET2", SORTING_SJS + "?index=2", {
+ fuzzyUrl: true,
+ status: 200,
+ statusText: "Meh",
+ type: "2",
+ fullMimeType: "text/2",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 19),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 19),
+ time: true
+ });
+ }
+ for (let i = 0, len = order.length / 5; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len * 2]),
+ "GET3", SORTING_SJS + "?index=3", {
+ fuzzyUrl: true,
+ status: 300,
+ statusText: "Meh",
+ type: "3",
+ fullMimeType: "text/3",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 29),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 29),
+ time: true
+ });
+ }
+ for (let i = 0, len = order.length / 5; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len * 3]),
+ "GET4", SORTING_SJS + "?index=4", {
+ fuzzyUrl: true,
+ status: 400,
+ statusText: "Meh",
+ type: "4",
+ fullMimeType: "text/4",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 39),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 39),
+ time: true
+ });
+ }
+ for (let i = 0, len = order.length / 5; i < len; i++) {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(order[i + len * 4]),
+ "GET5", SORTING_SJS + "?index=5", {
+ fuzzyUrl: true,
+ status: 500,
+ statusText: "Meh",
+ type: "5",
+ fullMimeType: "text/5",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 49),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 49),
+ time: true
+ });
+ }
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_statistics-01.js b/devtools/client/netmonitor/test/browser_net_statistics-01.js
new file mode 100644
index 000000000..d7e75b997
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_statistics-01.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the statistics view is populated correctly.
+ */
+
+add_task(function* () {
+ let { monitor } = yield initNetMonitor(STATISTICS_URL);
+ info("Starting test... ");
+
+ let panel = monitor.panelWin;
+ let { $, $all, EVENTS, NetMonitorView } = panel;
+ is(NetMonitorView.currentFrontendMode, "network-inspector-view",
+ "The initial frontend mode is correct.");
+
+ is($("#primed-cache-chart").childNodes.length, 0,
+ "There should be no primed cache chart created yet.");
+ is($("#empty-cache-chart").childNodes.length, 0,
+ "There should be no empty cache chart created yet.");
+
+ let onChartDisplayed = Promise.all([
+ panel.once(EVENTS.PRIMED_CACHE_CHART_DISPLAYED),
+ panel.once(EVENTS.EMPTY_CACHE_CHART_DISPLAYED)
+ ]);
+ let onPlaceholderDisplayed = panel.once(EVENTS.PLACEHOLDER_CHARTS_DISPLAYED);
+
+ info("Displaying statistics view");
+ NetMonitorView.toggleFrontendMode();
+ is(NetMonitorView.currentFrontendMode, "network-statistics-view",
+ "The current frontend mode is correct.");
+
+ info("Waiting for placeholder to display");
+ yield onPlaceholderDisplayed;
+ is($("#primed-cache-chart").childNodes.length, 1,
+ "There should be a placeholder primed cache chart created now.");
+ is($("#empty-cache-chart").childNodes.length, 1,
+ "There should be a placeholder empty cache chart created now.");
+
+ is($all(".pie-chart-container[placeholder=true]").length, 2,
+ "Two placeholder pie chart appear to be rendered correctly.");
+ is($all(".table-chart-container[placeholder=true]").length, 2,
+ "Two placeholder table chart appear to be rendered correctly.");
+
+ info("Waiting for chart to display");
+ yield onChartDisplayed;
+ is($("#primed-cache-chart").childNodes.length, 1,
+ "There should be a real primed cache chart created now.");
+ is($("#empty-cache-chart").childNodes.length, 1,
+ "There should be a real empty cache chart created now.");
+
+ yield waitUntil(
+ () => $all(".pie-chart-container:not([placeholder=true])").length == 2);
+ ok(true, "Two real pie charts appear to be rendered correctly.");
+
+ yield waitUntil(
+ () => $all(".table-chart-container:not([placeholder=true])").length == 2);
+ ok(true, "Two real table charts appear to be rendered correctly.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_statistics-02.js b/devtools/client/netmonitor/test/browser_net_statistics-02.js
new file mode 100644
index 000000000..361247e16
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_statistics-02.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if the network inspector view is shown when the target navigates
+ * away while in the statistics view.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(STATISTICS_URL);
+ info("Starting test... ");
+
+ let panel = monitor.panelWin;
+ let { EVENTS, NetMonitorView } = panel;
+ is(NetMonitorView.currentFrontendMode, "network-inspector-view",
+ "The initial frontend mode is correct.");
+
+ let onChartDisplayed = Promise.all([
+ panel.once(EVENTS.PRIMED_CACHE_CHART_DISPLAYED),
+ panel.once(EVENTS.EMPTY_CACHE_CHART_DISPLAYED)
+ ]);
+
+ info("Displaying statistics view");
+ NetMonitorView.toggleFrontendMode();
+ yield onChartDisplayed;
+ is(NetMonitorView.currentFrontendMode, "network-statistics-view",
+ "The frontend mode is currently in the statistics view.");
+
+ info("Reloading page");
+ let onWillNavigate = panel.once(EVENTS.TARGET_WILL_NAVIGATE);
+ let onDidNavigate = panel.once(EVENTS.TARGET_DID_NAVIGATE);
+ tab.linkedBrowser.reload();
+ yield onWillNavigate;
+ is(NetMonitorView.currentFrontendMode, "network-inspector-view",
+ "The frontend mode switched back to the inspector view.");
+ yield onDidNavigate;
+ is(NetMonitorView.currentFrontendMode, "network-inspector-view",
+ "The frontend mode is still in the inspector view.");
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_statistics-03.js b/devtools/client/netmonitor/test/browser_net_statistics-03.js
new file mode 100644
index 000000000..f3c6bf691
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_statistics-03.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test if the correct filtering predicates are used when filtering from
+ * the performance analysis view.
+ */
+
+add_task(function* () {
+ let { monitor } = yield initNetMonitor(FILTERING_URL);
+ info("Starting test... ");
+
+ let panel = monitor.panelWin;
+ let { $, EVENTS, NetMonitorView } = panel;
+
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-html-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-css-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-js-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-ws-button"));
+ EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-other-button"));
+ testFilterButtonsCustom(monitor, [0, 1, 1, 1, 0, 0, 0, 0, 0, 1]);
+ info("The correct filtering predicates are used before entering perf. analysis mode.");
+
+ let onEvents = promise.all([
+ panel.once(EVENTS.PRIMED_CACHE_CHART_DISPLAYED),
+ panel.once(EVENTS.EMPTY_CACHE_CHART_DISPLAYED)
+ ]);
+ NetMonitorView.toggleFrontendMode();
+ yield onEvents;
+
+ is(NetMonitorView.currentFrontendMode, "network-statistics-view",
+ "The frontend mode is switched to the statistics view.");
+
+ EventUtils.sendMouseEvent({ type: "click" }, $(".pie-chart-slice"));
+
+ is(NetMonitorView.currentFrontendMode, "network-inspector-view",
+ "The frontend mode is switched back to the inspector view.");
+
+ testFilterButtons(monitor, "html");
+ info("The correct filtering predicate is used when exiting perf. analysis mode.");
+
+ yield teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_status-codes.js b/devtools/client/netmonitor/test/browser_net_status-codes.js
new file mode 100644
index 000000000..f38ee71e4
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_status-codes.js
@@ -0,0 +1,213 @@
+/* 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";
+
+/**
+ * Tests if requests display the correct status code and text in the UI.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(STATUS_CODES_URL);
+
+ info("Starting test... ");
+
+ let { document, EVENTS, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu, NetworkDetails } = NetMonitorView;
+ let requestItems = [];
+
+ RequestsMenu.lazyUpdate = false;
+ NetworkDetails._params.lazyEmpty = false;
+
+ const REQUEST_DATA = [
+ {
+ // request #0
+ method: "GET",
+ uri: STATUS_CODES_SJS + "?sts=100",
+ details: {
+ status: 101,
+ statusText: "Switching Protocols",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 0),
+ time: true
+ }
+ },
+ {
+ // request #1
+ method: "GET",
+ uri: STATUS_CODES_SJS + "?sts=200",
+ details: {
+ status: 202,
+ statusText: "Created",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+ time: true
+ }
+ },
+ {
+ // request #2
+ method: "GET",
+ uri: STATUS_CODES_SJS + "?sts=300",
+ details: {
+ status: 303,
+ statusText: "See Other",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+ time: true
+ }
+ },
+ {
+ // request #3
+ method: "GET",
+ uri: STATUS_CODES_SJS + "?sts=400",
+ details: {
+ status: 404,
+ statusText: "Not Found",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+ time: true
+ }
+ },
+ {
+ // request #4
+ method: "GET",
+ uri: STATUS_CODES_SJS + "?sts=500",
+ details: {
+ status: 501,
+ statusText: "Not Implemented",
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22),
+ time: true
+ }
+ }
+ ];
+
+ let wait = waitForNetworkEvents(monitor, 5);
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests();
+ });
+ yield wait;
+
+ info("Performing tests");
+ yield verifyRequests();
+ yield testTab(0, testSummary);
+ yield testTab(2, testParams);
+
+ return teardown(monitor);
+
+ /**
+ * A helper that verifies all requests show the correct information and caches
+ * RequestsMenu items to requestItems array.
+ */
+ function* verifyRequests() {
+ info("Verifying requests contain correct information.");
+ let index = 0;
+ for (let request of REQUEST_DATA) {
+ let item = RequestsMenu.getItemAtIndex(index);
+ requestItems[index] = item;
+
+ info("Verifying request #" + index);
+ yield verifyRequestItemTarget(item, request.method, request.uri, request.details);
+
+ index++;
+ }
+ }
+
+ /**
+ * A helper that opens a given tab of request details pane, selects and passes
+ * all requests to the given test function.
+ *
+ * @param Number tabIdx
+ * The index of NetworkDetails tab to activate.
+ * @param Function testFn(requestItem)
+ * A function that should perform all necessary tests. It's called once
+ * for every item of REQUEST_DATA with that item being selected in the
+ * NetworkMonitor.
+ */
+ function* testTab(tabIdx, testFn) {
+ info("Testing tab #" + tabIdx);
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[tabIdx]);
+
+ let counter = 0;
+ for (let item of REQUEST_DATA) {
+ info("Waiting tab #" + tabIdx + " to update with request #" + counter);
+ yield chooseRequest(counter);
+
+ info("Tab updated. Performing checks");
+ yield testFn(item);
+
+ counter++;
+ }
+ }
+
+ /**
+ * A function that tests "Summary" contains correct information.
+ */
+ function* testSummary(data) {
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[0];
+
+ let { method, uri, details: { status, statusText } } = data;
+ is(tabpanel.querySelector("#headers-summary-url-value").getAttribute("value"),
+ uri, "The url summary value is incorrect.");
+ is(tabpanel.querySelector("#headers-summary-method-value").getAttribute("value"),
+ method, "The method summary value is incorrect.");
+ is(tabpanel.querySelector("#headers-summary-status-circle").getAttribute("code"),
+ status, "The status summary code is incorrect.");
+ is(tabpanel.querySelector("#headers-summary-status-value").getAttribute("value"),
+ status + " " + statusText, "The status summary value is incorrect.");
+ }
+
+ /**
+ * A function that tests "Params" tab contains correct information.
+ */
+ function* testParams(data) {
+ let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
+ let statusParamValue = data.uri.split("=").pop();
+ let statusParamShownValue = "\"" + statusParamValue + "\"";
+
+ is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
+ "There should be 1 param scope displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variable-or-property").length, 1,
+ "There should be 1 param value displayed in this tabpanel.");
+ is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+ "The empty notice should not be displayed in this tabpanel.");
+
+ let paramsScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+
+ is(paramsScope.querySelector(".name").getAttribute("value"),
+ L10N.getStr("paramsQueryString"),
+ "The params scope doesn't have the correct title.");
+
+ is(paramsScope.querySelectorAll(".variables-view-variable .name")[0]
+ .getAttribute("value"),
+ "sts", "The param name was incorrect.");
+ is(paramsScope.querySelectorAll(".variables-view-variable .value")[0]
+ .getAttribute("value"),
+ statusParamShownValue, "The param value was incorrect.");
+
+ is(tabpanel.querySelector("#request-params-box")
+ .hasAttribute("hidden"), false,
+ "The request params box should not be hidden.");
+ is(tabpanel.querySelector("#request-post-data-textarea-box")
+ .hasAttribute("hidden"), true,
+ "The request post data textarea box should be hidden.");
+ }
+
+ /**
+ * A helper that clicks on a specified request and returns a promise resolved
+ * when NetworkDetails has been populated with the data of the given request.
+ */
+ function chooseRequest(index) {
+ let onTabUpdated = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[index].target);
+ return onTabUpdated;
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_streaming-response.js b/devtools/client/netmonitor/test/browser_net_streaming-response.js
new file mode 100644
index 000000000..49a75ec32
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_streaming-response.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if reponses from streaming content types (MPEG-DASH, HLS) are
+ * displayed as XML or plain text
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+
+ info("Starting test... ");
+ let { panelWin } = monitor;
+ let { document, Editor, NetMonitorView } = panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ const REQUESTS = [
+ [ "hls-m3u8", /^#EXTM3U/, Editor.modes.text ],
+ [ "mpeg-dash", /^<\?xml/, Editor.modes.html ]
+ ];
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, REQUESTS.length);
+ for (let [fmt] of REQUESTS) {
+ let url = CONTENT_TYPE_SJS + "?fmt=" + fmt;
+ yield ContentTask.spawn(tab.linkedBrowser, { url }, function* (args) {
+ content.wrappedJSObject.performRequests(1, args.url);
+ });
+ }
+ yield wait;
+
+ REQUESTS.forEach(([ fmt ], i) => {
+ verifyRequestItemTarget(RequestsMenu.getItemAtIndex(i),
+ "GET", CONTENT_TYPE_SJS + "?fmt=" + fmt, {
+ status: 200,
+ statusText: "OK"
+ });
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.getElementById("details-pane-toggle"));
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ document.querySelectorAll("#details-pane tab")[3]);
+
+ yield panelWin.once(panelWin.EVENTS.RESPONSE_BODY_DISPLAYED);
+ let editor = yield NetMonitorView.editor("#response-content-textarea");
+
+ // the hls-m3u8 part
+ testEditorContent(editor, REQUESTS[0]);
+
+ RequestsMenu.selectedIndex = 1;
+ yield panelWin.once(panelWin.EVENTS.TAB_UPDATED);
+ yield panelWin.once(panelWin.EVENTS.RESPONSE_BODY_DISPLAYED);
+
+ // the mpeg-dash part
+ testEditorContent(editor, REQUESTS[1]);
+
+ return teardown(monitor);
+
+ function testEditorContent(e, [ fmt, textRe, mode ]) {
+ ok(e.getText().match(textRe),
+ "The text shown in the source editor for " + fmt + " is correct.");
+ is(e.getMode(), mode,
+ "The mode active in the source editor for " + fmt + " is correct.");
+ }
+});
diff --git a/devtools/client/netmonitor/test/browser_net_throttle.js b/devtools/client/netmonitor/test/browser_net_throttle.js
new file mode 100644
index 000000000..c1e7723b8
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_throttle.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Network throttling integration test.
+
+"use strict";
+
+add_task(function* () {
+ yield throttleTest(true);
+ yield throttleTest(false);
+});
+
+function* throttleTest(actuallyThrottle) {
+ requestLongerTimeout(2);
+
+ let { monitor } = yield initNetMonitor(SIMPLE_URL);
+ const {ACTIVITY_TYPE, EVENTS, NetMonitorController, NetMonitorView} = monitor.panelWin;
+
+ info("Starting test... (actuallyThrottle = " + actuallyThrottle + ")");
+
+ // When throttling, must be smaller than the length of the content
+ // of SIMPLE_URL in bytes.
+ const size = actuallyThrottle ? 200 : 0;
+
+ const request = {
+ "NetworkMonitor.throttleData": {
+ roundTripTimeMean: 0,
+ roundTripTimeMax: 0,
+ downloadBPSMean: size,
+ downloadBPSMax: size,
+ uploadBPSMean: 10000,
+ uploadBPSMax: 10000,
+ },
+ };
+ let client = monitor._controller.webConsoleClient;
+
+ info("sending throttle request");
+ let deferred = promise.defer();
+ client.setPreferences(request, response => {
+ deferred.resolve(response);
+ });
+ yield deferred.promise;
+
+ let eventPromise = monitor.panelWin.once(EVENTS.RECEIVED_EVENT_TIMINGS);
+ yield NetMonitorController.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_DISABLED);
+ yield eventPromise;
+
+ let requestItem = NetMonitorView.RequestsMenu.getItemAtIndex(0);
+ const reportedOneSecond = requestItem.attachment.eventTimings.timings.receive > 1000;
+ if (actuallyThrottle) {
+ ok(reportedOneSecond, "download reported as taking more than one second");
+ } else {
+ ok(!reportedOneSecond, "download reported as taking less than one second");
+ }
+
+ yield teardown(monitor);
+}
diff --git a/devtools/client/netmonitor/test/browser_net_timeline_ticks.js b/devtools/client/netmonitor/test/browser_net_timeline_ticks.js
new file mode 100644
index 000000000..2aafcb98d
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_timeline_ticks.js
@@ -0,0 +1,142 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if timeline correctly displays interval divisions.
+ */
+
+add_task(function* () {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+
+ let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
+ info("Starting test... ");
+
+ let { $, $all, NetMonitorView, NetMonitorController } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ // Disable transferred size column support for this test.
+ // Without this, the waterfall only has enough room for one division, which
+ // would remove most of the value of this test.
+ $("#requests-menu-transferred-header-box").hidden = true;
+ $("#requests-menu-item-template .requests-menu-transferred").hidden = true;
+
+ RequestsMenu.lazyUpdate = false;
+
+ ok($("#requests-menu-waterfall-label"),
+ "An timeline label should be displayed when the frontend is opened.");
+ ok($all(".requests-menu-timings-division").length == 0,
+ "No tick labels should be displayed when the frontend is opened.");
+
+ ok(!RequestsMenu._canvas, "No canvas should be created when the frontend is opened.");
+ ok(!RequestsMenu._ctx, "No 2d context should be created when the frontend is opened.");
+
+ let wait = waitForNetworkEvents(monitor, 1);
+ tab.linkedBrowser.reload();
+ yield wait;
+
+ // Make sure the DOMContentLoaded and load markers don't interfere with
+ // this test by removing them and redrawing the waterfall (bug 1224088).
+ NetMonitorController.NetworkEventsHandler.clearMarkers();
+ RequestsMenu._flushWaterfallViews(true);
+
+ ok(!$("#requests-menu-waterfall-label"),
+ "The timeline label should be hidden after the first request.");
+ ok($all(".requests-menu-timings-division").length >= 3,
+ "There should be at least 3 tick labels in the network requests header.");
+
+ is($all(".requests-menu-timings-division")[0].getAttribute("value"),
+ L10N.getFormatStr("networkMenu.millisecond", 0),
+ "The first tick label has an incorrect value");
+ is($all(".requests-menu-timings-division")[1].getAttribute("value"),
+ L10N.getFormatStr("networkMenu.millisecond", 80),
+ "The second tick label has an incorrect value");
+ is($all(".requests-menu-timings-division")[2].getAttribute("value"),
+ L10N.getFormatStr("networkMenu.millisecond", 160),
+ "The third tick label has an incorrect value");
+
+ is($all(".requests-menu-timings-division")[0].style.transform, "translateX(0px)",
+ "The first tick label has an incorrect translation");
+ is($all(".requests-menu-timings-division")[1].style.transform, "translateX(80px)",
+ "The second tick label has an incorrect translation");
+ is($all(".requests-menu-timings-division")[2].style.transform, "translateX(160px)",
+ "The third tick label has an incorrect translation");
+
+ ok(RequestsMenu._canvas, "A canvas should be created after the first request.");
+ ok(RequestsMenu._ctx, "A 2d context should be created after the first request.");
+
+ let imageData = RequestsMenu._ctx.getImageData(0, 0, 161, 1);
+ ok(imageData, "The image data should have been created.");
+
+ let data = imageData.data;
+ ok(data, "The image data should contain a pixel array.");
+
+ ok(hasPixelAt(0), "The tick at 0 is should not be empty.");
+ ok(!hasPixelAt(1), "The tick at 1 is should be empty.");
+ ok(!hasPixelAt(19), "The tick at 19 is should be empty.");
+ ok(hasPixelAt(20), "The tick at 20 is should not be empty.");
+ ok(!hasPixelAt(21), "The tick at 21 is should be empty.");
+ ok(!hasPixelAt(39), "The tick at 39 is should be empty.");
+ ok(hasPixelAt(40), "The tick at 40 is should not be empty.");
+ ok(!hasPixelAt(41), "The tick at 41 is should be empty.");
+ ok(!hasPixelAt(59), "The tick at 59 is should be empty.");
+ ok(hasPixelAt(60), "The tick at 60 is should not be empty.");
+ ok(!hasPixelAt(61), "The tick at 61 is should be empty.");
+ ok(!hasPixelAt(79), "The tick at 79 is should be empty.");
+ ok(hasPixelAt(80), "The tick at 80 is should not be empty.");
+ ok(!hasPixelAt(81), "The tick at 81 is should be empty.");
+ ok(!hasPixelAt(159), "The tick at 159 is should be empty.");
+ ok(hasPixelAt(160), "The tick at 160 is should not be empty.");
+ ok(!hasPixelAt(161), "The tick at 161 is should be empty.");
+
+ ok(isPixelBrighterAtThan(0, 20),
+ "The tick at 0 should be brighter than the one at 20");
+ ok(isPixelBrighterAtThan(40, 20),
+ "The tick at 40 should be brighter than the one at 20");
+ ok(isPixelBrighterAtThan(40, 60),
+ "The tick at 40 should be brighter than the one at 60");
+ ok(isPixelBrighterAtThan(80, 60),
+ "The tick at 80 should be brighter than the one at 60");
+
+ ok(isPixelBrighterAtThan(80, 100),
+ "The tick at 80 should be brighter than the one at 100");
+ ok(isPixelBrighterAtThan(120, 100),
+ "The tick at 120 should be brighter than the one at 100");
+ ok(isPixelBrighterAtThan(120, 140),
+ "The tick at 120 should be brighter than the one at 140");
+ ok(isPixelBrighterAtThan(160, 140),
+ "The tick at 160 should be brighter than the one at 140");
+
+ ok(isPixelEquallyBright(20, 60),
+ "The tick at 20 should be equally bright to the one at 60");
+ ok(isPixelEquallyBright(100, 140),
+ "The tick at 100 should be equally bright to the one at 140");
+
+ ok(isPixelEquallyBright(40, 120),
+ "The tick at 40 should be equally bright to the one at 120");
+
+ ok(isPixelEquallyBright(0, 80),
+ "The tick at 80 should be equally bright to the one at 160");
+ ok(isPixelEquallyBright(80, 160),
+ "The tick at 80 should be equally bright to the one at 160");
+
+ function hasPixelAt(x) {
+ let i = (x | 0) * 4;
+ return data[i] && data[i + 1] && data[i + 2] && data[i + 3];
+ }
+
+ function isPixelBrighterAtThan(x1, x2) {
+ let i = (x1 | 0) * 4;
+ let j = (x2 | 0) * 4;
+ return data[i + 3] > data [j + 3];
+ }
+
+ function isPixelEquallyBright(x1, x2) {
+ let i = (x1 | 0) * 4;
+ let j = (x2 | 0) * 4;
+ return data[i + 3] == data [j + 3];
+ }
+
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_timing-division.js b/devtools/client/netmonitor/test/browser_net_timing-division.js
new file mode 100644
index 000000000..0114ba235
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_timing-division.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if timing intervals are divided againts seconds when appropriate.
+ */
+
+add_task(function* () {
+ let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
+ info("Starting test... ");
+
+ let { $all, NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ let wait = waitForNetworkEvents(monitor, 2);
+ // Timeout needed for having enough divisions on the time scale.
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.wrappedJSObject.performRequests(2, null, 3000);
+ });
+ yield wait;
+
+ let milDivs = $all(".requests-menu-timings-division[division-scale=millisecond]");
+ let secDivs = $all(".requests-menu-timings-division[division-scale=second]");
+ let minDivs = $all(".requests-menu-timings-division[division-scale=minute]");
+
+ info("Number of millisecond divisions: " + milDivs.length);
+ info("Number of second divisions: " + secDivs.length);
+ info("Number of minute divisions: " + minDivs.length);
+
+ for (let div of milDivs) {
+ info("Millisecond division: " + div.getAttribute("value"));
+ }
+ for (let div of secDivs) {
+ info("Second division: " + div.getAttribute("value"));
+ }
+ for (let div of minDivs) {
+ info("Minute division: " + div.getAttribute("value"));
+ }
+
+ is(RequestsMenu.itemCount, 2,
+ "There should be only two requests made.");
+
+ let firstRequest = RequestsMenu.getItemAtIndex(0);
+ let lastRequest = RequestsMenu.getItemAtIndex(1);
+
+ info("First request happened at: " +
+ firstRequest.attachment.responseHeaders.headers.find(e => e.name == "Date").value);
+ info("Last request happened at: " +
+ lastRequest.attachment.responseHeaders.headers.find(e => e.name == "Date").value);
+
+ ok(secDivs.length,
+ "There should be at least one division on the seconds time scale.");
+ ok(secDivs[0].getAttribute("value").match(/\d+\.\d{2}\s\w+/),
+ "The division on the seconds time scale looks legit.");
+
+ return teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/browser_net_truncate.js b/devtools/client/netmonitor/test/browser_net_truncate.js
new file mode 100644
index 000000000..bfb5c896d
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_truncate.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Verifies that truncated response bodies still have the correct reported size.
+ */
+
+function test() {
+ let { L10N } = require("devtools/client/netmonitor/l10n");
+ const { RESPONSE_BODY_LIMIT } = require("devtools/shared/webconsole/network-monitor");
+
+ const URL = EXAMPLE_URL + "sjs_truncate-test-server.sjs?limit=" + RESPONSE_BODY_LIMIT;
+
+ // Another slow test on Linux debug.
+ requestLongerTimeout(2);
+
+ initNetMonitor(URL).then(({ tab, monitor }) => {
+ info("Starting test... ");
+
+ let { NetMonitorView } = monitor.panelWin;
+ let { RequestsMenu } = NetMonitorView;
+
+ RequestsMenu.lazyUpdate = false;
+
+ waitForNetworkEvents(monitor, 1)
+ .then(() => teardown(monitor))
+ .then(finish);
+
+ monitor.panelWin.once(monitor.panelWin.EVENTS.RECEIVED_RESPONSE_CONTENT, () => {
+ let requestItem = RequestsMenu.getItemAtIndex(0);
+
+ verifyRequestItemTarget(RequestsMenu, requestItem, "GET", URL, {
+ type: "plain",
+ fullMimeType: "text/plain; charset=utf-8",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeMB", 2),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeMB", 2),
+ });
+ });
+
+ tab.linkedBrowser.reload();
+ });
+}
diff --git a/devtools/client/netmonitor/test/dropmarker.svg b/devtools/client/netmonitor/test/dropmarker.svg
new file mode 100644
index 000000000..3e2987682
--- /dev/null
+++ b/devtools/client/netmonitor/test/dropmarker.svg
@@ -0,0 +1,6 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="8" height="4" viewBox="0 0 8 4">
+ <polygon points="0,0 4,4 8,0" fill="#b6babf"/>
+</svg>
diff --git a/devtools/client/netmonitor/test/head.js b/devtools/client/netmonitor/test/head.js
new file mode 100644
index 000000000..d733cc1d4
--- /dev/null
+++ b/devtools/client/netmonitor/test/head.js
@@ -0,0 +1,518 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* import-globals-from ../../framework/test/shared-head.js */
+
+// 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 NetworkHelper = require("devtools/shared/webconsole/network-helper");
+var { Toolbox } = require("devtools/client/framework/toolbox");
+
+const EXAMPLE_URL = "http://example.com/browser/devtools/client/netmonitor/test/";
+const HTTPS_EXAMPLE_URL = "https://example.com/browser/devtools/client/netmonitor/test/";
+
+const API_CALLS_URL = EXAMPLE_URL + "html_api-calls-test-page.html";
+const SIMPLE_URL = EXAMPLE_URL + "html_simple-test-page.html";
+const NAVIGATE_URL = EXAMPLE_URL + "html_navigate-test-page.html";
+const CONTENT_TYPE_URL = EXAMPLE_URL + "html_content-type-test-page.html";
+const CONTENT_TYPE_WITHOUT_CACHE_URL = EXAMPLE_URL + "html_content-type-without-cache-test-page.html";
+const CONTENT_TYPE_WITHOUT_CACHE_REQUESTS = 8;
+const CYRILLIC_URL = EXAMPLE_URL + "html_cyrillic-test-page.html";
+const STATUS_CODES_URL = EXAMPLE_URL + "html_status-codes-test-page.html";
+const POST_DATA_URL = EXAMPLE_URL + "html_post-data-test-page.html";
+const POST_JSON_URL = EXAMPLE_URL + "html_post-json-test-page.html";
+const POST_RAW_URL = EXAMPLE_URL + "html_post-raw-test-page.html";
+const POST_RAW_WITH_HEADERS_URL = EXAMPLE_URL + "html_post-raw-with-headers-test-page.html";
+const PARAMS_URL = EXAMPLE_URL + "html_params-test-page.html";
+const JSONP_URL = EXAMPLE_URL + "html_jsonp-test-page.html";
+const JSON_LONG_URL = EXAMPLE_URL + "html_json-long-test-page.html";
+const JSON_MALFORMED_URL = EXAMPLE_URL + "html_json-malformed-test-page.html";
+const JSON_CUSTOM_MIME_URL = EXAMPLE_URL + "html_json-custom-mime-test-page.html";
+const JSON_TEXT_MIME_URL = EXAMPLE_URL + "html_json-text-mime-test-page.html";
+const SORTING_URL = EXAMPLE_URL + "html_sorting-test-page.html";
+const FILTERING_URL = EXAMPLE_URL + "html_filter-test-page.html";
+const INFINITE_GET_URL = EXAMPLE_URL + "html_infinite-get-page.html";
+const CUSTOM_GET_URL = EXAMPLE_URL + "html_custom-get-page.html";
+const SINGLE_GET_URL = EXAMPLE_URL + "html_single-get-page.html";
+const STATISTICS_URL = EXAMPLE_URL + "html_statistics-test-page.html";
+const CURL_URL = EXAMPLE_URL + "html_copy-as-curl.html";
+const CURL_UTILS_URL = EXAMPLE_URL + "html_curl-utils.html";
+const SEND_BEACON_URL = EXAMPLE_URL + "html_send-beacon.html";
+const CORS_URL = EXAMPLE_URL + "html_cors-test-page.html";
+
+const SIMPLE_SJS = EXAMPLE_URL + "sjs_simple-test-server.sjs";
+const CONTENT_TYPE_SJS = EXAMPLE_URL + "sjs_content-type-test-server.sjs";
+const HTTPS_CONTENT_TYPE_SJS = HTTPS_EXAMPLE_URL + "sjs_content-type-test-server.sjs";
+const STATUS_CODES_SJS = EXAMPLE_URL + "sjs_status-codes-test-server.sjs";
+const SORTING_SJS = EXAMPLE_URL + "sjs_sorting-test-server.sjs";
+const HTTPS_REDIRECT_SJS = EXAMPLE_URL + "sjs_https-redirect-test-server.sjs";
+const CORS_SJS_PATH = "/browser/devtools/client/netmonitor/test/sjs_cors-test-server.sjs";
+const HSTS_SJS = EXAMPLE_URL + "sjs_hsts-test-server.sjs";
+
+const HSTS_BASE_URL = EXAMPLE_URL;
+const HSTS_PAGE_URL = CUSTOM_GET_URL;
+
+const TEST_IMAGE = EXAMPLE_URL + "test-image.png";
+const TEST_IMAGE_DATA_URI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==";
+
+const FRAME_SCRIPT_UTILS_URL = "chrome://devtools/content/shared/frame-script-utils.js";
+
+// All tests are asynchronous.
+waitForExplicitFinish();
+
+const gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
+// To enable logging for try runs, just set the pref to true.
+Services.prefs.setBoolPref("devtools.debugger.log", false);
+
+// Uncomment this pref to dump all devtools emitted events to the console.
+// Services.prefs.setBoolPref("devtools.dump.emit", true);
+
+// Always reset some prefs to their original values after the test finishes.
+const gDefaultFilters = Services.prefs.getCharPref("devtools.netmonitor.filters");
+
+registerCleanupFunction(() => {
+ info("finish() was called, cleaning up...");
+
+ Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
+ Services.prefs.setCharPref("devtools.netmonitor.filters", gDefaultFilters);
+ Services.prefs.clearUserPref("devtools.cache.disabled");
+});
+
+function waitForNavigation(aTarget) {
+ let deferred = promise.defer();
+ aTarget.once("will-navigate", () => {
+ aTarget.once("navigate", () => {
+ deferred.resolve();
+ });
+ });
+ return deferred.promise;
+}
+
+function reconfigureTab(aTarget, aOptions) {
+ let deferred = promise.defer();
+ aTarget.activeTab.reconfigure(aOptions, deferred.resolve);
+ return deferred.promise;
+}
+
+function toggleCache(aTarget, aDisabled) {
+ let options = { cacheDisabled: aDisabled, performReload: true };
+ let navigationFinished = waitForNavigation(aTarget);
+
+ // Disable the cache for any toolbox that it is opened from this point on.
+ Services.prefs.setBoolPref("devtools.cache.disabled", aDisabled);
+
+ return reconfigureTab(aTarget, options).then(() => navigationFinished);
+}
+
+function initNetMonitor(aUrl, aWindow, aEnableCache) {
+ info("Initializing a network monitor pane.");
+
+ return Task.spawn(function* () {
+ let tab = yield addTab(aUrl);
+ info("Net tab added successfully: " + aUrl);
+
+ let target = TargetFactory.forTab(tab);
+
+ yield target.makeRemote();
+ info("Target remoted.");
+
+ if (!aEnableCache) {
+ info("Disabling cache and reloading page.");
+ yield toggleCache(target, true);
+ info("Cache disabled when the current and all future toolboxes are open.");
+ // Remove any requests generated by the reload while toggling the cache to
+ // avoid interfering with the test.
+ isnot([...target.activeConsole.getNetworkEvents()].length, 0,
+ "Request to reconfigure the tab was recorded.");
+ target.activeConsole.clearNetworkRequests();
+ }
+
+ let toolbox = yield gDevTools.showToolbox(target, "netmonitor");
+ info("Network monitor pane shown successfully.");
+
+ let monitor = toolbox.getCurrentPanel();
+ return {tab, monitor};
+ });
+}
+
+function restartNetMonitor(monitor, newUrl) {
+ info("Restarting the specified network monitor.");
+
+ return Task.spawn(function* () {
+ let tab = monitor.target.tab;
+ let url = newUrl || tab.linkedBrowser.currentURI.spec;
+
+ let onDestroyed = monitor.once("destroyed");
+ yield removeTab(tab);
+ yield onDestroyed;
+
+ return initNetMonitor(url);
+ });
+}
+
+function teardown(monitor) {
+ info("Destroying the specified network monitor.");
+
+ return Task.spawn(function* () {
+ let tab = monitor.target.tab;
+
+ let onDestroyed = monitor.once("destroyed");
+ yield removeTab(tab);
+ yield onDestroyed;
+ });
+}
+
+function waitForNetworkEvents(aMonitor, aGetRequests, aPostRequests = 0) {
+ let deferred = promise.defer();
+
+ let panel = aMonitor.panelWin;
+ let events = panel.EVENTS;
+
+ let progress = {};
+ let genericEvents = 0;
+ let postEvents = 0;
+
+ let awaitedEventsToListeners = [
+ ["UPDATING_REQUEST_HEADERS", onGenericEvent],
+ ["RECEIVED_REQUEST_HEADERS", onGenericEvent],
+ ["UPDATING_REQUEST_COOKIES", onGenericEvent],
+ ["RECEIVED_REQUEST_COOKIES", onGenericEvent],
+ ["UPDATING_REQUEST_POST_DATA", onPostEvent],
+ ["RECEIVED_REQUEST_POST_DATA", onPostEvent],
+ ["UPDATING_RESPONSE_HEADERS", onGenericEvent],
+ ["RECEIVED_RESPONSE_HEADERS", onGenericEvent],
+ ["UPDATING_RESPONSE_COOKIES", onGenericEvent],
+ ["RECEIVED_RESPONSE_COOKIES", onGenericEvent],
+ ["STARTED_RECEIVING_RESPONSE", onGenericEvent],
+ ["UPDATING_RESPONSE_CONTENT", onGenericEvent],
+ ["RECEIVED_RESPONSE_CONTENT", onGenericEvent],
+ ["UPDATING_EVENT_TIMINGS", onGenericEvent],
+ ["RECEIVED_EVENT_TIMINGS", onGenericEvent]
+ ];
+
+ function initProgressForURL(url) {
+ if (progress[url]) return;
+ progress[url] = {};
+ awaitedEventsToListeners.forEach(([e]) => progress[url][e] = 0);
+ }
+
+ function updateProgressForURL(url, event) {
+ initProgressForURL(url);
+ progress[url][Object.keys(events).find(e => events[e] == event)] = 1;
+ }
+
+ function onGenericEvent(event, actor) {
+ genericEvents++;
+ maybeResolve(event, actor);
+ }
+
+ function onPostEvent(event, actor) {
+ postEvents++;
+ maybeResolve(event, actor);
+ }
+
+ function maybeResolve(event, actor) {
+ info("> Network events progress: " +
+ genericEvents + "/" + ((aGetRequests + aPostRequests) * 13) + ", " +
+ postEvents + "/" + (aPostRequests * 2) + ", " +
+ "got " + event + " for " + actor);
+
+ let networkInfo =
+ panel.NetMonitorController.webConsoleClient.getNetworkRequest(actor);
+ let url = networkInfo.request.url;
+ updateProgressForURL(url, event);
+
+ // Uncomment this to get a detailed progress logging (when debugging a test)
+ // info("> Current state: " + JSON.stringify(progress, null, 2));
+
+ // There are 15 updates which need to be fired for a request to be
+ // considered finished. The "requestPostData" packet isn't fired for
+ // non-POST requests.
+ if (genericEvents >= (aGetRequests + aPostRequests) * 13 &&
+ postEvents >= aPostRequests * 2) {
+
+ awaitedEventsToListeners.forEach(([e, l]) => panel.off(events[e], l));
+ executeSoon(deferred.resolve);
+ }
+ }
+
+ awaitedEventsToListeners.forEach(([e, l]) => panel.on(events[e], l));
+ return deferred.promise;
+}
+
+function verifyRequestItemTarget(aRequestItem, aMethod, aUrl, aData = {}) {
+ info("> Verifying: " + aMethod + " " + aUrl + " " + aData.toSource());
+ // This bloats log sizes significantly in automation (bug 992485)
+ // info("> Request: " + aRequestItem.attachment.toSource());
+
+ let requestsMenu = aRequestItem.ownerView;
+ let widgetIndex = requestsMenu.indexOfItem(aRequestItem);
+ let visibleIndex = requestsMenu.visibleItems.indexOf(aRequestItem);
+
+ info("Widget index of item: " + widgetIndex);
+ info("Visible index of item: " + visibleIndex);
+
+ let { fuzzyUrl, status, statusText, cause, type, fullMimeType,
+ transferred, size, time, displayedStatus } = aData;
+ let { attachment, target } = aRequestItem;
+
+ let uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
+ let unicodeUrl = NetworkHelper.convertToUnicode(unescape(aUrl));
+ let name = NetworkHelper.convertToUnicode(unescape(uri.fileName || uri.filePath || "/"));
+ let query = NetworkHelper.convertToUnicode(unescape(uri.query));
+ let hostPort = uri.hostPort;
+ let remoteAddress = attachment.remoteAddress;
+
+ if (fuzzyUrl) {
+ ok(attachment.method.startsWith(aMethod), "The attached method is correct.");
+ ok(attachment.url.startsWith(aUrl), "The attached url is correct.");
+ } else {
+ is(attachment.method, aMethod, "The attached method is correct.");
+ is(attachment.url, aUrl, "The attached url is correct.");
+ }
+
+ is(target.querySelector(".requests-menu-method").getAttribute("value"),
+ aMethod, "The displayed method is correct.");
+
+ if (fuzzyUrl) {
+ ok(target.querySelector(".requests-menu-file").getAttribute("value").startsWith(
+ name + (query ? "?" + query : "")), "The displayed file is correct.");
+ ok(target.querySelector(".requests-menu-file").getAttribute("tooltiptext").startsWith(unicodeUrl),
+ "The tooltip file is correct.");
+ } else {
+ is(target.querySelector(".requests-menu-file").getAttribute("value"),
+ name + (query ? "?" + query : ""), "The displayed file is correct.");
+ is(target.querySelector(".requests-menu-file").getAttribute("tooltiptext"),
+ unicodeUrl, "The tooltip file is correct.");
+ }
+
+ is(target.querySelector(".requests-menu-domain").getAttribute("value"),
+ hostPort, "The displayed domain is correct.");
+
+ let domainTooltip = hostPort + (remoteAddress ? " (" + remoteAddress + ")" : "");
+ is(target.querySelector(".requests-menu-domain").getAttribute("tooltiptext"),
+ domainTooltip, "The tooltip domain is correct.");
+
+ if (status !== undefined) {
+ let value = target.querySelector(".requests-menu-status-icon").getAttribute("code");
+ let codeValue = target.querySelector(".requests-menu-status-code").getAttribute("value");
+ let tooltip = target.querySelector(".requests-menu-status").getAttribute("tooltiptext");
+ info("Displayed status: " + value);
+ info("Displayed code: " + codeValue);
+ info("Tooltip status: " + tooltip);
+ is(value, displayedStatus ? displayedStatus : status, "The displayed status is correct.");
+ is(codeValue, status, "The displayed status code is correct.");
+ is(tooltip, status + " " + statusText, "The tooltip status is correct.");
+ }
+ if (cause !== undefined) {
+ let causeLabel = target.querySelector(".requests-menu-cause-label");
+ let value = causeLabel.getAttribute("value");
+ let tooltip = causeLabel.getAttribute("tooltiptext");
+ info("Displayed cause: " + value);
+ info("Tooltip cause: " + tooltip);
+ is(value, cause.type, "The displayed cause is correct.");
+ is(tooltip, cause.loadingDocumentUri, "The tooltip cause is correct.")
+ }
+ if (type !== undefined) {
+ let value = target.querySelector(".requests-menu-type").getAttribute("value");
+ let tooltip = target.querySelector(".requests-menu-type").getAttribute("tooltiptext");
+ info("Displayed type: " + value);
+ info("Tooltip type: " + tooltip);
+ is(value, type, "The displayed type is correct.");
+ is(tooltip, fullMimeType, "The tooltip type is correct.");
+ }
+ if (transferred !== undefined) {
+ let value = target.querySelector(".requests-menu-transferred").getAttribute("value");
+ let tooltip = target.querySelector(".requests-menu-transferred").getAttribute("tooltiptext");
+ info("Displayed transferred size: " + value);
+ info("Tooltip transferred size: " + tooltip);
+ is(value, transferred, "The displayed transferred size is correct.");
+ is(tooltip, transferred, "The tooltip transferred size is correct.");
+ }
+ if (size !== undefined) {
+ let value = target.querySelector(".requests-menu-size").getAttribute("value");
+ let tooltip = target.querySelector(".requests-menu-size").getAttribute("tooltiptext");
+ info("Displayed size: " + value);
+ info("Tooltip size: " + tooltip);
+ is(value, size, "The displayed size is correct.");
+ is(tooltip, size, "The tooltip size is correct.");
+ }
+ if (time !== undefined) {
+ let value = target.querySelector(".requests-menu-timings-total").getAttribute("value");
+ let tooltip = target.querySelector(".requests-menu-timings-total").getAttribute("tooltiptext");
+ info("Displayed time: " + value);
+ info("Tooltip time: " + tooltip);
+ ok(~~(value.match(/[0-9]+/)) >= 0, "The displayed time is correct.");
+ ok(~~(tooltip.match(/[0-9]+/)) >= 0, "The tooltip time is correct.");
+ }
+
+ if (visibleIndex != -1) {
+ if (visibleIndex % 2 == 0) {
+ ok(aRequestItem.target.hasAttribute("even"),
+ aRequestItem.value + " should have 'even' attribute.");
+ ok(!aRequestItem.target.hasAttribute("odd"),
+ aRequestItem.value + " shouldn't have 'odd' attribute.");
+ } else {
+ ok(!aRequestItem.target.hasAttribute("even"),
+ aRequestItem.value + " shouldn't have 'even' attribute.");
+ ok(aRequestItem.target.hasAttribute("odd"),
+ aRequestItem.value + " should have 'odd' attribute.");
+ }
+ }
+}
+
+/**
+ * Helper function for waiting for an event to fire before resolving a promise.
+ * Example: waitFor(aMonitor.panelWin, aMonitor.panelWin.EVENTS.TAB_UPDATED);
+ *
+ * @param object subject
+ * The event emitter object that is being listened to.
+ * @param string eventName
+ * The name of the event to listen to.
+ * @return object
+ * Returns a promise that resolves upon firing of the event.
+ */
+function waitFor(subject, eventName) {
+ let deferred = promise.defer();
+ subject.once(eventName, deferred.resolve);
+ return deferred.promise;
+}
+
+/**
+ * Tests if a button for a filter of given type is the only one checked.
+ *
+ * @param string filterType
+ * The type of the filter that should be the only one checked.
+ */
+function testFilterButtons(monitor, filterType) {
+ let doc = monitor.panelWin.document;
+ let target = doc.querySelector("#requests-menu-filter-" + filterType + "-button");
+ ok(target, `Filter button '${filterType}' was found`);
+ let buttons = [...doc.querySelectorAll(".menu-filter-button")];
+ ok(buttons.length > 0, "More than zero filter buttons were found");
+
+ // Only target should be checked.
+ let checkStatus = buttons.map(button => button == target ? 1 : 0);
+ testFilterButtonsCustom(monitor, checkStatus);
+}
+
+/**
+ * Tests if filter buttons have 'checked' attributes set correctly.
+ *
+ * @param array aIsChecked
+ * An array specifying if a button at given index should have a
+ * 'checked' attribute. For example, if the third item of the array
+ * evaluates to true, the third button should be checked.
+ */
+function testFilterButtonsCustom(aMonitor, aIsChecked) {
+ let doc = aMonitor.panelWin.document;
+ let buttons = doc.querySelectorAll(".menu-filter-button");
+ for (let i = 0; i < aIsChecked.length; i++) {
+ let button = buttons[i];
+ if (aIsChecked[i]) {
+ is(button.classList.contains("checked"), true,
+ "The " + button.id + " button should have a 'checked' class.");
+ } else {
+ is(button.classList.contains("checked"), false,
+ "The " + button.id + " button should not have a 'checked' class.");
+ }
+ }
+}
+
+/**
+ * Loads shared/frame-script-utils.js in the specified tab.
+ *
+ * @param tab
+ * Optional tab to load the frame script in. Defaults to the current tab.
+ */
+function loadCommonFrameScript(tab) {
+ let browser = tab ? tab.linkedBrowser : gBrowser.selectedBrowser;
+
+ browser.messageManager.loadFrameScript(FRAME_SCRIPT_UTILS_URL, false);
+}
+
+/**
+ * Perform the specified requests in the context of the page content.
+ *
+ * @param Array requests
+ * An array of objects specifying the requests to perform. See
+ * shared/frame-script-utils.js for more information.
+ *
+ * @return A promise that resolves once the requests complete.
+ */
+function performRequestsInContent(requests) {
+ info("Performing requests in the context of the content.");
+ return executeInContent("devtools:test:xhr", requests);
+}
+
+/**
+ * Send an async message to the frame script (chrome -> content) and wait for a
+ * response message with the same name (content -> chrome).
+ *
+ * @param String name
+ * The message name. Should be one of the messages defined
+ * shared/frame-script-utils.js
+ * @param Object data
+ * Optional data to send along
+ * @param Object objects
+ * Optional CPOW objects to send along
+ * @param Boolean expectResponse
+ * If set to false, don't wait for a response with the same name from the
+ * content script. Defaults to true.
+ *
+ * @return Promise
+ * Resolves to the response data if a response is expected, immediately
+ * resolves otherwise
+ */
+function executeInContent(name, data = {}, objects = {}, expectResponse = true) {
+ let mm = gBrowser.selectedBrowser.messageManager;
+
+ mm.sendAsyncMessage(name, data, objects);
+ if (expectResponse) {
+ return waitForContentMessage(name);
+ } else {
+ return promise.resolve();
+ }
+}
+
+/**
+ * Wait for a content -> chrome message on the message manager (the window
+ * messagemanager is used).
+ * @param {String} name The message name
+ * @return {Promise} A promise that resolves to the response data when the
+ * message has been received
+ */
+function waitForContentMessage(name) {
+ let mm = gBrowser.selectedBrowser.messageManager;
+
+ let def = promise.defer();
+ mm.addMessageListener(name, function onMessage(msg) {
+ mm.removeMessageListener(name, onMessage);
+ def.resolve(msg);
+ });
+ return def.promise;
+}
+
+/**
+ * Open the requestMenu menu and return all of it's items in a flat array
+ * @param {netmonitorPanel} netmonitor
+ * @param {Event} event mouse event with screenX and screenX coordinates
+ * @return An array of MenuItems
+ */
+function openContextMenuAndGetAllItems(netmonitor, event) {
+ let menu = netmonitor.RequestsMenu.contextMenu.open(event);
+
+ // Flatten all menu items into a single array to make searching through it easier
+ let allItems = [].concat.apply([], menu.items.map(function addItem(item) {
+ if (item.submenu) {
+ return addItem(item.submenu.items);
+ }
+ return item;
+ }));
+
+ return allItems;
+}
diff --git a/devtools/client/netmonitor/test/html_api-calls-test-page.html b/devtools/client/netmonitor/test/html_api-calls-test-page.html
new file mode 100644
index 000000000..e31872319
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_api-calls-test-page.html
@@ -0,0 +1,46 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>API calls request test</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send();
+ }
+
+ function performRequests() {
+ get("/api/fileName.xml", function() {
+ get("/api/file%E2%98%A2.xml", function() {
+ get("/api/ascii/get/", function() {
+ get("/api/unicode/%E2%98%A2/", function() {
+ get("/api/search/?q=search%E2%98%A2", function() {
+ // Done.
+ });
+ });
+ });
+ });
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_brotli-test-page.html b/devtools/client/netmonitor/test/html_brotli-test-page.html
new file mode 100644
index 000000000..d5afae4b3
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_brotli-test-page.html
@@ -0,0 +1,38 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Brotli test</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ function performRequests() {
+ get("sjs_content-type-test-server.sjs?fmt=br", function() {
+ // Done.
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_cause-test-page.html b/devtools/client/netmonitor/test/html_cause-test-page.html
new file mode 100644
index 000000000..d2b86682b
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_cause-test-page.html
@@ -0,0 +1,48 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ <link rel="stylesheet" type="text/css" href="stylesheet_request" />
+ </head>
+
+ <body>
+ <p>Request cause test</p>
+ <img src="img_request" />
+ <script type="text/javascript">
+ function performXhrRequest() {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", "xhr_request", true);
+ xhr.send();
+ }
+
+ function performFetchRequest() {
+ fetch("fetch_request");
+ }
+
+ function performBeaconRequest() {
+ navigator.sendBeacon("beacon_request");
+ }
+
+ performXhrRequest();
+ performFetchRequest();
+
+ // Perform some requests with async stacks
+ Promise.resolve().then(function performPromiseFetchRequest() {
+ fetch("promise_fetch_request");
+ setTimeout(function performTimeoutFetchRequest() {
+ fetch("timeout_fetch_request");
+
+ // Finally, send a beacon request
+ performBeaconRequest();
+ }, 0);
+ });
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/netmonitor/test/html_content-type-test-page.html b/devtools/client/netmonitor/test/html_content-type-test-page.html
new file mode 100644
index 000000000..23ecf1f44
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_content-type-test-page.html
@@ -0,0 +1,48 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Content type test</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ function performRequests() {
+ get("sjs_content-type-test-server.sjs?fmt=xml", function() {
+ get("sjs_content-type-test-server.sjs?fmt=css", function() {
+ get("sjs_content-type-test-server.sjs?fmt=js", function() {
+ get("sjs_content-type-test-server.sjs?fmt=json", function() {
+ get("sjs_content-type-test-server.sjs?fmt=bogus", function() {
+ get("test-image.png", function() {
+ // Done.
+ });
+ });
+ });
+ });
+ });
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_content-type-without-cache-test-page.html b/devtools/client/netmonitor/test/html_content-type-without-cache-test-page.html
new file mode 100644
index 000000000..f27e6e105
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_content-type-without-cache-test-page.html
@@ -0,0 +1,52 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Content type test</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ function performRequests() {
+ get("sjs_content-type-test-server.sjs?fmt=xml", function() {
+ get("sjs_content-type-test-server.sjs?fmt=css", function() {
+ get("sjs_content-type-test-server.sjs?fmt=js", function() {
+ get("sjs_content-type-test-server.sjs?fmt=json", function() {
+ get("sjs_content-type-test-server.sjs?fmt=bogus", function() {
+ get("test-image.png?v=" + Math.random(), function() {
+ get("sjs_content-type-test-server.sjs?fmt=gzip", function() {
+ get("sjs_content-type-test-server.sjs?fmt=br", function() {
+ // Done.
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_copy-as-curl.html b/devtools/client/netmonitor/test/html_copy-as-curl.html
new file mode 100644
index 000000000..3ddcfbced
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_copy-as-curl.html
@@ -0,0 +1,30 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Performing a GET request</p>
+
+ <script type="text/javascript">
+ function performRequest(aUrl) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aUrl, true);
+ xhr.setRequestHeader("Accept-Language", window.navigator.language);
+ xhr.setRequestHeader("X-Custom-Header-1", "Custom value");
+ xhr.setRequestHeader("X-Custom-Header-2", "8.8.8.8");
+ xhr.setRequestHeader("X-Custom-Header-3", "Mon, 3 Mar 2014 11:11:11 GMT");
+ xhr.send(null);
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_cors-test-page.html b/devtools/client/netmonitor/test/html_cors-test-page.html
new file mode 100644
index 000000000..179b2ed00
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_cors-test-page.html
@@ -0,0 +1,31 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>POST with CORS test page</p>
+
+ <script type="text/javascript">
+ function post(url, contentType, postData) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", url, true);
+ xhr.setRequestHeader("Content-Type", contentType);
+ xhr.send(postData);
+ }
+
+ function performRequests(url, contentType, postData) {
+ post(url, contentType, postData);
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_curl-utils.html b/devtools/client/netmonitor/test/html_curl-utils.html
new file mode 100644
index 000000000..8ff7ecdf0
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_curl-utils.html
@@ -0,0 +1,102 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Performing requests</p>
+
+ <p>
+ <canvas width="100" height="100"></canvas>
+ </p>
+
+ <hr/>
+
+ <form method="post" action="#" enctype="multipart/form-data" target="target" id="post-form">
+ <input type="text" name="param1" value="value1"/>
+ <input type="text" name="param2" value="value2"/>
+ <input type="text" name="param3" value="value3"/>
+ <input type="submit"/>
+ </form>
+ <iframe name="target"></iframe>
+
+ <script type="text/javascript">
+
+ function ajaxGet(aUrl, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aUrl + "?param1=value1&param2=value2&param3=value3", true);
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+ xhr.onload = function() {
+ aCallback();
+ };
+ xhr.send();
+ }
+
+ function ajaxPost(aUrl, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", aUrl, true);
+ xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+ xhr.onload = function() {
+ aCallback();
+ };
+ var params = "param1=value1&param2=value2&param3=value3";
+ xhr.send(params);
+ }
+
+ function ajaxMultipart(aUrl, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", aUrl, true);
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+ xhr.onload = function() {
+ aCallback();
+ };
+
+ getCanvasElem().toBlob((blob) => {
+ var formData = new FormData();
+ formData.append("param1", "value1");
+ formData.append("file", blob, "filename.png");
+ xhr.send(formData);
+ });
+ }
+
+ function submitForm() {
+ var form = document.querySelector("#post-form");
+ form.submit();
+ }
+
+ function getCanvasElem() {
+ return document.querySelector("canvas");
+ }
+
+ function initCanvas() {
+ var canvas = getCanvasElem();
+ var ctx = canvas.getContext("2d");
+ ctx.fillRect(0,0,100,100);
+ ctx.clearRect(20,20,60,60);
+ ctx.strokeRect(25,25,50,50);
+ }
+
+ function performRequests(aUrl) {
+ ajaxGet(aUrl, () => {
+ ajaxPost(aUrl, () => {
+ ajaxMultipart(aUrl, () => {
+ submitForm();
+ });
+ });
+ });
+ }
+
+ initCanvas();
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_custom-get-page.html b/devtools/client/netmonitor/test/html_custom-get-page.html
new file mode 100644
index 000000000..19e40f93a
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_custom-get-page.html
@@ -0,0 +1,44 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Performing a custom number of GETs</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ // Use a count parameter to defeat caching.
+ var count = 0;
+
+ function performRequests(aTotal, aUrl, aTimeout = 0) {
+ if (!aTotal) {
+ return;
+ }
+ get(aUrl || "request_" + (count++), function() {
+ setTimeout(performRequests.bind(this, --aTotal, aUrl, aTimeout), aTimeout);
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_cyrillic-test-page.html b/devtools/client/netmonitor/test/html_cyrillic-test-page.html
new file mode 100644
index 000000000..8735ac674
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_cyrillic-test-page.html
@@ -0,0 +1,39 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Cyrillic type test</p>
+ <p>Братан, ты вообще качаешься?</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ function performRequests() {
+ get("sjs_content-type-test-server.sjs?fmt=txt", function() {
+ // Done.
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_filter-test-page.html b/devtools/client/netmonitor/test/html_filter-test-page.html
new file mode 100644
index 000000000..eb5d02ed9
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_filter-test-page.html
@@ -0,0 +1,60 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Filtering test</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ // Use a random parameter to defeat caching.
+ xhr.open("GET", aAddress + "&" + Math.random(), true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ function performRequests(aOptions) {
+ var options = JSON.parse(aOptions);
+ get("sjs_content-type-test-server.sjs?fmt=html&res=" + options.htmlContent, function() {
+ get("sjs_content-type-test-server.sjs?fmt=css", function() {
+ get("sjs_content-type-test-server.sjs?fmt=js", function() {
+ if (!options.getMedia) {
+ return;
+ }
+ get("sjs_content-type-test-server.sjs?fmt=font", function() {
+ get("sjs_content-type-test-server.sjs?fmt=image", function() {
+ get("sjs_content-type-test-server.sjs?fmt=audio", function() {
+ get("sjs_content-type-test-server.sjs?fmt=video", function() {
+ if (!options.getFlash) {
+ return;
+ }
+ get("sjs_content-type-test-server.sjs?fmt=flash", function() {
+ // Done.
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_frame-subdocument.html b/devtools/client/netmonitor/test/html_frame-subdocument.html
new file mode 100644
index 000000000..9e800582c
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_frame-subdocument.html
@@ -0,0 +1,48 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ <link rel="stylesheet" type="text/css" href="stylesheet_request" />
+ </head>
+
+ <body>
+ <p>Request frame test</p>
+ <img src="img_request" />
+ <script type="text/javascript">
+ function performXhrRequest() {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", "xhr_request", true);
+ xhr.send();
+ }
+
+ function performFetchRequest() {
+ fetch("fetch_request");
+ }
+
+ function performBeaconRequest() {
+ navigator.sendBeacon("beacon_request");
+ }
+
+ performXhrRequest();
+ performFetchRequest();
+
+ // Perform some requests with async stacks
+ Promise.resolve().then(function performPromiseFetchRequest() {
+ fetch("promise_fetch_request");
+ setTimeout(function performTimeoutFetchRequest() {
+ fetch("timeout_fetch_request");
+
+ // Finally, send a beacon request
+ performBeaconRequest();
+ }, 0);
+ });
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/netmonitor/test/html_frame-test-page.html b/devtools/client/netmonitor/test/html_frame-test-page.html
new file mode 100644
index 000000000..66f6620af
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_frame-test-page.html
@@ -0,0 +1,49 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ <link rel="stylesheet" type="text/css" href="stylesheet_request" />
+ </head>
+
+ <body>
+ <p>Request frame test</p>
+ <img src="img_request" />
+ <iframe src="html_frame-subdocument.html"></iframe>
+ <script type="text/javascript">
+ function performXhrRequest() {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", "xhr_request", true);
+ xhr.send();
+ }
+
+ function performFetchRequest() {
+ fetch("fetch_request");
+ }
+
+ function performBeaconRequest() {
+ navigator.sendBeacon("beacon_request");
+ }
+
+ performXhrRequest();
+ performFetchRequest();
+
+ // Perform some requests with async stacks
+ Promise.resolve().then(function performPromiseFetchRequest() {
+ fetch("promise_fetch_request");
+ setTimeout(function performTimeoutFetchRequest() {
+ fetch("timeout_fetch_request");
+
+ // Finally, send a beacon request
+ performBeaconRequest();
+ }, 0);
+ });
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/netmonitor/test/html_image-tooltip-test-page.html b/devtools/client/netmonitor/test/html_image-tooltip-test-page.html
new file mode 100644
index 000000000..c39db909e
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_image-tooltip-test-page.html
@@ -0,0 +1,26 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>tooltip test</p>
+
+ <script type="text/javascript">
+ function performRequests() {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", "test-image.png?v=" + Math.random(), true);
+ xhr.send(null);
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_infinite-get-page.html b/devtools/client/netmonitor/test/html_infinite-get-page.html
new file mode 100644
index 000000000..f51b718ad
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_infinite-get-page.html
@@ -0,0 +1,41 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Infinite GETs</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ // Use a count parameter to defeat caching.
+ var count = 0;
+
+ (function performRequests() {
+ get("request_" + (count++), function() {
+ setTimeout(performRequests, 50);
+ });
+ })();
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_json-custom-mime-test-page.html b/devtools/client/netmonitor/test/html_json-custom-mime-test-page.html
new file mode 100644
index 000000000..646fc60ea
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_json-custom-mime-test-page.html
@@ -0,0 +1,38 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>JSONP test</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ function performRequests() {
+ get("sjs_content-type-test-server.sjs?fmt=json-custom-mime", function() {
+ // Done.
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_json-long-test-page.html b/devtools/client/netmonitor/test/html_json-long-test-page.html
new file mode 100644
index 000000000..b538b4c27
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_json-long-test-page.html
@@ -0,0 +1,38 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>JSON long string test</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ function performRequests() {
+ get("sjs_content-type-test-server.sjs?fmt=json-long", function() {
+ // Done.
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_json-malformed-test-page.html b/devtools/client/netmonitor/test/html_json-malformed-test-page.html
new file mode 100644
index 000000000..0c8627ab5
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_json-malformed-test-page.html
@@ -0,0 +1,38 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>JSON malformed test</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ function performRequests() {
+ get("sjs_content-type-test-server.sjs?fmt=json-malformed", function() {
+ // Done.
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_json-text-mime-test-page.html b/devtools/client/netmonitor/test/html_json-text-mime-test-page.html
new file mode 100644
index 000000000..2c64e2531
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_json-text-mime-test-page.html
@@ -0,0 +1,38 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>JSON text test</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ function performRequests() {
+ get("sjs_content-type-test-server.sjs?fmt=json-text-mime", function() {
+ // Done.
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_jsonp-test-page.html b/devtools/client/netmonitor/test/html_jsonp-test-page.html
new file mode 100644
index 000000000..78c0da08b
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_jsonp-test-page.html
@@ -0,0 +1,40 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>JSONP test</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ function performRequests() {
+ get("sjs_content-type-test-server.sjs?fmt=jsonp&jsonp=$_0123Fun", function() {
+ get("sjs_content-type-test-server.sjs?fmt=jsonp2&jsonp=$_4567Sad", function() {
+ // Done.
+ });
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_navigate-test-page.html b/devtools/client/netmonitor/test/html_navigate-test-page.html
new file mode 100644
index 000000000..23f00f3df
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_navigate-test-page.html
@@ -0,0 +1,18 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Navigation test</p>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_params-test-page.html b/devtools/client/netmonitor/test/html_params-test-page.html
new file mode 100644
index 000000000..3f30e3d76
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_params-test-page.html
@@ -0,0 +1,67 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Request params type test</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aQuery) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress + aQuery, true);
+ xhr.send();
+ }
+
+ function post(aAddress, aQuery, aContentType, aPostBody) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", aAddress + aQuery, true);
+ xhr.setRequestHeader("content-type", aContentType);
+ xhr.send(aPostBody);
+ }
+
+ function performRequests() {
+ var urlencoded = "application/x-www-form-urlencoded";
+
+ setTimeout(function() {
+ post("baz", "?a", urlencoded, '{ "foo": "bar" }');
+
+ setTimeout(function() {
+ post("baz", "?a=b", urlencoded, '{ "foo": "bar" }');
+
+ setTimeout(function() {
+ post("baz", "?a=b", urlencoded, '?foo=bar');
+
+ setTimeout(function() {
+ post("baz", "?a", undefined, '{ "foo": "bar" }');
+
+ setTimeout(function() {
+ post("baz", "?a=b", undefined, '{ "foo": "bar" }');
+
+ setTimeout(function() {
+ post("baz", "?a=b", undefined, '?foo=bar');
+
+ setTimeout(function() {
+ get("baz", "");
+
+ // Done.
+ }, 10);
+ }, 10);
+ }, 10);
+ }, 10);
+ }, 10);
+ }, 10);
+ }, 10);
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_post-data-test-page.html b/devtools/client/netmonitor/test/html_post-data-test-page.html
new file mode 100644
index 000000000..8dedc7b60
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_post-data-test-page.html
@@ -0,0 +1,77 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ <style>
+ input {
+ display: block;
+ margin: 12px;
+ }
+ </style>
+ </head>
+
+ <body>
+ <p>POST data test</p>
+ <form enctype="multipart/form-data" method="post" name="form-name">
+ <input type="text" name="text" placeholder="text" value="Some text..."/>
+ <input type="email" name="email" placeholder="email"/>
+ <input type="range" name="range" value="42"/>
+ <input type="button" value="Post me!" onclick="window.form()">
+ </form>
+
+ <script type="text/javascript">
+ function post(aAddress, aMessage, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", aAddress, true);
+ xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+
+ var data = "";
+ for (var i in aMessage) {
+ data += "&" + i + "=" + aMessage[i];
+ }
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(data);
+ }
+
+ function form(aAddress, aForm, aCallback) {
+ var formData = new FormData(document.forms.namedItem(aForm));
+ formData.append("Custom field", "Extra data");
+
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(formData);
+ }
+
+ function performRequests() {
+ var url = "sjs_simple-test-server.sjs";
+ var url1 = url + "?foo=bar&baz=42&type=urlencoded";
+ var url2 = url + "?foo=bar&baz=42&type=multipart";
+
+ post(url1, { foo: "bar", baz: 123 }, function() {
+ form(url2, "form-name", function() {
+ // Done.
+ });
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_post-json-test-page.html b/devtools/client/netmonitor/test/html_post-json-test-page.html
new file mode 100644
index 000000000..129feaf08
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_post-json-test-page.html
@@ -0,0 +1,39 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>POST raw test</p>
+
+ <script type="text/javascript">
+ function post(address, message, callback) {
+ let xhr = new XMLHttpRequest();
+ xhr.open("POST", address, true);
+ xhr.setRequestHeader("Content-Type", "application/json");
+
+ xhr.onreadystatechange = function () {
+ if (this.readyState == this.DONE) {
+ callback();
+ }
+ };
+ xhr.send(message);
+ }
+
+ function performRequests() {
+ post("sjs_simple-test-server.sjs", JSON.stringify({a: 1}), function () {
+ // Done.
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_post-raw-test-page.html b/devtools/client/netmonitor/test/html_post-raw-test-page.html
new file mode 100644
index 000000000..b4456348c
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_post-raw-test-page.html
@@ -0,0 +1,40 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>POST raw test</p>
+
+ <script type="text/javascript">
+ function post(aAddress, aMessage, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", aAddress, true);
+ xhr.setRequestHeader("content-type", "application/x-www-form-urlencoded");
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(aMessage);
+ }
+
+ function performRequests() {
+ var rawData = "foo=bar&baz=123";
+ post("sjs_simple-test-server.sjs", rawData, function() {
+ // Done.
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_post-raw-with-headers-test-page.html b/devtools/client/netmonitor/test/html_post-raw-with-headers-test-page.html
new file mode 100644
index 000000000..3bb8f9071
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_post-raw-with-headers-test-page.html
@@ -0,0 +1,45 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>POST raw with headers test</p>
+
+ <script type="text/javascript">
+ function post(aAddress, aMessage, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(aMessage);
+ }
+
+ function performRequests() {
+ var rawData = [
+ "content-type: application/x-www-form-urlencoded\r",
+ "custom-header: hello world!\r",
+ "\r",
+ "\r",
+ "foo=bar&baz=123"
+ ];
+ post("sjs_simple-test-server.sjs", rawData.join("\n"), function() {
+ // Done.
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_send-beacon.html b/devtools/client/netmonitor/test/html_send-beacon.html
new file mode 100644
index 000000000..95cc005bd
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_send-beacon.html
@@ -0,0 +1,23 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Send beacon test</p>
+
+ <script type="text/javascript">
+ function performRequest() {
+ navigator.sendBeacon("beacon_request");
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/netmonitor/test/html_simple-test-page.html b/devtools/client/netmonitor/test/html_simple-test-page.html
new file mode 100644
index 000000000..846681dbd
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_simple-test-page.html
@@ -0,0 +1,18 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Simple test</p>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_single-get-page.html b/devtools/client/netmonitor/test/html_single-get-page.html
new file mode 100644
index 000000000..0055d4ee0
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_single-get-page.html
@@ -0,0 +1,36 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Performing a custom number of GETs</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ (function performRequests() {
+ get("request_0", function() {});
+ })();
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_sorting-test-page.html b/devtools/client/netmonitor/test/html_sorting-test-page.html
new file mode 100644
index 000000000..640c58b8e
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_sorting-test-page.html
@@ -0,0 +1,18 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Sorting test</p>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_statistics-test-page.html b/devtools/client/netmonitor/test/html_statistics-test-page.html
new file mode 100644
index 000000000..b4b15b82b
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_statistics-test-page.html
@@ -0,0 +1,40 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Statistics test</p>
+
+ <script type="text/javascript">
+ function get(aAddress) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+ xhr.send(null);
+ }
+
+ get("sjs_content-type-test-server.sjs?sts=304&fmt=txt");
+ get("sjs_content-type-test-server.sjs?sts=304&fmt=xml");
+ get("sjs_content-type-test-server.sjs?sts=304&fmt=html");
+ get("sjs_content-type-test-server.sjs?sts=304&fmt=css");
+ get("sjs_content-type-test-server.sjs?sts=304&fmt=js");
+ get("sjs_content-type-test-server.sjs?sts=304&fmt=json");
+ get("sjs_content-type-test-server.sjs?sts=304&fmt=jsonp");
+ get("sjs_content-type-test-server.sjs?sts=304&fmt=font");
+ get("sjs_content-type-test-server.sjs?sts=304&fmt=image");
+ get("sjs_content-type-test-server.sjs?sts=304&fmt=audio");
+ get("sjs_content-type-test-server.sjs?sts=304&fmt=video");
+ get("sjs_content-type-test-server.sjs?sts=304&fmt=flash");
+ get("test-image.png?v=" + Math.random());
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/html_status-codes-test-page.html b/devtools/client/netmonitor/test/html_status-codes-test-page.html
new file mode 100644
index 000000000..4be779bd4
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_status-codes-test-page.html
@@ -0,0 +1,55 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Status codes test</p>
+
+ <script type="text/javascript">
+ function get(aAddress, aCallback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", aAddress, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ aCallback();
+ }
+ };
+ xhr.send(null);
+ }
+
+ function performRequests() {
+ get("sjs_status-codes-test-server.sjs?sts=100", function() {
+ get("sjs_status-codes-test-server.sjs?sts=200", function() {
+ get("sjs_status-codes-test-server.sjs?sts=300", function() {
+ get("sjs_status-codes-test-server.sjs?sts=400", function() {
+ get("sjs_status-codes-test-server.sjs?sts=500", function() {
+ // Done.
+ });
+ });
+ });
+ });
+ });
+ }
+
+ function performCachedRequests() {
+ get("sjs_status-codes-test-server.sjs?sts=ok&cached", function() {
+ get("sjs_status-codes-test-server.sjs?sts=redirect&cached", function() {
+ // Done.
+ });
+ });
+ }
+
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/service-workers/status-codes-service-worker.js b/devtools/client/netmonitor/test/service-workers/status-codes-service-worker.js
new file mode 100644
index 000000000..3c70c7dcb
--- /dev/null
+++ b/devtools/client/netmonitor/test/service-workers/status-codes-service-worker.js
@@ -0,0 +1,15 @@
+/* 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";
+
+self.addEventListener("activate", event => {
+ // start controlling the already loaded page
+ event.waitUntil(self.clients.claim());
+});
+
+self.addEventListener("fetch", event => {
+ let response = new Response("Service worker response");
+ event.respondWith(response);
+});
diff --git a/devtools/client/netmonitor/test/service-workers/status-codes.html b/devtools/client/netmonitor/test/service-workers/status-codes.html
new file mode 100644
index 000000000..65c79ee00
--- /dev/null
+++ b/devtools/client/netmonitor/test/service-workers/status-codes.html
@@ -0,0 +1,59 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Status codes test</p>
+
+ <script type="text/javascript">
+ let swRegistration;
+
+ function registerServiceWorker() {
+ let sw = navigator.serviceWorker;
+ return sw.register("status-codes-service-worker.js")
+ .then(registration => {
+ swRegistration = registration;
+ console.log("Registered, scope is:", registration.scope);
+ return sw.ready;
+ }).then(() => {
+ // wait until the page is controlled
+ return new Promise(resolve => {
+ if (sw.controller) {
+ resolve();
+ } else {
+ sw.addEventListener('controllerchange', function onControllerChange() {
+ sw.removeEventListener('controllerchange', onControllerChange);
+ resolve();
+ });
+ }
+ });
+ }).catch(err => {
+ console.error("Registration failed");
+ });
+ }
+
+ function unregisterServiceWorker() {
+ return swRegistration.unregister();
+ }
+
+ function performRequests() {
+ return new Promise(function doXHR(done) {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", "test/200", true);
+ xhr.onreadystatechange = done;
+ xhr.send(null);
+ });
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/netmonitor/test/sjs_content-type-test-server.sjs b/devtools/client/netmonitor/test/sjs_content-type-test-server.sjs
new file mode 100644
index 000000000..ee9a82e27
--- /dev/null
+++ b/devtools/client/netmonitor/test/sjs_content-type-test-server.sjs
@@ -0,0 +1,273 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { classes: Cc, interfaces: Ci } = Components;
+
+function gzipCompressString(string, obs) {
+
+ let scs = Cc["@mozilla.org/streamConverters;1"]
+ .getService(Ci.nsIStreamConverterService);
+ let listener = Cc["@mozilla.org/network/stream-loader;1"]
+ .createInstance(Ci.nsIStreamLoader);
+ listener.init(obs);
+ let converter = scs.asyncConvertData("uncompressed", "gzip",
+ listener, null);
+ let stringStream = Cc["@mozilla.org/io/string-input-stream;1"]
+ .createInstance(Ci.nsIStringInputStream);
+ stringStream.data = string;
+ converter.onStartRequest(null, null);
+ converter.onDataAvailable(null, null, stringStream, 0, string.length);
+ converter.onStopRequest(null, null, null);
+}
+
+function doubleGzipCompressString(string, observer) {
+ let observer2 = {
+ onStreamComplete: function(loader, context, status, length, result) {
+ let buffer = String.fromCharCode.apply(this, result);
+ gzipCompressString(buffer, observer);
+ }
+ };
+ gzipCompressString(string, observer2);
+}
+
+function handleRequest(request, response) {
+ response.processAsync();
+
+ let params = request.queryString.split("&");
+ let format = (params.filter((s) => s.includes("fmt="))[0] || "").split("=")[1];
+ let status = (params.filter((s) => s.includes("sts="))[0] || "").split("=")[1] || 200;
+
+ let cachedCount = 0;
+ let cacheExpire = 60; // seconds
+
+ function setCacheHeaders() {
+ if (status != 304) {
+ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("Expires", "0");
+ return;
+ }
+ // Spice things up a little!
+ if (cachedCount % 2) {
+ response.setHeader("Cache-Control", "max-age=" + cacheExpire, false);
+ } else {
+ response.setHeader("Expires", Date(Date.now() + cacheExpire * 1000), false);
+ }
+ cachedCount++;
+ }
+
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(() => {
+ // to avoid garbage collection
+ timer = null;
+ switch (format) {
+ case "txt": {
+ response.setStatusLine(request.httpVersion, status, "DA DA DA");
+ response.setHeader("Content-Type", "text/plain", false);
+ setCacheHeaders();
+ response.write("Братан, ты вообще качаешься?");
+ response.finish();
+ break;
+ }
+ case "xml": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "text/xml; charset=utf-8", false);
+ setCacheHeaders();
+ response.write("<label value='greeting'>Hello XML!</label>");
+ response.finish();
+ break;
+ }
+ case "html": {
+ let content = (params.filter((s) => s.includes("res="))[0] || "").split("=")[1];
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+ setCacheHeaders();
+ response.write(content || "<p>Hello HTML!</p>");
+ response.finish();
+ break;
+ }
+ case "html-long": {
+ let str = new Array(102400 /* 100 KB in bytes */).join(".");
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+ setCacheHeaders();
+ response.write("<p>" + str + "</p>");
+ response.finish();
+ break;
+ }
+ case "css": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "text/css; charset=utf-8", false);
+ setCacheHeaders();
+ response.write("body:pre { content: 'Hello CSS!' }");
+ response.finish();
+ break;
+ }
+ case "js": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "application/javascript; charset=utf-8", false);
+ setCacheHeaders();
+ response.write("function() { return 'Hello JS!'; }");
+ response.finish();
+ break;
+ }
+ case "json": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "application/json; charset=utf-8", false);
+ setCacheHeaders();
+ response.write("{ \"greeting\": \"Hello JSON!\" }");
+ response.finish();
+ break;
+ }
+ case "jsonp": {
+ let fun = (params.filter((s) => s.includes("jsonp="))[0] || "").split("=")[1];
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "text/json; charset=utf-8", false);
+ setCacheHeaders();
+ response.write(fun + "({ \"greeting\": \"Hello JSONP!\" })");
+ response.finish();
+ break;
+ }
+ case "jsonp2": {
+ let fun = (params.filter((s) => s.includes("jsonp="))[0] || "").split("=")[1];
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "text/json; charset=utf-8", false);
+ setCacheHeaders();
+ response.write(" " + fun + " ( { \"greeting\": \"Hello weird JSONP!\" } ) ; ");
+ response.finish();
+ break;
+ }
+ case "json-long": {
+ let str = "{ \"greeting\": \"Hello long string JSON!\" },";
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "text/json; charset=utf-8", false);
+ setCacheHeaders();
+ response.write("[" + new Array(2048).join(str).slice(0, -1) + "]");
+ response.finish();
+ break;
+ }
+ case "json-malformed": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "text/json; charset=utf-8", false);
+ setCacheHeaders();
+ response.write("{ \"greeting\": \"Hello malformed JSON!\" },");
+ response.finish();
+ break;
+ }
+ case "json-text-mime": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
+ setCacheHeaders();
+ response.write("{ \"greeting\": \"Hello third-party JSON!\" }");
+ response.finish();
+ break;
+ }
+ case "json-custom-mime": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "text/x-bigcorp-json; charset=utf-8", false);
+ setCacheHeaders();
+ response.write("{ \"greeting\": \"Hello oddly-named JSON!\" }");
+ response.finish();
+ break;
+ }
+ case "font": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "font/woff", false);
+ setCacheHeaders();
+ response.finish();
+ break;
+ }
+ case "image": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "image/png", false);
+ setCacheHeaders();
+ response.finish();
+ break;
+ }
+ case "audio": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "audio/ogg", false);
+ setCacheHeaders();
+ response.finish();
+ break;
+ }
+ case "video": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "video/webm", false);
+ setCacheHeaders();
+ response.finish();
+ break;
+ }
+ case "flash": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "application/x-shockwave-flash", false);
+ setCacheHeaders();
+ response.finish();
+ break;
+ }
+ case "ws": {
+ response.setStatusLine(request.httpVersion, 101, "Switching Protocols");
+ response.setHeader("Connection", "upgrade", false);
+ response.setHeader("Upgrade", "websocket", false);
+ setCacheHeaders();
+ response.finish();
+ break;
+ }
+ case "gzip": {
+ // Note: we're doing a double gzip encoding to test multiple
+ // converters in network monitor.
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Content-Encoding", "gzip\t ,gzip", false);
+ setCacheHeaders();
+ let observer = {
+ onStreamComplete: function(loader, context, status, length, result) {
+ let buffer = String.fromCharCode.apply(this, result);
+ response.setHeader("Content-Length", "" + buffer.length, false);
+ response.write(buffer);
+ response.finish();
+ }
+ };
+ let data = new Array(1000).join("Hello gzip!");
+ doubleGzipCompressString(data, observer);
+ break;
+ }
+ case "br": {
+ response.setStatusLine(request.httpVersion, status, "Connected");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Content-Encoding", "br", false);
+ setCacheHeaders();
+ response.setHeader("Content-Length", "10", false);
+ // Use static data since we cannot encode brotli.
+ response.write("\x1b\x3f\x00\x00\x24\xb0\xe2\x99\x80\x12");
+ response.finish();
+ break;
+ }
+ case "hls-m3u8": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "application/x-mpegurl", false);
+ setCacheHeaders();
+ response.write("#EXTM3U\n");
+ response.finish();
+ break;
+ }
+ case "mpeg-dash": {
+ response.setStatusLine(request.httpVersion, status, "OK");
+ response.setHeader("Content-Type", "video/vnd.mpeg.dash.mpd", false);
+ setCacheHeaders();
+ response.write('<?xml version="1.0" encoding="UTF-8"?>\n');
+ response.write('<MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></MPD>\n');
+ response.finish();
+ break;
+ }
+ default: {
+ response.setStatusLine(request.httpVersion, 404, "Not Found");
+ response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+ setCacheHeaders();
+ response.write("<blink>Not Found</blink>");
+ response.finish();
+ break;
+ }
+ }
+ }, 10, Ci.nsITimer.TYPE_ONE_SHOT); // Make sure this request takes a few ms.
+}
diff --git a/devtools/client/netmonitor/test/sjs_cors-test-server.sjs b/devtools/client/netmonitor/test/sjs_cors-test-server.sjs
new file mode 100644
index 000000000..0bab80901
--- /dev/null
+++ b/devtools/client/netmonitor/test/sjs_cors-test-server.sjs
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+ response.setStatusLine(request.httpVersion, 200, "Och Aye");
+
+ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("Expires", "0");
+
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ response.setHeader("Access-Control-Allow-Headers", "content-type", false);
+
+ response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
+
+ response.write("Access-Control-Allow-Origin: *");
+}
diff --git a/devtools/client/netmonitor/test/sjs_hsts-test-server.sjs b/devtools/client/netmonitor/test/sjs_hsts-test-server.sjs
new file mode 100644
index 000000000..c5715886e
--- /dev/null
+++ b/devtools/client/netmonitor/test/sjs_hsts-test-server.sjs
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("Expires", "0");
+
+ if (request.queryString === "reset") {
+ // Reset the HSTS policy, prevent influencing other tests
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Strict-Transport-Security", "max-age=0");
+ response.write("Resetting HSTS");
+ } else if (request.scheme === "http") {
+ response.setStatusLine(request.httpVersion, 302, "Found");
+ response.setHeader("Location", "https://" + request.host + request.path);
+ } else {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Strict-Transport-Security", "max-age=100");
+ response.write("Page was accessed over HTTPS!");
+ }
+}
diff --git a/devtools/client/netmonitor/test/sjs_https-redirect-test-server.sjs b/devtools/client/netmonitor/test/sjs_https-redirect-test-server.sjs
new file mode 100644
index 000000000..14ea34559
--- /dev/null
+++ b/devtools/client/netmonitor/test/sjs_https-redirect-test-server.sjs
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("Expires", "0");
+
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+
+ if (request.scheme === "http") {
+ response.setStatusLine(request.httpVersion, 302, "Found");
+ response.setHeader("Location", "https://" + request.host + request.path);
+ } else {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write("Page was accessed over HTTPS!");
+ }
+
+}
diff --git a/devtools/client/netmonitor/test/sjs_simple-test-server.sjs b/devtools/client/netmonitor/test/sjs_simple-test-server.sjs
new file mode 100644
index 000000000..9a3d44b6d
--- /dev/null
+++ b/devtools/client/netmonitor/test/sjs_simple-test-server.sjs
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+ response.setStatusLine(request.httpVersion, 200, "Och Aye");
+
+ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("Expires", "0");
+
+ response.setHeader("Set-Cookie", "bob=true; Max-Age=10; HttpOnly", true);
+ response.setHeader("Set-Cookie", "tom=cool; Max-Age=10; HttpOnly", true);
+
+ response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
+ response.setHeader("Foo-Bar", "baz", false);
+ response.write("Hello world!");
+}
diff --git a/devtools/client/netmonitor/test/sjs_sorting-test-server.sjs b/devtools/client/netmonitor/test/sjs_sorting-test-server.sjs
new file mode 100644
index 000000000..54c62866b
--- /dev/null
+++ b/devtools/client/netmonitor/test/sjs_sorting-test-server.sjs
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { classes: Cc, interfaces: Ci } = Components;
+
+function handleRequest(request, response) {
+ response.processAsync();
+
+ let params = request.queryString.split("&");
+ let index = params.filter((s) => s.includes("index="))[0].split("=")[1];
+
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(() => {
+ // to avoid garbage collection
+ timer = null;
+ response.setStatusLine(request.httpVersion, index == 1 ? 101 : index * 100, "Meh");
+
+ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("Expires", "0");
+
+ response.setHeader("Content-Type", "text/" + index, false);
+ response.write(new Array(index * 10).join(index)); // + 0.01 KB
+ response.finish();
+ }, 10, Ci.nsITimer.TYPE_ONE_SHOT); // Make sure this request takes a few ms.
+}
diff --git a/devtools/client/netmonitor/test/sjs_status-codes-test-server.sjs b/devtools/client/netmonitor/test/sjs_status-codes-test-server.sjs
new file mode 100644
index 000000000..4f17d1235
--- /dev/null
+++ b/devtools/client/netmonitor/test/sjs_status-codes-test-server.sjs
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { classes: Cc, interfaces: Ci } = Components;
+
+function handleRequest(request, response) {
+ response.processAsync();
+
+ let params = request.queryString.split("&");
+ let status = params.filter(s => s.includes("sts="))[0].split("=")[1];
+ let cached = params.filter(s => s === 'cached').length !== 0;
+
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(() => {
+ // to avoid garbage collection
+ timer = null;
+ switch (status) {
+ case "100":
+ response.setStatusLine(request.httpVersion, 101, "Switching Protocols");
+ break;
+ case "200":
+ response.setStatusLine(request.httpVersion, 202, "Created");
+ break;
+ case "300":
+ response.setStatusLine(request.httpVersion, 303, "See Other");
+ break;
+ case "400":
+ response.setStatusLine(request.httpVersion, 404, "Not Found");
+ break;
+ case "500":
+ response.setStatusLine(request.httpVersion, 501, "Not Implemented");
+ break;
+ case "ok":
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ break;
+ case "redirect":
+ response.setStatusLine(request.httpVersion, 301, "Moved Permanently");
+ response.setHeader("Location", "http://example.com/redirected");
+ break;
+ }
+
+ if(!cached) {
+ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("Expires", "0");
+ }
+ else {
+ response.setHeader("Cache-Control", "no-transform,public,max-age=300,s-maxage=900");
+ response.setHeader("Expires", "Thu, 01 Dec 2100 20:00:00 GMT");
+ }
+
+ response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
+ response.write("Hello status code " + status + "!");
+ response.finish();
+ }, 10, Ci.nsITimer.TYPE_ONE_SHOT); // Make sure this request takes a few ms.
+}
diff --git a/devtools/client/netmonitor/test/sjs_truncate-test-server.sjs b/devtools/client/netmonitor/test/sjs_truncate-test-server.sjs
new file mode 100644
index 000000000..54db23d9a
--- /dev/null
+++ b/devtools/client/netmonitor/test/sjs_truncate-test-server.sjs
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+ let params = request.queryString.split("&");
+ let limit = (params.filter((s) => s.includes("limit="))[0] || "").split("=")[1];
+
+ response.setStatusLine(request.httpVersion, 200, "Och Aye");
+
+ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("Expires", "0");
+ response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
+
+ response.write("x".repeat(2 * parseInt(limit, 10)));
+
+ response.write("Hello world!");
+}
diff --git a/devtools/client/netmonitor/test/test-image.png b/devtools/client/netmonitor/test/test-image.png
new file mode 100644
index 000000000..769c63634
--- /dev/null
+++ b/devtools/client/netmonitor/test/test-image.png
Binary files differ
diff --git a/devtools/client/netmonitor/toolbar-view.js b/devtools/client/netmonitor/toolbar-view.js
new file mode 100644
index 000000000..28c3cf99b
--- /dev/null
+++ b/devtools/client/netmonitor/toolbar-view.js
@@ -0,0 +1,77 @@
+/* globals dumpn, $, NetMonitorView */
+"use strict";
+
+const { createFactory, DOM } = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+const Provider = createFactory(require("devtools/client/shared/vendor/react-redux").Provider);
+const FilterButtons = createFactory(require("./components/filter-buttons"));
+const ToggleButton = createFactory(require("./components/toggle-button"));
+const SearchBox = createFactory(require("./components/search-box"));
+const { L10N } = require("./l10n");
+
+// Shortcuts
+const { button } = DOM;
+
+/**
+ * Functions handling the toolbar view: expand/collapse button etc.
+ */
+function ToolbarView() {
+ dumpn("ToolbarView was instantiated");
+}
+
+ToolbarView.prototype = {
+ /**
+ * Initialization function, called when the debugger is started.
+ */
+ initialize: function (store) {
+ dumpn("Initializing the ToolbarView");
+
+ this._clearContainerNode = $("#react-clear-button-hook");
+ this._filterContainerNode = $("#react-filter-buttons-hook");
+ this._toggleContainerNode = $("#react-details-pane-toggle-hook");
+ this._searchContainerNode = $("#react-search-box-hook");
+
+ // clear button
+ ReactDOM.render(button({
+ id: "requests-menu-clear-button",
+ className: "devtools-button devtools-clear-icon",
+ title: L10N.getStr("netmonitor.toolbar.clear"),
+ onClick: () => {
+ NetMonitorView.RequestsMenu.clear();
+ }
+ }), this._clearContainerNode);
+
+ // filter button
+ ReactDOM.render(Provider(
+ { store },
+ FilterButtons()
+ ), this._filterContainerNode);
+
+ // search box
+ ReactDOM.render(Provider(
+ { store },
+ SearchBox()
+ ), this._searchContainerNode);
+
+ // details pane toggle button
+ ReactDOM.render(Provider(
+ { store },
+ ToggleButton()
+ ), this._toggleContainerNode);
+ },
+
+ /**
+ * Destruction function, called when the debugger is closed.
+ */
+ destroy: function () {
+ dumpn("Destroying the ToolbarView");
+
+ ReactDOM.unmountComponentAtNode(this._clearContainerNode);
+ ReactDOM.unmountComponentAtNode(this._filterContainerNode);
+ ReactDOM.unmountComponentAtNode(this._toggleContainerNode);
+ ReactDOM.unmountComponentAtNode(this._searchContainerNode);
+ }
+
+};
+
+exports.ToolbarView = ToolbarView;