diff options
-rw-r--r-- | devtools/client/inspector/inspector.js | 32 | ||||
-rw-r--r-- | devtools/client/inspector/test/browser_inspector_menu-01-sensitivity.js | 1 | ||||
-rw-r--r-- | devtools/client/inspector/test/browser_inspector_menu-02-copy-items.js | 6 | ||||
-rw-r--r-- | devtools/client/locales/en-US/inspector.properties | 6 | ||||
-rw-r--r-- | devtools/client/shared/telemetry.js | 6 | ||||
-rw-r--r-- | devtools/server/actors/inspector.js | 12 | ||||
-rw-r--r-- | devtools/server/actors/root.js | 2 | ||||
-rw-r--r-- | devtools/server/css-logic.js | 49 | ||||
-rw-r--r-- | devtools/shared/specs/node.js | 6 | ||||
-rw-r--r-- | devtools/shared/tests/mochitest/chrome.ini | 3 | ||||
-rw-r--r-- | devtools/shared/tests/mochitest/test_css-logic-getCssPath.html | 121 |
11 files changed, 241 insertions, 3 deletions
diff --git a/devtools/client/inspector/inspector.js b/devtools/client/inspector/inspector.js index c056c213f..d0458fc1f 100644 --- a/devtools/client/inspector/inspector.js +++ b/devtools/client/inspector/inspector.js @@ -169,6 +169,10 @@ Inspector.prototype = { return this._target.client.traits.getUniqueSelector; }, + get canGetCssPath() { + return this._target.client.traits.getCssPath; + }, + get canGetUsedFontFaces() { return this._target.client.traits.getUsedFontFaces; }, @@ -1074,6 +1078,15 @@ Inspector.prototype = { click: () => this.copyUniqueSelector(), })); copySubmenu.append(new MenuItem({ + id: "node-menu-copycsspath", + label: INSPECTOR_L10N.getStr("inspectorCopyCSSPath.label"), + accesskey: + INSPECTOR_L10N.getStr("inspectorCopyCSSPath.accesskey"), + disabled: !isSelectionElement, + hidden: !this.canGetCssPath, + click: () => this.copyCssPath(), + })); + copySubmenu.append(new MenuItem({ id: "node-menu-copyimagedatauri", label: INSPECTOR_L10N.getStr("inspectorImageDataUri.label"), disabled: !isSelectionElement || !markupContainer || @@ -1677,9 +1690,24 @@ Inspector.prototype = { return; } - this.selection.nodeFront.getUniqueSelector().then((selector) => { + this.telemetry.toolOpened("copyuniquecssselector"); + this.selection.nodeFront.getUniqueSelector().then(selector => { clipboardHelper.copyString(selector); - }).then(null, console.error); + }).catch(e => console.error); + }, + + /** + * Copy the full CSS Path of the selected Node to the clipboard. + */ + copyCssPath: function () { + if (!this.selection.isNode()) { + return; + } + + this.telemetry.toolOpened("copyfullcssselector"); + this.selection.nodeFront.getCssPath().then(path => { + clipboardHelper.copyString(path); + }).catch(e => console.error); }, /** diff --git a/devtools/client/inspector/test/browser_inspector_menu-01-sensitivity.js b/devtools/client/inspector/test/browser_inspector_menu-01-sensitivity.js index 59dbbbcc0..052e9da68 100644 --- a/devtools/client/inspector/test/browser_inspector_menu-01-sensitivity.js +++ b/devtools/client/inspector/test/browser_inspector_menu-01-sensitivity.js @@ -26,6 +26,7 @@ const ALL_MENU_ITEMS = [ "node-menu-copyinner", "node-menu-copyouter", "node-menu-copyuniqueselector", + "node-menu-copycsspath", "node-menu-copyimagedatauri", "node-menu-delete", "node-menu-pseudo-hover", diff --git a/devtools/client/inspector/test/browser_inspector_menu-02-copy-items.js b/devtools/client/inspector/test/browser_inspector_menu-02-copy-items.js index 0c96e9bbe..57a5dbaa0 100644 --- a/devtools/client/inspector/test/browser_inspector_menu-02-copy-items.js +++ b/devtools/client/inspector/test/browser_inspector_menu-02-copy-items.js @@ -26,6 +26,12 @@ const COPY_ITEMS_TEST_DATA = [ text: "body > div:nth-child(1) > p:nth-child(2)", }, { + desc: "copy css path", + id: "node-menu-copycsspath", + selector: "[data-id=\"copy\"]", + text: "html body div p", + }, + { desc: "copy image data uri", id: "node-menu-copyimagedatauri", selector: "#copyimage", diff --git a/devtools/client/locales/en-US/inspector.properties b/devtools/client/locales/en-US/inspector.properties index 4f4829678..b6f3e072b 100644 --- a/devtools/client/locales/en-US/inspector.properties +++ b/devtools/client/locales/en-US/inspector.properties @@ -154,6 +154,12 @@ inspectorCopyOuterHTML.accesskey=O inspectorCopyCSSSelector.label=CSS Selector inspectorCopyCSSSelector.accesskey=S +# LOCALIZATION NOTE (inspectorCopyCSSPath.label): This is the label +# shown in the inspector contextual-menu for the item that lets users copy +# the full CSS path of the current node +inspectorCopyCSSPath.label=CSS Path +inspectorCopyCSSPath.accesskey=P + # LOCALIZATION NOTE (inspectorPasteOuterHTML.label): This is the label shown # in the inspector contextual-menu for the item that lets users paste outer # HTML in the current node diff --git a/devtools/client/shared/telemetry.js b/devtools/client/shared/telemetry.js index 64a299581..38a21cef6 100644 --- a/devtools/client/shared/telemetry.js +++ b/devtools/client/shared/telemetry.js @@ -163,6 +163,12 @@ Telemetry.prototype = { toolbareyedropper: { histogram: "DEVTOOLS_TOOLBAR_EYEDROPPER_OPENED_COUNT", }, + copyuniquecssselector: { + histogram: "DEVTOOLS_COPY_UNIQUE_CSS_SELECTOR_OPENED_COUNT", + }, + copyfullcssselector: { + histogram: "DEVTOOLS_COPY_FULL_CSS_SELECTOR_OPENED_COUNT", + }, developertoolbar: { histogram: "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_COUNT", timerHistogram: "DEVTOOLS_DEVELOPERTOOLBAR_TIME_ACTIVE_SECONDS" diff --git a/devtools/server/actors/inspector.js b/devtools/server/actors/inspector.js index 20a227a40..883809b6c 100644 --- a/devtools/server/actors/inspector.js +++ b/devtools/server/actors/inspector.js @@ -626,6 +626,18 @@ var NodeActor = exports.NodeActor = protocol.ActorClassWithSpec(nodeSpec, { }, /** + * Get the full CSS path for this node. + * + * @return {String} A CSS selector with a part for the node and each of its ancestors. + */ + getCssPath: function () { + if (Cu.isDeadWrapper(this.rawNode)) { + return ""; + } + return CssLogic.getCssPath(this.rawNode); + }, + + /** * Scroll the selected node into view. */ scrollIntoView: function () { diff --git a/devtools/server/actors/root.js b/devtools/server/actors/root.js index b6f8c0ee4..a5df148c2 100644 --- a/devtools/server/actors/root.js +++ b/devtools/server/actors/root.js @@ -145,6 +145,8 @@ RootActor.prototype = { addNewRule: true, // Whether the dom node actor implements the getUniqueSelector method getUniqueSelector: true, + // Whether the dom node actor implements the getCssPath method + getCssPath: true, // Whether the director scripts are supported directorScripts: true, // Whether the debugger server supports diff --git a/devtools/server/css-logic.js b/devtools/server/css-logic.js index f632871e1..c4a073635 100644 --- a/devtools/server/css-logic.js +++ b/devtools/server/css-logic.js @@ -793,6 +793,55 @@ CssLogic.findCssSelector = function (ele) { }; /** + * Get the full CSS path for a given element. + * @returns a string that can be used as a CSS selector for the element. It might not + * match the element uniquely. It does however, represent the full path from the root + * node to the element. + */ +CssLogic.getCssPath = function (ele) { + ele = getRootBindingParent(ele); + const document = ele.ownerDocument; + if (!document || !document.contains(ele)) { + throw new Error("getCssPath received element not inside document"); + } + + const getElementSelector = element => { + if (!element.localName) { + return ""; + } + + let label = element.nodeName == element.nodeName.toUpperCase() + ? element.localName.toLowerCase() + : element.localName; + + if (element.id) { + label += "#" + element.id; + } + + if (element.classList) { + for (let cl of element.classList) { + label += "." + cl; + } + } + + return label; + }; + + let paths = []; + + while (ele) { + if (!ele || ele.nodeType !== Node.ELEMENT_NODE) { + break; + } + + paths.splice(0, 0, getElementSelector(ele)); + ele = ele.parentNode; + } + + return paths.length ? paths.join(" ") : ""; +} + +/** * A safe way to access cached bits of information about a stylesheet. * * @constructor diff --git a/devtools/shared/specs/node.js b/devtools/shared/specs/node.js index ea3d1b264..022d7f1ac 100644 --- a/devtools/shared/specs/node.js +++ b/devtools/shared/specs/node.js @@ -37,6 +37,12 @@ const nodeSpec = generateActorSpec({ value: RetVal("string") } }, + getCssPath: { + request: {}, + response: { + value: RetVal("string") + } + }, scrollIntoView: { request: {}, response: {} diff --git a/devtools/shared/tests/mochitest/chrome.ini b/devtools/shared/tests/mochitest/chrome.ini index 85ece7c48..3e4e028d1 100644 --- a/devtools/shared/tests/mochitest/chrome.ini +++ b/devtools/shared/tests/mochitest/chrome.ini @@ -2,6 +2,7 @@ tags = devtools skip-if = os == 'android' -[test_eventemitter_basic.html] +[test_css-logic-getCssPath.html] [test_devtools_extensions.html] +[test_eventemitter_basic.html] skip-if = os == 'linux' && debug # Bug 1205739 diff --git a/devtools/shared/tests/mochitest/test_css-logic-getCssPath.html b/devtools/shared/tests/mochitest/test_css-logic-getCssPath.html new file mode 100644 index 000000000..2c444308a --- /dev/null +++ b/devtools/shared/tests/mochitest/test_css-logic-getCssPath.html @@ -0,0 +1,121 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1323700 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1323700</title> + + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript;version=1.8"> +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +let { require } = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const CssLogic = require("devtools/shared/inspector/css-logic"); + +var _tests = []; +function addTest(test) { + _tests.push(test); +} + +function runNextTest() { + if (_tests.length == 0) { + SimpleTest.finish() + return; + } + _tests.shift()(); +} + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + runNextTest(); +} + +addTest(function getCssPathForUnattachedElement() { + var unattached = document.createElement("div"); + unattached.id = "unattached"; + try { + CssLogic.getCssPath(unattached); + ok(false, "Unattached node did not throw") + } catch(e) { + ok(e, "Unattached node throws an exception"); + } + + var unattachedChild = document.createElement("div"); + unattached.appendChild(unattachedChild); + try { + CssLogic.getCssPath(unattachedChild); + ok(false, "Unattached child node did not throw") + } catch(e) { + ok(e, "Unattached child node throws an exception"); + } + + var unattachedBody = document.createElement("body"); + try { + CssLogic.getCssPath(unattachedBody); + ok(false, "Unattached body node did not throw") + } catch(e) { + ok(e, "Unattached body node throws an exception"); + } + + runNextTest(); +}); + +addTest(function cssPathHasOneStepForEachAncestor() { + for (let el of [...document.querySelectorAll('*')]) { + let splitPath = CssLogic.getCssPath(el).split(" "); + + let expectedNbOfParts = 0; + var parent = el.parentNode; + while (parent) { + expectedNbOfParts ++; + parent = parent.parentNode; + } + + is(splitPath.length, expectedNbOfParts, "There are enough parts in the full path"); + } + + runNextTest(); +}); + +addTest(function getCssPath() { + let data = [{ + selector: "#id", + path: "html body div div div.class div#id" + }, { + selector: "html", + path: "html" + }, { + selector: "body", + path: "html body" + }, { + selector: ".c1.c2.c3", + path: "html body span.c1.c2.c3" + }, { + selector: "#i", + path: "html body span#i.c1.c2" + }]; + + for (let {selector, path} of data) { + let node = document.querySelector(selector); + is (CssLogic.getCssPath(node), path, `Full css path is correct for ${selector}`); + } + + runNextTest(); +}); + </script> +</head> +<body> + <div> + <div> + <div class="class"> + <div id="id"></div> + </div> + </div> + </div> + <span class="c1 c2 c3"></span> + <span id="i" class="c1 c2"></span> +</body> +</html> |