Bug 1356415 - get CSS selector in content for contextmenu to fix devtools CPOW;r=mixedpuppy
authorJulian Descottes <jdescottes@mozilla.com>
Wed, 26 Apr 2017 19:41:19 +0200
changeset 356801 52a37a9d52a38524f37e3def10f768862e61d091
parent 356800 8659404d72eaec75b1b8fa913a8702f968d7569a
child 356802 aa507b3a8893b16dabb02f6b5a08feb9e5546543
push id89970
push userkwierso@gmail.com
push dateFri, 05 May 2017 21:20:56 +0000
treeherdermozilla-inbound@c3d254b2070d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy
bugs1356415
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1356415 - get CSS selector in content for contextmenu to fix devtools CPOW;r=mixedpuppy MozReview-Commit-ID: 3TYaYcS1W0h
browser/base/content/content.js
browser/base/content/nsContextMenu.js
devtools/client/framework/devtools-browser.js
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -46,16 +46,21 @@ XPCOMUtils.defineLazyGetter(this, "PageM
   let tmp = {};
   Cu.import("resource://gre/modules/PageMenu.jsm", tmp);
   return new tmp.PageMenuChild();
 });
 XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
   "resource://gre/modules/WebNavigationFrames.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Feeds",
   "resource:///modules/Feeds.jsm");
+XPCOMUtils.defineLazyGetter(this, "findCssSelector", () => {
+  let { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+  let { findCssSelector } = require("devtools/shared/inspector/css-logic");
+  return findCssSelector;
+});
 
 Cu.importGlobalProperties(["URL"]);
 
 // TabChildGlobal
 var global = this;
 
 // Load the form validation popup handler
 var formSubmitObserver = new FormSubmitObserver(content, this);
@@ -159,16 +164,17 @@ var handleContentContextMenu = function(
       } catch (e) {}
     } catch (e) {}
   }
 
   let selectionInfo = BrowserUtils.getSelectionDetails(content);
 
   let loadContext = docShell.QueryInterface(Ci.nsILoadContext);
   let userContextId = loadContext.originAttributes.userContextId;
+  let popupNodeSelectors = getNodeSelectors(event.target);
 
   if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
     let editFlags = SpellCheckHelper.isEditable(event.target, content);
     let spellInfo;
     if (editFlags &
         (SpellCheckHelper.EDITABLE | SpellCheckHelper.CONTENTEDITABLE)) {
       spellInfo =
         InlineSpellCheckerContent.initContextMenu(event, editFlags, this);
@@ -178,31 +184,37 @@ var handleContentContextMenu = function(
     // determine what was context-clicked on. Then, update the state of the
     // commands on the context menu.
     docShell.contentViewer.QueryInterface(Ci.nsIContentViewerEdit)
             .setCommandNode(event.target);
     event.target.ownerGlobal.updateCommands("contentcontextmenu");
 
     let customMenuItems = PageMenuChild.build(event.target);
     let principal = doc.nodePrincipal;
+
     sendRpcMessage("contextmenu",
                    { editFlags, spellInfo, customMenuItems, addonInfo,
                      principal, docLocation, charSet, baseURI, referrer,
                      referrerPolicy, contentType, contentDisposition,
                      frameOuterWindowID, selectionInfo, disableSetDesktopBg,
-                     loginFillInfo, parentAllowsMixedContent, userContextId },
-                   { event, popupNode: event.target });
+                     loginFillInfo, parentAllowsMixedContent, userContextId,
+                     popupNodeSelectors,
+                   }, {
+                     event,
+                     popupNode: event.target,
+                   });
   } else {
     // Break out to the parent window and pass the add-on info along
     let browser = docShell.chromeEventHandler;
     let mainWin = browser.ownerGlobal;
     mainWin.gContextMenuContentData = {
       isRemote: false,
       event,
       popupNode: event.target,
+      popupNodeSelectors,
       browser,
       addonInfo,
       documentURIObject: doc.documentURIObject,
       docLocation,
       charSet,
       referrer,
       referrerPolicy,
       contentType,
@@ -240,16 +252,38 @@ const MOZILLA_PKIX_ERROR_NOT_YET_VALID_I
 const PREF_BLOCKLIST_CLOCK_SKEW_SECONDS = "services.blocklist.clock_skew_seconds";
 
 const PREF_SSL_IMPACT_ROOTS = ["security.tls.version.", "security.ssl3."];
 
 const PREF_SSL_IMPACT = PREF_SSL_IMPACT_ROOTS.reduce((prefs, root) => {
   return prefs.concat(Services.prefs.getChildList(root));
 }, []);
 
+/**
+ * Retrieve the array of CSS selectors corresponding to the provided node. The first item
+ * of the array is the selector of the node in its owner document. Additional items are
+ * used if the node is inside a frame, each representing the CSS selector for finding the
+ * frame element in its parent document.
+ *
+ * This format is expected by DevTools in order to handle the Inspect Node context menu
+ * item.
+ *
+ * @param  {Node}
+ *         The node for which the CSS selectors should be computed
+ * @return {Array} array of css selectors (strings).
+ */
+function getNodeSelectors(node) {
+  let selectors = [];
+  while (node) {
+    selectors.push(findCssSelector(node));
+    node = node.ownerGlobal.frameElement;
+  }
+
+  return selectors;
+}
 
 function getSerializedSecurityInfo(docShell) {
   let serhelper = Cc["@mozilla.org/network/serialization-helper;1"]
                     .getService(Ci.nsISerializationHelper);
 
   let securityInfo = docShell.failedChannel && docShell.failedChannel.securityInfo;
   if (!securityInfo) {
     return "";
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -28,16 +28,17 @@ function openContextMenu(aMessage) {
   if (spellInfo)
     spellInfo.target = aMessage.target.messageManager;
   let documentURIObject = makeURI(data.docLocation,
                                   data.charSet,
                                   makeURI(data.baseURI));
   gContextMenuContentData = { isRemote: true,
                               event: aMessage.objects.event,
                               popupNode: aMessage.objects.popupNode,
+                              popupNodeSelectors: data.popupNodeSelectors,
                               browser,
                               editFlags: data.editFlags,
                               spellInfo,
                               principal: data.principal,
                               customMenuItems: data.customMenuItems,
                               addonInfo: data.addonInfo,
                               documentURIObject,
                               docLocation: data.docLocation,
@@ -615,17 +616,17 @@ nsContextMenu.prototype = {
   openPasswordManager() {
     LoginHelper.openPasswordManager(window, gContextMenuContentData.documentURIObject.host);
   },
 
   inspectNode() {
     let gBrowser = this.browser.ownerGlobal.gBrowser;
     let { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
     let { gDevToolsBrowser } = require("devtools/client/framework/devtools-browser");
-    return gDevToolsBrowser.inspectNode(gBrowser.selectedTab, this.target);
+    return gDevToolsBrowser.inspectNode(gBrowser.selectedTab, this.targetSelectors);
   },
 
   /**
    * Set various context menu attributes based on the state of the world.
    * Note: If the context menu is on a remote process the supplied parameters
    * will be overwritten with data from gContextMenuContentData.
    *
    * @param {Object} aNode The node that this menu is being opened on.
@@ -697,16 +698,22 @@ nsContextMenu.prototype = {
     }
 
     this.textSelected      = this.selectionInfo.text;
     this.isTextSelected    = this.textSelected.length != 0;
 
     // Remember the node that was clicked.
     this.target = aNode;
 
+    // Remember the CSS selectors corresponding to clicked node. gContextMenuContentData
+    // can be null if the menu was triggered by tests in which case use an empty array.
+    this.targetSelectors = gContextMenuContentData
+                              ? gContextMenuContentData.popupNodeSelectors
+                              : [];
+
     let ownerDoc = this.target.ownerDocument;
     this.ownerDoc = ownerDoc;
 
     let editFlags;
 
     // If this is a remote context menu event, use the information from
     // gContextMenuContentData instead.
     if (this.isRemote) {
--- a/devtools/client/framework/devtools-browser.js
+++ b/devtools/client/framework/devtools-browser.js
@@ -19,17 +19,16 @@ const Telemetry = require("devtools/clie
 const {gDevTools} = require("./devtools");
 
 // Load target and toolbox lazily as they need gDevTools to be fully initialized
 loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
 loader.lazyRequireGetter(this, "Toolbox", "devtools/client/framework/toolbox", true);
 loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
 loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true);
 loader.lazyRequireGetter(this, "BrowserMenus", "devtools/client/framework/browser-menus");
-loader.lazyRequireGetter(this, "findCssSelector", "devtools/shared/inspector/css-logic", true);
 loader.lazyRequireGetter(this, "appendStyleSheet", "devtools/client/shared/stylesheet-utils", true);
 
 loader.lazyImporter(this, "CustomizableUI", "resource:///modules/CustomizableUI.jsm");
 loader.lazyImporter(this, "AppConstants", "resource://gre/modules/AppConstants.jsm");
 loader.lazyImporter(this, "LightweightThemeManager", "resource://gre/modules/LightweightThemeManager.jsm");
 
 const {LocalizationHelper} = require("devtools/shared/l10n");
 const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
@@ -302,41 +301,34 @@ var gDevToolsBrowser = exports.gDevTools
     let win = Services.wm.getMostRecentWindow("devtools:webide");
     if (win) {
       win.focus();
     } else {
       Services.ww.openWindow(null, "chrome://webide/content/", "webide", "chrome,centerscreen,resizable", null);
     }
   },
 
-  async inspectNode(tab, node) {
+  async inspectNode(tab, nodeSelectors) {
     let target = TargetFactory.forTab(tab);
 
-    // Generate a cross iframes query selector
-    let selectors = [];
-    while (node) {
-      selectors.push(findCssSelector(node));
-      node = node.ownerDocument.defaultView.frameElement;
-    }
-
     let toolbox = await gDevTools.showToolbox(target, "inspector");
     let inspector = toolbox.getCurrentPanel();
 
     // new-node-front tells us when the node has been selected, whether the
     // browser is remote or not.
     let onNewNode = inspector.selection.once("new-node-front");
 
     // Evaluate the cross iframes query selectors
     async function querySelectors(nodeFront) {
-      let selector = selectors.pop();
+      let selector = nodeSelectors.pop();
       if (!selector) {
         return nodeFront;
       }
       nodeFront = await inspector.walker.querySelector(nodeFront, selector);
-      if (selectors.length > 0) {
+      if (nodeSelectors.length > 0) {
         let { nodes } = await inspector.walker.children(nodeFront);
         // This is the NodeFront for the document node inside the iframe
         nodeFront = nodes[0];
       }
       return querySelectors(nodeFront);
     }
     let nodeFront = await inspector.walker.getRootNode();
     nodeFront = await querySelectors(nodeFront);