diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 6d8ddf2caca..44acd447db7 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1058,6 +1058,7 @@ pref("devtools.toolbox.sideEnabled", true); pref("devtools.inspector.enabled", true); pref("devtools.inspector.activeSidebar", "ruleview"); pref("devtools.inspector.markupPreview", false); +pref("devtools.inspector.remote", false); // Enable the Layout View pref("devtools.layoutview.enabled", true); diff --git a/browser/devtools/fontinspector/font-inspector.js b/browser/devtools/fontinspector/font-inspector.js index 9983db21fd0..7c92217db8f 100644 --- a/browser/devtools/fontinspector/font-inspector.js +++ b/browser/devtools/fontinspector/font-inspector.js @@ -54,6 +54,7 @@ FontInspector.prototype = { */ onNewNode: function FI_onNewNode() { if (this.isActive() && + this.inspector.selection.isLocal() && this.inspector.selection.isConnected() && this.inspector.selection.isElementNode() && this.inspector.selection.reason != "highlighter") { diff --git a/browser/devtools/framework/sidebar.js b/browser/devtools/framework/sidebar.js index e312db4512d..af41f4d47ca 100644 --- a/browser/devtools/framework/sidebar.js +++ b/browser/devtools/framework/sidebar.js @@ -148,6 +148,11 @@ ToolSidebar.prototype = { */ handleEvent: function ToolSidebar_eventHandler(event) { if (event.type == "select") { + if (this._currentTool == this.getCurrentTabID()) { + // Tool hasn't changed. + return; + } + let previousTool = this._currentTool; this._currentTool = this.getCurrentTabID(); if (previousTool) { diff --git a/browser/devtools/framework/target.js b/browser/devtools/framework/target.js index d76ec7f9802..7a93b775046 100644 --- a/browser/devtools/framework/target.js +++ b/browser/devtools/framework/target.js @@ -15,6 +15,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer", XPCOMUtils.defineLazyModuleGetter(this, "DebuggerClient", "resource://gre/modules/devtools/dbg-client.jsm"); +loader.lazyGetter(this, "InspectorFront", () => require("devtools/server/actors/inspector").InspectorFront); + const targets = new WeakMap(); const promiseTargets = new WeakMap(); @@ -250,6 +252,17 @@ TabTarget.prototype = { return !!this._isThreadPaused; }, + get inspector() { + if (!this.form) { + throw new Error("Target.inspector requires an initialized remote actor."); + } + if (this._inspector) { + return this._inspector; + } + this._inspector = InspectorFront(this.client, this.form); + return this._inspector; + }, + /** * Adds remote protocol capabilities to the target, so that it can be used * for tools that support the Remote Debugging Protocol even for local diff --git a/browser/devtools/inspector/breadcrumbs.js b/browser/devtools/inspector/breadcrumbs.js index 820ff6b7973..7b52b3e76ec 100644 --- a/browser/devtools/inspector/breadcrumbs.js +++ b/browser/devtools/inspector/breadcrumbs.js @@ -12,6 +12,9 @@ const ENSURE_SELECTION_VISIBLE_DELAY = 50; // ms Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource:///modules/devtools/DOMHelpers.jsm"); Cu.import("resource:///modules/devtools/LayoutHelpers.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +let promise = require("sdk/core/promise"); const LOW_PRIORITY_ELEMENTS = { "HEAD": true, @@ -25,6 +28,18 @@ const LOW_PRIORITY_ELEMENTS = { "TITLE": true, }; +function resolveNextTick(value) { + let deferred = promise.defer(); + Services.tm.mainThread.dispatch(() => { + try { + deferred.resolve(value); + } catch(ex) { + console.error(ex); + } + }, Ci.nsIThread.DISPATCH_NORMAL); + return deferred.promise; +} + /////////////////////////////////////////////////////////////////////////// //// HTML Breadcrumbs @@ -53,6 +68,8 @@ function HTMLBreadcrumbs(aInspector) exports.HTMLBreadcrumbs = HTMLBreadcrumbs; HTMLBreadcrumbs.prototype = { + get walker() this.inspector.walker, + _init: function BC__init() { this.container = this.chromeDoc.getElementById("inspector-breadcrumbs"); @@ -83,12 +100,37 @@ HTMLBreadcrumbs.prototype = { this.update = this.update.bind(this); this.updateSelectors = this.updateSelectors.bind(this); - this.selection.on("new-node", this.update); + this.selection.on("new-node-front", this.update); this.selection.on("pseudoclass", this.updateSelectors); this.selection.on("attribute-changed", this.updateSelectors); this.update(); }, + /** + * Include in a promise's then() chain to reject the chain + * when the breadcrumbs' selection has changed while the promise + * was outstanding. + */ + selectionGuard: function() { + let selection = this.selection.nodeFront; + return (result) => { + if (selection != this.selection.nodeFront) { + return promise.reject("selection-changed"); + } + return result; + } + }, + + /** + * Print any errors (except selection guard errors). + */ + selectionGuardEnd: function(err) { + if (err != "selection-changed") { + console.error(err); + } + promise.reject(err); + }, + /** * Build a string that represents the node: tagName#id.class1.class2. * @@ -101,12 +143,19 @@ HTMLBreadcrumbs.prototype = { if (aNode.id) { text += "#" + aNode.id; } - for (let i = 0; i < aNode.classList.length; i++) { - text += "." + aNode.classList[i]; + + if (aNode.className) { + let classList = aNode.className.split(/\s+/); + for (let i = 0; i < classList.length; i++) { + text += "." + classList[i]; + } } + + // XXX: needs updating when pseudoclass-lock is remotable + let rawNode = aNode.rawNode(); for (let i = 0; i < PSEUDO_CLASSES.length; i++) { let pseudo = PSEUDO_CLASSES[i]; - if (DOMUtils.hasPseudoClassLock(aNode, pseudo)) { + if (DOMUtils.hasPseudoClassLock(rawNode, pseudo)) { text += pseudo; } } @@ -143,14 +192,20 @@ HTMLBreadcrumbs.prototype = { tagLabel.textContent = aNode.tagName.toLowerCase(); idLabel.textContent = aNode.id ? ("#" + aNode.id) : ""; - let classesText = ""; - for (let i = 0; i < aNode.classList.length; i++) { - classesText += "." + aNode.classList[i]; + if (aNode.className) { + let classesText = ""; + let classList = aNode.className.split(/\s+/); + for (let i = 0; i < classList.length; i++) { + classesText += "." + classList[i]; + } + classesLabel.textContent = classesText; } - classesLabel.textContent = classesText; + + // XXX: Until we have pseudoclass lock in the node. + let rawNode = aNode.rawNode(); let pseudos = PSEUDO_CLASSES.filter(function(pseudo) { - return DOMUtils.hasPseudoClassLock(aNode, pseudo); + return DOMUtils.hasPseudoClassLock(rawNode, pseudo); }, this); pseudosLabel.textContent = pseudos.join(""); @@ -173,7 +228,7 @@ HTMLBreadcrumbs.prototype = { // We make sure that the targeted node is selected // because we want to use the nodemenu that only works // for inspector.selection - this.selection.setNode(aNode, "breadcrumbs"); + this.selection.setNodeFront(aNode, "breadcrumbs"); let title = this.chromeDoc.createElement("menuitem"); title.setAttribute("label", this.inspector.strings.GetStringFromName("breadcrumbs.siblings")); @@ -183,9 +238,11 @@ HTMLBreadcrumbs.prototype = { let items = [title, separator]; - let nodes = aNode.parentNode.childNodes; - for (let i = 0; i < nodes.length; i++) { - if (nodes[i].nodeType == aNode.ELEMENT_NODE) { + this.walker.siblings(aNode, { + whatToShow: Ci.nsIDOMNodeFilter.SHOW_ELEMENT + }).then(siblings => { + let nodes = siblings.nodes; + for (let i = 0; i < nodes.length; i++) { let item = this.chromeDoc.createElement("menuitem"); if (nodes[i] === aNode) { item.setAttribute("disabled", "true"); @@ -198,14 +255,14 @@ HTMLBreadcrumbs.prototype = { let selection = this.selection; item.onmouseup = (function(aNode) { return function() { - selection.setNode(aNode, "breadcrumbs"); + selection.setNodeFront(aNode, "breadcrumbs"); } })(nodes[i]); items.push(item); + this.inspector.showNodeMenu(aButton, "before_start", items); } - } - this.inspector.showNodeMenu(aButton, "before_start", items); + }); }, /** @@ -252,33 +309,40 @@ HTMLBreadcrumbs.prototype = { if (event.type == "keypress" && this.selection.isElementNode()) { let node = null; - switch (event.keyCode) { - case this.chromeWin.KeyEvent.DOM_VK_LEFT: - if (this.currentIndex != 0) { - node = this.nodeHierarchy[this.currentIndex - 1].node; + + + this._keyPromise = this._keyPromise || promise.resolve(null); + + this._keyPromise = (this._keyPromise || promise.resolve(null)).then(() => { + switch (event.keyCode) { + case this.chromeWin.KeyEvent.DOM_VK_LEFT: + if (this.currentIndex != 0) { + node = promise.resolve(this.nodeHierarchy[this.currentIndex - 1].node); + } + break; + case this.chromeWin.KeyEvent.DOM_VK_RIGHT: + if (this.currentIndex < this.nodeHierarchy.length - 1) { + node = promise.resolve(this.nodeHierarchy[this.currentIndex + 1].node); + } + break; + case this.chromeWin.KeyEvent.DOM_VK_UP: + node = this.walker.previousSibling(this.selection.nodeFront, { + whatToShow: Ci.nsIDOMNodeFilter.SHOW_ELEMENT + }); + break; + case this.chromeWin.KeyEvent.DOM_VK_DOWN: + node = this.walker.nextSibling(this.selection.nodeFront, { + whatToShow: Ci.nsIDOMNodeFilter.SHOW_ELEMENT + }); + break; + } + + return node.then((node) => { + if (node) { + this.selection.setNodeFront(node, "breadcrumbs"); } - break; - case this.chromeWin.KeyEvent.DOM_VK_RIGHT: - if (this.currentIndex < this.nodeHierarchy.length - 1) { - node = this.nodeHierarchy[this.currentIndex + 1].node; - } - break; - case this.chromeWin.KeyEvent.DOM_VK_UP: - node = this.selection.node.previousSibling; - while (node && (node.nodeType != node.ELEMENT_NODE)) { - node = node.previousSibling; - } - break; - case this.chromeWin.KeyEvent.DOM_VK_DOWN: - node = this.selection.node.nextSibling; - while (node && (node.nodeType != node.ELEMENT_NODE)) { - node = node.nextSibling; - } - break; - } - if (node) { - this.selection.setNode(node, "breadcrumbs"); - } + }); + }); event.preventDefault(); event.stopPropagation(); } @@ -290,12 +354,18 @@ HTMLBreadcrumbs.prototype = { destroy: function BC_destroy() { this.nodeHierarchy.forEach(function(crumb) { - if (LayoutHelpers.isNodeConnected(crumb.node)) { - DOMUtils.clearPseudoClassLocks(crumb.node); + // This node might have already been destroyed during + // shutdown. Will clean this up when pseudo-class lock + // is ported to the walker. + if (crumb.node.actorID) { + let rawNode = crumb.node.rawNode(); + if (LayoutHelpers.isNodeConnected(rawNode)) { + DOMUtils.clearPseudoClassLocks(rawNode); + } } }); - this.selection.off("new-node", this.update); + this.selection.off("new-node-front", this.update); this.selection.off("pseudoclass", this.updateSelectors); this.selection.off("attribute-changed", this.updateSelectors); @@ -401,7 +471,7 @@ HTMLBreadcrumbs.prototype = { } button.onBreadcrumbsClick = function onBreadcrumbsClick() { - this.selection.setNode(aNode, "breadcrumbs"); + this.selection.setNodeFront(aNode, "breadcrumbs"); }.bind(this); button.onclick = (function _onBreadcrumbsRightClick(event) { @@ -437,7 +507,7 @@ HTMLBreadcrumbs.prototype = { fragment.insertBefore(button, lastButtonInserted); lastButtonInserted = button; this.nodeHierarchy.splice(originalLength, 0, {node: toAppend, button: button}); - toAppend = this.DOMHelpers.getParentObject(toAppend); + toAppend = toAppend.parentNode(); } this.container.appendChild(fragment, this.container.firstChild); }, @@ -451,24 +521,37 @@ HTMLBreadcrumbs.prototype = { */ getInterestingFirstNode: function BC_getInterestingFirstNode(aNode) { - let nextChild = this.DOMHelpers.getChildObject(aNode, 0); - let fallback = null; + let deferred = promise.defer(); - while (nextChild) { - if (nextChild.nodeType == aNode.ELEMENT_NODE) { - if (!(nextChild.tagName in LOW_PRIORITY_ELEMENTS)) { - return nextChild; + var fallback = null; + + var moreChildren = () => { + this.walker.children(aNode, { + start: fallback, + maxNodes: 10, + whatToShow: Ci.nsIDOMNodeFilter.SHOW_ELEMENT + }).then(this.selectionGuard()).then(response => { + for (let node of response.nodes) { + if (!(node.tagName in LOW_PRIORITY_ELEMENTS)) { + deferred.resolve(node); + return; + } + if (!fallback) { + fallback = node; + } } - if (!fallback) { - fallback = nextChild; + if (response.hasLast) { + deferred.resolve(fallback); + return; + } else { + moreChildren(); } - } - nextChild = this.DOMHelpers.getNextSibling(nextChild); + }).then(null, this.selectionGuardEnd); } - return fallback; + moreChildren(); + return deferred.promise; }, - /** * Find the "youngest" ancestor of a node which is already in the breadcrumbs. * @@ -483,7 +566,7 @@ HTMLBreadcrumbs.prototype = { if (idx > -1) { return idx; } else { - node = this.DOMHelpers.getParentObject(node); + node = node.parentNode(); } } return -1; @@ -498,13 +581,16 @@ HTMLBreadcrumbs.prototype = { // If the last displayed node is the selected node if (this.currentIndex == this.nodeHierarchy.length - 1) { let node = this.nodeHierarchy[this.currentIndex].node; - let child = this.getInterestingFirstNode(node); - // If the node has a child - if (child) { - // Show this child - this.expand(child); - } + return this.getInterestingFirstNode(node).then(child => { + // If the node has a child + if (child) { + // Show this child + this.expand(child); + } + }); } + + return resolveNextTick(true); }, /** @@ -560,7 +646,7 @@ HTMLBreadcrumbs.prototype = { return; } - let idx = this.indexOf(this.selection.node); + let idx = this.indexOf(this.selection.nodeFront); // Is the node already displayed in the breadcrumbs? if (idx > -1) { @@ -571,24 +657,32 @@ HTMLBreadcrumbs.prototype = { if (this.nodeHierarchy.length > 0) { // No. We drop all the element that are not direct ancestors // of the selection - let parent = this.DOMHelpers.getParentObject(this.selection.node); + let parent = this.selection.nodeFront.parentNode(); let idx = this.getCommonAncestor(parent); this.cutAfter(idx); } // we append the missing button between the end of the breadcrumbs display // and the current node. - this.expand(this.selection.node); + this.expand(this.selection.nodeFront); // we select the current node button - idx = this.indexOf(this.selection.node); + idx = this.indexOf(this.selection.nodeFront); this.setCursor(idx); } - // Add the first child of the very last node of the breadcrumbs if possible. - this.ensureFirstChild(); - this.updateSelectors(); - // Make sure the selected node and its neighbours are visible. - this.scroll(); + let doneUpdating = this.inspector.updating("breadcrumbs"); + // Add the first child of the very last node of the breadcrumbs if possible. + this.ensureFirstChild().then(this.selectionGuard()).then(() => { + this.updateSelectors(); + + // Make sure the selected node and its neighbours are visible. + this.scroll(); + this.inspector.emit("breadcrumbs-updated", this.selection.nodeFront); + doneUpdating(); + }).then(null, err => { + doneUpdating(this.selection.nodeFront); + this.selectionGuardEnd(err); + }); }, } diff --git a/browser/devtools/inspector/highlighter.js b/browser/devtools/inspector/highlighter.js index 753c882ebba..dfba4a03fac 100644 --- a/browser/devtools/inspector/highlighter.js +++ b/browser/devtools/inspector/highlighter.js @@ -228,6 +228,15 @@ Highlighter.prototype = { */ invalidateSize: function Highlighter_invalidateSize() { + // The highlighter runs locally while the selection runs remotely, + // so we can't quite trust the selection's isConnected to protect us + // here, do the check manually. + if (!this.selection.node || + !this.selection.node.ownerDocument || + !this.selection.node.ownerDocument.defaultView) { + return; + } + let canHiglightNode = this.selection.isNode() && this.selection.isConnected() && this.selection.isElementNode(); diff --git a/browser/devtools/inspector/inspector-panel.js b/browser/devtools/inspector/inspector-panel.js index 5d809cfa099..c7a36f38dc7 100644 --- a/browser/devtools/inspector/inspector-panel.js +++ b/browser/devtools/inspector/inspector-panel.js @@ -13,7 +13,7 @@ let EventEmitter = require("devtools/shared/event-emitter"); let {CssLogic} = require("devtools/styleinspector/css-logic"); loader.lazyGetter(this, "MarkupView", () => require("devtools/markupview/markup-view").MarkupView); -loader.lazyGetter(this, "Selection", () => require ("devtools/inspector/selection").Selection); +loader.lazyGetter(this, "Selection", () => require("devtools/inspector/selection").Selection); loader.lazyGetter(this, "HTMLBreadcrumbs", () => require("devtools/inspector/breadcrumbs").HTMLBreadcrumbs); loader.lazyGetter(this, "Highlighter", () => require("devtools/inspector/highlighter").Highlighter); loader.lazyGetter(this, "ToolSidebar", () => require("devtools/framework/sidebar").ToolSidebar); @@ -44,6 +44,20 @@ InspectorPanel.prototype = { * open is effectively an asynchronous constructor */ open: function InspectorPanel_open() { + return this.target.makeRemote().then(() => { + return this.target.inspector.getWalker(); + }).then(walker => { + if (this._destroyPromise) { + walker.release().then(null, console.error); + } + this.walker = walker; + return this._getDefaultNodeForSelection(); + }).then(defaultSelection => { + return this._deferredOpen(defaultSelection); + }).then(null, console.error); + }, + + _deferredOpen: function(defaultSelection) { let deferred = Promise.defer(); this.onNavigatedAway = this.onNavigatedAway.bind(this); @@ -57,13 +71,13 @@ InspectorPanel.prototype = { this.nodemenu.addEventListener("popuphiding", this._resetNodeMenu, true); // Create an empty selection - this._selection = new Selection(); + this._selection = new Selection(this.walker); this.onNewSelection = this.onNewSelection.bind(this); - this.selection.on("new-node", this.onNewSelection); + this.selection.on("new-node-front", this.onNewSelection); this.onBeforeNewSelection = this.onBeforeNewSelection.bind(this); - this.selection.on("before-new-node", this.onBeforeNewSelection); + this.selection.on("before-new-node-front", this.onBeforeNewSelection); this.onDetached = this.onDetached.bind(this); - this.selection.on("detached", this.onDetached); + this.selection.on("detached-front", this.onDetached); this.breadcrumbs = new HTMLBreadcrumbs(this); @@ -121,13 +135,7 @@ InspectorPanel.prototype = { this.isReady = true; // All the components are initialized. Let's select a node. - if (this.target.isLocalTab) { - this._selection.setNode( - this._getDefaultNodeForSelection(this.browser.contentDocument)); - } else if (this.target.window) { - this._selection.setNode( - this._getDefaultNodeForSelection(this.target.window.document)); - } + this._selection.setNodeFront(defaultSelection); if (this.highlighter) { this.highlighter.unlock(); @@ -146,13 +154,17 @@ InspectorPanel.prototype = { }, /** - * Select node for default selection + * Return a promise that will resolve to the default node for selection. */ - _getDefaultNodeForSelection : function(document) { + _getDefaultNodeForSelection : function() { // if available set body node as default selected node // else set documentElement - var defaultNode = document.body || document.documentElement; - return defaultNode; + return this.walker.querySelector(this.walker.rootNode, "body").then(front => { + if (front) { + return front; + } + return this.walker.documentElement(this.walker.rootNode); + }); }, /** @@ -262,35 +274,34 @@ InspectorPanel.prototype = { */ onNavigatedAway: function InspectorPanel_onNavigatedAway(event, payload) { let newWindow = payload._navPayload || payload; - this.selection.setNode(null); + this.walker.release().then(null, console.error); + this.walker = null; + this.selection.setNodeFront(null); + this.selection.setWalker(null); this._destroyMarkup(); this.isDirty = false; - let onDOMReady = function() { - newWindow.removeEventListener("DOMContentLoaded", onDOMReady, true); - - if (this._destroyed) { + this.target.inspector.getWalker().then(walker => { + if (this._destroyPromise) { + walker.release().then(null, console.error); return; } - if (!this.selection.node) { - let defaultNode = this._getDefaultNodeForSelection(newWindow.document); - this.selection.setNode(defaultNode, "navigateaway"); - } - this._initMarkup(); + this.walker = walker; + this.selection.setWalker(walker); + this._getDefaultNodeForSelection().then(defaultNode => { + if (this._destroyPromise) { + return; + } + this.selection.setNodeFront(defaultNode, "navigateaway"); - this.once("markuploaded", () => { - this.markup.expandNode(this.selection.node); + this._initMarkup(); + this.once("markuploaded", () => { + this.markup.expandNode(this.selection.node); + this.setupSearchBox(); + }); }); - - this.setupSearchBox(); - }.bind(this); - - if (newWindow.document.readyState == "loading") { - newWindow.addEventListener("DOMContentLoaded", onDOMReady, true); - } else { - onDOMReady(); - } + }); }, /** @@ -298,6 +309,69 @@ InspectorPanel.prototype = { */ onNewSelection: function InspectorPanel_onNewSelection() { this.cancelLayoutChange(); + + // Wait for all the known tools to finish updating and then let the + // client know. + let selection = this.selection.nodeFront; + let selfUpdate = this.updating("inspector-panel"); + Services.tm.mainThread.dispatch(() => { + try { + selfUpdate(selection); + } catch(ex) { + console.error(ex); + } + }, Ci.nsIThread.DISPATCH_NORMAL); + }, + + /** + * Delay the "inspector-updated" notification while a tool + * is updating itself. Returns a function that must be + * invoked when the tool is done updating with the node + * that the tool is viewing. + */ + updating: function(name) { + if (this._updateProgress && this._updateProgress.node != this.selection.nodeFront) { + this.cancelUpdate(); + } + + if (!this._updateProgress) { + // Start an update in progress. + var self = this; + this._updateProgress = { + node: this.selection.nodeFront, + outstanding: new Set(), + checkDone: function() { + if (this !== self._updateProgress) { + return; + } + if (this.node !== self.selection.nodeFront) { + self.cancelUpdate(); + return; + } + if (this.outstanding.size !== 0) { + return; + } + + self._updateProgress = null; + self.emit("inspector-updated"); + }, + } + } + + let progress = this._updateProgress; + let done = function() { + progress.outstanding.delete(done); + progress.checkDone(); + }; + progress.outstanding.add(done); + return done; + }, + + /** + * Cancel notification of inspector updates. + */ + cancelUpdate: function() { + this._updateProgress = null; }, /** @@ -317,18 +391,24 @@ InspectorPanel.prototype = { onDetached: function InspectorPanel_onDetached(event, parentNode) { this.cancelLayoutChange(); this.breadcrumbs.cutAfter(this.breadcrumbs.indexOf(parentNode)); - this.selection.setNode(parentNode, "detached"); + this.selection.setNodeFront(parentNode, "detached"); }, /** * Destroy the inspector. */ destroy: function InspectorPanel__destroy() { - if (this._destroyed) { - return Promise.resolve(null); + if (this._destroyPromise) { + return this._destroyPromise; + } + if (this.walker) { + this._destroyPromise = this.walker.release().then(null, console.error); + delete this.walker; + } else { + this._destroyPromise = Promise.resolve(null); } - this._destroyed = true; + this.cancelUpdate(); this.cancelLayoutChange(); if (this.browser) { @@ -358,9 +438,10 @@ InspectorPanel.prototype = { this.nodemenu.removeEventListener("popuphiding", this._resetNodeMenu, true); this.breadcrumbs.destroy(); this.searchSuggestions.destroy(); - this.selection.off("new-node", this.onNewSelection); + this.selection.off("new-node-front", this.onNewSelection); this.selection.off("before-new-node", this.onBeforeNewSelection); - this.selection.off("detached", this.onDetached); + this.selection.off("before-new-node-front", this.onBeforeNewSelection); + this.selection.off("detached-front", this.onDetached); this._destroyMarkup(); this._selection.destroy(); this._selection = null; @@ -374,7 +455,7 @@ InspectorPanel.prototype = { this.nodemenu = null; this.highlighter = null; - return Promise.resolve(null); + return this._destroyPromise; }, /** @@ -501,7 +582,7 @@ InspectorPanel.prototype = { if (this.selection.isElementNode()) { if (DOMUtils.hasPseudoClassLock(this.selection.node, aPseudo)) { this.breadcrumbs.nodeHierarchy.forEach(function(crumb) { - DOMUtils.removePseudoClassLock(crumb.node, aPseudo); + DOMUtils.removePseudoClassLock(crumb.node.rawNode(), aPseudo); }); } else { let hierarchical = aPseudo == ":hover" || aPseudo == ":active"; @@ -522,7 +603,7 @@ InspectorPanel.prototype = { clearPseudoClasses: function InspectorPanel_clearPseudoClasses() { this.breadcrumbs.nodeHierarchy.forEach(function(crumb) { try { - DOMUtils.clearPseudoClassLocks(crumb.node); + DOMUtils.clearPseudoClassLocks(crumb.node.rawNode()); } catch(e) { // Ignore dead nodes after navigation. } @@ -534,6 +615,9 @@ InspectorPanel.prototype = { */ toggleHighlighter: function InspectorPanel_toggleHighlighter(event) { + if (!this.highlighter) { + return; + } if (event.type == "mouseover") { this.highlighter.hide(); } diff --git a/browser/devtools/inspector/selection.js b/browser/devtools/inspector/selection.js index 6963f6fe362..a7364fab8d2 100644 --- a/browser/devtools/inspector/selection.js +++ b/browser/devtools/inspector/selection.js @@ -4,13 +4,13 @@ * 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/. */ -const {Cu} = require("chrome"); +const {Cu, Ci} = require("chrome"); let EventEmitter = require("devtools/shared/event-emitter"); /** * API * - * new Selection(node=null, track={attributes,detached}); + * new Selection(walker=null, node=null, track={attributes,detached}); * destroy() * node (readonly) * setNode(node, origin="unknown") @@ -53,19 +53,22 @@ let EventEmitter = require("devtools/shared/event-emitter"); * @param node Inner node. * Can be null. Can be (un)set in the future via the "node" property; * @param trackAttribute Tell if events should be fired when the attributes of - * the ndoe change. + * the node change. * */ -function Selection(node=null, track={attributes:true,detached:true}) { +function Selection(walker, node=null, track={attributes:true,detached:true}) { EventEmitter.decorate(this); + this._onMutations = this._onMutations.bind(this); this.track = track; + this.setWalker(walker); this.setNode(node); } exports.Selection = Selection; Selection.prototype = { + _walker: null, _node: null, _onMutations: function(mutations) { @@ -86,60 +89,41 @@ Selection.prototype = { if (attributeChange) this.emit("attribute-changed"); - if (detached) - this.emit("detached", parentNode); - }, - - _attachEvents: function SN__attachEvents() { - if (!this.window || !this.isNode() || !this.track) { - return; + if (detached) { + this.emit("detached", parentNode ? parentNode.rawNode() : null); + this.emit("detached-front", parentNode); } - - if (this.track.attributes) { - this._nodeObserver = new this.window.MutationObserver(this._onMutations); - this._nodeObserver.observe(this.node, {attributes: true}); - } - - if (this.track.detached) { - this._docObserver = new this.window.MutationObserver(this._onMutations); - this._docObserver.observe(this.document.documentElement, {childList: true, subtree: true}); - } - }, - - _detachEvents: function SN__detachEvents() { - // `disconnect` fail if node's document has - // been deleted. - try { - if (this._nodeObserver) - this._nodeObserver.disconnect(); - } catch(e) {} - try { - if (this._docObserver) - this._docObserver.disconnect(); - } catch(e) {} }, destroy: function SN_destroy() { - this._detachEvents(); this.setNode(null); + this.setWalker(null); }, - setNode: function SN_setNode(value, reason="unknown") { - this.reason = reason; - if (value !== this._node) { - this.emit("before-new-node", value, reason); - let previousNode = this._node; - this._detachEvents(); - this._node = value; - this._attachEvents(); - this.emit("new-node", previousNode, this.reason); + setWalker: function(walker) { + if (this._walker) { + this._walker.off("mutations", this._onMutations); + } + this._walker = walker; + if (this._walker) { + this._walker.on("mutations", this._onMutations); } }, + // Not remote-safe + setNode: function SN_setNode(value, reason="unknown") { + if (value) { + value = this._walker.frontForRawNode(value); + } + this.setNodeFront(value, reason); + }, + + // Not remote-safe get node() { return this._node; }, + // Not remote-safe get window() { if (this.isNode()) { return this.node.ownerDocument.defaultView; @@ -147,6 +131,7 @@ Selection.prototype = { return null; }, + // Not remote-safe get document() { if (this.isNode()) { return this.node.ownerDocument; @@ -154,28 +139,79 @@ Selection.prototype = { return null; }, + setNodeFront: function(value, reason="unknown") { + this.reason = reason; + if (value !== this._nodeFront) { + let rawValue = value ? value.rawNode() : value; + this.emit("before-new-node", rawValue, reason); + this.emit("before-new-node-front", value, reason); + let previousNode = this._node; + let previousFront = this._nodeFront; + this._node = rawValue; + this._nodeFront = value; + this.emit("new-node", previousNode, this.reason); + this.emit("new-node-front", value, this.reason); + } + }, + + get documentFront() { + return this._walker.document(this._nodeFront); + }, + + get nodeFront() { + return this._nodeFront; + }, + isRoot: function SN_isRootNode() { return this.isNode() && this.isConnected() && - this.node.ownerDocument.documentElement === this.node; + this._nodeFront.isDocumentElement; }, isNode: function SN_isNode() { - return (this.node && - !Cu.isDeadWrapper(this.node) && - this.node.ownerDocument && - this.node.ownerDocument.defaultView && - this.node instanceof this.node.ownerDocument.defaultView.Node); + if (!this._nodeFront) { + return false; + } + + // As long as tools are still accessing node.rawNode(), + // this needs to stay here. + if (this._node && Cu.isDeadWrapper(this._node)) { + return false; + } + + return true; + }, + + isLocal: function SN_nsLocal() { + return !!this._node; }, isConnected: function SN_isConnected() { - try { - let doc = this.document; - return doc && doc.defaultView && doc.documentElement.contains(this.node); - } catch (e) { - // "can't access dead object" error + let node = this._nodeFront; + if (!node || !node.actorID) { return false; } + + // As long as there are still tools going around + // accessing node.rawNode, this needs to stay. + let rawNode = node.rawNode(); + if (rawNode) { + try { + let doc = this.document; + return (doc && doc.defaultView && doc.documentElement.contains(rawNode)); + } catch (e) { + // "can't access dead object" error + return false; + } + } + + while(node) { + if (node === this._walker.rootNode) { + return true; + } + node = node.parentNode(); + }; + return false; }, isHTMLNode: function SN_isHTMLNode() { @@ -186,50 +222,50 @@ Selection.prototype = { // Node type isElementNode: function SN_isElementNode() { - return this.isNode() && this.node.nodeType == this.window.Node.ELEMENT_NODE; + return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ELEMENT_NODE; }, isAttributeNode: function SN_isAttributeNode() { - return this.isNode() && this.node.nodeType == this.window.Node.ATTRIBUTE_NODE; + return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ATTRIBUTE_NODE; }, isTextNode: function SN_isTextNode() { - return this.isNode() && this.node.nodeType == this.window.Node.TEXT_NODE; + return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.TEXT_NODE; }, isCDATANode: function SN_isCDATANode() { - return this.isNode() && this.node.nodeType == this.window.Node.CDATA_SECTION_NODE; + return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.CDATA_SECTION_NODE; }, isEntityRefNode: function SN_isEntityRefNode() { - return this.isNode() && this.node.nodeType == this.window.Node.ENTITY_REFERENCE_NODE; + return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ENTITY_REFERENCE_NODE; }, isEntityNode: function SN_isEntityNode() { - return this.isNode() && this.node.nodeType == this.window.Node.ENTITY_NODE; + return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ENTITY_NODE; }, isProcessingInstructionNode: function SN_isProcessingInstructionNode() { - return this.isNode() && this.node.nodeType == this.window.Node.PROCESSING_INSTRUCTION_NODE; + return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.PROCESSING_INSTRUCTION_NODE; }, isCommentNode: function SN_isCommentNode() { - return this.isNode() && this.node.nodeType == this.window.Node.PROCESSING_INSTRUCTION_NODE; + return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.PROCESSING_INSTRUCTION_NODE; }, isDocumentNode: function SN_isDocumentNode() { - return this.isNode() && this.node.nodeType == this.window.Node.DOCUMENT_NODE; + return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE; }, isDocumentTypeNode: function SN_isDocumentTypeNode() { - return this.isNode() && this.node.nodeType ==this.window. Node.DOCUMENT_TYPE_NODE; + return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE; }, isDocumentFragmentNode: function SN_isDocumentFragmentNode() { - return this.isNode() && this.node.nodeType == this.window.Node.DOCUMENT_FRAGMENT_NODE; + return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE; }, isNotationNode: function SN_isNotationNode() { - return this.isNode() && this.node.nodeType == this.window.Node.NOTATION_NODE; + return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.NOTATION_NODE; }, } diff --git a/browser/devtools/inspector/test/browser_inspector_breadcrumbs.js b/browser/devtools/inspector/test/browser_inspector_breadcrumbs.js index a6d1b2c3158..d2b98a7c4d3 100644 --- a/browser/devtools/inspector/test/browser_inspector_breadcrumbs.js +++ b/browser/devtools/inspector/test/browser_inspector_breadcrumbs.js @@ -44,7 +44,7 @@ function test() { inspector = aInspector; cursor = 0; - inspector.selection.on("new-node", nodeSelected); + inspector.on("breadcrumbs-updated", nodeSelected); executeSoon(function() { inspector.selection.setNode(nodes[0].node); }); @@ -52,17 +52,16 @@ function test() function nodeSelected() { - executeSoon(function() { - performTest(); - cursor++; - if (cursor >= nodes.length) { - inspector.selection.off("new-node", nodeSelected); - finishUp(); - } else { - let node = nodes[cursor].node; - inspector.selection.setNode(node); - } - }); + performTest(); + cursor++; + + if (cursor >= nodes.length) { + inspector.off("breadcrumbs-updated", nodeSelected); + finishUp(); + } else { + let node = nodes[cursor].node; + inspector.selection.setNode(node); + } } function performTest() diff --git a/browser/devtools/inspector/test/browser_inspector_bug_672902_keyboard_shortcuts.js b/browser/devtools/inspector/test/browser_inspector_bug_672902_keyboard_shortcuts.js index 79ba64580ff..c67db1e87d0 100644 --- a/browser/devtools/inspector/test/browser_inspector_bug_672902_keyboard_shortcuts.js +++ b/browser/devtools/inspector/test/browser_inspector_bug_672902_keyboard_shortcuts.js @@ -34,11 +34,12 @@ function test() { inspector = aInspector; - executeSoon(function() { - inspector.selection.once("new-node", highlightHeaderNode); - // Test that navigating around without a selected node gets us to the - // head element. - node = doc.querySelector("h1"); + // Make sure the body element is selected initially. + node = doc.querySelector("body"); + inspector.once("inspector-updated", () => { + is(inspector.selection.node, node, "Body should be selected initially."); + node = doc.querySelector("h1") + inspector.once("inspector-updated", highlightHeaderNode); let bc = inspector.breadcrumbs; bc.nodeHierarchy[bc.currentIndex].button.focus(); EventUtils.synthesizeKey("VK_RIGHT", { }); @@ -50,7 +51,7 @@ function test() is(inspector.selection.node, node, "selected h1 element"); executeSoon(function() { - inspector.selection.once("new-node", highlightParagraphNode); + inspector.once("inspector-updated", highlightParagraphNode); // Test that moving to the next sibling works. node = doc.querySelector("p"); EventUtils.synthesizeKey("VK_DOWN", { }); @@ -62,7 +63,7 @@ function test() is(inspector.selection.node, node, "selected p element"); executeSoon(function() { - inspector.selection.once("new-node", highlightHeaderNodeAgain); + inspector.once("inspector-updated", highlightHeaderNodeAgain); // Test that moving to the previous sibling works. node = doc.querySelector("h1"); EventUtils.synthesizeKey("VK_UP", { }); @@ -74,7 +75,7 @@ function test() is(inspector.selection.node, node, "selected h1 element"); executeSoon(function() { - inspector.selection.once("new-node", highlightParentNode); + inspector.once("inspector-updated", highlightParentNode); // Test that moving to the parent works. node = doc.querySelector("body"); EventUtils.synthesizeKey("VK_LEFT", { }); diff --git a/browser/devtools/inspector/test/browser_inspector_bug_817558_delete_node.js b/browser/devtools/inspector/test/browser_inspector_bug_817558_delete_node.js index da286979bb1..b500be2b543 100644 --- a/browser/devtools/inspector/test/browser_inspector_bug_817558_delete_node.js +++ b/browser/devtools/inspector/test/browser_inspector_bug_817558_delete_node.js @@ -34,9 +34,10 @@ function test() let tmp = {}; Cu.import("resource:///modules/devtools/LayoutHelpers.jsm", tmp); ok(!tmp.LayoutHelpers.isNodeConnected(node), "Node considered as disconnected."); - executeSoon(function() { - is(inspector.selection.node, parentNode, "parent of selection got selected"); + // Wait for the selection to process the mutation + inspector.walker.on("mutations", () => { + is(inspector.selection.node, parentNode, "parent of selection got selected"); finishUp(); }); } diff --git a/browser/devtools/inspector/test/browser_inspector_initialization.js b/browser/devtools/inspector/test/browser_inspector_initialization.js index 56e6ec493c5..66f93c38d58 100644 --- a/browser/devtools/inspector/test/browser_inspector_initialization.js +++ b/browser/devtools/inspector/test/browser_inspector_initialization.js @@ -40,27 +40,29 @@ function startInspectorTests(toolbox) let p = doc.querySelector("p"); inspector.selection.setNode(p); + inspector.once("inspector-updated", () => { + testHighlighter(p); + testMarkupView(p); + testBreadcrumbs(p); - testHighlighter(p); - testMarkupView(p); - testBreadcrumbs(p); + let span = doc.querySelector("span"); + span.scrollIntoView(); - let span = doc.querySelector("span"); - span.scrollIntoView(); + inspector.selection.setNode(span); + inspector.once("inspector-updated", () => { + testHighlighter(span); + testMarkupView(span); + testBreadcrumbs(span); - inspector.selection.setNode(span); - - testHighlighter(span); - testMarkupView(span); - testBreadcrumbs(span); - - toolbox.once("destroyed", function() { - ok("true", "'destroyed' notification received."); - let target = TargetFactory.forTab(gBrowser.selectedTab); - ok(!gDevTools.getToolbox(target), "Toolbox destroyed."); - executeSoon(runContextMenuTest); + toolbox.once("destroyed", function() { + ok("true", "'destroyed' notification received."); + let target = TargetFactory.forTab(gBrowser.selectedTab); + ok(!gDevTools.getToolbox(target), "Toolbox destroyed."); + executeSoon(runContextMenuTest); + }); + toolbox.destroy(); + }); }); - toolbox.destroy(); } @@ -79,7 +81,7 @@ function testMarkupView(node) function testBreadcrumbs(node) { let b = getActiveInspector().breadcrumbs; - let expectedText = b.prettyPrintNodeAsText(node); + let expectedText = b.prettyPrintNodeAsText(getNodeFront(node)); let button = b.container.querySelector("button[checked=true]"); ok(button, "A crumbs is checked=true"); is(button.getAttribute("tooltiptext"), expectedText, "Crumb refers to the right node"); diff --git a/browser/devtools/inspector/test/browser_inspector_menu.js b/browser/devtools/inspector/test/browser_inspector_menu.js index 8e257b1b80e..378cb2f9945 100644 --- a/browser/devtools/inspector/test/browser_inspector_menu.js +++ b/browser/devtools/inspector/test/browser_inspector_menu.js @@ -31,21 +31,23 @@ function test() { function checkDocTypeMenuItems() { info("Checking context menu entries for doctype node"); inspector.selection.setNode(doc.doctype); - let docTypeNode = getMarkupTagNodeContaining(""); + inspector.once("inspector-updated", () => { + let docTypeNode = getMarkupTagNodeContaining(""); - // Right-click doctype tag - contextMenuClick(docTypeNode); + // Right-click doctype tag + contextMenuClick(docTypeNode); - checkDisabled("node-menu-copyinner"); - checkDisabled("node-menu-copyouter"); - checkDisabled("node-menu-copyuniqueselector"); - checkDisabled("node-menu-delete"); + checkDisabled("node-menu-copyinner"); + checkDisabled("node-menu-copyouter"); + checkDisabled("node-menu-copyuniqueselector"); + checkDisabled("node-menu-delete"); - for (let name of ["hover", "active", "focus"]) { - checkDisabled("node-menu-pseudo-" + name); - } + for (let name of ["hover", "active", "focus"]) { + checkDisabled("node-menu-pseudo-" + name); + } - checkElementMenuItems(); + checkElementMenuItems(); + }); } function checkElementMenuItems() { diff --git a/browser/devtools/inspector/test/browser_inspector_pseudoclass_lock.js b/browser/devtools/inspector/test/browser_inspector_pseudoclass_lock.js index b0d9e17fc95..623c73b0bc0 100644 --- a/browser/devtools/inspector/test/browser_inspector_pseudoclass_lock.js +++ b/browser/devtools/inspector/test/browser_inspector_pseudoclass_lock.js @@ -51,11 +51,11 @@ function createDocument() function selectNode(aInspector) { inspector = aInspector; - inspector.selection.setNode(div); inspector.sidebar.once("ruleview-ready", function() { ruleview = inspector.sidebar.getWindowForTab("ruleview").ruleview.view; inspector.sidebar.select("ruleview"); - performTests(); + inspector.selection.setNode(div); + inspector.once("inspector-updated", performTests); }); } @@ -75,29 +75,37 @@ function performTests() // toggle it back on inspector.togglePseudoClass(pseudo); - testNavigate(); - - // close the inspector - finishUp(); + testNavigate(() => { + // close the inspector + finishUp(); + }); } -function testNavigate() +function testNavigate(callback) { inspector.selection.setNode(parentDiv); + inspector.once("inspector-updated", () => { - // make sure it's still on after naving to parent - is(DOMUtils.hasPseudoClassLock(div, pseudo), true, - "pseudo-class lock is still applied after inspecting ancestor"); + // make sure it's still on after naving to parent + is(DOMUtils.hasPseudoClassLock(div, pseudo), true, + "pseudo-class lock is still applied after inspecting ancestor"); - inspector.selection.setNode(div2); + inspector.selection.setNode(div2); - // make sure it's removed after naving to a non-hierarchy node - is(DOMUtils.hasPseudoClassLock(div, pseudo), false, - "pseudo-class lock is removed after inspecting sibling node"); + inspector.once("inspector-updated", () => { - // toggle it back on - inspector.selection.setNode(div); - inspector.togglePseudoClass(pseudo); + // make sure it's removed after naving to a non-hierarchy node + is(DOMUtils.hasPseudoClassLock(div, pseudo), false, + "pseudo-class lock is removed after inspecting sibling node"); + + // toggle it back on + inspector.selection.setNode(div); + inspector.once("inspector-updated", () => { + inspector.togglePseudoClass(pseudo); + callback(); + }); + }); + }); } function testAdded() diff --git a/browser/devtools/inspector/test/head.js b/browser/devtools/inspector/test/head.js index e5b84ee0c51..62894973ee3 100644 --- a/browser/devtools/inspector/test/head.js +++ b/browser/devtools/inspector/test/head.js @@ -5,6 +5,13 @@ const Cu = Components.utils; const Ci = Components.interfaces; const Cc = Components.classes; + +Services.prefs.setBoolPref("devtools.debugger.log", true); +SimpleTest.registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.debugger.log"); +}); + + let tempScope = {}; Cu.import("resource:///modules/devtools/LayoutHelpers.jsm", tempScope); let LayoutHelpers = tempScope.LayoutHelpers; @@ -19,6 +26,12 @@ let console = tempScope.console; let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/")); Services.scriptloader.loadSubScript(testDir + "../../../commandline/test/helpers.js", this); +SimpleTest.registerCleanupFunction(() => { + console.error("Here we are\n") + let {DebuggerServer} = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {}); + console.error("DebuggerServer open connections: " + Object.getOwnPropertyNames(DebuggerServer._connections).length); +}); + function openInspector(callback) { let target = TargetFactory.forTab(gBrowser.selectedTab); @@ -33,6 +46,12 @@ function getActiveInspector() return gDevTools.getToolbox(target).getPanel("inspector"); } +function getNodeFront(node) +{ + let inspector = getActiveInspector(); + return inspector.walker.frontForRawNode(node); +} + function isHighlighting() { let outline = getActiveInspector().highlighter.outline; diff --git a/browser/devtools/markupview/test/browser_inspector_markup_edit.js b/browser/devtools/markupview/test/browser_inspector_markup_edit.js index 89af699149e..2a88bf1c82e 100644 --- a/browser/devtools/markupview/test/browser_inspector_markup_edit.js +++ b/browser/devtools/markupview/test/browser_inspector_markup_edit.js @@ -307,8 +307,11 @@ function test() { id: "node18", }); - is(inspector.highlighter.nodeInfo.classesBox.textContent, "", - "No classes in the infobar before edit."); + /** + * XXX: disabled until the remote markup view is enabled + * is(inspector.highlighter.nodeInfo.classesBox.textContent, "", + * "No classes in the infobar before edit."); + */ }, execute: function(after) { inspector.once("markupmutation", function() { @@ -326,8 +329,12 @@ function test() { class: "newclass", style: "color:green" }); - is(inspector.highlighter.nodeInfo.classesBox.textContent, ".newclass", - "Correct classes in the infobar after edit."); + + /** + * XXX: disabled until the remote markup view is enabled + *is(inspector.highlighter.nodeInfo.classesBox.textContent, ".newclass", + * "Correct classes in the infobar after edit."); + */ } }; testAsyncSetup(test, editTagName); diff --git a/browser/devtools/netmonitor/netmonitor-controller.js b/browser/devtools/netmonitor/netmonitor-controller.js index 72e7a8a7056..1da4336b5c5 100644 --- a/browser/devtools/netmonitor/netmonitor-controller.js +++ b/browser/devtools/netmonitor/netmonitor-controller.js @@ -250,6 +250,7 @@ TargetEventsHandler.prototype = { case "will-navigate": { // Reset UI. NetMonitorView.RequestsMenu.reset(); + NetMonitorView.Sidebar.reset(); NetMonitorView.NetworkDetails.reset(); // Reset global helpers cache. diff --git a/browser/devtools/netmonitor/netmonitor-view.js b/browser/devtools/netmonitor/netmonitor-view.js index b8437dc6b57..afdd1e59fc4 100644 --- a/browser/devtools/netmonitor/netmonitor-view.js +++ b/browser/devtools/netmonitor/netmonitor-view.js @@ -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(); diff --git a/browser/devtools/netmonitor/netmonitor.css b/browser/devtools/netmonitor/netmonitor.css index cae41f8aa2e..ce087d7dca1 100644 --- a/browser/devtools/netmonitor/netmonitor.css +++ b/browser/devtools/netmonitor/netmonitor.css @@ -15,6 +15,10 @@ overflow: auto; } +#custom-pane { + overflow: auto; +} + #timings-summary-blocked { display: none; /* This doesn't work yet. */ } diff --git a/browser/devtools/netmonitor/netmonitor.xul b/browser/devtools/netmonitor/netmonitor.xul index 06f4f1e09ec..48165c5376f 100644 --- a/browser/devtools/netmonitor/netmonitor.xul +++ b/browser/devtools/netmonitor/netmonitor.xul @@ -17,6 +17,16 @@ + + +

iframe console test

+ + + + + \ No newline at end of file diff --git a/browser/devtools/webconsole/test/test-iframe1.html b/browser/devtools/webconsole/test/test-iframe1.html new file mode 100644 index 00000000000..4dd4eddfedc --- /dev/null +++ b/browser/devtools/webconsole/test/test-iframe1.html @@ -0,0 +1,10 @@ + + + + + +

iframe 1

+ + \ No newline at end of file diff --git a/browser/devtools/webconsole/test/test-iframe2.html b/browser/devtools/webconsole/test/test-iframe2.html new file mode 100644 index 00000000000..c15884795fd --- /dev/null +++ b/browser/devtools/webconsole/test/test-iframe2.html @@ -0,0 +1,11 @@ + + + + + +

iframe 2

+ + \ No newline at end of file diff --git a/browser/devtools/webconsole/test/test-iframe3.html b/browser/devtools/webconsole/test/test-iframe3.html new file mode 100644 index 00000000000..f0df8b6692e --- /dev/null +++ b/browser/devtools/webconsole/test/test-iframe3.html @@ -0,0 +1,11 @@ + + + + + +

iframe 3

+ + + \ No newline at end of file diff --git a/browser/devtools/webconsole/webconsole.js b/browser/devtools/webconsole/webconsole.js index c711a8c9be4..43d3b3bcbd9 100644 --- a/browser/devtools/webconsole/webconsole.js +++ b/browser/devtools/webconsole/webconsole.js @@ -383,18 +383,12 @@ WebConsoleFrame.prototype = { }.bind(this)); }, - _persistLog: null, - /** - * Getter for the persistent logging preference. This value is cached per - * instance to avoid reading the pref too often. + * Getter for the persistent logging preference. * @type boolean */ get persistLog() { - if (this._persistLog === null) { - this._persistLog = Services.prefs.getBoolPref(PREF_PERSISTLOG); - } - return this._persistLog; + return Services.prefs.getBoolPref(PREF_PERSISTLOG); }, /** diff --git a/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd b/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd index bdf28c90c9b..79654af2a53 100644 --- a/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd +++ b/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd @@ -168,3 +168,36 @@ - in the network details timings tab identifying the amount of time spent - in a "receive" state. --> + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/themes/linux/devtools/netmonitor.css b/browser/themes/linux/devtools/netmonitor.css index a93106ca32e..59a2854009c 100644 --- a/browser/themes/linux/devtools/netmonitor.css +++ b/browser/themes/linux/devtools/netmonitor.css @@ -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 { diff --git a/browser/themes/osx/devtools/netmonitor.css b/browser/themes/osx/devtools/netmonitor.css index 906ae13feea..ad31a365888 100644 --- a/browser/themes/osx/devtools/netmonitor.css +++ b/browser/themes/osx/devtools/netmonitor.css @@ -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 { diff --git a/browser/themes/windows/devtools/netmonitor.css b/browser/themes/windows/devtools/netmonitor.css index ee72aa607a4..1dc29e0676d 100644 --- a/browser/themes/windows/devtools/netmonitor.css +++ b/browser/themes/windows/devtools/netmonitor.css @@ -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 { diff --git a/toolkit/devtools/server/actors/webconsole.js b/toolkit/devtools/server/actors/webconsole.js index e83dcbf3579..3f64d30eeb4 100644 --- a/toolkit/devtools/server/actors/webconsole.js +++ b/toolkit/devtools/server/actors/webconsole.js @@ -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; @@ -551,7 +563,7 @@ WebConsoleActor.prototype = else { message = { _type: "LogMessage", - message: aMessage.message, + message: this._createStringGrip(aMessage.message), timeStamp: aMessage.timeStamp, }; } @@ -948,10 +960,15 @@ WebConsoleActor.prototype = */ preparePageErrorForRemote: function WCA_preparePageErrorForRemote(aPageError) { + let lineText = aPageError.sourceLine; + if (lineText && lineText.length > DebuggerServer.LONG_STRING_INITIAL_LENGTH) { + lineText = lineText.substr(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH); + } + return { errorMessage: this._createStringGrip(aPageError.errorMessage), sourceName: aPageError.sourceName, - lineText: this._createStringGrip(aPageError.sourceLine), + lineText: lineText, lineNumber: aPageError.lineNumber, columnNumber: aPageError.columnNumber, category: aPageError.category, @@ -995,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, @@ -1011,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. @@ -1108,7 +1176,7 @@ WebConsoleActor.prototype = }); break; } - }, + } }; WebConsoleActor.prototype.requestTypes = @@ -1120,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: {}, }; @@ -1159,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 = @@ -1201,6 +1264,10 @@ NetworkEventActor.prototype = } } this._longStringActors = new Set(); + + if (this.channel) { + this.parent._netEvents.delete(this.channel); + } this.parent.releaseActor(this); }, @@ -1213,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. * @@ -1540,4 +1628,3 @@ NetworkEventActor.prototype.requestTypes = DebuggerServer.addTabActor(WebConsoleActor, "consoleActor"); DebuggerServer.addGlobalActor(WebConsoleActor, "consoleActor"); - diff --git a/toolkit/devtools/webconsole/WebConsoleClient.jsm b/toolkit/devtools/webconsole/WebConsoleClient.jsm index 9c7ced146bc..8f87661e227 100644 --- a/toolkit/devtools/webconsole/WebConsoleClient.jsm +++ b/toolkit/devtools/webconsole/WebConsoleClient.jsm @@ -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. * diff --git a/toolkit/devtools/webconsole/WebConsoleUtils.jsm b/toolkit/devtools/webconsole/WebConsoleUtils.jsm index dc53c1115f5..3f560a0572d 100644 --- a/toolkit/devtools/webconsole/WebConsoleUtils.jsm +++ b/toolkit/devtools/webconsole/WebConsoleUtils.jsm @@ -144,9 +144,33 @@ this.WebConsoleUtils = { getInnerWindowId: function WCU_getInnerWindowId(aWindow) { return aWindow.QueryInterface(Ci.nsIInterfaceRequestor). - getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID; + getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID; }, + /** + * Recursively gather a list of inner window ids given a + * top level window. + * + * @param nsIDOMWindow aWindow + * @return Array + * list of inner window ids. + */ + getInnerWindowIDsForFrames: function WCU_getInnerWindowIDsForFrames(aWindow) + { + let innerWindowID = this.getInnerWindowId(aWindow); + let ids = [innerWindowID]; + + if (aWindow.frames) { + for (let i = 0; i < aWindow.frames.length; i++) { + let frame = aWindow.frames[i]; + ids = ids.concat(this.getInnerWindowIDsForFrames(frame)); + } + } + + return ids; + }, + + /** * Gets the ID of the outer window of this DOM window. * @@ -986,7 +1010,7 @@ ConsoleServiceListener.prototype = }, /** - * Get the cached page errors for the current inner window. + * Get the cached page errors for the current inner window and its (i)frames. * * @param boolean [aIncludePrivate=false] * Tells if you want to also retrieve messages coming from private @@ -997,22 +1021,36 @@ ConsoleServiceListener.prototype = */ getCachedMessages: function CSL_getCachedMessages(aIncludePrivate = false) { - let innerWindowID = this.window ? - WebConsoleUtils.getInnerWindowId(this.window) : null; let errors = Services.console.getMessageArray() || []; + // if !this.window, we're in a browser console. Still need to filter + // private messages. + if (!this.window) { + return errors.filter((aError) => { + if (aError instanceof Ci.nsIScriptError) { + if (!aIncludePrivate && aError.isFromPrivateWindow) { + return false; + } + } + + return true; + }); + } + + let ids = WebConsoleUtils.getInnerWindowIDsForFrames(this.window); + return errors.filter((aError) => { if (aError instanceof Ci.nsIScriptError) { if (!aIncludePrivate && aError.isFromPrivateWindow) { return false; } - if (innerWindowID && - (aError.innerWindowID != innerWindowID || + if (ids && + (ids.indexOf(aError.innerWindowID) == -1 || !this.isCategoryAllowed(aError.category))) { return false; } } - else if (innerWindowID) { + else if (ids && ids[0]) { // If this is not an nsIScriptError and we need to do window-based // filtering we skip this message. return false; @@ -1115,7 +1153,7 @@ ConsoleAPIListener.prototype = }, /** - * Get the cached messages for the current inner window. + * Get the cached messages for the current inner window and its (i)frames. * * @param boolean [aIncludePrivate=false] * Tells if you want to also retrieve messages coming from private @@ -1125,14 +1163,24 @@ ConsoleAPIListener.prototype = */ getCachedMessages: function CAL_getCachedMessages(aIncludePrivate = false) { - let innerWindowId = this.window ? - WebConsoleUtils.getInnerWindowId(this.window) : null; - let events = ConsoleAPIStorage.getEvents(innerWindowId); - if (aIncludePrivate) { - return events; + let messages = []; + + // if !this.window, we're in a browser console. Retrieve all events + // for filtering based on privacy. + if (!this.window) { + messages = ConsoleAPIStorage.getEvents(); + } else { + let ids = WebConsoleUtils.getInnerWindowIDsForFrames(this.window); + ids.forEach((id) => { + messages = messages.concat(ConsoleAPIStorage.getEvents(id)); + }); } - return events.filter((m) => !m.private); + if (aIncludePrivate) { + return messages; + } + + return messages.filter((m) => !m.private); }, /** @@ -1700,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. @@ -2004,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);