Bug 1588773 - Move css-selector.js helpers back to DevTools css-logic.js r=pbro

Depends on D49303

Some methods from css-logic were extracted from the devtools codebase to be used by context-menu files.
This was only needed in order to compute the css-selectors for Inspect Element.
If we use ContentDOMReference instead, those helpers can move back in the devtools codebase
(leaving them in css-selector.js fails the all-files-referenced test for some reason as well)

Differential Revision: https://phabricator.services.mozilla.com/D49330

--HG--
rename : toolkit/modules/tests/chrome/test_findCssSelector.html => devtools/shared/tests/mochitest/test_css-logic-findCssSelector.html
extra : moz-landing-system : lando
This commit is contained in:
Julian Descottes 2019-10-28 09:11:02 +00:00
Родитель c72c1dcdcc
Коммит a399da3be9
6 изменённых файлов: 300 добавлений и 384 удалений

Просмотреть файл

@ -19,22 +19,6 @@ const MAX_DATA_URL_LENGTH = 40;
const Services = require("Services");
loader.lazyImporter(
this,
"findCssSelector",
"resource://gre/modules/css-selector.js"
);
loader.lazyImporter(
this,
"findAllCssSelectors",
"resource://gre/modules/css-selector.js"
);
loader.lazyImporter(
this,
"getCssPath",
"resource://gre/modules/css-selector.js"
);
loader.lazyImporter(this, "getXPath", "resource://gre/modules/css-selector.js");
loader.lazyRequireGetter(
this,
"getCSSLexer",
@ -47,7 +31,7 @@ loader.lazyRequireGetter(
"devtools/shared/indentation",
true
);
const { getRootBindingParent } = require("devtools/shared/layout/utils");
const { LocalizationHelper } = require("devtools/shared/l10n");
const styleInspectorL10N = new LocalizationHelper(
"devtools/shared/locales/styleinspector.properties"
@ -495,38 +479,6 @@ function prettifyCSS(text, ruleCount) {
exports.prettifyCSS = prettifyCSS;
/**
* Find a unique CSS selector for a given element
* @returns a string such that ele.ownerDocument.querySelector(reply) === ele
* and ele.ownerDocument.querySelectorAll(reply).length === 1
*/
exports.findCssSelector = findCssSelector;
/**
* Retrieve the array of CSS selectors corresponding to the provided node.
*
* The selectors are ordered starting with the root document and ending with the deepest
* nested frame. Additional items are used if the node is inside a frame or a shadow root,
* each representing the CSS selector for finding the frame or root element in its parent
* document.
*/
exports.findAllCssSelectors = findAllCssSelectors;
/**
* 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.
*/
exports.getCssPath = getCssPath;
/**
* Get the xpath for a given element.
* @param {DomNode} ele
* @returns a string that can be used as an XPath to find the element uniquely.
*/
exports.getXPath = getXPath;
/**
* Given a node, check to see if it is a ::marker, ::before, or ::after element.
* If so, return the node that is accessible from within the document
@ -585,3 +537,298 @@ function hasVisitedState(node) {
);
}
exports.hasVisitedState = hasVisitedState;
/**
* Return the node's parent shadow root if the node in shadow DOM, null
* otherwise.
*/
function getShadowRoot(node) {
const doc = node.ownerDocument;
if (!doc) {
return null;
}
const parent = doc.getBindingParent(node);
const shadowRoot = parent && parent.openOrClosedShadowRoot;
if (shadowRoot) {
return shadowRoot;
}
return null;
}
/**
* Find the position of [element] in [nodeList].
* @returns an index of the match, or -1 if there is no match
*/
function positionInNodeList(element, nodeList) {
for (let i = 0; i < nodeList.length; i++) {
if (element === nodeList[i]) {
return i;
}
}
return -1;
}
/**
* For a provided node, find the appropriate container/node couple so that
* container.contains(node) and a CSS selector can be created from the
* container to the node.
*/
function findNodeAndContainer(node) {
const shadowRoot = getShadowRoot(node);
if (shadowRoot) {
// If the node is under a shadow root, the shadowRoot contains the node and
// we can find the node via shadowRoot.querySelector(path).
return {
containingDocOrShadow: shadowRoot,
node,
};
}
// Otherwise, get the root binding parent to get a non anonymous element that
// will be accessible from the ownerDocument.
const bindingParent = getRootBindingParent(node);
return {
containingDocOrShadow: bindingParent.ownerDocument,
node: bindingParent,
};
}
/**
* Find a unique CSS selector for a given element
* @returns a string such that:
* - ele.containingDocOrShadow.querySelector(reply) === ele
* - ele.containingDocOrShadow.querySelectorAll(reply).length === 1
*/
const findCssSelector = function(ele) {
const { node, containingDocOrShadow } = findNodeAndContainer(ele);
ele = node;
if (!containingDocOrShadow || !containingDocOrShadow.contains(ele)) {
// findCssSelector received element not inside container.
return "";
}
const cssEscape = ele.ownerGlobal.CSS.escape;
// document.querySelectorAll("#id") returns multiple if elements share an ID
if (
ele.id &&
containingDocOrShadow.querySelectorAll("#" + cssEscape(ele.id)).length === 1
) {
return "#" + cssEscape(ele.id);
}
// Inherently unique by tag name
const tagName = ele.localName;
if (tagName === "html") {
return "html";
}
if (tagName === "head") {
return "head";
}
if (tagName === "body") {
return "body";
}
// We might be able to find a unique class name
let selector, index, matches;
for (let i = 0; i < ele.classList.length; i++) {
// Is this className unique by itself?
selector = "." + cssEscape(ele.classList.item(i));
matches = containingDocOrShadow.querySelectorAll(selector);
if (matches.length === 1) {
return selector;
}
// Maybe it's unique with a tag name?
selector = cssEscape(tagName) + selector;
matches = containingDocOrShadow.querySelectorAll(selector);
if (matches.length === 1) {
return selector;
}
// Maybe it's unique using a tag name and nth-child
index = positionInNodeList(ele, ele.parentNode.children) + 1;
selector = selector + ":nth-child(" + index + ")";
matches = containingDocOrShadow.querySelectorAll(selector);
if (matches.length === 1) {
return selector;
}
}
// Not unique enough yet.
index = positionInNodeList(ele, ele.parentNode.children) + 1;
selector = cssEscape(tagName) + ":nth-child(" + index + ")";
if (ele.parentNode !== containingDocOrShadow) {
selector = findCssSelector(ele.parentNode) + " > " + selector;
}
return selector;
};
exports.findCssSelector = findCssSelector;
/**
* If the element is in a frame or under a shadowRoot, return the corresponding
* element.
*/
function getSelectorParent(node) {
const shadowRoot = getShadowRoot(node);
if (shadowRoot) {
// The element is in a shadowRoot, return the host component.
return shadowRoot.host;
}
// Otherwise return the parent frameElement.
return node.ownerGlobal.frameElement;
}
/**
* Retrieve the array of CSS selectors corresponding to the provided node.
*
* The selectors are ordered starting with the root document and ending with the deepest
* nested frame. Additional items are used if the node is inside a frame or a shadow root,
* each representing the CSS selector for finding the frame or root 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}
* An array of CSS selectors to find the target node. Several selectors can be
* needed if the element is nested in frames and not directly in the root
* document. The selectors are ordered starting with the root document and
* ending with the deepest nested frame or shadow root.
*/
const findAllCssSelectors = function(node) {
const selectors = [];
while (node) {
selectors.unshift(findCssSelector(node));
node = getSelectorParent(node);
}
return selectors;
};
exports.findAllCssSelectors = findAllCssSelectors;
/**
* 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.
*/
function getCssPath(ele) {
const { node, containingDocOrShadow } = findNodeAndContainer(ele);
ele = node;
if (!containingDocOrShadow || !containingDocOrShadow.contains(ele)) {
// getCssPath received element not inside container.
return "";
}
const nodeGlobal = ele.ownerGlobal.Node;
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 (const cl of element.classList) {
label += "." + cl;
}
}
return label;
};
const paths = [];
while (ele) {
if (!ele || ele.nodeType !== nodeGlobal.ELEMENT_NODE) {
break;
}
paths.splice(0, 0, getElementSelector(ele));
ele = ele.parentNode;
}
return paths.length ? paths.join(" ") : "";
}
exports.getCssPath = getCssPath;
/**
* Get the xpath for a given element.
*
* @param {DomNode} ele
* @returns a string that can be used as an XPath to find the element uniquely.
*/
function getXPath(ele) {
const { node, containingDocOrShadow } = findNodeAndContainer(ele);
ele = node;
if (!containingDocOrShadow || !containingDocOrShadow.contains(ele)) {
// getXPath received element not inside container.
return "";
}
// Create a short XPath for elements with IDs.
if (ele.id) {
return `//*[@id="${ele.id}"]`;
}
// Otherwise walk the DOM up and create a part for each ancestor.
const parts = [];
const nodeGlobal = ele.ownerGlobal.Node;
// Use nodeName (instead of localName) so namespace prefix is included (if any).
while (ele && ele.nodeType === nodeGlobal.ELEMENT_NODE) {
let nbOfPreviousSiblings = 0;
let hasNextSiblings = false;
// Count how many previous same-name siblings the element has.
let sibling = ele.previousSibling;
while (sibling) {
// Ignore document type declaration.
if (
sibling.nodeType !== nodeGlobal.DOCUMENT_TYPE_NODE &&
sibling.nodeName == ele.nodeName
) {
nbOfPreviousSiblings++;
}
sibling = sibling.previousSibling;
}
// Check if the element has at least 1 next same-name sibling.
sibling = ele.nextSibling;
while (sibling) {
if (sibling.nodeName == ele.nodeName) {
hasNextSiblings = true;
break;
}
sibling = sibling.nextSibling;
}
const prefix = ele.prefix ? ele.prefix + ":" : "";
const nth =
nbOfPreviousSiblings || hasNextSiblings
? `[${nbOfPreviousSiblings + 1}]`
: "";
parts.push(prefix + ele.localName + nth);
ele = ele.parentNode;
}
return parts.length ? "/" + parts.reverse().join("/") : "";
}
exports.getXPath = getXPath;

Просмотреть файл

@ -2,6 +2,7 @@
tags = devtools
skip-if = os == 'android'
[test_css-logic-findCssSelector.html]
[test_css-logic-getCssPath.html]
[test_css-logic-getXPath.html]
skip-if = os == 'linux' && debug # Bug 1205739

Просмотреть файл

@ -1,18 +1,15 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=
-->
<head>
<meta charset="utf-8">
<title>Test for Bug </title>
<title>Test for CSS logic helper </title>
<script 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">
/* globals findCssSelector */
ChromeUtils.import("resource://gre/modules/css-selector.js", this);
const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
const { findCssSelector } = require("devtools/shared/inspector/css-logic");
var _tests = [];
function addTest(test) {

Просмотреть файл

@ -1,327 +0,0 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* 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/. */
"use strict";
var EXPORTED_SYMBOLS = [
"findAllCssSelectors",
"findCssSelector",
"getCssPath",
"getXPath",
];
/**
* Traverse getBindingParent until arriving upon the bound element
* responsible for the generation of the specified node.
* See https://developer.mozilla.org/en-US/docs/XBL/XBL_1.0_Reference/DOM_Interfaces#getBindingParent.
*
* @param {DOMNode} node
* @return {DOMNode}
* If node is not anonymous, this will return node. Otherwise,
* it will return the bound element
*
*/
function getRootBindingParent(node) {
let doc = node.ownerDocument;
if (!doc) {
return node;
}
let parent;
while ((parent = doc.getBindingParent(node))) {
node = parent;
}
return node;
}
/**
* Return the node's parent shadow root if the node in shadow DOM, null
* otherwise.
*/
function getShadowRoot(node) {
let doc = node.ownerDocument;
if (!doc) {
return null;
}
const parent = doc.getBindingParent(node);
const shadowRoot = parent && parent.openOrClosedShadowRoot;
if (shadowRoot) {
return shadowRoot;
}
return null;
}
/**
* Find the position of [element] in [nodeList].
* @returns an index of the match, or -1 if there is no match
*/
function positionInNodeList(element, nodeList) {
for (let i = 0; i < nodeList.length; i++) {
if (element === nodeList[i]) {
return i;
}
}
return -1;
}
/**
* For a provided node, find the appropriate container/node couple so that
* container.contains(node) and a CSS selector can be created from the
* container to the node.
*/
function findNodeAndContainer(node) {
const shadowRoot = getShadowRoot(node);
if (shadowRoot) {
// If the node is under a shadow root, the shadowRoot contains the node and
// we can find the node via shadowRoot.querySelector(path).
return {
containingDocOrShadow: shadowRoot,
node,
};
}
// Otherwise, get the root binding parent to get a non anonymous element that
// will be accessible from the ownerDocument.
const bindingParent = getRootBindingParent(node);
return {
containingDocOrShadow: bindingParent.ownerDocument,
node: bindingParent,
};
}
/**
* Find a unique CSS selector for a given element
* @returns a string such that:
* - ele.containingDocOrShadow.querySelector(reply) === ele
* - ele.containingDocOrShadow.querySelectorAll(reply).length === 1
*/
const findCssSelector = function(ele) {
const { node, containingDocOrShadow } = findNodeAndContainer(ele);
ele = node;
if (!containingDocOrShadow || !containingDocOrShadow.contains(ele)) {
// findCssSelector received element not inside container.
return "";
}
let cssEscape = ele.ownerGlobal.CSS.escape;
// document.querySelectorAll("#id") returns multiple if elements share an ID
if (
ele.id &&
containingDocOrShadow.querySelectorAll("#" + cssEscape(ele.id)).length === 1
) {
return "#" + cssEscape(ele.id);
}
// Inherently unique by tag name
let tagName = ele.localName;
if (tagName === "html") {
return "html";
}
if (tagName === "head") {
return "head";
}
if (tagName === "body") {
return "body";
}
// We might be able to find a unique class name
let selector, index, matches;
for (let i = 0; i < ele.classList.length; i++) {
// Is this className unique by itself?
selector = "." + cssEscape(ele.classList.item(i));
matches = containingDocOrShadow.querySelectorAll(selector);
if (matches.length === 1) {
return selector;
}
// Maybe it's unique with a tag name?
selector = cssEscape(tagName) + selector;
matches = containingDocOrShadow.querySelectorAll(selector);
if (matches.length === 1) {
return selector;
}
// Maybe it's unique using a tag name and nth-child
index = positionInNodeList(ele, ele.parentNode.children) + 1;
selector = selector + ":nth-child(" + index + ")";
matches = containingDocOrShadow.querySelectorAll(selector);
if (matches.length === 1) {
return selector;
}
}
// Not unique enough yet.
index = positionInNodeList(ele, ele.parentNode.children) + 1;
selector = cssEscape(tagName) + ":nth-child(" + index + ")";
if (ele.parentNode !== containingDocOrShadow) {
selector = findCssSelector(ele.parentNode) + " > " + selector;
}
return selector;
};
/**
* If the element is in a frame or under a shadowRoot, return the corresponding
* element.
*/
function getSelectorParent(node) {
const shadowRoot = getShadowRoot(node);
if (shadowRoot) {
// The element is in a shadowRoot, return the host component.
return shadowRoot.host;
}
// Otherwise return the parent frameElement.
return node.ownerGlobal.frameElement;
}
/**
* Retrieve the array of CSS selectors corresponding to the provided node.
*
* The selectors are ordered starting with the root document and ending with the deepest
* nested frame. Additional items are used if the node is inside a frame or a shadow root,
* each representing the CSS selector for finding the frame or root 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}
* An array of CSS selectors to find the target node. Several selectors can be
* needed if the element is nested in frames and not directly in the root
* document. The selectors are ordered starting with the root document and
* ending with the deepest nested frame or shadow root.
*/
const findAllCssSelectors = function(node) {
let selectors = [];
while (node) {
selectors.unshift(findCssSelector(node));
node = getSelectorParent(node);
}
return selectors;
};
/**
* 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.
*/
function getCssPath(ele) {
const { node, containingDocOrShadow } = findNodeAndContainer(ele);
ele = node;
if (!containingDocOrShadow || !containingDocOrShadow.contains(ele)) {
// getCssPath received element not inside container.
return "";
}
const nodeGlobal = ele.ownerGlobal.Node;
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 (const cl of element.classList) {
label += "." + cl;
}
}
return label;
};
const paths = [];
while (ele) {
if (!ele || ele.nodeType !== nodeGlobal.ELEMENT_NODE) {
break;
}
paths.splice(0, 0, getElementSelector(ele));
ele = ele.parentNode;
}
return paths.length ? paths.join(" ") : "";
}
/**
* Get the xpath for a given element.
* @param {DomNode} ele
* @returns a string that can be used as an XPath to find the element uniquely.
*/
function getXPath(ele) {
const { node, containingDocOrShadow } = findNodeAndContainer(ele);
ele = node;
if (!containingDocOrShadow || !containingDocOrShadow.contains(ele)) {
// getXPath received element not inside container.
return "";
}
// Create a short XPath for elements with IDs.
if (ele.id) {
return `//*[@id="${ele.id}"]`;
}
// Otherwise walk the DOM up and create a part for each ancestor.
const parts = [];
const nodeGlobal = ele.ownerGlobal.Node;
// Use nodeName (instead of localName) so namespace prefix is included (if any).
while (ele && ele.nodeType === nodeGlobal.ELEMENT_NODE) {
let nbOfPreviousSiblings = 0;
let hasNextSiblings = false;
// Count how many previous same-name siblings the element has.
let sibling = ele.previousSibling;
while (sibling) {
// Ignore document type declaration.
if (
sibling.nodeType !== nodeGlobal.DOCUMENT_TYPE_NODE &&
sibling.nodeName == ele.nodeName
) {
nbOfPreviousSiblings++;
}
sibling = sibling.previousSibling;
}
// Check if the element has at least 1 next same-name sibling.
sibling = ele.nextSibling;
while (sibling) {
if (sibling.nodeName == ele.nodeName) {
hasNextSiblings = true;
break;
}
sibling = sibling.nextSibling;
}
const prefix = ele.prefix ? ele.prefix + ":" : "";
const nth =
nbOfPreviousSiblings || hasNextSiblings
? `[${nbOfPreviousSiblings + 1}]`
: "";
parts.push(prefix + ele.localName + nth);
ele = ele.parentNode;
}
return parts.length ? "/" + parts.reverse().join("/") : "";
}

Просмотреть файл

@ -175,7 +175,6 @@ EXTRA_JS_MODULES += [
'Console.jsm',
'ContentDOMReference.jsm',
'CreditCard.jsm',
'css-selector.js',
'DateTimePickerPanel.jsm',
'DeferredTask.jsm',
'Deprecated.jsm',

Просмотреть файл

@ -2,4 +2,3 @@
[test_bug544442_checkCert.xul]
skip-if = verify
[test_findCssSelector.html]