From 48694b9903c1639325cd6989ada2a909d868139c Mon Sep 17 00:00:00 2001 From: Julian Viereck Date: Sat, 28 Aug 2010 21:31:12 -0700 Subject: [PATCH] Bug 573103 - Implement WebConsole Network Panel, r=dietrich, a=blocking2.0 (beta5) --- .../console/hudservice/HUDService.jsm | 523 +++++++++++++++++- .../console/hudservice/NetworkPanel.xhtml | 121 ++++ .../browser/browser_HUDServiceTestsAll.js | 252 ++++++++- .../browser/browser_webconsole_netlogging.js | 27 +- toolkit/components/console/jar.mn | 1 + .../chrome/global/headsUpDisplay.properties | 15 + .../en-US/chrome/global/webConsole.dtd | 12 + toolkit/locales/jar.mn | 1 + toolkit/themes/gnomestripe/global/jar.mn | 1 + .../global/webConsole_networkPanel.css | 108 ++++ toolkit/themes/pinstripe/global/jar.mn | 1 + .../global/webConsole_networkPanel.css | 108 ++++ toolkit/themes/winstripe/global/jar.mn | 2 + .../global/webConsole_networkPanel.css | 108 ++++ 14 files changed, 1271 insertions(+), 9 deletions(-) create mode 100644 toolkit/components/console/hudservice/NetworkPanel.xhtml create mode 100644 toolkit/locales/en-US/chrome/global/webConsole.dtd create mode 100644 toolkit/themes/gnomestripe/global/webConsole_networkPanel.css create mode 100644 toolkit/themes/pinstripe/global/webConsole_networkPanel.css create mode 100644 toolkit/themes/winstripe/global/webConsole_networkPanel.css diff --git a/toolkit/components/console/hudservice/HUDService.jsm b/toolkit/components/console/hudservice/HUDService.jsm index 23b11e1efdd5..e941e6c98b00 100644 --- a/toolkit/components/console/hudservice/HUDService.jsm +++ b/toolkit/components/console/hudservice/HUDService.jsm @@ -241,6 +241,15 @@ ResponseListener.prototype = if (HUDService.lastFinishedRequestCallback) { HUDService.lastFinishedRequestCallback(this.httpActivity); } + + // Call update on all panels. + this.httpActivity.panels.forEach(function(weakRef) { + let panel = weakRef.get(); + if (panel) { + panel.update(); + } + }); + this.httpActivity.response.isDone = true; this.httpActivity = null; }, @@ -457,6 +466,471 @@ var NetworkHelper = // FIREBUG CODE END. +/////////////////////////////////////////////////////////////////////////// +//// Helper for creating the network panel. + +/** + * Creates a DOMNode and sets all the attributes of aAttributes on the created + * element. + * + * @param nsIDOMDocument aDocument + * Document to create the new DOMNode. + * @param string aTag + * Name of the tag for the DOMNode. + * @param object aAttributes + * Attributes set on the created DOMNode. + * + * @returns nsIDOMNode + */ +function createElement(aDocument, aTag, aAttributes) +{ + let node = aDocument.createElement(aTag); + for (var attr in aAttributes) { + node.setAttribute(attr, aAttributes[attr]); + } + return node; +} + +/** + * Creates a new DOMNode and appends it to aParent. + * + * @param nsIDOMNode aParent + * A parent node to append the created element. + * @param string aTag + * Name of the tag for the DOMNode. + * @param object aAttributes + * Attributes set on the created DOMNode. + * + * @returns nsIDOMNode + */ +function createAndAppendElement(aParent, aTag, aAttributes) +{ + let node = createElement(aParent.ownerDocument, aTag, aAttributes); + aParent.appendChild(node); + return node; +} + +/////////////////////////////////////////////////////////////////////////// +//// NetworkPanel + +/** + * Creates a new NetworkPanel. + * + * @param nsIDOMNode aParent + * Parent node to append the created panel to. + * @param object aHttpActivity + * HttpActivity to display in the panel. + */ +function NetworkPanel(aParent, aHttpActivity) +{ + let doc = aParent.ownerDocument; + this.httpActivity = aHttpActivity; + + // Create the underlaying panel + this.panel = createElement(doc, "panel", { + label: HUDService.getStr("NetworkPanel.label"), + titlebar: "normal", + noautofocus: "true", + noautohide: "true", + close: "true" + }); + + // Create the browser that displays the NetworkPanel XHTML. + this.browser = createAndAppendElement(this.panel, "browser", { + src: "chrome://global/content/NetworkPanel.xhtml", + disablehistory: "true", + flex: "1" + }); + + // Destroy the panel when it's closed. + this.panel.addEventListener("popuphidden", function onPopupHide() { + self.panel.removeEventListener("popuphidden", onPopupHide, false); + self.panel.parentNode.removeChild(self.panel); + self.panel = null; + self.browser = null; + self.document = null; + self.httpActivity = null; + }, false); + + // Set the document object and update the content once the panel is loaded. + let self = this; + this.panel.addEventListener("load", function onLoad() { + self.panel.removeEventListener("load", onLoad, true) + self.document = self.browser.contentWindow.document; + self.update(); + }, true); + + // Create the footer. + let footer = createElement(doc, "hbox", { align: "end" }); + createAndAppendElement(footer, "spacer", { flex: 1 }); + + createAndAppendElement(footer, "resizer", { dir: "bottomend" }); + this.panel.appendChild(footer); + + aParent.appendChild(this.panel); +} + +NetworkPanel.prototype = +{ + /** + * The current state of the output. + */ + _state: 0, + + /** + * State variables. + */ + _INIT: 0, + _DISPLAYED_REQUEST_HEADER: 1, + _DISPLAYED_REQUEST_BODY: 2, + _DISPLAYED_RESPONSE_HEADER: 3, + _TRANSITION_CLOSED: 4, + + /** + * Small helper function that is nearly equal to HUDService.getFormatStr + * except that it prefixes aName with "NetworkPanel.". + * + * @param string aName + * The name of an i10n string to format. This string is prefixed with + * "NetworkPanel." before calling the HUDService.getFormatStr function. + * @param array aArray + * Values used as placeholder for the i10n string. + * @returns string + * The i10n formated string. + */ + _format: function NP_format(aName, aArray) + { + return HUDService.getFormatStr("NetworkPanel." + aName, aArray); + }, + + /** + * + * @returns boolean + * True if the response is an image, false otherwise. + */ + get _responseIsImage() + { + let response = this.httpActivity.response; + if (!response || !response.header || !response.header["Content-Type"]) { + let request = this.httpActivity.request; + if (request.header["Accept"] && + request.header["Accept"].indexOf("image/") != -1) { + return true; + } + else { + return false; + } + } + return response.header["Content-Type"].indexOf("image/") != -1; + }, + + /** + * + * @returns boolean + * Returns true if the server responded that the request is already + * in the browser's cache, false otherwise. + */ + get _isResponseCached() + { + return this.httpActivity.response.status.indexOf("304") != -1; + }, + + /** + * Appends the node with id=aId by the text aValue. + * + * @param string aId + * @param string aValue + * @returns void + */ + _appendTextNode: function NP_appendTextNode(aId, aValue) + { + let textNode = this.document.createTextNode(aValue); + this.document.getElementById(aId).appendChild(textNode); + }, + + /** + * Generates some HTML to display the key-value pair of the aList data. The + * generated HTML is added to node with id=aParentId. + * + * @param string aParentId + * Id of the parent node to append the list to. + * @oaram object aList + * Object that holds the key-value information to display in aParentId. + * @param boolean aIgnoreCookie + * If true, the key-value named "Cookie" is not added to the list. + * @returns void + */ + _appendList: function NP_appendList(aParentId, aList, aIgnoreCookie) + { + let parent = this.document.getElementById(aParentId); + let doc = this.document; + + let sortedList = {}; + Object.keys(aList).sort().forEach(function(aKey) { + sortedList[aKey] = aList[aKey]; + }); + + for (let key in sortedList) { + if (aIgnoreCookie && key == "Cookie") { + continue; + } + + /** + * The following code creates the HTML: + * + * ${line}: + * ${aList[line]}
+ * + * and adds it to parent. + */ + let textNode = doc.createTextNode(key + ":"); + let span = doc.createElement("span"); + span.setAttribute("class", "property-name"); + span.appendChild(textNode); + parent.appendChild(span); + + textNode = doc.createTextNode(sortedList[key]); + span = doc.createElement("span"); + span.setAttribute("class", "property-value"); + span.appendChild(textNode); + parent.appendChild(span); + + parent.appendChild(doc.createElement("br")); + } + }, + + /** + * Displays the node with id=aId. + * + * @param string aId + * @returns void + */ + _displayNode: function NP_displayNode(aId) + { + this.document.getElementById(aId).style.display = "block"; + }, + + /** + * Sets the request URL, request method, the timing information when the + * request started and the request header content on the NetworkPanel. + * If the request header contains cookie data, a list of sent cookies is + * generated and a special sent cookie section is displayed + the cookie list + * added to it. + * + * @returns void + */ + _displayRequestHeader: function NP_displayRequestHeader() + { + let timing = this.httpActivity.timing; + let request = this.httpActivity.request; + + this._appendTextNode("headUrl", this.httpActivity.url); + this._appendTextNode("headMethod", this.httpActivity.method); + + this._appendTextNode("requestHeadersInfo", + ConsoleUtils.timestampString(timing.REQUEST_HEADER/1000)); + + this._appendList("requestHeadersContent", request.header, true); + + if ("Cookie" in request.header) { + this._displayNode("requestCookie"); + + let cookies = request.header.Cookie.split(";"); + let cookieList = {}; + let cookieListSorted = {}; + cookies.forEach(function(cookie) { + let name, value; + [name, value] = cookie.trim().split("="); + cookieList[name] = value; + }); + this._appendList("requestCookieContent", cookieList); + } + }, + + /** + * Displays the request body section of the NetworkPanel and set the request + * body content on the NetworkPanel. + * + * @returns void + */ + _displayRequestBody: function NP_displayRequestBody() { + this._displayNode("requestBody"); + this._appendTextNode("requestBodyContent", this.httpActivity.request.body); + }, + + /** + * Displays the response section of the NetworkPanel, sets the response status, + * the duration between the start of the request and the receiving of the + * response header as well as the response header content on the the NetworkPanel. + * + * @returns void + */ + _displayResponseHeader: function NP_displayResponseHeader() + { + let timing = this.httpActivity.timing; + let response = this.httpActivity.response; + + this._appendTextNode("headStatus", response.status); + + let deltaDuration = + Math.round((timing.RESPONSE_HEADER - timing.REQUEST_HEADER) / 1000); + this._appendTextNode("responseHeadersInfo", + this._format("durationMS", [deltaDuration])); + + this._displayNode("responseContainer"); + this._appendList("responseHeadersContent", response.header); + }, + + /** + * Displays the respones image section, sets the source of the image displayed + * in the image response section to the request URL and the duration between + * the receiving of the response header and the end of the request. Once the + * image is loaded, the size of the requested image is set. + * + * @returns void + */ + _displayResponseImage: function NP_displayResponseImage() + { + let self = this; + let timing = this.httpActivity.timing; + let response = this.httpActivity.response; + let cached = ""; + + if (this._isResponseCached) { + cached = "Cached"; + } + + let imageNode = this.document.getElementById("responseImage" + cached +"Node"); + imageNode.setAttribute("src", this.httpActivity.url); + + // This function is called to set the imageInfo. + function setImageInfo() { + let deltaDuration = + Math.round((timing.RESPONSE_COMPLETE - timing.RESPONSE_HEADER) / 1000); + self._appendTextNode("responseImage" + cached + "Info", + self._format("imageSizeDeltaDurationMS", [ + imageNode.width, imageNode.height, deltaDuration + ] + )); + } + + // Check if the image is already loaded. + if (imageNode.width != 0) { + setImageInfo(); + } + else { + // Image is not loaded yet therefore add a load event. + imageNode.addEventListener("load", function imageNodeLoad() { + imageNode.removeEventListener("load", imageNodeLoad, false); + setImageInfo(); + }, false); + } + + this._displayNode("responseImage" + cached); + }, + + /** + * Displays the response body section, sets the the duration between + * the receiving of the response header and the end of the request as well as + * the content of the response body on the NetworkPanel. + * + * @returns void + */ + _displayResponseBody: function NP_displayResponseBody() + { + let timing = this.httpActivity.timing; + let response = this.httpActivity.response; + + let deltaDuration = + Math.round((timing.RESPONSE_COMPLETE - timing.RESPONSE_HEADER) / 1000); + this._appendTextNode("responseBodyInfo", + this._format("durationMS", [deltaDuration])); + + this._displayNode("responseBody"); + this._appendTextNode("responseBodyContent", response.body); + }, + + /** + * Displays the `no response body` section and sets the the duration between + * the receiving of the response header and the end of the request. + * + * @returns void + */ + _displayNoResponseBody: function NP_displayNoResponseBody() + { + let timing = this.httpActivity.timing; + + this._displayNode("responseNoBody"); + let deltaDuration = + Math.round((timing.RESPONSE_COMPLETE - timing.RESPONSE_HEADER) / 1000); + this._appendTextNode("responseNoBodyInfo", + this._format("durationMS", [deltaDuration])); + }, + + /** + * Updates the content of the NetworkPanel's browser. + * + * @returns void + */ + update: function NP_update() + { + /** + * After the browser contentWindow is ready, the document object is set. + * If the document object isn't set yet, then the page is loaded and nothing + * can be updated. + */ + if (!this.document) { + return; + } + + let timing = this.httpActivity.timing; + let request = this.httpActivity.request; + let response = this.httpActivity.response; + + switch (this._state) { + case this._INIT: + this._displayRequestHeader(); + this._state = this._DISPLAYED_REQUEST_HEADER; + // FALL THROUGH + + case this._DISPLAYED_REQUEST_HEADER: + // Process the request body if there is one. + if (request.body) { + this._displayRequestBody(); + this._state = this._DISPLAYED_REQUEST_BODY; + } + // FALL THROUGH + + case this._DISPLAYED_REQUEST_BODY: + // There is always a response header. Therefore we can skip here if + // we don't have a response header yet and don't have to try updating + // anything else in the NetworkPanel. + if (!response.header) { + break + } + this._displayResponseHeader(); + this._state = this._DISPLAYED_RESPONSE_HEADER; + // FALL THROUGH + + case this._DISPLAYED_RESPONSE_HEADER: + // Check if the transition is done. + if (timing.TRANSACTION_CLOSE && response.isDone) { + if (this._responseIsImage) { + this._displayResponseImage(); + } + else if (response.body) { + this._displayResponseBody(); + } + else { + this._displayNoResponseBody(); + } + this._state = this._TRANSITION_CLOSED; + } + break; + } + } +} + function HUD_SERVICE() { // TODO: provide mixins for FENNEC: bug 568621 @@ -1008,6 +1482,11 @@ HUD_SERVICE.prototype = */ unregisterDisplay: function HS_unregisterDisplay(aId) { + // Remove children from the output. If the output is not cleared, there can + // be leaks as some nodes has node.onclick = function; set and GC can't + // remove the nodes then. + HUDService.clearDisplay(aId); + // remove HUD DOM node and // remove display references from local registries get the outputNode var outputNode = this.mixins.getOutputNodeById(aId); @@ -1366,6 +1845,28 @@ HUD_SERVICE.prototype = */ lastFinishedRequestCallback: null, + /** + * Opens a NetworkPanel. + * + * @param nsIDOMNode aNode + * DOMNode to display the panel next to. + * @param object aHttpActivity + * httpActivity object. The data of this object is displayed in the + * NetworkPanel. + * @returns NetworkPanel + */ + openNetworkPanel: function (aNode, aHttpActivity) { + let doc = aNode.ownerDocument; + let parent = doc.getElementById("mainPopupSet"); + let netPanel = new NetworkPanel(parent, aHttpActivity); + + let panel = netPanel.panel; + panel.openPopup(aNode, "after_pointer", 0, 0, false, false); + panel.sizeTo(350, 400); + aHttpActivity.panels.push(Cu.getWeakReference(netPanel)); + return netPanel; + }, + /** * Begin observing HTTP traffic that we care about, * namely traffic that originates inside any context that a Heads Up Display @@ -1413,6 +1914,7 @@ HUD_SERVICE.prototype = method: aChannel.requestMethod, channel: aChannel, + panels: [], request: { header: { } }, @@ -1450,6 +1952,13 @@ HUD_SERVICE.prototype = // Store the loggedNode and the httpActivity object for later reuse. httpActivity.messageObject = loggedNode; self.openRequests[httpActivity.id] = httpActivity; + + // Make the network span clickable. + let linkNode = loggedNode.messageNode; + linkNode.setAttribute("aria-haspopup", "true"); + linkNode.onclick = function() { + self.openNetworkPanel(linkNode, httpActivity); + } } else { // Iterate over all currently ongoing requests. If aChannel can't @@ -1467,7 +1976,7 @@ HUD_SERVICE.prototype = return; } - let msgObject; + let msgObject, updatePanel = false; let data, textNode; // Store the time information for this activity subtype. httpActivity.timing[transCodes[aActivitySubtype]] = aTimestamp; @@ -1481,7 +1990,7 @@ HUD_SERVICE.prototype = if (!sentBody) { // If the request URL is the same as the current page url, then // we can try to get the posted text from the page directly. - // This is necessary as otherwise the + // This check is necessary as otherwise the // NetworkHelper.readPostTextFromPage // function is called for image requests as well but these // are not web pages and as such don't store the posted text @@ -1549,8 +2058,18 @@ HUD_SERVICE.prototype = self.getFormatStr("networkUrlWithStatusAndDuration", data))); delete self.openRequests[item.id]; + updatePanel = true; break; } + + if (updatePanel) { + httpActivity.panels.forEach(function(weakRef) { + let panel = weakRef.get(); + if (panel) { + panel.update(); + } + }); + } } } }, diff --git a/toolkit/components/console/hudservice/NetworkPanel.xhtml b/toolkit/components/console/hudservice/NetworkPanel.xhtml new file mode 100644 index 000000000000..3fcdb1893bf4 --- /dev/null +++ b/toolkit/components/console/hudservice/NetworkPanel.xhtml @@ -0,0 +1,121 @@ + + + +%webConsoleDTD; +]> + + + + + + + + + + + + +
+

+ &networkPanel.requestHeaders; + +

+
+ + + + +
+ + + + diff --git a/toolkit/components/console/hudservice/tests/browser/browser_HUDServiceTestsAll.js b/toolkit/components/console/hudservice/tests/browser/browser_HUDServiceTestsAll.js index dfd130cfcd02..8fefd84b8959 100644 --- a/toolkit/components/console/hudservice/tests/browser/browser_HUDServiceTestsAll.js +++ b/toolkit/components/console/hudservice/tests/browser/browser_HUDServiceTestsAll.js @@ -70,6 +70,8 @@ const TEST_ERROR_URI = "http://example.com/browser/toolkit/components/console/hu const TEST_DUPLICATE_ERROR_URI = "http://example.com/browser/toolkit/components/console/hudservice/tests/browser/test-duplicate-error.html"; +const TEST_IMG = "http://example.com/browser/toolkit/components/console/hudservice/tests/browser/test-image.png"; + function noCacheUriSpec(aUriSpec) { return aUriSpec + "?_=" + Date.now(); } @@ -569,6 +571,254 @@ function testConsoleHistory() is (input.value, executeList[idxLast], "check history next idx:" + idxLast); } +function testNetworkPanel() +{ + function checkIsVisible(aPanel, aList) { + for (let id in aList) { + let node = aPanel.document.getElementById(id); + let isVisible = aList[id]; + is(node.style.display, (isVisible ? "block" : "none"), id + " isVisible=" + isVisible); + } + } + + function checkNodeContent(aPanel, aId, aContent) { + let node = aPanel.document.getElementById(aId); + if (node == null) { + ok(false, "Tried to access node " + aId + " that doesn't exist!"); + } + else if (node.textContent.indexOf(aContent) != -1) { + ok(true, "checking content of " + aId); + } + else { + ok(false, "Got false value for " + aId + ": " + node.textContent + " doesn't have " + aContent); + } + } + + function checkNodeKeyValue(aPanel, aId, aKey, aValue) { + let node = aPanel.document.getElementById(aId); + + let testHTML = '' + aKey + ':'; + testHTML += '' + aValue + ''; + isnot(node.innerHTML.indexOf(testHTML), -1, "checking content of " + aId); + } + + let testDriver; + function testGen() { + var httpActivity = { + url: "http://www.testpage.com", + method: "GET", + + panels: [], + request: { + header: { + foo: "bar" + } + }, + response: { }, + timing: { + "REQUEST_HEADER": 0 + } + }; + + let networkPanel = HUDService.openNetworkPanel(filterBox, httpActivity); + + is (networkPanel, httpActivity.panels[0].get(), "Network panel stored on httpActivity object"); + networkPanel.panel.addEventListener("load", function onLoad() { + networkPanel.panel.removeEventListener("load", onLoad, true); + testDriver.next(); + }, true); + yield; + + checkIsVisible(networkPanel, { + requestCookie: false, + requestBody: false, + responseContainer: false, + responseBody: false, + responseNoBody: false, + responseImage: false, + responseImageCached: false + }); + + checkNodeContent(networkPanel, "header", "http://www.testpage.com"); + checkNodeContent(networkPanel, "header", "GET"); + checkNodeKeyValue(networkPanel, "requestHeadersContent", "foo", "bar"); + + // Test request body. + httpActivity.request.body = "hello world"; + networkPanel.update(); + checkIsVisible(networkPanel, { + requestBody: true, + requestCookie: false, + responseContainer: false, + responseBody: false, + responseNoBody: false, + responseImage: false, + responseImageCached: false + }); + checkNodeContent(networkPanel, "requestBodyContent", "hello world"); + + // Test response header. + httpActivity.timing.RESPONSE_HEADER = 1000; + httpActivity.response.status = "999 earthquake win"; + httpActivity.response.header = { + leaveHouses: "true" + } + networkPanel.update(); + checkIsVisible(networkPanel, { + requestBody: true, + requestCookie: false, + responseContainer: true, + responseBody: false, + responseNoBody: false, + responseImage: false, + responseImageCached: false + }); + + checkNodeContent(networkPanel, "header", "999 earthquake win"); + checkNodeKeyValue(networkPanel, "responseHeadersContent", "leaveHouses", "true"); + checkNodeContent(networkPanel, "responseHeadersInfo", "1ms"); + + httpActivity.timing.RESPONSE_COMPLETE = 2500; + // This is necessary to show that the request is done. + httpActivity.timing.TRANSACTION_CLOSE = 2500; + networkPanel.update(); + + checkIsVisible(networkPanel, { + requestBody: true, + requestCookie: false, + responseContainer: true, + responseBody: false, + responseNoBody: false, + responseImage: false, + responseImageCached: false + }); + + httpActivity.response.isDone = true; + networkPanel.update(); + + checkNodeContent(networkPanel, "responseNoBodyInfo", "2ms"); + checkIsVisible(networkPanel, { + requestBody: true, + requestCookie: false, + responseContainer: true, + responseBody: false, + responseNoBody: true, + responseImage: false, + responseImageCached: false + }); + + networkPanel.panel.hidePopup(); + + // Second run: Test for cookies and response body. + httpActivity.request.header.Cookie = "foo=bar; hello=world"; + httpActivity.response.body = "get out here"; + + networkPanel = HUDService.openNetworkPanel(filterBox, httpActivity); + is (networkPanel, httpActivity.panels[1].get(), "Network panel stored on httpActivity object"); + networkPanel.panel.addEventListener("load", function onLoad() { + networkPanel.panel.removeEventListener("load", onLoad, true); + testDriver.next(); + }, true); + yield; + + + checkIsVisible(networkPanel, { + requestBody: true, + requestCookie: true, + responseContainer: true, + responseBody: true, + responseNoBody: false, + responseImage: false, + responseImageCached: false + }); + + checkNodeKeyValue(networkPanel, "requestCookieContent", "foo", "bar"); + checkNodeKeyValue(networkPanel, "requestCookieContent", "hello", "world"); + checkNodeContent(networkPanel, "responseBodyContent", "get out here"); + checkNodeContent(networkPanel, "responseBodyInfo", "2ms"); + + networkPanel.panel.hidePopup(); + + // Check image request. + httpActivity.response.header["Content-Type"] = "image/png"; + httpActivity.url = TEST_IMG; + + networkPanel = HUDService.openNetworkPanel(filterBox, httpActivity); + networkPanel.panel.addEventListener("load", function onLoad() { + networkPanel.panel.removeEventListener("load", onLoad, true); + testDriver.next(); + }, true); + yield; + + checkIsVisible(networkPanel, { + requestBody: true, + requestCookie: true, + responseContainer: true, + responseBody: false, + responseNoBody: false, + responseImage: true, + responseImageCached: false + }); + + let imgNode = networkPanel.document.getElementById("responseImageNode"); + is(imgNode.getAttribute("src"), TEST_IMG, "Displayed image is correct"); + + function checkImageResponseInfo() { + checkNodeContent(networkPanel, "responseImageInfo", "2ms"); + checkNodeContent(networkPanel, "responseImageInfo", "16x16px"); + } + + // Check if the image is loaded already. + if (imgNode.width == 0) { + imgNode.addEventListener("load", function onLoad() { + imgNode.removeEventListener("load", onLoad, false); + checkImageResponseInfo(); + networkPanel.panel.hidePopup(); + testDriver.next(); + }, false); + // Wait until the image is loaded. + yield; + } + else { + checkImageResponseInfo(); + networkPanel.panel.hidePopup(); + } + + // Check cached image request. + httpActivity.response.status = "HTTP/1.1 304 Not Modified"; + + networkPanel = HUDService.openNetworkPanel(filterBox, httpActivity); + networkPanel.panel.addEventListener("load", function onLoad() { + networkPanel.panel.removeEventListener("load", onLoad, true); + testDriver.next(); + }, true); + yield; + + checkIsVisible(networkPanel, { + requestBody: true, + requestCookie: true, + responseContainer: true, + responseBody: false, + responseNoBody: false, + responseImage: false, + responseImageCached: true + }); + + let imgNode = networkPanel.document.getElementById("responseImageCachedNode"); + is(imgNode.getAttribute("src"), TEST_IMG, "Displayed image is correct"); + + networkPanel.panel.hidePopup(); + + // Run the next test. + testErrorOnPageReload(); + + yield; + }; + + testDriver = testGen(); + testDriver.next(); +} + // test property provider function testPropertyProvider() { @@ -829,7 +1079,7 @@ function testPageReload() { is(typeof console.error, "function", "console.error is a function"); is(typeof console.exception, "function", "console.exception is a function"); - testErrorOnPageReload(); + testNetworkPanel(); }, false); content.location.reload(); diff --git a/toolkit/components/console/hudservice/tests/browser/browser_webconsole_netlogging.js b/toolkit/components/console/hudservice/tests/browser/browser_webconsole_netlogging.js index a5b87823fb27..5cb9ed630789 100644 --- a/toolkit/components/console/hudservice/tests/browser/browser_webconsole_netlogging.js +++ b/toolkit/components/console/hudservice/tests/browser/browser_webconsole_netlogging.js @@ -17,6 +17,8 @@ Cu.import("resource://gre/modules/HUDService.jsm"); const TEST_NETWORK_REQUEST_URI = "http://example.com/browser/toolkit/components/console/hudservice/tests/browser/test-network-request.html"; +const TEST_IMG = "http://example.com/browser/toolkit/components/console/hudservice/tests/browser/test-image.png"; + const TEST_DATA_JSON_CONTENT = '{ id: "test JSON data", myArray: [ "foo", "bar", "baz", "biff" ] }'; @@ -29,7 +31,7 @@ function testOpenWebConsole() is(HUDService.displaysIndex().length, 1, "WebConsole was opened"); hudId = HUDService.displaysIndex()[0]; - hud = HUDService.hudWeakReferences[hudId].get(); + hud = HUDService.getHeadsUpDisplay(hudId); testNetworkLogging(); } @@ -145,11 +147,24 @@ function testNetworkLogging() lastFinishedRequest = null - // All tests are done. Shutdown. - browser = null; - lastFinishedRequest = null; - HUDService.lastFinishedRequestCallback = null; - finishTest(); + // Open the NetworkPanel. The functionality of the NetworkPanel is tested + // within the testNetworkPanel() function. + let filterBox = hud.querySelectorAll(".hud-filter-box")[0]; + let networkPanel = HUDService.openNetworkPanel(filterBox, httpActivity); + is (networkPanel, httpActivity.panels[0].get(), "Network panel stored on httpActivity object"); + networkPanel.panel.addEventListener("load", function onLoad() { + networkPanel.panel.removeEventListener("load", onLoad, true); + + ok(true, "NetworkPanel was opened"); + networkPanel.panel.hidePopup(); + + // All tests are done. Shutdown. + browser = null; + lastFinishedRequest = null; + HUDService.lastFinishedRequestCallback = null; + + finishTest(); + }, true); } loggingGen = loggingGeneratorFunc(); diff --git a/toolkit/components/console/jar.mn b/toolkit/components/console/jar.mn index 4a5c2ee72c23..e8676791f6c2 100644 --- a/toolkit/components/console/jar.mn +++ b/toolkit/components/console/jar.mn @@ -1,5 +1,6 @@ toolkit.jar: *+ content/global/console.js (content/console.js) *+ content/global/console.xul (content/console.xul) ++ content/global/NetworkPanel.xhtml (hudservice/NetworkPanel.xhtml) + content/global/console.css (content/console.css) + content/global/consoleBindings.xml (content/consoleBindings.xml) diff --git a/toolkit/locales/en-US/chrome/global/headsUpDisplay.properties b/toolkit/locales/en-US/chrome/global/headsUpDisplay.properties index 34fd99e59aa0..a91529907305 100644 --- a/toolkit/locales/en-US/chrome/global/headsUpDisplay.properties +++ b/toolkit/locales/en-US/chrome/global/headsUpDisplay.properties @@ -84,3 +84,18 @@ networkUrlWithStatus=%1$S [%2$S # %2$S = response status code from the server (e.g. `HTTP/1.1 200 OK`) # %3$S = duration for the complete network request in milliseconds networkUrlWithStatusAndDuration=%1$S [%2$S %3$Sms] +NetworkPanel.label=Inspect Network Request +# LOCALIZATION NOTE (NetworkPanel.deltaDurationMS): +# +# This string is used to show the duration between two network events (e.g +# request and respones header or response header and response body). +NetworkPanel.durationMS=%Sms +# LOCALIZATION NOTE (NetworkPanel.imageSizeDeltaDurationMS): +# This string is used to show the duration between the response header and the +# response body event. It also shows the size of the received or cached image. +# +# The first %S is replace by the width of the inspected image. +# The second %S is replaced by the height of the inspected image. +# The third %S is replaced by the duration between the response header and the +# response body event. +NetworkPanel.imageSizeDeltaDurationMS=%Sx%Spx, Δ%Sms diff --git a/toolkit/locales/en-US/chrome/global/webConsole.dtd b/toolkit/locales/en-US/chrome/global/webConsole.dtd new file mode 100644 index 000000000000..b4dbeea01688 --- /dev/null +++ b/toolkit/locales/en-US/chrome/global/webConsole.dtd @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/toolkit/locales/jar.mn b/toolkit/locales/jar.mn index 08f7ae88261a..a554efe77483 100644 --- a/toolkit/locales/jar.mn +++ b/toolkit/locales/jar.mn @@ -37,6 +37,7 @@ + locale/@AB_CD@/global/finddialog.properties (%chrome/global/finddialog.properties) locale/@AB_CD@/global/globalKeys.dtd (%chrome/global/globalKeys.dtd) + locale/@AB_CD@/global/headsUpDisplay.properties (%chrome/global/headsUpDisplay.properties) ++ locale/@AB_CD@/global/webConsole.dtd (%chrome/global/webConsole.dtd) + locale/@AB_CD@/global/intl.css (%chrome/global/intl.css) + locale/@AB_CD@/global/intl.properties (%chrome/global/intl.properties) + locale/@AB_CD@/global/keys.properties (%chrome/global/keys.properties) diff --git a/toolkit/themes/gnomestripe/global/jar.mn b/toolkit/themes/gnomestripe/global/jar.mn index 41026e38d1b0..eff524eb4a53 100644 --- a/toolkit/themes/gnomestripe/global/jar.mn +++ b/toolkit/themes/gnomestripe/global/jar.mn @@ -28,6 +28,7 @@ toolkit.jar: + skin/classic/global/toolbarbutton.css + skin/classic/global/tree.css + skin/classic/global/webConsole.css ++ skin/classic/global/webConsole_networkPanel.css + skin/classic/global/alerts/alert.css (alerts/alert.css) + skin/classic/global/console/console.css (console/console.css) + skin/classic/global/console/console.png (console/console.png) diff --git a/toolkit/themes/gnomestripe/global/webConsole_networkPanel.css b/toolkit/themes/gnomestripe/global/webConsole_networkPanel.css new file mode 100644 index 000000000000..f2f1fe71cd84 --- /dev/null +++ b/toolkit/themes/gnomestripe/global/webConsole_networkPanel.css @@ -0,0 +1,108 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is DevTools code + * + * The Initial Developer of the Original Code is + * Mozilla Corporation + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Joe Walker + * Julian Viereck + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +body { + font-family: Lucida Grande, sans-serif; + font-size: 11px; + background: #EEE; +} + +div#header { + padding: 5px; + overflow-x:auto; +} + +h1 { + font-size: 13px; + padding: 2px 10px; + margin: 0px; + background: -moz-linear-gradient(top, #BBB, #999); + -moz-border-radius: 2px; + text-shadow: #FFF 0px 1px 0px; +} + +h1 .info { + font-size: 11px; + float: right; + color: #333; + padding-right: 3px; +} + +div.property-header { + padding: 2px 5px; + background: -moz-linear-gradient(top, #FFF, #F8F8F8); + color: #333; + max-height: 330px; + overflow-y: auto; + overflow-x: auto; + white-space: pre-wrap; +} + +span.property-name { + font-size: 11px; + font-weight: bold; + padding-right: 4px; + color: #000; +} + +span.property-value { + padding-right: 5px; + font-size: 11px; +} + +div.group { + margin-top: 10px; +} + +div.group, div#header { + background: #FFF; + border-color: #E1E1E1; + border-style: solid; + border-width: 1px; + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + -moz-border-radius: 4px 4px 4px 4px; +} + +img#responseImageNode { + -moz-box-shadow: rgba(0,0,0,0.2) 0px 3px 5px; + max-width: 100%; +} + +#responseImageNodeDiv { + padding: 5px; +} diff --git a/toolkit/themes/pinstripe/global/jar.mn b/toolkit/themes/pinstripe/global/jar.mn index 6a5bd6e8e825..2f2921a8c91d 100644 --- a/toolkit/themes/pinstripe/global/jar.mn +++ b/toolkit/themes/pinstripe/global/jar.mn @@ -50,6 +50,7 @@ toolkit.jar: skin/classic/global/tree.css * skin/classic/global/viewbuttons.css * skin/classic/global/webConsole.css +* skin/classic/global/webConsole_networkPanel.css skin/classic/global/wizard.css skin/classic/global/arrow/arrow-dn-dis.gif (arrow/arrow-dn-dis.gif) skin/classic/global/arrow/arrow-dn-dis.png (arrow/arrow-dn-dis.png) diff --git a/toolkit/themes/pinstripe/global/webConsole_networkPanel.css b/toolkit/themes/pinstripe/global/webConsole_networkPanel.css new file mode 100644 index 000000000000..f2f1fe71cd84 --- /dev/null +++ b/toolkit/themes/pinstripe/global/webConsole_networkPanel.css @@ -0,0 +1,108 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is DevTools code + * + * The Initial Developer of the Original Code is + * Mozilla Corporation + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Joe Walker + * Julian Viereck + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +body { + font-family: Lucida Grande, sans-serif; + font-size: 11px; + background: #EEE; +} + +div#header { + padding: 5px; + overflow-x:auto; +} + +h1 { + font-size: 13px; + padding: 2px 10px; + margin: 0px; + background: -moz-linear-gradient(top, #BBB, #999); + -moz-border-radius: 2px; + text-shadow: #FFF 0px 1px 0px; +} + +h1 .info { + font-size: 11px; + float: right; + color: #333; + padding-right: 3px; +} + +div.property-header { + padding: 2px 5px; + background: -moz-linear-gradient(top, #FFF, #F8F8F8); + color: #333; + max-height: 330px; + overflow-y: auto; + overflow-x: auto; + white-space: pre-wrap; +} + +span.property-name { + font-size: 11px; + font-weight: bold; + padding-right: 4px; + color: #000; +} + +span.property-value { + padding-right: 5px; + font-size: 11px; +} + +div.group { + margin-top: 10px; +} + +div.group, div#header { + background: #FFF; + border-color: #E1E1E1; + border-style: solid; + border-width: 1px; + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + -moz-border-radius: 4px 4px 4px 4px; +} + +img#responseImageNode { + -moz-box-shadow: rgba(0,0,0,0.2) 0px 3px 5px; + max-width: 100%; +} + +#responseImageNodeDiv { + padding: 5px; +} diff --git a/toolkit/themes/winstripe/global/jar.mn b/toolkit/themes/winstripe/global/jar.mn index 78439f9b71d0..d4779815789f 100644 --- a/toolkit/themes/winstripe/global/jar.mn +++ b/toolkit/themes/winstripe/global/jar.mn @@ -54,6 +54,7 @@ toolkit.jar: skin/classic/global/toolbarbutton.css skin/classic/global/tree.css * skin/classic/global/webConsole.css +* skin/classic/global/webConsole_networkPanel.css skin/classic/global/wizard.css skin/classic/global/alerts/alert.css (alerts/alert.css) skin/classic/global/arrow/arrow-dn.gif (arrow/arrow-dn.gif) @@ -222,6 +223,7 @@ toolkit.jar: * skin/classic/aero/global/toolbarbutton.css (toolbarbutton-aero.css) * skin/classic/aero/global/tree.css (tree-aero.css) * skin/classic/aero/global/webConsole.css +* skin/classic/global/webConsole_networkPanel.css skin/classic/aero/global/wizard.css skin/classic/aero/global/alerts/alert.css (alerts/alert.css) skin/classic/aero/global/arrow/arrow-dn.gif (arrow/arrow-dn.gif) diff --git a/toolkit/themes/winstripe/global/webConsole_networkPanel.css b/toolkit/themes/winstripe/global/webConsole_networkPanel.css new file mode 100644 index 000000000000..f2f1fe71cd84 --- /dev/null +++ b/toolkit/themes/winstripe/global/webConsole_networkPanel.css @@ -0,0 +1,108 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is DevTools code + * + * The Initial Developer of the Original Code is + * Mozilla Corporation + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Joe Walker + * Julian Viereck + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +body { + font-family: Lucida Grande, sans-serif; + font-size: 11px; + background: #EEE; +} + +div#header { + padding: 5px; + overflow-x:auto; +} + +h1 { + font-size: 13px; + padding: 2px 10px; + margin: 0px; + background: -moz-linear-gradient(top, #BBB, #999); + -moz-border-radius: 2px; + text-shadow: #FFF 0px 1px 0px; +} + +h1 .info { + font-size: 11px; + float: right; + color: #333; + padding-right: 3px; +} + +div.property-header { + padding: 2px 5px; + background: -moz-linear-gradient(top, #FFF, #F8F8F8); + color: #333; + max-height: 330px; + overflow-y: auto; + overflow-x: auto; + white-space: pre-wrap; +} + +span.property-name { + font-size: 11px; + font-weight: bold; + padding-right: 4px; + color: #000; +} + +span.property-value { + padding-right: 5px; + font-size: 11px; +} + +div.group { + margin-top: 10px; +} + +div.group, div#header { + background: #FFF; + border-color: #E1E1E1; + border-style: solid; + border-width: 1px; + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + -moz-border-radius: 4px 4px 4px 4px; +} + +img#responseImageNode { + -moz-box-shadow: rgba(0,0,0,0.2) 0px 3px 5px; + max-width: 100%; +} + +#responseImageNodeDiv { + padding: 5px; +}