import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo

This commit is contained in:
Roy Tam 2018-01-19 03:59:58 +08:00
commit dcd9973243
150858 changed files with 23884658 additions and 0 deletions

View file

@ -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)/.*)$"],
},
};

View file

@ -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,
};

View file

@ -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);

View file

@ -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',
)

View file

@ -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,
};

View file

@ -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);

View file

@ -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',
)

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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']

View file

@ -0,0 +1,6 @@
"use strict";
module.exports = {
// Extend from the shared list of defined globals for mochitests.
"extends": "../../../../.eslintrc.mochitests.js"
};

View file

@ -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]

View file

@ -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);
});

View file

@ -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);
});

View file

@ -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);
}

View file

@ -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/";

View file

@ -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>

View file

@ -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;

View file

@ -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);

View file

@ -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']

View file

@ -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");

File diff suppressed because it is too large Load diff

View file

@ -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>

View file

@ -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;
})
};

View file

@ -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;

View file

@ -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"]
});

View file

@ -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;

View file

@ -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,
});

View file

@ -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',
)

View file

@ -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;

View file

@ -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;

View file

@ -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";
};

File diff suppressed because it is too large Load diff

View file

@ -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...
};

View file

@ -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'
)

View file

@ -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,
};

View file

@ -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;

View file

@ -0,0 +1,6 @@
"use strict";
module.exports = {
// Extend from the shared list of defined globals for mochitests.
"extends": "../../../.eslintrc.mochitests.js"
};

View file

@ -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]

View file

@ -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);
});

View file

@ -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);
});

View file

@ -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);
});

View file

@ -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);
});

View file

@ -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");
}
});

View file

@ -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);
}
});

View file

@ -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;
}
});

View file

@ -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);
});

View file

@ -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);
});
}
});

View file

@ -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);
});

View file

@ -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);
});

View file

@ -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);
});

View file

@ -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);
});

View file

@ -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);
});

View file

@ -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);
});

View file

@ -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);
});

View file

@ -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.");
}
});

View file

@ -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.");
}
});

View file

@ -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);
}
});

View file

@ -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);
});

View file

@ -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);
});

View file

@ -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);
});

View file

@ -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.");
}
});

View file

@ -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);
});

View file

@ -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);
});

View file

@ -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);
});

View file

@ -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);
});

View file

@ -0,0 +1,228 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests Curl Utils functionality.
*/
const { CurlUtils } = require("devtools/client/shared/curl");
add_task(function* () {
let { tab, monitor } = yield initNetMonitor(CURL_UTILS_URL);
info("Starting test... ");
let { NetMonitorView, gNetwork } = monitor.panelWin;
let { RequestsMenu } = NetMonitorView;
RequestsMenu.lazyUpdate = false;
let wait = waitForNetworkEvents(monitor, 1, 3);
yield ContentTask.spawn(tab.linkedBrowser, SIMPLE_SJS, function* (url) {
content.wrappedJSObject.performRequests(url);
});
yield wait;
let requests = {
get: RequestsMenu.getItemAtIndex(0),
post: RequestsMenu.getItemAtIndex(1),
multipart: RequestsMenu.getItemAtIndex(2),
multipartForm: RequestsMenu.getItemAtIndex(3)
};
let data = yield createCurlData(requests.get.attachment, gNetwork);
testFindHeader(data);
data = yield createCurlData(requests.post.attachment, gNetwork);
testIsUrlEncodedRequest(data);
testWritePostDataTextParams(data);
data = yield createCurlData(requests.multipart.attachment, gNetwork);
testIsMultipartRequest(data);
testGetMultipartBoundary(data);
testRemoveBinaryDataFromMultipartText(data);
data = yield createCurlData(requests.multipartForm.attachment, gNetwork);
testGetHeadersFromMultipartText(data);
if (Services.appinfo.OS != "WINNT") {
testEscapeStringPosix();
} else {
testEscapeStringWin();
}
yield teardown(monitor);
});
function testIsUrlEncodedRequest(data) {
let isUrlEncoded = CurlUtils.isUrlEncodedRequest(data);
ok(isUrlEncoded, "Should return true for url encoded requests.");
}
function testIsMultipartRequest(data) {
let isMultipart = CurlUtils.isMultipartRequest(data);
ok(isMultipart, "Should return true for multipart/form-data requests.");
}
function testFindHeader(data) {
let headers = data.headers;
let hostName = CurlUtils.findHeader(headers, "Host");
let requestedWithLowerCased = CurlUtils.findHeader(headers, "x-requested-with");
let doesNotExist = CurlUtils.findHeader(headers, "X-Does-Not-Exist");
is(hostName, "example.com",
"Header with name 'Host' should be found in the request array.");
is(requestedWithLowerCased, "XMLHttpRequest",
"The search should be case insensitive.");
is(doesNotExist, null,
"Should return null when a header is not found.");
}
function testWritePostDataTextParams(data) {
let params = CurlUtils.writePostDataTextParams(data.postDataText);
is(params, "param1=value1&param2=value2&param3=value3",
"Should return a serialized representation of the request parameters");
}
function testGetMultipartBoundary(data) {
let boundary = CurlUtils.getMultipartBoundary(data);
ok(/-{3,}\w+/.test(boundary),
"A boundary string should be found in a multipart request.");
}
function testRemoveBinaryDataFromMultipartText(data) {
let generatedBoundary = CurlUtils.getMultipartBoundary(data);
let text = data.postDataText;
let binaryRemoved =
CurlUtils.removeBinaryDataFromMultipartText(text, generatedBoundary);
let boundary = "--" + generatedBoundary;
const EXPECTED_POSIX_RESULT = [
"$'",
boundary,
"\\r\\n\\r\\n",
"Content-Disposition: form-data; name=\"param1\"",
"\\r\\n\\r\\n",
"value1",
"\\r\\n",
boundary,
"\\r\\n\\r\\n",
"Content-Disposition: form-data; name=\"file\"; filename=\"filename.png\"",
"\\r\\n",
"Content-Type: image/png",
"\\r\\n\\r\\n",
boundary + "--",
"\\r\\n",
"'"
].join("");
const EXPECTED_WIN_RESULT = [
'"' + boundary + '"^',
"\u000d\u000A\u000d\u000A",
'"Content-Disposition: form-data; name=""param1"""^',
"\u000d\u000A\u000d\u000A",
'"value1"^',
"\u000d\u000A",
'"' + boundary + '"^',
"\u000d\u000A\u000d\u000A",
'"Content-Disposition: form-data; name=""file""; filename=""filename.png"""^',
"\u000d\u000A",
'"Content-Type: image/png"^',
"\u000d\u000A\u000d\u000A",
'"' + boundary + '--"^',
"\u000d\u000A",
'""'
].join("");
if (Services.appinfo.OS != "WINNT") {
is(CurlUtils.escapeStringPosix(binaryRemoved), EXPECTED_POSIX_RESULT,
"The mulitpart request payload should not contain binary data.");
} else {
is(CurlUtils.escapeStringWin(binaryRemoved), EXPECTED_WIN_RESULT,
"WinNT: The mulitpart request payload should not contain binary data.");
}
}
function testGetHeadersFromMultipartText(data) {
let headers = CurlUtils.getHeadersFromMultipartText(data.postDataText);
ok(Array.isArray(headers), "Should return an array.");
ok(headers.length > 0, "There should exist at least one request header.");
is(headers[0].name, "Content-Type", "The first header name should be 'Content-Type'.");
}
function testEscapeStringPosix() {
let surroundedWithQuotes = "A simple string";
is(CurlUtils.escapeStringPosix(surroundedWithQuotes), "'A simple string'",
"The string should be surrounded with single quotes.");
let singleQuotes = "It's unusual to put crickets in your coffee.";
is(CurlUtils.escapeStringPosix(singleQuotes),
"$'It\\'s unusual to put crickets in your coffee.'",
"Single quotes should be escaped.");
let newLines = "Line 1\r\nLine 2\u000d\u000ALine3";
is(CurlUtils.escapeStringPosix(newLines), "$'Line 1\\r\\nLine 2\\r\\nLine3'",
"Newlines should be escaped.");
let controlChars = "\u0007 \u0009 \u000C \u001B";
is(CurlUtils.escapeStringPosix(controlChars), "$'\\x07 \\x09 \\x0c \\x1b'",
"Control characters should be escaped.");
let extendedAsciiChars = "æ ø ü ß ö é";
is(CurlUtils.escapeStringPosix(extendedAsciiChars),
"$'\\xc3\\xa6 \\xc3\\xb8 \\xc3\\xbc \\xc3\\x9f \\xc3\\xb6 \\xc3\\xa9'",
"Character codes outside of the decimal range 32 - 126 should be escaped.");
}
function testEscapeStringWin() {
let surroundedWithDoubleQuotes = "A simple string";
is(CurlUtils.escapeStringWin(surroundedWithDoubleQuotes), '"A simple string"',
"The string should be surrounded with double quotes.");
let doubleQuotes = "Quote: \"Time is an illusion. Lunchtime doubly so.\"";
is(CurlUtils.escapeStringWin(doubleQuotes),
'"Quote: ""Time is an illusion. Lunchtime doubly so."""',
"Double quotes should be escaped.");
let percentSigns = "%AppData%";
is(CurlUtils.escapeStringWin(percentSigns), '""%"AppData"%""',
"Percent signs should be escaped.");
let backslashes = "\\A simple string\\";
is(CurlUtils.escapeStringWin(backslashes), '"\\\\A simple string\\\\"',
"Backslashes should be escaped.");
let newLines = "line1\r\nline2\r\nline3";
is(CurlUtils.escapeStringWin(newLines),
'"line1"^\u000d\u000A"line2"^\u000d\u000A"line3"',
"Newlines should be escaped.");
}
function* createCurlData(selected, network, controller) {
let { url, method, httpVersion } = selected;
// Create a sanitized object for the Curl command generator.
let data = {
url,
method,
headers: [],
httpVersion,
postDataText: null
};
// Fetch header values.
for (let { name, value } of selected.requestHeaders.headers) {
let text = yield network.getString(value);
data.headers.push({ name: name, value: text });
}
// Fetch the request payload.
if (selected.requestPostData) {
let postData = selected.requestPostData.postData.text;
data.postDataText = yield network.getString(postData);
}
return data;
}

View file

@ -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);
});

View file

@ -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);
});

View file

@ -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.");
}
});

View file

@ -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",
});
}
});

View file

@ -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"
});
}
}
});

View file

@ -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"
});
}
}
});

View file

@ -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.");
});

View file

@ -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.");
}
});

View file

@ -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);
});

View file

@ -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);
});

View file

@ -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.");
}
});

View file

@ -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.");
}
});

View file

@ -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.");
}
});

View file

@ -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);
});

View file

@ -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.");
}
});

View file

@ -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.");
}
});

View file

@ -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.");
}
});

View file

@ -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();
});

View file

@ -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);
});

View file

@ -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();
});

View file

@ -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.");
}
});

View file

@ -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);
});

View file

@ -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);
});

View file

@ -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;
}
});

Some files were not shown because too many files have changed in this diff Show more