diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /devtools/client/netmonitor | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-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')
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¶m2=value2¶m3=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¶m2=value2¶m3=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¶m2=value2¶m3=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 Binary files differnew file mode 100644 index 000000000..769c63634 --- /dev/null +++ b/devtools/client/netmonitor/test/test-image.png 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; |