Bug 731311 - Network monitor should allow to replay and edit requests; r=vporof

This commit is contained in:
Victor Porof 2013-06-26 09:00:49 +03:00
parent 92444c24d0
commit 85595c397e
14 changed files with 1001 additions and 243 deletions

View File

@ -250,6 +250,7 @@ TargetEventsHandler.prototype = {
case "will-navigate": {
// Reset UI.
NetMonitorView.RequestsMenu.reset();
NetMonitorView.Sidebar.reset();
NetMonitorView.NetworkDetails.reset();
// Reset global helpers cache.

View File

@ -165,7 +165,7 @@ let NetMonitorView = {
}
if (aTabIndex !== undefined) {
$("#details-pane").selectedIndex = aTabIndex;
$("#event-details-pane").selectedIndex = aTabIndex;
}
},
@ -351,8 +351,63 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
this.refreshSummary();
this.refreshZebra();
if (aId == this._preferredItemId) {
this.selectedItem = requestItem;
}
},
/**
* Create a new custom request form populated with the data from
* the currently selected request.
*/
cloneSelectedRequest: function() {
let selected = this.selectedItem.attachment;
// Create the element node for the network request item.
let menuView = this._createMenuView(selected.method, selected.url);
let newItem = this.push([menuView], {
attachment: Object.create(selected, {
isCustom: { value: true }
})
});
// Immediately switch to new request pane.
this.selectedItem = newItem;
},
/**
* Send a new HTTP request using the data in the custom request form.
*/
sendCustomRequest: function() {
let selected = this.selectedItem.attachment;
let data = Object.create(selected, {
headers: { value: selected.requestHeaders.headers }
});
if (selected.requestPostData) {
data.body = selected.requestPostData.postData.text;
}
NetMonitorController.webConsoleClient.sendHTTPRequest(data, (response) => {
let id = response.eventActor.actor;
this._preferredItemId = id;
});
this.closeCustomRequest();
},
/**
* Remove the currently selected custom request.
*/
closeCustomRequest: function() {
this.remove(this.selectedItem);
NetMonitorView.Sidebar.toggle(false);
},
/**
* Filters all network requests in this container by a specified type.
*
@ -690,11 +745,11 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
break;
case "status":
requestItem.attachment.status = value;
this._updateMenuView(requestItem, key, value);
this.updateMenuView(requestItem, key, value);
break;
case "statusText":
requestItem.attachment.statusText = value;
this._updateMenuView(requestItem, key,
this.updateMenuView(requestItem, key,
requestItem.attachment.status + " " +
requestItem.attachment.statusText);
break;
@ -703,11 +758,11 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
break;
case "contentSize":
requestItem.attachment.contentSize = value;
this._updateMenuView(requestItem, key, value);
this.updateMenuView(requestItem, key, value);
break;
case "mimeType":
requestItem.attachment.mimeType = value;
this._updateMenuView(requestItem, key, value);
this.updateMenuView(requestItem, key, value);
break;
case "responseContent":
requestItem.attachment.responseContent = value;
@ -715,7 +770,7 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
case "totalTime":
requestItem.attachment.totalTime = value;
requestItem.attachment.endedMillis = requestItem.attachment.startedMillis + value;
this._updateMenuView(requestItem, key, value);
this.updateMenuView(requestItem, key, value);
this._registerLastRequestEnd(requestItem.attachment.endedMillis);
break;
case "eventTimings":
@ -757,23 +812,11 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
* The network request view.
*/
_createMenuView: function(aMethod, aUrl) {
let uri = nsIURL(aUrl);
let nameWithQuery = this._getUriNameWithQuery(uri);
let hostPort = this._getUriHostPort(uri);
let template = $("#requests-menu-item-template");
let fragment = document.createDocumentFragment();
let method = $(".requests-menu-method", template);
method.setAttribute("value", aMethod);
let file = $(".requests-menu-file", template);
file.setAttribute("value", nameWithQuery);
file.setAttribute("tooltiptext", nameWithQuery);
let domain = $(".requests-menu-domain", template);
domain.setAttribute("value", hostPort);
domain.setAttribute("tooltiptext", hostPort);
this.updateMenuView(template, 'method', aMethod);
this.updateMenuView(template, 'url', aUrl);
let waterfall = $(".requests-menu-waterfall", template);
waterfall.style.backgroundImage = this._cachedWaterfallBackground;
@ -796,22 +839,48 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
* @param any aValue
* The new value to be shown.
*/
_updateMenuView: function(aItem, aKey, aValue) {
updateMenuView: function(aItem, aKey, aValue) {
let target = aItem.target || aItem;
switch (aKey) {
case "method": {
let node = $(".requests-menu-method", target);
node.setAttribute("value", aValue);
break;
}
case "url": {
let uri;
try {
uri = nsIURL(aValue);
} catch(e) {
break; // User input may not make a well-formed url yet.
}
let nameWithQuery = this._getUriNameWithQuery(uri);
let hostPort = this._getUriHostPort(uri);
let node = $(".requests-menu-file", target);
node.setAttribute("value", nameWithQuery);
node.setAttribute("tooltiptext", nameWithQuery);
let domain = $(".requests-menu-domain", target);
domain.setAttribute("value", hostPort);
domain.setAttribute("tooltiptext", hostPort);
break;
}
case "status": {
let node = $(".requests-menu-status", aItem.target);
let node = $(".requests-menu-status", target);
node.setAttribute("code", aValue);
break;
}
case "statusText": {
let node = $(".requests-menu-status-and-method", aItem.target);
let node = $(".requests-menu-status-and-method", target);
node.setAttribute("tooltiptext", aValue);
break;
}
case "contentSize": {
let kb = aValue / 1024;
let size = L10N.numberWithDecimals(kb, CONTENT_SIZE_DECIMALS);
let node = $(".requests-menu-size", aItem.target);
let node = $(".requests-menu-size", target);
let text = L10N.getFormatStr("networkMenu.sizeKB", size);
node.setAttribute("value", text);
node.setAttribute("tooltiptext", text);
@ -819,14 +888,14 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
}
case "mimeType": {
let type = this._getAbbreviatedMimeType(aValue);
let node = $(".requests-menu-type", aItem.target);
let node = $(".requests-menu-type", target);
let text = CONTENT_MIME_TYPE_ABBREVIATIONS[type] || type;
node.setAttribute("value", text);
node.setAttribute("tooltiptext", aValue);
break;
}
case "totalTime": {
let node = $(".requests-menu-timings-total", aItem.target);
let node = $(".requests-menu-timings-total", target);
let text = L10N.getFormatStr("networkMenu.totalMS", aValue); // integer
node.setAttribute("value", text);
node.setAttribute("tooltiptext", text);
@ -1089,10 +1158,10 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
*/
_onSelect: function({ detail: item }) {
if (item) {
NetMonitorView.NetworkDetails.populate(item.attachment);
NetMonitorView.NetworkDetails.toggle(true);
NetMonitorView.Sidebar.populate(item.attachment);
NetMonitorView.Sidebar.toggle(true);
} else {
NetMonitorView.NetworkDetails.toggle(false);
NetMonitorView.Sidebar.toggle(false);
}
},
@ -1104,6 +1173,14 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
drain("resize-events", RESIZE_REFRESH_RATE, () => this._flushWaterfallViews(true));
},
/**
* Handle the context menu opening. Hide items if no request is selected.
*/
_onContextShowing: function() {
let element = $("#request-menu-context-resend");
element.hidden = !this.selectedItem || this.selectedItem.attachment.isCustom;
},
/**
* Checks if the specified unix time is the first one to be known of,
* and saves it if so.
@ -1239,6 +1316,162 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
_resizeTimeout: null
});
/**
* Functions handling the sidebar details view.
*/
function SidebarView() {
dumpn("SidebarView was instantiated");
}
SidebarView.prototype = {
/**
* Sets this view hidden or visible. It's visible by default.
*
* @param boolean aVisibleFlag
* Specifies the intended visibility.
*/
toggle: function(aVisibleFlag) {
NetMonitorView.toggleDetailsPane({ visible: aVisibleFlag });
NetMonitorView.RequestsMenu._flushWaterfallViews(true);
},
/**
* Populates this view with the specified data.
*
* @param object aData
* The data source (this should be the attachment of a request item).
*/
populate: function(aData) {
if (aData.isCustom) {
NetMonitorView.CustomRequest.populate(aData);
$("#details-pane").selectedIndex = 0;
} else {
NetMonitorView.NetworkDetails.populate(aData);
$("#details-pane").selectedIndex = 1;
}
},
/**
* Hides this container.
*/
reset: function() {
this.toggle(false);
}
}
/**
* Functions handling the custom request view.
*/
function CustomRequestView() {
dumpn("CustomRequestView was instantiated");
}
CustomRequestView.prototype = {
/**
* Populates this view with the specified data.
*
* @param object aData
* The data source (this should be the attachment of a request item).
*/
populate: function(aData) {
$("#custom-url-value").value = aData.url;
$("#custom-method-value").value = aData.method;
$("#custom-headers-value").value =
writeHeaderText(aData.requestHeaders.headers);
if (aData.requestPostData) {
let body = aData.requestPostData.postData.text;
gNetwork.getString(body).then((aString) => {
$("#custom-postdata-value").value = aString;
});
}
this.updateCustomQuery(aData.url);
},
/**
* Handle user input in the custom request form.
*
* @param object aField
* the field that the user updated.
*/
onUpdate: function(aField) {
let selectedItem = NetMonitorView.RequestsMenu.selectedItem;
let field = aField;
let value;
switch(aField) {
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 = parseHeaderText(headersText);
selectedItem.attachment.requestHeaders = {
headers: value
};
break;
}
NetMonitorView.RequestsMenu.updateMenuView(selectedItem, field, value);
},
/**
* Update the query string field based on the url.
*
* @param object aUrl
* url to extract query string from.
*/
updateCustomQuery: function(aUrl) {
let paramsArray = parseQueryString(nsIURL(aUrl).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 aQueryText
* contents of the query string field.
*/
updateCustomUrl: function(aQueryText) {
let params = parseQueryText(aQueryText);
let queryString = writeQueryString(params);
let url = $("#custom-url-value").value;
let oldQuery = nsIURL(url).query;
let path = url.replace(oldQuery, queryString);
$("#custom-url-value").value = path;
}
}
/**
* Functions handling the requests details view.
*/
@ -1253,9 +1486,9 @@ NetworkDetailsView.prototype = {
* Initialization function, called when the network monitor is started.
*/
initialize: function() {
dumpn("Initializing the RequestsMenuView");
dumpn("Initializing the NetworkDetailsView");
this.widget = $("#details-pane");
this.widget = $("#event-details-pane");
this._headers = new VariablesView($("#all-headers"),
Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
@ -1292,25 +1525,13 @@ NetworkDetailsView.prototype = {
* Destruction function, called when the network monitor is closed.
*/
destroy: function() {
dumpn("Destroying the SourcesView");
dumpn("Destroying the NetworkDetailsView");
},
/**
* Sets this view hidden or visible. It's visible by default.
*
* @param boolean aVisibleFlag
* Specifies the intended visibility.
*/
toggle: function(aVisibleFlag) {
NetMonitorView.toggleDetailsPane({ visible: aVisibleFlag });
NetMonitorView.RequestsMenu._flushWaterfallViews(true);
},
/**
* Hides and resets this container (removes all the networking information).
* Resets this container (removes all the networking information).
*/
reset: function() {
this.toggle(false);
this._dataSrc = null;
},
@ -1581,21 +1802,14 @@ NetworkDetailsView.prototype = {
*
* @param string aName
* The type of params to populate (get or post).
* @param string aParams
* @param string aQueryString
* A query string of params (e.g. "?foo=bar&baz=42").
*/
_addParams: function(aName, aParams) {
// Make sure there's at least one param available.
if (!aParams || !aParams.contains("=")) {
_addParams: function(aName, aQueryString) {
let paramsArray = parseQueryString(aQueryString);
if (!paramsArray) {
return;
}
// Turn the params string into an array containing { name: value } tuples.
let paramsArray = aParams.replace(/^[?&]/, "").split("&").map((e) =>
let (param = e.split("=")) {
name: NetworkHelper.convertToUnicode(unescape(param[0])),
value: NetworkHelper.convertToUnicode(unescape(param[1]))
});
let paramsScope = this._params.addScope(aName);
paramsScope.expanded = true;
@ -1807,6 +2021,110 @@ function nsIURL(aUrl, aStore = nsIURL.store) {
}
nsIURL.store = new Map();
/**
* Parse a url's query string into its components
*
* @param string aQueryString
* The query part of a url
* @return array
* Array of query params {name, value}
*/
function parseQueryString(aQueryString) {
// Make sure there's at least one param available.
if (!aQueryString || !aQueryString.contains("=")) {
return;
}
// Turn the params string into an array containing { name: value } tuples.
let paramsArray = aQueryString.replace(/^[?&]/, "").split("&").map((e) =>
let (param = e.split("=")) {
name: NetworkHelper.convertToUnicode(unescape(param[0])),
value: NetworkHelper.convertToUnicode(unescape(param[1]))
});
return paramsArray;
}
/**
* Parse text representation of HTTP headers.
*
* @param string aText
* Text of headers
* @return array
* Array of headers info {name, value}
*/
function parseHeaderText(aText) {
return parseRequestText(aText, ":");
}
/**
* Parse readable text list of a query string.
*
* @param string aText
* Text of query string represetation
* @return array
* Array of query params {name, value}
*/
function parseQueryText(aText) {
return parseRequestText(aText, "=");
}
/**
* Parse a text representation of a name:value list with
* the given name:value divider character.
*
* @param string aText
* Text of list
* @return array
* Array of headers info {name, value}
*/
function parseRequestText(aText, aDivider) {
let regex = new RegExp("(.+?)\\" + aDivider + "\\s*(.+)");
let pairs = [];
for (let line of aText.split("\n")) {
let matches;
if (matches = regex.exec(line)) {
let [, name, value] = matches;
pairs.push({name: name, value: value});
}
}
return pairs;
}
/**
* Write out a list of headers into a chunk of text
*
* @param array aHeaders
* Array of headers info {name, value}
* @return string aText
* List of headers in text format
*/
function writeHeaderText(aHeaders) {
return [(name + ": " + value) for ({name, value} of aHeaders)].join("\n");
}
/**
* Write out a list of query params into a chunk of text
*
* @param array aParams
* Array of query params {name, value}
* @return string
* List of query params in text format
*/
function writeQueryText(aParams) {
return [(name + "=" + value) for ({name, value} of aParams)].join("\n");
}
/**
* Write out a list of query params into a query string
*
* @param array aParams
* Array of query params {name, value}
* @return string
* Query string that can be appended to a url.
*/
function writeQueryString(aParams) {
return [(name + "=" + value) for ({name, value} of aParams)].join("&");
}
/**
* Helper for draining a rapid succession of events and invoking a callback
* once everything settles down.
@ -1822,4 +2140,6 @@ drain.store = new Map();
*/
NetMonitorView.Toolbar = new ToolbarView();
NetMonitorView.RequestsMenu = new RequestsMenuView();
NetMonitorView.Sidebar = new SidebarView();
NetMonitorView.CustomRequest = new CustomRequestView();
NetMonitorView.NetworkDetails = new NetworkDetailsView();

View File

@ -15,6 +15,10 @@
overflow: auto;
}
#custom-pane {
overflow: auto;
}
#timings-summary-blocked {
display: none; /* This doesn't work yet. */
}

View File

@ -17,6 +17,16 @@
<script type="text/javascript" src="netmonitor-controller.js"/>
<script type="text/javascript" src="netmonitor-view.js"/>
<popupset id="networkPopupSet">
<menupopup id="network-request-popup"
onpopupshowing="NetMonitorView.RequestsMenu._onContextShowing(event);">
<menuitem id="request-menu-context-resend"
label="&netmonitorUI.summary.resend;"
accesskey="&netmonitorUI.summary.resend.accesskey;"
oncommand="NetMonitorView.RequestsMenu.cloneSelectedRequest();"/>
</menupopup>
</popupset>
<box id="body"
class="devtools-responsive-container"
flex="1">
@ -103,7 +113,7 @@
</toolbar>
<label id="requests-menu-empty-notice"
value="&netmonitorUI.emptyNotice2;"/>
<vbox id="requests-menu-contents" flex="1">
<vbox id="requests-menu-contents" flex="1" context="network-request-popup">
<hbox id="requests-menu-item-template" hidden="true">
<hbox class="requests-menu-subitem requests-menu-status-and-method"
align="center">
@ -194,184 +204,251 @@
<splitter class="devtools-side-splitter"/>
<tabbox id="details-pane"
class="devtools-sidebar-tabs"
hidden="true">
<tabs>
<tab label="&netmonitorUI.tab.headers;"/>
<tab label="&netmonitorUI.tab.cookies;"/>
<tab label="&netmonitorUI.tab.params;"/>
<tab label="&netmonitorUI.tab.response;"/>
<tab label="&netmonitorUI.tab.timings;"/>
</tabs>
<tabpanels flex="1">
<tabpanel id="headers-tabppanel"
class="tabpanel-content">
<vbox flex="1">
<hbox id="headers-summary-url"
class="tabpanel-summary-container"
align="center">
<label class="plain tabpanel-summary-label"
value="&netmonitorUI.summary.url;"/>
<label id="headers-summary-url-value"
class="plain tabpanel-summary-value"
crop="end"
flex="1"/>
</hbox>
<hbox id="headers-summary-method"
class="tabpanel-summary-container"
align="center">
<label class="plain tabpanel-summary-label"
value="&netmonitorUI.summary.method;"/>
<label id="headers-summary-method-value"
class="plain tabpanel-summary-value"
crop="end"
flex="1"/>
</hbox>
<hbox id="headers-summary-status"
class="tabpanel-summary-container"
align="center">
<label class="plain tabpanel-summary-label"
value="&netmonitorUI.summary.status;"/>
<box id="headers-summary-status-circle"
class="requests-menu-status"/>
<label id="headers-summary-status-value"
class="plain tabpanel-summary-value"
crop="end"
flex="1"/>
</hbox>
<hbox id="headers-summary-version"
class="tabpanel-summary-container"
align="center">
<label class="plain tabpanel-summary-label"
value="&netmonitorUI.summary.version;"/>
<label id="headers-summary-version-value"
class="plain tabpanel-summary-value"
crop="end"
flex="1"/>
</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"/>
</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>
<deck id="details-pane"
hidden="true">
<vbox id="custom-pane"
class="tabpanel-content">
<hbox align="baseline">
<label value="&netmonitorUI.custom.newRequest;"
class="plain tabpanel-summary-label
custom-header"/>
<hbox flex="1" pack="end">
<button class="devtools-toolbarbutton"
label="&netmonitorUI.custom.send;"
onclick="NetMonitorView.RequestsMenu.sendCustomRequest();"/>
<button class="devtools-toolbarbutton"
label="&netmonitorUI.custom.cancel;"
onclick="NetMonitorView.RequestsMenu.closeCustomRequest();"/>
</hbox>
</hbox>
<hbox id="custom-method-and-url"
class="tabpanel-summary-container"
align="center">
<textbox id="custom-method-value"
oninput="NetMonitorView.CustomRequest.onUpdate('method');"
multiline="true"
cols="6"
rows="1"/>
<textbox id="custom-url-value"
flex="1"
oninput="NetMonitorView.CustomRequest.onUpdate('url');"/>
</hbox>
<vbox id="custom-query"
class="tabpanel-summary-container custom-section">
<label class="plain tabpanel-summary-label"
value="&netmonitorUI.custom.query;"/>
<textbox id="custom-query-value"
class="tabpanel-summary-input"
multiline="true"
rows="4"
wrap="off"
oninput="NetMonitorView.CustomRequest.onUpdate('query');"/>
</vbox>
<vbox id="custom-headers"
class="tabpanel-summary-container custom-section">
<label class="plain tabpanel-summary-label"
value="&netmonitorUI.custom.headers;"/>
<textbox id="custom-headers-value"
class="tabpanel-summary-input"
multiline="true"
rows="6"
wrap="off"
oninput="NetMonitorView.CustomRequest.onUpdate('headers');"/>
</vbox>
<vbox id="custom-postdata"
class="tabpanel-summary-container custom-section">
<label class="plain tabpanel-summary-label"
value="&netmonitorUI.custom.postData;"/>
<textbox id="custom-postdata-value"
class="tabpanel-summary-input"
multiline="true"
rows="6"
wrap="off"
oninput="NetMonitorView.CustomRequest.onUpdate('body');"/>
</vbox>
</vbox>
<tabbox id="event-details-pane"
class="devtools-sidebar-tabs">
<tabs>
<tab label="&netmonitorUI.tab.headers;"/>
<tab label="&netmonitorUI.tab.cookies;"/>
<tab label="&netmonitorUI.tab.params;"/>
<tab label="&netmonitorUI.tab.response;"/>
<tab label="&netmonitorUI.tab.timings;"/>
</tabs>
<tabpanels flex="1">
<tabpanel id="headers-tabppanel"
class="tabpanel-content">
<vbox flex="1">
<hbox id="headers-summary-url"
class="tabpanel-summary-container"
align="center">
<label class="plain tabpanel-summary-label"
value="&netmonitorUI.response.name;"/>
<label id="response-content-image-name-value"
value="&netmonitorUI.summary.url;"/>
<label id="headers-summary-url-value"
class="plain tabpanel-summary-value"
crop="end"
flex="1"/>
</hbox>
<hbox>
<hbox id="headers-summary-method"
class="tabpanel-summary-container"
align="center">
<label class="plain tabpanel-summary-label"
value="&netmonitorUI.response.dimensions;"/>
<label id="response-content-image-dimensions-value"
value="&netmonitorUI.summary.method;"/>
<label id="headers-summary-method-value"
class="plain tabpanel-summary-value"
crop="end"
flex="1"/>
</hbox>
<hbox>
<hbox id="headers-summary-status"
class="tabpanel-summary-container"
align="center">
<label class="plain tabpanel-summary-label"
value="&netmonitorUI.response.mime;"/>
<label id="response-content-image-mime-value"
value="&netmonitorUI.summary.status;"/>
<box id="headers-summary-status-circle"
class="requests-menu-status"/>
<label id="headers-summary-status-value"
class="plain tabpanel-summary-value"
crop="end"
flex="1"/>
<button id="headers-summary-resend"
label="&netmonitorUI.summary.resend;"
class="devtools-toolbarbutton"
onclick="NetMonitorView.RequestsMenu.cloneSelectedRequest();"/>
</hbox>
<hbox id="headers-summary-version"
class="tabpanel-summary-container"
align="center">
<label class="plain tabpanel-summary-label"
value="&netmonitorUI.summary.version;"/>
<label id="headers-summary-version-value"
class="plain tabpanel-summary-value"
crop="end"
flex="1"/>
</hbox>
<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"/>
</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"
value="&netmonitorUI.response.name;"/>
<label id="response-content-image-name-value"
class="plain tabpanel-summary-value"
crop="end"
flex="1"/>
</hbox>
<hbox>
<label class="plain tabpanel-summary-label"
value="&netmonitorUI.response.dimensions;"/>
<label id="response-content-image-dimensions-value"
class="plain tabpanel-summary-value"
crop="end"
flex="1"/>
</hbox>
<hbox>
<label class="plain tabpanel-summary-label"
value="&netmonitorUI.response.mime;"/>
<label id="response-content-image-mime-value"
class="plain tabpanel-summary-value"
crop="end"
flex="1"/>
</hbox>
<hbox>
<label class="plain tabpanel-summary-label"
value="&netmonitorUI.response.encoding;"/>
<label id="response-content-image-encoding-value"
class="plain tabpanel-summary-value"
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"
value="&netmonitorUI.response.encoding;"/>
<label id="response-content-image-encoding-value"
class="plain tabpanel-summary-value"
crop="end"
flex="1"/>
value="&netmonitorUI.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"
value="&netmonitorUI.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"
value="&netmonitorUI.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"
value="&netmonitorUI.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"
value="&netmonitorUI.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"
value="&netmonitorUI.timings.receive;"/>
<hbox class="requests-menu-timings-box receive"/>
<label class="plain requests-menu-timings-total"/>
</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"
value="&netmonitorUI.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"
value="&netmonitorUI.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"
value="&netmonitorUI.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"
value="&netmonitorUI.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"
value="&netmonitorUI.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"
value="&netmonitorUI.timings.receive;"/>
<hbox class="requests-menu-timings-box receive"/>
<label class="plain requests-menu-timings-total"/>
</hbox>
</vbox>
</tabpanel>
</tabpanels>
</tabbox>
</tabpanel>
</tabpanels>
</tabbox>
</deck>
</box>
</window>

View File

@ -42,6 +42,7 @@ MOCHITEST_BROWSER_TESTS = \
browser_net_accessibility-01.js \
browser_net_accessibility-02.js \
browser_net_footer-summary.js \
browser_net_resend.js \
head.js \
$(NULL)

View File

@ -20,8 +20,8 @@ function test() {
NetMonitorView.toggleDetailsPane({ visible: true }, 2)
RequestsMenu.selectedIndex = 0;
let tab = document.querySelectorAll("#details-pane tab")[2];
let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
let tab = document.querySelectorAll("#event-details-pane tab")[2];
let tabpanel = document.querySelectorAll("#event-details-pane tabpanel")[2];
is(tab.getAttribute("selected"), "true",
"The params tab in the network details pane should be selected.");

View File

@ -0,0 +1,166 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
let gPanelWin;
let gPanelDoc;
const ADD_QUERY = "t1=t2";
const ADD_HEADER = "Test-header: true";
const ADD_POSTDATA = "t3=t4";
/**
* Tests if resending a request works.
*/
function test() {
initNetMonitor(POST_DATA_URL).then(([aTab, aDebuggee, aMonitor]) => {
info("Starting test... ");
gPanelWin = aMonitor.panelWin;
gPanelDoc = gPanelWin.document;
let { NetMonitorView } = gPanelWin;
let { RequestsMenu } = NetMonitorView;
RequestsMenu.lazyUpdate = false;
waitForNetworkEvents(aMonitor, 0, 2).then(() => {
let origItem = RequestsMenu.getItemAtIndex(0);
RequestsMenu.selectedItem = origItem;
// add a new custom request cloned from selected request
RequestsMenu.cloneSelectedRequest();
testCustomForm(origItem.attachment);
let customItem = RequestsMenu.selectedItem;
testCustomItem(customItem, origItem);
// edit the custom request
editCustomForm(() => {
testCustomItemChanged(customItem, origItem);
waitForNetworkEvents(aMonitor, 0, 1).then(() => {
let sentItem = RequestsMenu.selectedItem;
testSentRequest(sentItem.attachment, origItem.attachment);
finishUp(aMonitor);
});
// send the new request
RequestsMenu.sendCustomRequest();
});
});
aDebuggee.performRequests();
});
}
function testCustomItem(aItem, aOrigItem) {
let method = aItem.target.querySelector(".requests-menu-method").value;
let origMethod = aOrigItem.target.querySelector(".requests-menu-method").value;
is(method, origMethod, "menu item is showing the same method as original request");
let file = aItem.target.querySelector(".requests-menu-file").value;
let origFile = aOrigItem.target.querySelector(".requests-menu-file").value;
is(file, origFile, "menu item is showing the same file name as original request");
let domain = aItem.target.querySelector(".requests-menu-domain").value;
let origDomain = aOrigItem.target.querySelector(".requests-menu-domain").value;
is(domain, origDomain, "menu item is showing the same domain as original request");
}
function testCustomItemChanged(aItem, aOrigItem) {
let file = aItem.target.querySelector(".requests-menu-file").value;
let expectedFile = aOrigItem.target.querySelector(".requests-menu-file").value + "&" + ADD_QUERY;
is(file, expectedFile, "menu item is updated to reflect url entered in form");
}
/*
* Test that the New Request form was populated correctly
*/
function testCustomForm(aData) {
is(gPanelDoc.getElementById("custom-method-value").value, aData.method,
"new request form showing correct method");
is(gPanelDoc.getElementById("custom-url-value").value, aData.url,
"new request form showing correct url");
let query = gPanelDoc.getElementById("custom-query-value");
is(query.value, "foo=bar\nbaz=42\ntype=urlencoded",
"new request form showing correct query string");
let headers = gPanelDoc.getElementById("custom-headers-value").value.split("\n");
for (let {name, value} of aData.requestHeaders.headers) {
ok(headers.indexOf(name + ": " + value) >= 0, "form contains header from request");
}
let postData = gPanelDoc.getElementById("custom-postdata-value");
is(postData.value, aData.requestPostData.postData.text,
"new request form showing correct post data");
}
/*
* Add some params and headers to the request form
*/
function editCustomForm(callback) {
gPanelWin.focus();
let query = gPanelDoc.getElementById("custom-query-value");
query.addEventListener("focus", function onFocus() {
query.removeEventListener("focus", onFocus, false);
// add params to url query string field
type(["VK_RETURN"]);
type(ADD_QUERY);
let headers = gPanelDoc.getElementById("custom-headers-value");
headers.addEventListener("focus", function onFocus() {
headers.removeEventListener("focus", onFocus, false);
// add a header
type(["VK_RETURN"]);
type(ADD_HEADER);
let postData = gPanelDoc.getElementById("custom-postdata-value");
postData.addEventListener("focus", function onFocus() {
postData.removeEventListener("focus", onFocus, false);
// add to POST data
type(ADD_POSTDATA);
callback();
}, false);
postData.focus();
}, false);
headers.focus();
}, false);
query.focus();
}
/*
* Make sure newly created event matches expected request
*/
function testSentRequest(aData, aOrigData) {
is(aData.method, aOrigData.method, "correct method in sent request");
is(aData.url, aOrigData.url + "&" + ADD_QUERY, "correct url in sent request");
let hasHeader = aData.requestHeaders.headers.some((header) => {
return (header.name + ": " + header.value) == ADD_HEADER;
})
ok(hasHeader, "new header added to sent request");
is(aData.requestPostData.postData.text,
aOrigData.requestPostData.postData.text + ADD_POSTDATA,
"post data added to sent request");
}
function type(aString) {
for (let ch of aString) {
EventUtils.synthesizeKey(ch, {}, gPanelWin);
}
}
function finishUp(aMonitor) {
gPanelWin = null;
gPanelDoc = null;
teardown(aMonitor).then(finish);
}

View File

@ -168,3 +168,36 @@
- in the network details timings tab identifying the amount of time spent
- in a "receive" state. -->
<!ENTITY netmonitorUI.timings.receive "Receiving:">
<!-- LOCALIZATION NOTE (debuggerUI.custom.headers): This is the label displayed
- on the button in the headers tab that opens a form to resend the currently
displayed request -->
<!ENTITY netmonitorUI.summary.resend "Resend">
<!-- LOCALIZATION NOTE (debuggerUI.custom.headers): This is the access key
- for the Resend menu item displayed in the context menu for a request -->
<!ENTITY netmonitorUI.summary.resend.accesskey "R">
<!-- LOCALIZATION NOTE (debuggerUI.custom.newRequest): This is the label displayed
- as the title of the new custom request form -->
<!ENTITY netmonitorUI.custom.newRequest "New Request">
<!-- LOCALIZATION NOTE (debuggerUI.custom.query): This is the label displayed
- above the query string entry in the custom request form -->
<!ENTITY netmonitorUI.custom.query "Query String:">
<!-- LOCALIZATION NOTE (debuggerUI.custom.headers): This is the label displayed
- above the request headers entry in the custom request form -->
<!ENTITY netmonitorUI.custom.headers "Request Headers:">
<!-- LOCALIZATION NOTE (debuggerUI.custom.headers): This is the label displayed
- above the request body entry in the custom request form -->
<!ENTITY netmonitorUI.custom.postData "Request Body:">
<!-- LOCALIZATION NOTE (debuggerUI.custom.headers): This is the label displayed
- on the button which sends the custom request -->
<!ENTITY netmonitorUI.custom.send "Send">
<!-- LOCALIZATION NOTE (debuggerUI.custom.headers): This is the label displayed
- on the button which cancels and closes the custom request form -->
<!ENTITY netmonitorUI.custom.cancel "Cancel">

View File

@ -383,6 +383,11 @@ box.requests-menu-status[code^="5"] {
padding-top: 2px;
}
#headers-summary-resend {
margin: 0 6px;
min-height: 20px;
}
/* Response tabpanel */
#response-content-info-header {
@ -422,6 +427,20 @@ box.requests-menu-status[code^="5"] {
transition: transform 0.2s ease-out;
}
/* Custom request form */
#custom-pane {
padding: 0.6em 0.5em;
}
.custom-header {
font-size: 1.1em;
}
.custom-section {
margin-top: 0.5em;
}
/* Footer */
#requests-menu-footer {

View File

@ -383,6 +383,11 @@ box.requests-menu-status[code^="5"] {
padding-top: 2px;
}
#headers-summary-resend {
margin: 0 6px;
min-height: 20px;
}
/* Response tabpanel */
#response-content-info-header {
@ -422,6 +427,20 @@ box.requests-menu-status[code^="5"] {
transition: transform 0.2s ease-out;
}
/* Custom request form */
#custom-pane {
padding: 0.6em 0.5em;
}
.custom-header {
font-size: 1.1em;
}
.custom-section {
margin-top: 0.5em;
}
/* Footer */
#requests-menu-footer {

View File

@ -383,6 +383,11 @@ box.requests-menu-status[code^="5"] {
padding-top: 2px;
}
#headers-summary-resend {
margin: 0 6px;
min-height: 20px;
}
/* Response tabpanel */
#response-content-info-header {
@ -422,6 +427,20 @@ box.requests-menu-status[code^="5"] {
transition: transform 0.2s ease-out;
}
/* Custom request form */
#custom-pane {
padding: 0.6em 0.5em;
}
.custom-header {
font-size: 1.1em;
}
.custom-section {
margin-top: 0.5em;
}
/* Footer */
#requests-menu-footer {

View File

@ -89,6 +89,7 @@ function WebConsoleActor(aConnection, aParentActor)
this._protoChains = new Map();
this._dbgGlobals = new Map();
this._netEvents = new Map();
this._getDebuggerGlobal(this.window);
this._onObserverNotification = this._onObserverNotification.bind(this);
@ -147,6 +148,15 @@ WebConsoleActor.prototype =
*/
_dbgGlobals: null,
/**
* Holds a map between nsIChannel objects and NetworkEventActors for requests
* created with sendHTTPRequest.
*
* @private
* @type Map
*/
_netEvents: null,
/**
* Object that holds the JSTerm API, the helper functions, for the default
* window object.
@ -252,6 +262,8 @@ WebConsoleActor.prototype =
"last-pb-context-exited");
}
this._actorPool = null;
this._netEvents.clear();
this._protoChains.clear();
this._dbgGlobals.clear();
this._jstermHelpers = null;
@ -1000,10 +1012,10 @@ WebConsoleActor.prototype =
* A new NetworkEventActor is returned. This is used for tracking the
* network request and response.
*/
onNetworkEvent: function WCA_onNetworkEvent(aEvent)
onNetworkEvent: function WCA_onNetworkEvent(aEvent, aChannel)
{
let actor = new NetworkEventActor(aEvent, this);
this._actorPool.addActor(actor);
let actor = this.getNetworkEventActor(aChannel);
actor.init(aEvent);
let packet = {
from: this.actorID,
@ -1016,6 +1028,57 @@ WebConsoleActor.prototype =
return actor;
},
/**
* Get the NetworkEventActor for a nsIChannel, if it exists,
* otherwise create a new one.
*
* @param object aChannel
* The channel for the network event.
*/
getNetworkEventActor: function WCA_getNetworkEventActor(aChannel) {
let actor = this._netEvents.get(aChannel);
if (actor) {
// delete from map as we should only need to do this check once
this._netEvents.delete(aChannel);
actor.channel = null;
return actor;
}
actor = new NetworkEventActor(aChannel, this);
this._actorPool.addActor(actor);
return actor;
},
/**
* Send a new HTTP request from the target's window.
*
* @param object aMessage
* Object with 'request' - the HTTP request details.
*/
onSendHTTPRequest: function WCA_onSendHTTPRequest(aMessage)
{
let details = aMessage.request;
// send request from target's window
let request = new this._window.XMLHttpRequest();
request.open(details.method, details.url, true);
for (let {name, value} of details.headers) {
request.setRequestHeader(name, value);
}
request.send(details.body);
let actor = this.getNetworkEventActor(request.channel);
// map channel to actor so we can associate future events with it
this._netEvents.set(request.channel, actor);
return {
from: this.actorID,
eventActor: actor.grip()
};
},
/**
* Handler for file activity. This method sends the file request information
* to the remote Web Console client.
@ -1113,7 +1176,7 @@ WebConsoleActor.prototype =
});
break;
}
},
}
};
WebConsoleActor.prototype.requestTypes =
@ -1125,32 +1188,31 @@ WebConsoleActor.prototype.requestTypes =
autocomplete: WebConsoleActor.prototype.onAutocomplete,
clearMessagesCache: WebConsoleActor.prototype.onClearMessagesCache,
setPreferences: WebConsoleActor.prototype.onSetPreferences,
sendHTTPRequest: WebConsoleActor.prototype.onSendHTTPRequest
};
/**
* Creates an actor for a network event.
*
* @constructor
* @param object aNetworkEvent
* The network event you want to use the actor for.
* @param object aChannel
* The nsIChannel associated with this event.
* @param object aWebConsoleActor
* The parent WebConsoleActor instance for this object.
*/
function NetworkEventActor(aNetworkEvent, aWebConsoleActor)
function NetworkEventActor(aChannel, aWebConsoleActor)
{
this.parent = aWebConsoleActor;
this.conn = this.parent.conn;
this._startedDateTime = aNetworkEvent.startedDateTime;
this._isXHR = aNetworkEvent.isXHR;
this.channel = aChannel;
this._request = {
method: aNetworkEvent.method,
url: aNetworkEvent.url,
httpVersion: aNetworkEvent.httpVersion,
method: null,
url: null,
httpVersion: null,
headers: [],
cookies: [],
headersSize: aNetworkEvent.headersSize,
headersSize: null,
postData: {},
};
@ -1164,10 +1226,6 @@ function NetworkEventActor(aNetworkEvent, aWebConsoleActor)
// Keep track of LongStringActors owned by this NetworkEventActor.
this._longStringActors = new Set();
this._discardRequestBody = aNetworkEvent.discardRequestBody;
this._discardResponseBody = aNetworkEvent.discardResponseBody;
this._private = aNetworkEvent.private;
}
NetworkEventActor.prototype =
@ -1206,6 +1264,10 @@ NetworkEventActor.prototype =
}
}
this._longStringActors = new Set();
if (this.channel) {
this.parent._netEvents.delete(this.channel);
}
this.parent.releaseActor(this);
},
@ -1218,6 +1280,27 @@ NetworkEventActor.prototype =
return {};
},
/**
* Set the properties of this actor based on it's corresponding
* network event.
*
* @param object aNetworkEvent
* The network event associated with this actor.
*/
init: function NEA_init(aNetworkEvent)
{
this._startedDateTime = aNetworkEvent.startedDateTime;
this._isXHR = aNetworkEvent.isXHR;
for (let prop of ['method', 'url', 'httpVersion', 'headersSize']) {
this._request[prop] = aNetworkEvent[prop];
}
this._discardRequestBody = aNetworkEvent.discardRequestBody;
this._discardResponseBody = aNetworkEvent.discardResponseBody;
this._private = aNetworkEvent.private;
},
/**
* The "getRequestHeaders" packet type handler.
*
@ -1545,4 +1628,3 @@ NetworkEventActor.prototype.requestTypes =
DebuggerServer.addTabActor(WebConsoleActor, "consoleActor");
DebuggerServer.addGlobalActor(WebConsoleActor, "consoleActor");

View File

@ -284,6 +284,23 @@ WebConsoleClient.prototype = {
this._client.request(packet, aOnResponse);
},
/**
* Send a HTTP request with the given data.
*
* @param string aData
* The details of the HTTP request.
* @param function aOnResponse
* The function invoked when the response is received.
*/
sendHTTPRequest: function WCC_sendHTTPRequest(aData, aOnResponse) {
let packet = {
to: this._actor,
type: "sendHTTPRequest",
request: aData
};
this._client.request(packet, aOnResponse);
},
/**
* Start the given Web Console listeners.
*

View File

@ -1748,10 +1748,10 @@ NetworkResponseListener.prototype = {
* window is given, all browser network requests are logged.
* @param object aOwner
* The network monitor owner. This object needs to hold:
* - onNetworkEvent(aRequestInfo). This method is invoked once for every
* new network request and it is given one arguments: the initial network
* request information. onNetworkEvent() must return an object which
* holds several add*() methods which are used to add further network
* - onNetworkEvent(aRequestInfo, aChannel). This method is invoked once for
* every new network request and it is given two arguments: the initial network
* request information, and the channel. onNetworkEvent() must return an object
* which holds several add*() methods which are used to add further network
* request/response information.
* - saveRequestAndResponseBodies property which tells if you want to log
* request and response bodies.
@ -2052,7 +2052,7 @@ NetworkMonitor.prototype = {
cookies = NetworkHelper.parseCookieHeader(cookieHeader);
}
httpActivity.owner = this.owner.onNetworkEvent(event);
httpActivity.owner = this.owner.onNetworkEvent(event, aChannel);
this._setupResponseListener(httpActivity);