Title: [206059] trunk
Revision
206059
Author
joep...@webkit.org
Date
2016-09-16 19:14:53 -0700 (Fri, 16 Sep 2016)

Log Message

Web Inspector: Implement Copy CSS Selector and Copy Xpath Selector context menus
https://bugs.webkit.org/show_bug.cgi?id=158881
<rdar://problem/8181156>

Reviewed by Matt Baker.

Source/WebInspectorUI:

This is based off of the Blink implementation (DOMPresentationUtils)
with some minor modifications and using our own utility methods.

* Localizations/en.lproj/localizedStrings.js:
New context menu strings.

* UserInterface/Base/DOMUtilities.js:
(WebInspector.cssPath):
(WebInspector.cssPathComponent.classNames):
(WebInspector.cssPathComponent):
(WebInspector.xpath):
(WebInspector.xpathIndex.isSimiliarNode):
(WebInspector.xpathIndex):
Build strings for a CSS selector path or XPath path to a node.

* UserInterface/Views/DOMTreeElement.js:
(WebInspector.DOMTreeElement.prototype._populateNodeContextMenu):
* UserInterface/Views/DOMTreeOutline.js:
(WebInspector.DOMTreeOutline.prototype.populateContextMenu):
Include copy path context menu items on nodes.
Pseudo elements do not get Copy XPath.
Non-node elements do not get Copy Selector Path.

LayoutTests:

* inspector/dom/domutilities-csspath-expected.txt: Added.
* inspector/dom/domutilities-csspath.html: Added.
* inspector/dom/domutilities-path-dump-expected.txt: Added.
* inspector/dom/domutilities-path-dump.html: Added.
* inspector/dom/domutilities-xpath-expected.txt: Added.
* inspector/dom/domutilities-xpath.html: Added.

Modified Paths

Added Paths

Diff

Modified: trunk/LayoutTests/ChangeLog (206058 => 206059)


--- trunk/LayoutTests/ChangeLog	2016-09-17 00:43:55 UTC (rev 206058)
+++ trunk/LayoutTests/ChangeLog	2016-09-17 02:14:53 UTC (rev 206059)
@@ -1,3 +1,18 @@
+2016-09-16  Joseph Pecoraro  <pecor...@apple.com>
+
+        Web Inspector: Implement Copy CSS Selector and Copy Xpath Selector context menus
+        https://bugs.webkit.org/show_bug.cgi?id=158881
+        <rdar://problem/8181156>
+
+        Reviewed by Matt Baker.
+
+        * inspector/dom/domutilities-csspath-expected.txt: Added.
+        * inspector/dom/domutilities-csspath.html: Added.
+        * inspector/dom/domutilities-path-dump-expected.txt: Added.
+        * inspector/dom/domutilities-path-dump.html: Added.
+        * inspector/dom/domutilities-xpath-expected.txt: Added.
+        * inspector/dom/domutilities-xpath.html: Added.
+
 2016-09-16  Jer Noble  <jer.no...@apple.com>
 
         Unreviewed gardening; enable newly passing media/media-source/ tests.

Added: trunk/LayoutTests/inspector/dom/domutilities-csspath-expected.txt (0 => 206059)


--- trunk/LayoutTests/inspector/dom/domutilities-csspath-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/inspector/dom/domutilities-csspath-expected.txt	2016-09-17 02:14:53 UTC (rev 206059)
@@ -0,0 +1,38 @@
+Test for WebInspector.cssPath.
+
+
+== Running test suite: WebInspector.cssPath
+-- Running test case: WebInspector.cssPath.TopLevelNode
+PASS: HTML element should have simple selector 'html'.
+PASS: BODY element should have simple selector 'body'.
+PASS: HEAD element should have simple selector 'head'.
+
+-- Running test case: WebInspector.cssPath.ElementWithID
+PASS: Element with id should have simple selector '#id-test'.
+PASS: Element inside element with id should have path from id.
+
+-- Running test case: WebInspector.cssPath.InputElementFlair
+PASS: Input element should include type.
+
+-- Running test case: WebInspector.cssPath.UniqueTagName
+PASS: Elements with unique tag name should not need nth-child().
+
+-- Running test case: WebInspector.cssPath.NonUniqueTagName
+PASS: Elements with non-unique tag name should need nth-child().
+
+-- Running test case: WebInspector.cssPath.UniqueClassName
+PASS: Elements with unique class names should include their class names.
+
+-- Running test case: WebInspector.cssPath.NonUniqueClassName
+PASS: Elements with non-unique class names should not include their class names.
+
+-- Running test case: WebInspector.cssPath.UniqueTagAndClassName
+PASS: Elements with unique tag and class names should just have simple tag.
+
+-- Running test case: WebInspector.cssPath.DeepPath
+PASS: Should be able to create path for deep elements.
+
+-- Running test case: WebInspector.cssPath.PseudoElement
+PASS: Should be able to create path for ::before pseudo elements.
+PASS: Should be able to create path for ::after pseudo elements.
+

Added: trunk/LayoutTests/inspector/dom/domutilities-csspath.html (0 => 206059)


--- trunk/LayoutTests/inspector/dom/domutilities-csspath.html	                        (rev 0)
+++ trunk/LayoutTests/inspector/dom/domutilities-csspath.html	2016-09-17 02:14:53 UTC (rev 206059)
@@ -0,0 +1,211 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src=""
+<script>
+function test()
+{
+    let documentNode;
+
+    function nodeForSelector(selector, callback) {
+        WebInspector.domTreeManager.querySelector(documentNode.id, selector, (nodeId) => {
+            callback(WebInspector.domTreeManager.nodeForId(nodeId));
+        });
+    }
+
+    let suite = InspectorTest.createAsyncSuite("WebInspector.cssPath");
+
+    suite.addTestCase({
+        name: "WebInspector.cssPath.TopLevelNode",
+        description: "Top level nodes like html, body, and head are unique.",
+        test(resolve, reject) {
+            nodeForSelector("html", (node) => {
+                InspectorTest.expectEqual(WebInspector.cssPath(node), "html", "HTML element should have simple selector 'html'.");
+            });
+            nodeForSelector("html > body", (node) => {
+                InspectorTest.expectEqual(WebInspector.cssPath(node), "body", "BODY element should have simple selector 'body'.");
+            });
+            nodeForSelector("html > head", (node) => {
+                InspectorTest.expectEqual(WebInspector.cssPath(node), "head", "HEAD element should have simple selector 'head'.");
+                resolve();
+            });
+        }
+    });
+
+    suite.addTestCase({
+        name: "WebInspector.cssPath.ElementWithID",
+        description: "Element with ID is unique (#id). Path does not need to go past it.",
+        test(resolve, reject) {
+            nodeForSelector("#id-test", (node) => {
+                InspectorTest.expectEqual(WebInspector.cssPath(node), "#id-test", "Element with id should have simple selector '#id-test'.");
+            });
+            nodeForSelector("#id-test > div", (node) => {
+                InspectorTest.expectEqual(WebInspector.cssPath(node), "#id-test > div", "Element inside element with id should have path from id.");
+                resolve();
+            });
+        }
+    });
+
+    suite.addTestCase({
+        name: "WebInspector.cssPath.InputElementFlair",
+        description: "Input elements include their type.",
+        test(resolve, reject) {
+            nodeForSelector("#input-test input", (node) => {
+                InspectorTest.expectEqual(WebInspector.cssPath(node), "#input-test > input[type=\"password\"]", "Input element should include type.");
+                resolve();
+            });
+        }
+    });
+
+    suite.addTestCase({
+        name: "WebInspector.cssPath.UniqueTagName",
+        description: "Elements with unique tag name do not need nth-child.",
+        test(resolve, reject) {
+            nodeForSelector("#unique-tag-test > span", (node) => {
+                InspectorTest.expectEqual(WebInspector.cssPath(node), "#unique-tag-test > span", "Elements with unique tag name should not need nth-child().");
+                resolve();
+            });
+        }
+    });
+
+    suite.addTestCase({
+        name: "WebInspector.cssPath.NonUniqueTagName",
+        description: "Elements with non-unique tag name need nth-child.",
+        test(resolve, reject) {
+            nodeForSelector("#non-unique-tag-test > span ~ span", (node) => {
+                InspectorTest.expectEqual(WebInspector.cssPath(node), "#non-unique-tag-test > span:nth-child(3)", "Elements with non-unique tag name should need nth-child().");
+                resolve();
+            });
+        }
+    });
+
+    suite.addTestCase({
+        name: "WebInspector.cssPath.UniqueClassName",
+        description: "Elements with unique class names should include their class names.",
+        test(resolve, reject) {
+            nodeForSelector("#unique-class-test > .beta", (node) => {
+                InspectorTest.expectEqual(WebInspector.cssPath(node), "#unique-class-test > div.alpha.beta", "Elements with unique class names should include their class names.");
+                resolve();
+            });
+        }
+    });
+
+    suite.addTestCase({
+        name: "WebInspector.cssPath.NonUniqueClassName",
+        description: "Elements with non-unique class names should not include their class names.",
+        test(resolve, reject) {
+            nodeForSelector("#non-unique-class-test > div ~ div", (node) => {
+                InspectorTest.expectEqual(WebInspector.cssPath(node), "#non-unique-class-test > div:nth-child(2)", "Elements with non-unique class names should not include their class names.");
+                resolve();
+            });
+        }
+    });
+
+    suite.addTestCase({
+        name: "WebInspector.cssPath.UniqueTagAndClassName",
+        description: "Elements with unique tag and class name just use tag for simplicity.",
+        test(resolve, reject) {
+            nodeForSelector("#unique-tag-and-class-test > .alpha", (node) => {
+                InspectorTest.expectEqual(WebInspector.cssPath(node), "#unique-tag-and-class-test > div", "Elements with unique tag and class names should just have simple tag.");
+                resolve();
+            });
+        }
+    });
+
+    suite.addTestCase({
+        name: "WebInspector.cssPath.DeepPath",
+        description: "Tests for element with complex path.",
+        test(resolve, reject) {
+            nodeForSelector("small", (node) => {
+                InspectorTest.expectEqual(WebInspector.cssPath(node), "body > div > div.deep-path-test > ul > li > div:nth-child(4) > ul > li.active > a > small", "Should be able to create path for deep elements.");
+                resolve();
+            });
+        }
+    });
+
+    suite.addTestCase({
+        name: "WebInspector.cssPath.PseudoElement",
+        description: "For a pseudo element we should get the path of the parent and append the pseudo element selector.",
+        test(resolve, reject) {
+            nodeForSelector("#pseudo-element-test > div ~ div", (node) => {
+                let pseudoElementBefore = node.beforePseudoElement();
+                InspectorTest.assert(pseudoElementBefore);
+                InspectorTest.expectEqual(WebInspector.cssPath(pseudoElementBefore), "#pseudo-element-test > div:nth-child(3)::before", "Should be able to create path for ::before pseudo elements.");
+                let pseudoElementAfter = node.afterPseudoElement();
+                InspectorTest.assert(pseudoElementAfter);
+                InspectorTest.expectEqual(WebInspector.cssPath(pseudoElementAfter), "#pseudo-element-test > div:nth-child(3)::after", "Should be able to create path for ::after pseudo elements.");
+                resolve();
+            });
+        }
+    });
+
+    // FIXME: Write tests for nodes inside a Shadow DOM Tree.
+
+    WebInspector.domTreeManager.requestDocument((node) => {
+        documentNode = node;
+        suite.runTestCasesAndFinish();
+    });
+}
+</script>
+</head>
+<body _onload_="runTest()">
+<p>Test for WebInspector.cssPath.</p>
+<!-- If display:none pseudo elements are not created. -->
+<div style="visibility:hidden">
+    <div id="id-test">
+        <div></div>
+    </div>
+    <div id="input-test">
+        <input type="password">
+    </div>
+    <div id="unique-tag-test">
+        <div></div>
+        <span></span>
+        <div></div>
+    </div>
+    <div id="non-unique-tag-test">
+        <div></div>
+        <span></span>
+        <span></span>
+        <div></div>
+    </div>
+    <div id="unique-class-test">
+        <div class="alpha"></div>
+        <div class="alpha beta"></div>
+        <div class="alpha"></div>
+    </div>
+    <div id="non-unique-class-test">
+        <div class="alpha"></div>
+        <div class="alpha"></div>
+        <div class="alpha"></div>
+    </div>
+    <div id="unique-tag-and-class-test">
+        <div class="alpha"></div>
+    </div>
+    <div class="deep-path-test">
+        <ul>
+            <li>
+                <h1></h1>
+                <div></div>
+                <div></div>
+                <div>
+                    <ul class="list">
+                        <li></li>
+                        <li class="active"><a href=""
+                        <li></li>
+                    </ul>
+                </div>
+            </li>
+        </ul>
+    </div>
+    <div id="pseudo-element-test">
+        <style>
+        #pseudo-element-test > div~div::before { content: "before"; }
+        #pseudo-element-test > div~div::after { content: "after"; }
+        </style>
+        <div></div>
+        <div></div>
+    </div>
+</div>
+</body>
+</html>

Added: trunk/LayoutTests/inspector/dom/domutilities-path-dump-expected.txt (0 => 206059)


--- trunk/LayoutTests/inspector/dom/domutilities-path-dump-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/inspector/dom/domutilities-path-dump-expected.txt	2016-09-17 02:14:53 UTC (rev 206059)
@@ -0,0 +1,111 @@
+Test for WebInspector.cssPath.
+
+ид
+класс
+
+-- CSS Selector Paths --
+html
+  head
+    head > meta
+    #script-id
+    #test-script
+  body
+    body > p
+    body > article:nth-child(2)
+    body > article:nth-child(3)
+    #ids
+      #ids > div:nth-child(1)
+      #ids > div:nth-child(2)
+      #inner-id
+      #__proto__
+      [id="\#\"ridiculous\"\.id"]
+      [id="\'quoted\.value\'"]
+      #\.foo\.bar
+      #\-
+      #-a
+      [id="-\30 "]
+      [id="\37 "]
+      #ид
+      #ids > p
+    #classes
+      #classes > div:nth-child(1)
+      #classes > div:nth-child(2)
+      #classes > div.\.foo
+      #classes > div.\.foo\.bar
+      #classes > div.\-
+      #classes > div.-a
+      #classes > div.-\30 
+      #classes > div.\37 
+      #classes > div.класс
+      #classes > div:nth-child(10)
+      #classes > div:nth-child(11)
+      #classes > span
+      #id-with-class
+    #non-unique-classes
+      #non-unique-classes > span:nth-child(1)
+      #non-unique-classes > span:nth-child(2)
+      #non-unique-classes > span:nth-child(3)
+      #non-unique-classes > span:nth-child(4)
+      #non-unique-classes > span:nth-child(5)
+      #non-unique-classes > div:nth-child(6)
+      #non-unique-classes > div:nth-child(7)
+      #non-unique-classes > div:nth-child(8)
+      #non-unique-classes > div:nth-child(9)
+      #non-unique-classes > div:nth-child(10)
+      #non-unique-classes > div:nth-child(11)
+
+-- XPaths --
+/html
+  /html/head
+    /html/head/meta
+    //*[@id="script-id"]
+    //*[@id="test-script"]
+      //*[@id="test-script"]/text()
+  /html/body
+    /html/body/p
+      /html/body/p/text()
+    /html/body/article[1]
+    /html/body/article[2]
+    //*[@id="ids"]
+      //*[@id="ids"]/div[1]
+      //*[@id="ids"]/div[2]
+      //*[@id="inner-id"]
+      //*[@id="__proto__"]
+      //*[@id="#"ridiculous".id"]
+      //*[@id="'quoted.value'"]
+      //*[@id=".foo.bar"]
+      //*[@id="-"]
+      //*[@id="-a"]
+      //*[@id="-0"]
+      //*[@id="7"]
+      //*[@id="ид"]
+        //*[@id="ид"]/text()
+      //*[@id="ids"]/p
+    //*[@id="classes"]
+      //*[@id="classes"]/div[1]
+      //*[@id="classes"]/div[2]
+      //*[@id="classes"]/div[3]
+      //*[@id="classes"]/div[4]
+      //*[@id="classes"]/div[5]
+      //*[@id="classes"]/div[6]
+      //*[@id="classes"]/div[7]
+      //*[@id="classes"]/div[8]
+      //*[@id="classes"]/div[9]
+        //*[@id="classes"]/div[9]/text()
+      //*[@id="classes"]/div[10]
+      //*[@id="classes"]/div[11]
+      //*[@id="classes"]/span
+      //*[@id="id-with-class"]
+    //*[@id="non-unique-classes"]
+      //*[@id="non-unique-classes"]/span[1]
+      //*[@id="non-unique-classes"]/span[2]
+      //*[@id="non-unique-classes"]/span[3]
+      //*[@id="non-unique-classes"]/span[4]
+      //*[@id="non-unique-classes"]/span[5]
+      //*[@id="non-unique-classes"]/div[1]
+      //*[@id="non-unique-classes"]/div[2]
+      //*[@id="non-unique-classes"]/div[3]
+      //*[@id="non-unique-classes"]/div[4]
+      //*[@id="non-unique-classes"]/div[5]
+      //*[@id="non-unique-classes"]/div[6]
+

Added: trunk/LayoutTests/inspector/dom/domutilities-path-dump.html (0 => 206059)


--- trunk/LayoutTests/inspector/dom/domutilities-path-dump.html	                        (rev 0)
+++ trunk/LayoutTests/inspector/dom/domutilities-path-dump.html	2016-09-17 02:14:53 UTC (rev 206059)
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<script src="" id="script-id"></script>
+<script id="test-script">
+function verifySelector(selector) {
+    let nodes = document.querySelectorAll(selector);
+    if (nodes.length !== 1)
+        console.log("Selector was not unique: " + selector);
+}
+
+function verifyXPath(xpath) {
+    let nodes = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
+    if (nodes.snapshotLength !== 1)
+        console.log("XPath was not unique: " + xpath);
+}
+
+function test()
+{
+    let nodes = [];
+
+    function buildNodeList(node, depth) {
+        nodes.push({node, depth});
+        if (!node.children)
+            return;
+        for (let child of node.children)
+            buildNodeList(child, depth + 1);
+    }
+
+    function processList(func, verifier) {
+        for (let {node, depth} of nodes) {
+            let prefix = " ".repeat(depth * 2);
+            let path = func(node);
+            if (path) {
+                InspectorTest.log(prefix + path);
+                verifier(path);
+            }   
+        }
+    }
+
+    WebInspector.domTreeManager.requestDocument((documentNode) => {
+        // Push all the nodes to the frontend.
+        WebInspector.domTreeManager.querySelector(documentNode.id, "html", (nodeId) => {
+            let htmlNode = WebInspector.domTreeManager.nodeForId(nodeId);
+            htmlNode.getSubtree(10, () => {
+                buildNodeList(htmlNode, 0);
+
+                InspectorTest.log("");
+                InspectorTest.log("-- CSS Selector Paths --");
+                processList(WebInspector.cssPath, (selector) => {
+                    InspectorTest.evaluateInPage("verifySelector(" + JSON.stringify(selector) + ")");
+                });
+
+                InspectorTest.log("");
+                InspectorTest.log("-- XPaths --");
+                processList(WebInspector.xpath, (xpath) => {
+                    InspectorTest.evaluateInPage("verifyXPath(" + JSON.stringify(xpath) + ")");
+                });
+
+                InspectorBackend.runAfterPendingDispatches(() => {
+                    InspectorTest.completeTest();
+                })
+            });
+        });
+    });
+}
+</script>
+</head>
+<body _onload_="runTest()">
+<p>Test for WebInspector.cssPath.</p>
+
+<article></article>
+<article></article>
+
+<div id="ids">
+    <div></div>
+    <div></div>
+    <div id="inner-id"></div>
+    <div id="__proto__"></div>
+    <div id='#"ridiculous".id'></div>
+    <div id="'quoted.value'"></div>
+    <div id=".foo.bar"></div>
+    <div id="-"></div>
+    <div id="-a"></div>
+    <div id="-0"></div>
+    <div id="7"></div>
+    <div id="ид">ид</div>
+    <p></p>
+</div>
+
+<div id="classes">
+    <div class="foo bar"></div>
+    <div class=" foo foo "></div>
+    <div class=".foo"></div>
+    <div class=".foo.bar"></div>
+    <div class="-"></div>
+    <div class="-a"></div>
+    <div class="-0"></div>
+    <div class="7"></div>
+    <div class="класс">класс</div>
+    <div class="__proto__"></div>
+    <div class="__proto__ foo"></div>
+    <span class="bar"></span>
+    <div id="id-with-class" class="moo"></div>
+</div>
+
+<div id="non-unique-classes">
+  <span class="c1"></span>
+  <span class="c1"></span>
+  <span class="c1 c2"></span>
+  <span class="c1 c2 c3"></span>
+  <span></span>
+  <div class="c1"></div>
+  <div class="c1 c2"></div>
+  <div class="c3 c2"></div>
+  <div class="c3 c4"></div>
+  <div class="c1 c4"></div>
+  <div></div>
+</div>
+</body>
+</html>

Added: trunk/LayoutTests/inspector/dom/domutilities-xpath-expected.txt (0 => 206059)


--- trunk/LayoutTests/inspector/dom/domutilities-xpath-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/inspector/dom/domutilities-xpath-expected.txt	2016-09-17 02:14:53 UTC (rev 206059)
@@ -0,0 +1,29 @@
+Test for WebInspector.xpath.
+
+
+
+
+== Running test suite: WebInspector.xpath
+-- Running test case: WebInspector.xpath.TopLevelNode
+PASS: HTML element should have simple XPath '/html'.
+PASS: BODY element should have simple XPath '/html/body'.
+PASS: HEAD element should have simple XPath '/html/head'.
+
+-- Running test case: WebInspector.xpath.ElementWithID
+PASS: Element with id should have a single path component '//*[@id="id-test"]'.
+PASS: Element inside element with id should have path from id.
+
+-- Running test case: WebInspector.xpath.UniqueTagName
+PASS: Elements with unique tag name should not need XPath index.
+
+-- Running test case: WebInspector.xpath.NonUniqueTagName
+PASS: Elements with non-unique tag name should need XPath index.
+
+-- Running test case: WebInspector.xpath.DeepPath
+/html/body/div/div[7]/ul/li/div[3]/ul/li[2]/a/small
+PASS: Should be able to get XPath for deep elements.
+
+-- Running test case: WebInspector.xpath.TextAndCommentNode
+PASS: Should be able to get XPath for TEXT_NODE.
+PASS: Should be able to get XPath for COMMENT_NODE.
+

Added: trunk/LayoutTests/inspector/dom/domutilities-xpath.html (0 => 206059)


--- trunk/LayoutTests/inspector/dom/domutilities-xpath.html	                        (rev 0)
+++ trunk/LayoutTests/inspector/dom/domutilities-xpath.html	2016-09-17 02:14:53 UTC (rev 206059)
@@ -0,0 +1,166 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src=""
+<script>
+function test()
+{
+    let documentNode;
+
+    function nodeForSelector(selector, callback) {
+        WebInspector.domTreeManager.querySelector(documentNode.id, selector, (nodeId) => {
+            callback(WebInspector.domTreeManager.nodeForId(nodeId));
+        });
+    }
+
+    let suite = InspectorTest.createAsyncSuite("WebInspector.xpath");
+
+    suite.addTestCase({
+        name: "WebInspector.xpath.TopLevelNode",
+        description: "Top level nodes like html, body, and head are unique.",
+        test(resolve, reject) {
+            nodeForSelector("html", (node) => {
+                InspectorTest.expectEqual(WebInspector.xpath(node), "/html", "HTML element should have simple XPath '/html'.");
+            });
+            nodeForSelector("html > body", (node) => {
+                InspectorTest.expectEqual(WebInspector.xpath(node), "/html/body", "BODY element should have simple XPath '/html/body'.");
+            });
+            nodeForSelector("html > head", (node) => {
+                InspectorTest.expectEqual(WebInspector.xpath(node), "/html/head", "HEAD element should have simple XPath '/html/head'.");
+                resolve();
+            });
+        }
+    });
+
+    suite.addTestCase({
+        name: "WebInspector.xpath.ElementWithID",
+        description: "Element with ID is unique (#id). Path does not need to go past it.",
+        test(resolve, reject) {
+            nodeForSelector("#id-test", (node) => {
+                InspectorTest.expectEqual(WebInspector.xpath(node), "//*[@id=\"id-test\"]", "Element with id should have a single path component '//*[@id=\"id-test\"]'.");
+            });
+            nodeForSelector("#id-test > div", (node) => {
+                InspectorTest.expectEqual(WebInspector.xpath(node), "//*[@id=\"id-test\"]/div", "Element inside element with id should have path from id.");
+                resolve();
+            });
+        }
+    });
+
+    suite.addTestCase({
+        name: "WebInspector.xpath.UniqueTagName",
+        description: "Elements with unique tag name do not need nth-child.",
+        test(resolve, reject) {
+            nodeForSelector("#unique-tag-test > span", (node) => {
+                InspectorTest.expectEqual(WebInspector.xpath(node), "//*[@id=\"unique-tag-test\"]/span", "Elements with unique tag name should not need XPath index.");
+                resolve();
+            });
+        }
+    });
+
+    suite.addTestCase({
+        name: "WebInspector.xpath.NonUniqueTagName",
+        description: "Elements with non-unique tag name need index.",
+        test(resolve, reject) {
+            nodeForSelector("#non-unique-tag-test > span ~ span", (node) => {
+                InspectorTest.expectEqual(WebInspector.xpath(node), "//*[@id=\"non-unique-tag-test\"]/span[2]", "Elements with non-unique tag name should need XPath index.");
+                resolve();
+            });
+        }
+    });
+
+    suite.addTestCase({
+        name: "WebInspector.xpath.DeepPath",
+        description: "Tests for element with complex path.",
+        test(resolve, reject) {
+            nodeForSelector("small", (node) => {
+                InspectorTest.log(WebInspector.xpath(node));
+                InspectorTest.expectEqual(WebInspector.xpath(node), "/html/body/div/div[7]/ul/li/div[3]/ul/li[2]/a/small", "Should be able to get XPath for deep elements.");
+                resolve();
+            });
+        }
+    });
+
+    suite.addTestCase({
+        name: "WebInspector.xpath.TextAndCommentNode",
+        description: "Tests for non-Element nodes.",
+        test(resolve, reject) {
+            nodeForSelector("#non-element-test > p > br", (node) => {
+                let paragraphChildren = node.parentNode.children;
+                let lastTextChild = paragraphChildren[paragraphChildren.length - 1];
+                let lastCommentChild = paragraphChildren[paragraphChildren.length - 2];
+                InspectorTest.expectEqual(WebInspector.xpath(lastTextChild), "//*[@id=\"non-element-test\"]/p/text()[3]", "Should be able to get XPath for TEXT_NODE.");
+                InspectorTest.expectEqual(WebInspector.xpath(lastCommentChild), "//*[@id=\"non-element-test\"]/p/comment()", "Should be able to get XPath for COMMENT_NODE.");
+                resolve();
+            });
+        }
+    });
+
+    // FIXME: Write tests for nodes inside a Shadow DOM Tree.
+    // FIXME: Write test for CDATA.
+
+    WebInspector.domTreeManager.requestDocument((node) => {
+        documentNode = node;
+        suite.runTestCasesAndFinish();
+    });
+}
+</script>
+</head>
+<body _onload_="runTest()">
+<p>Test for WebInspector.xpath.</p>
+<!-- If display:none pseudo elements are not created. -->
+<div style="visibility:hidden">
+    <div id="id-test">
+        <div></div>
+    </div>
+    <div id="unique-tag-test">
+        <div></div>
+        <span></span>
+        <div></div>
+    </div>
+    <div id="non-unique-tag-test">
+        <div></div>
+        <span></span>
+        <span></span>
+        <div></div>
+    </div>
+    <div id="unique-class-test">
+        <div class="alpha"></div>
+        <div class="alpha beta"></div>
+        <div class="alpha"></div>
+    </div>
+    <div id="non-unique-class-test">
+        <div class="alpha"></div>
+        <div class="alpha"></div>
+        <div class="alpha"></div>
+    </div>
+    <div id="unique-tag-and-class-test">
+        <div class="alpha"></div>
+    </div>
+    <div class="deep-path-test">
+        <ul>
+            <li>
+                <h1></h1>
+                <div></div>
+                <div></div>
+                <div>
+                    <ul class="list">
+                        <li></li>
+                        <li class="active"><a href=""
+                        <li></li>
+                    </ul>
+                </div>
+            </li>
+        </ul>
+    </div>
+    <div id="non-element-test">
+        <p>
+            Some leading text
+            <br>
+            Some trailing text
+            <!-- Comment -->
+            Some final text
+        </p>
+    </div>
+</div>
+</body>
+</html>

Modified: trunk/Source/WebInspectorUI/ChangeLog (206058 => 206059)


--- trunk/Source/WebInspectorUI/ChangeLog	2016-09-17 00:43:55 UTC (rev 206058)
+++ trunk/Source/WebInspectorUI/ChangeLog	2016-09-17 02:14:53 UTC (rev 206059)
@@ -1,3 +1,34 @@
+2016-09-16  Joseph Pecoraro  <pecor...@apple.com>
+
+        Web Inspector: Implement Copy CSS Selector and Copy Xpath Selector context menus
+        https://bugs.webkit.org/show_bug.cgi?id=158881
+        <rdar://problem/8181156>
+
+        Reviewed by Matt Baker.
+
+        This is based off of the Blink implementation (DOMPresentationUtils)
+        with some minor modifications and using our own utility methods.
+
+        * Localizations/en.lproj/localizedStrings.js:
+        New context menu strings.
+
+        * UserInterface/Base/DOMUtilities.js:
+        (WebInspector.cssPath):
+        (WebInspector.cssPathComponent.classNames):
+        (WebInspector.cssPathComponent):
+        (WebInspector.xpath):
+        (WebInspector.xpathIndex.isSimiliarNode):
+        (WebInspector.xpathIndex):
+        Build strings for a CSS selector path or XPath path to a node.
+
+        * UserInterface/Views/DOMTreeElement.js:
+        (WebInspector.DOMTreeElement.prototype._populateNodeContextMenu):
+        * UserInterface/Views/DOMTreeOutline.js:
+        (WebInspector.DOMTreeOutline.prototype.populateContextMenu):
+        Include copy path context menu items on nodes.
+        Pseudo elements do not get Copy XPath.
+        Non-node elements do not get Copy Selector Path.
+
 2016-09-16  Nikita Vasilyev  <nvasil...@apple.com>
 
         Web Inspector: Make console session dividers more pronounced

Modified: trunk/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js (206058 => 206059)


--- trunk/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js	2016-09-17 00:43:55 UTC (rev 206058)
+++ trunk/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js	2016-09-17 02:14:53 UTC (rev 206059)
@@ -208,7 +208,9 @@
 localizedStrings["Copy Row"] = "Copy Row";
 localizedStrings["Copy Rule"] = "Copy Rule";
 localizedStrings["Copy Selected"] = "Copy Selected";
+localizedStrings["Copy Selector Path"] = "Copy Selector Path";
 localizedStrings["Copy Table"] = "Copy Table";
+localizedStrings["Copy XPath"] = "Copy XPath";
 localizedStrings["Copy as HTML"] = "Copy as HTML";
 localizedStrings["Copy as cURL"] = "Copy as cURL";
 localizedStrings["Could not fetch properties. Object may no longer exist."] = "Could not fetch properties. Object may no longer exist.";

Modified: trunk/Source/WebInspectorUI/UserInterface/Base/DOMUtilities.js (206058 => 206059)


--- trunk/Source/WebInspectorUI/UserInterface/Base/DOMUtilities.js	2016-09-17 00:43:55 UTC (rev 206058)
+++ trunk/Source/WebInspectorUI/UserInterface/Base/DOMUtilities.js	2016-09-17 02:14:53 UTC (rev 206059)
@@ -80,3 +80,231 @@
 {
     return document.createElementNS("http://www.w3.org/2000/svg", tagName);
 }
+
+WebInspector.cssPath = function(node)
+{
+    console.assert(node instanceof WebInspector.DOMNode, "Expected a DOMNode.");
+    if (node.nodeType() !== Node.ELEMENT_NODE)
+        return "";
+
+    let suffix = "";
+    if (node.isPseudoElement()) {
+        suffix = "::" + node.pseudoType();
+        node = node.parentNode;
+    }
+
+    let components = [];
+    while (node) {
+        let component = WebInspector.cssPathComponent(node);
+        if (!component)
+            break;
+        components.push(component);
+        if (component.done)
+            break;
+        node = node.parentNode;
+    }
+
+    components.reverse();
+    return components.map((x) => x.value).join(" > ") + suffix;
+};
+
+WebInspector.cssPathComponent = function(node)
+{
+    console.assert(node instanceof WebInspector.DOMNode, "Expected a DOMNode.");
+    console.assert(!node.isPseudoElement());
+    if (node.nodeType() !== Node.ELEMENT_NODE)
+        return null;
+
+    let nodeName = node.nodeNameInCorrectCase();
+    let lowerNodeName = node.nodeName().toLowerCase();
+
+    // html, head, and body are unique nodes.
+    if (lowerNodeName === "body" || lowerNodeName === "head" || lowerNodeName === "html")
+        return {value: nodeName, done: true};
+
+    // #id is unique.
+    let id = node.getAttribute("id");
+    if (id)
+        return {value: node.escapedIdSelector, done: true};
+
+    // Root node does not have siblings.
+    if (!node.parentNode || node.parentNode.nodeType() === Node.DOCUMENT_NODE)
+        return {value: nodeName, done: true};
+
+    // Find uniqueness among siblings.
+    //   - look for a unique className
+    //   - look for a unique tagName
+    //   - fallback to nth-child()
+
+    function classNames(node) {
+        let classAttribute = node.getAttribute("class");
+        return classAttribute ? classAttribute.trim().split(/\s+/) : [];
+    }
+
+    let nthChildIndex = -1;
+    let hasUniqueTagName = true;
+    let uniqueClasses = new Set(classNames(node));
+
+    let siblings = node.parentNode.children;
+    let elementIndex = 0;
+    for (let sibling of siblings) {
+        if (sibling.nodeType() !== Node.ELEMENT_NODE)
+            continue;
+
+        elementIndex++;
+        if (sibling === node) {
+            nthChildIndex = elementIndex;
+            continue;
+        }
+
+        if (sibling.nodeNameInCorrectCase() === nodeName)
+            hasUniqueTagName = false;
+
+        if (uniqueClasses.size) {
+            let siblingClassNames = classNames(sibling);
+            for (let className of siblingClassNames)
+                uniqueClasses.delete(className);
+        }
+    }
+
+    let selector = nodeName;
+    if (lowerNodeName === "input" && node.getAttribute("type") && !uniqueClasses.size)
+        selector += `[type="${node.getAttribute("type")}"]`;
+    if (!hasUniqueTagName) {
+        if (uniqueClasses.size)
+            selector += node.escapedClassSelector;
+        else
+            selector += `:nth-child(${nthChildIndex})`;
+    }
+
+    return {value: selector, done: false};
+};
+
+WebInspector.xpath = function(node)
+{
+    console.assert(node instanceof WebInspector.DOMNode, "Expected a DOMNode.");
+
+    if (node.nodeType() === Node.DOCUMENT_NODE)
+        return "/";
+
+    let components = [];
+    while (node) {
+        let component = WebInspector.xpathComponent(node);
+        if (!component)
+            break;
+        components.push(component);
+        if (component.done)
+            break;
+        node = node.parentNode;
+    }
+
+    components.reverse();
+
+    let prefix = components.length && components[0].done ? "" : "/";
+    return prefix + components.map((x) => x.value).join("/");
+};
+
+WebInspector.xpathComponent = function(node)
+{
+    console.assert(node instanceof WebInspector.DOMNode, "Expected a DOMNode.");
+
+    let index = WebInspector.xpathIndex(node);
+    if (index === -1)
+        return null;
+
+    let value;
+
+    switch (node.nodeType()) {
+    case Node.DOCUMENT_NODE:
+        return {value: "", done: true};
+    case Node.ELEMENT_NODE:
+        var id = node.getAttribute("id");
+        if (id)
+            return {value: `//*[@id="${id}"]`, done: true};
+        value = node.localName();
+        break;
+    case Node.ATTRIBUTE_NODE:
+        value = `@${node.nodeName()}`;
+        break;
+    case Node.TEXT_NODE:
+    case Node.CDATA_SECTION_NODE:
+        value = "text()";
+        break;
+    case Node.COMMENT_NODE:
+        value = "comment()";
+        break;
+    case Node.PROCESSING_INSTRUCTION_NODE:
+        value = "processing-instruction()";
+        break
+    default:
+        value = "";
+        break;
+    }
+
+    if (index > 0)
+        value += `[${index}]`;
+
+    return {value, done: false};
+};
+
+WebInspector.xpathIndex = function(node)
+{
+    // Root node.
+    if (!node.parentNode)
+        return 0;
+
+    // No siblings.
+    let siblings = node.parentNode.children;
+    if (siblings.length <= 1)
+        return 0;
+
+    // Find uniqueness among siblings.
+    //   - look for a unique localName
+    //   - fallback to index
+
+    function isSimiliarNode(a, b) {
+        if (a === b)
+            return true;
+
+        let aType = a.nodeType();
+        let bType = b.nodeType();
+
+        if (aType === Node.ELEMENT_NODE && bType === Node.ELEMENT_NODE)
+            return a.localName() === b.localName();
+
+        // XPath CDATA and text() are the same.
+        if (aType === Node.CDATA_SECTION_NODE)
+            aType === Node.TEXT_NODE;
+        if (bType === Node.CDATA_SECTION_NODE)
+            bType === Node.TEXT_NODE;
+
+        return aType === bType;
+    }
+
+    let unique = true;
+    let xPathIndex = -1;
+
+    let xPathIndexCounter = 1; // XPath indices start at 1.
+    for (let sibling of siblings) {
+        if (!isSimiliarNode(node, sibling))
+            continue;
+
+        if (node === sibling) {
+            xPathIndex = xPathIndexCounter;
+            if (!unique)
+                return xPathIndex;
+        } else {
+            unique = false;
+            if (xPathIndex !== -1)
+                return xPathIndex;
+        }
+
+        xPathIndexCounter++;
+    }
+
+    if (unique)
+        return 0;
+
+    console.assert(xPathIndex > 0, "Should have found the node.");
+    return xPathIndex;
+};

Modified: trunk/Source/WebInspectorUI/UserInterface/Views/DOMTreeElement.js (206058 => 206059)


--- trunk/Source/WebInspectorUI/UserInterface/Views/DOMTreeElement.js	2016-09-17 00:43:55 UTC (rev 206058)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/DOMTreeElement.js	2016-09-17 02:14:53 UTC (rev 206059)
@@ -710,16 +710,33 @@
 
     _populateNodeContextMenu(contextMenu)
     {
+        let node = this.representedObject;
+
         // Add free-form node-related actions.
         if (this.editable)
             contextMenu.appendItem(WebInspector.UIString("Edit as HTML"), this._editAsHTML.bind(this));
-        contextMenu.appendItem(WebInspector.UIString("Copy as HTML"), this._copyHTML.bind(this));
+        if (!node.isPseudoElement())
+            contextMenu.appendItem(WebInspector.UIString("Copy as HTML"), this._copyHTML.bind(this));
         if (this.editable)
             contextMenu.appendItem(WebInspector.UIString("Delete Node"), this.remove.bind(this));
-
-        let node = this.representedObject;
         if (node.nodeType() === Node.ELEMENT_NODE)
             contextMenu.appendItem(WebInspector.UIString("Scroll Into View"), this._scrollIntoView.bind(this));
+
+        contextMenu.appendSeparator();
+
+        if (node.nodeType() === Node.ELEMENT_NODE) {
+            contextMenu.appendItem(WebInspector.UIString("Copy Selector Path"), () => {
+                let cssPath = WebInspector.cssPath(this.representedObject);
+                InspectorFrontendHost.copyText(cssPath);
+            });
+        }
+
+        if (!node.isPseudoElement()) {
+            contextMenu.appendItem(WebInspector.UIString("Copy XPath"), () => {
+                let xpath = WebInspector.xpath(this.representedObject);
+                InspectorFrontendHost.copyText(xpath);
+            });
+        }
     }
 
     _startEditing()

Modified: trunk/Source/WebInspectorUI/UserInterface/Views/DOMTreeOutline.js (206058 => 206059)


--- trunk/Source/WebInspectorUI/UserInterface/Views/DOMTreeOutline.js	2016-09-17 00:43:55 UTC (rev 206058)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/DOMTreeOutline.js	2016-09-17 02:14:53 UTC (rev 206059)
@@ -240,11 +240,12 @@
 
     populateContextMenu(contextMenu, event, treeElement)
     {
-        var tag = event.target.enclosingNodeOrSelfWithClass("html-tag");
-        var textNode = event.target.enclosingNodeOrSelfWithClass("html-text-node");
-        var commentNode = event.target.enclosingNodeOrSelfWithClass("html-comment");
+        let tag = event.target.enclosingNodeOrSelfWithClass("html-tag");
+        let textNode = event.target.enclosingNodeOrSelfWithClass("html-text-node");
+        let commentNode = event.target.enclosingNodeOrSelfWithClass("html-comment");
+        let pseudoElement = event.target.enclosingNodeOrSelfWithClass("html-pseudo-element");
 
-        var populated = false;
+        let populated = false;
         if (tag && treeElement._populateTagContextMenu) {
             if (populated)
                 contextMenu.appendSeparator();
@@ -255,7 +256,7 @@
                 contextMenu.appendSeparator();
             treeElement._populateTextContextMenu(contextMenu, textNode);
             populated = true;
-        } else if (commentNode && treeElement._populateNodeContextMenu) {
+        } else if ((commentNode || pseudoElement) && treeElement._populateNodeContextMenu) {
             if (populated)
                 contextMenu.appendSeparator();
             treeElement._populateNodeContextMenu(contextMenu);
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to