Skip to content

Instantly share code, notes, and snippets.

@Hobart2967
Last active October 6, 2021 10:46
Show Gist options
  • Select an option

  • Save Hobart2967/42d8d474819af1028e45126e9b798aa9 to your computer and use it in GitHub Desktop.

Select an option

Save Hobart2967/42d8d474819af1028e45126e9b798aa9 to your computer and use it in GitHub Desktop.
Quick and dirty fix for supporting XPath including shadow root.
function convertToCssSelector(xPath: string): string {
let selector = '';
let filterOpenedAt: boolean|number = false;
let squareBracketsOpened = 0;
for (let i = 0; i < xPath.length; i++) {
if (xPath[i] === '/') {
if (i === 0 || i === 1) {
continue;
}
if (xPath[i + 1] === '/') {
i++;
selector += ' ';
} else {
selector += ' > ';
}
continue;
}
if (xPath[i] === '[') {
if (filterOpenedAt === false) {
filterOpenedAt = i;
}
squareBracketsOpened++;
}
if (xPath[i] === ']') {
squareBracketsOpened = Math.max(0, squareBracketsOpened - 1);
if (!squareBracketsOpened) {
filterOpenedAt = false;
}
}
if (xPath[i] === '@' && filterOpenedAt === i - 1) {
continue;
}
selector += xPath[i];
}
return selector;
}
import { getAllShadowRootElements } from "./get-all-shadow-root-elements.js";
import { getXPath } from "./get-xpath.js"
import { convertToCssSelector } from './convert-to-css-selector.js';
((oldHandler) => {
const newHandler = (xpathExpression, contextNode, namespaceResolver, resultType, result) => {
const actualContextNode = contextNode;
const allShadowDomElements = getAllShadowRootElements(contextNode);
const allShadowDomElementNames = Array.from(new Set(allShadowDomElements
.map(x => x.host.tagName.toLowerCase())));
const webComponentBasedXPath = getXpath(xpathExpression, allShadowDomElementNames);
if (webComponentBasedXPath.length === 1) {
return oldHandler.apply(document, [
xpathExpression,
contextNode,
namespaceResolver,
resultType,
result
]);
}
try {
for (const pathPart of webComponentBasedXPath) {
const cssSelector = convertToCssSelector(pathPart);
const possibleNode = contextNode.querySelector(cssSelector);
if (possibleNode.shadowRoot) {
contextNode = possibleNode.shadowRoot;
} else {
contextNode = possibleNode;
}
}
return oldHandler.apply(document, [
`.`,
contextNode,
namespaceResolver,
resultType,
result
]);
} catch {
const evaluatedResult = allShadowDomElements.find(shadowRootedElement => {
try {
return document.evaluate(xpathExpression, shadowRootedElement, namespaceResolver, resultType, result);
} catch {
return null;
}
});
return evaluatedResult || oldHandler.apply(document, [
xpathExpression,
actualContextNode,
namespaceResolver,
resultType,
result]);
}
};
document.evaluate = newHandler;
})(document.evaluate);
export function getAllShadowRootElements(parent: HTMLElement): ShadowRoot[] {
const allElements = Array.from(parent.querySelectorAll('*')) as any[];
const shadowElements = allElements
.filter(x => x.shadowRoot)
.map(x => x.shadowRoot);
const resultList = [];
for (const shadowElement of shadowElements) {
resultList.push(shadowElement, ...getAllShadowRootElements(shadowElement));
}
return resultList;
}
function getXpath(xpath: string, possibleShadowDomHosts: string[]): string[] {
const segments = xpath.split('/');
const finalPath = [];
let currentSegmentPart = '';
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
if (!segment) {
if (i > 0 ) {
currentSegmentPart += '/';
}
continue;
}
currentSegmentPart += `/${segment}`;
if (possibleShadowDomHosts.includes(segment)) {
finalPath.push(currentSegmentPart);
currentSegmentPart = '';
}
}
finalPath.push(currentSegmentPart);
return finalPath;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment