Merge pull request #709 from getmaxun/deep-element

feat: improved overall selector generation
This commit is contained in:
Rohit
2025-07-28 17:19:40 +05:30
committed by GitHub

View File

@@ -123,6 +123,7 @@ class ClientSelectorGenerator {
private selectorElementCache = new Map<string, HTMLElement[]>(); private selectorElementCache = new Map<string, HTMLElement[]>();
private elementSelectorCache = new WeakMap<HTMLElement, string[]>(); private elementSelectorCache = new WeakMap<HTMLElement, string[]>();
private lastCachedDocument: Document | null = null; private lastCachedDocument: Document | null = null;
private classCache = new Map<string, string[]>();
private spatialIndex = new Map<string, string[]>(); private spatialIndex = new Map<string, string[]>();
private performanceConfig = { private performanceConfig = {
@@ -892,41 +893,68 @@ class ClientSelectorGenerator {
elementsAtPoint.forEach((element) => { elementsAtPoint.forEach((element) => {
if (element.tagName === "TD" || element.tagName === "TH") { if (element.tagName === "TD" || element.tagName === "TH") {
// Find parent TR for table cells
const parentRow = element.closest("tr") as HTMLElement; const parentRow = element.closest("tr") as HTMLElement;
if (parentRow && !transformedElements.includes(parentRow)) { if (parentRow && !transformedElements.includes(parentRow)) {
transformedElements.push(parentRow); transformedElements.push(parentRow);
} }
} else { } else {
// Keep non-table-cell elements as is
if (!transformedElements.includes(element)) { if (!transformedElements.includes(element)) {
transformedElements.push(element); transformedElements.push(element);
} }
} }
}); });
// Now filter for grouped elements from the transformed list
const groupedElementsAtPoint = transformedElements.filter((element) => const groupedElementsAtPoint = transformedElements.filter((element) =>
this.isElementGrouped(element) this.isElementGrouped(element)
); );
if (groupedElementsAtPoint.length > 0) { if (groupedElementsAtPoint.length > 0) {
const hasAnchorTag = groupedElementsAtPoint.some(
(el) => el.tagName === "A"
);
let filteredElements = groupedElementsAtPoint;
if (hasAnchorTag) {
// Apply parent-child filtering when anchor tags are present
filteredElements = this.filterParentChildGroupedElements(
groupedElementsAtPoint
);
}
// Sort by DOM depth (deeper elements first for more specificity) // Sort by DOM depth (deeper elements first for more specificity)
groupedElementsAtPoint.sort((a, b) => { filteredElements.sort((a, b) => {
const aDepth = this.getElementDepth(a); const aDepth = this.getElementDepth(a);
const bDepth = this.getElementDepth(b); const bDepth = this.getElementDepth(b);
return bDepth - aDepth; return bDepth - aDepth;
}); });
const selectedElement = groupedElementsAtPoint[0]; const selectedElement = filteredElements[0];
return selectedElement; return selectedElement;
} }
return null; return null;
} }
// For other modes or when list selector exists, return regular element return this.getDeepestElementFromPoint(x, y, iframeDoc);
return this.getDeepestElementFromPoint(elementsAtPoint, x, y); }
private filterParentChildGroupedElements(
groupedElements: HTMLElement[]
): HTMLElement[] {
const result: HTMLElement[] = [];
for (const element of groupedElements) {
const hasGroupedChild = groupedElements.some(
(other) => other !== element && element.contains(other)
);
if (hasGroupedChild) {
result.push(element);
}
}
return result.length > 0 ? result : groupedElements;
} }
public getElementInformation = ( public getElementInformation = (
@@ -937,191 +965,11 @@ class ClientSelectorGenerator {
) => { ) => {
try { try {
if (!getList || listSelector !== "") { if (!getList || listSelector !== "") {
const getDeepestElementFromPoint = ( const el = this.getDeepestElementFromPoint(
x: number, coordinates.x,
y: number coordinates.y,
): HTMLElement | null => { iframeDoc
let elements = iframeDoc.elementsFromPoint(x, y) as HTMLElement[];
if (!elements.length) return null;
const findDeepestElement = (
elements: HTMLElement[]
): HTMLElement | null => {
if (!elements.length) return null;
if (elements.length === 1) return elements[0];
let deepestElement = elements[0];
let maxDepth = 0;
for (const element of elements) {
let depth = 0;
let current = element;
while (current) {
depth++;
if (current.parentElement) {
current = current.parentElement;
} else {
break;
}
}
if (depth > maxDepth) {
maxDepth = depth;
deepestElement = element;
}
}
return deepestElement;
};
let deepestElement = findDeepestElement(elements);
if (!deepestElement) return null;
const traverseShadowDOM = (element: HTMLElement): HTMLElement => {
let current = element;
let shadowRoot = current.shadowRoot;
let deepest = current;
let depth = 0;
const MAX_SHADOW_DEPTH = 4;
while (shadowRoot && depth < MAX_SHADOW_DEPTH) {
const shadowElement = shadowRoot.elementFromPoint(
x,
y
) as HTMLElement;
if (!shadowElement || shadowElement === current) break;
deepest = shadowElement;
current = shadowElement;
shadowRoot = current.shadowRoot;
depth++;
}
return deepest;
};
const isInFrameset = () => {
let node = deepestElement;
while (node && node.parentElement) {
if (node.tagName === "FRAMESET" || node.tagName === "FRAME") {
return true;
}
node = node.parentElement;
}
return false;
};
if (deepestElement.tagName === "IFRAME") {
let currentIframe = deepestElement as HTMLIFrameElement;
let depth = 0;
const MAX_IFRAME_DEPTH = 4;
while (currentIframe && depth < MAX_IFRAME_DEPTH) {
try {
const iframeRect = currentIframe.getBoundingClientRect();
const iframeX = x - iframeRect.left;
const iframeY = y - iframeRect.top;
const iframeDocument =
currentIframe.contentDocument ||
currentIframe.contentWindow?.document;
if (!iframeDocument) break;
const iframeElement = iframeDocument.elementFromPoint(
iframeX,
iframeY
) as HTMLElement;
if (!iframeElement) break;
deepestElement = traverseShadowDOM(iframeElement);
if (iframeElement.tagName === "IFRAME") {
currentIframe = iframeElement as HTMLIFrameElement;
depth++;
} else {
break;
}
} catch (error) {
console.warn("Cannot access iframe content:", error);
break;
}
}
} else if (deepestElement.tagName === "FRAME" || isInFrameset()) {
const framesToCheck = [];
if (deepestElement.tagName === "FRAME") {
framesToCheck.push(deepestElement as HTMLFrameElement);
}
if (isInFrameset()) {
iframeDoc.querySelectorAll("frame").forEach((frame) => {
framesToCheck.push(frame as HTMLFrameElement);
});
}
let frameDepth = 0;
const MAX_FRAME_DEPTH = 4;
const processFrames = (
frames: HTMLFrameElement[],
currentDepth: number
) => {
if (currentDepth >= MAX_FRAME_DEPTH) return;
for (const frameElement of frames) {
try {
const frameRect = frameElement.getBoundingClientRect();
const frameX = x - frameRect.left;
const frameY = y - frameRect.top;
if (
frameX < 0 ||
frameY < 0 ||
frameX > frameRect.width ||
frameY > frameRect.height
) {
continue;
}
const frameDocument =
frameElement.contentDocument ||
frameElement.contentWindow?.document;
if (!frameDocument) continue;
const frameElementAtPoint = frameDocument.elementFromPoint(
frameX,
frameY
) as HTMLElement;
if (!frameElementAtPoint) continue;
deepestElement = traverseShadowDOM(frameElementAtPoint);
if (frameElementAtPoint.tagName === "FRAME") {
processFrames(
[frameElementAtPoint as HTMLFrameElement],
currentDepth + 1
); );
}
break;
} catch (error) {
console.warn("Cannot access frame content:", error);
continue;
}
}
};
processFrames(framesToCheck, frameDepth);
} else {
deepestElement = traverseShadowDOM(deepestElement);
}
return deepestElement;
};
const el = getDeepestElementFromPoint(coordinates.x, coordinates.y);
if (el) { if (el) {
// Prioritize Link (DO NOT REMOVE) // Prioritize Link (DO NOT REMOVE)
@@ -1394,191 +1242,11 @@ class ClientSelectorGenerator {
) => { ) => {
try { try {
if (!getList || listSelector !== "") { if (!getList || listSelector !== "") {
const getDeepestElementFromPoint = ( const el = this.getDeepestElementFromPoint(
x: number, coordinates.x,
y: number coordinates.y,
): HTMLElement | null => { iframeDoc
let elements = iframeDoc.elementsFromPoint(x, y) as HTMLElement[];
if (!elements.length) return null;
const findDeepestElement = (
elements: HTMLElement[]
): HTMLElement | null => {
if (!elements.length) return null;
if (elements.length === 1) return elements[0];
let deepestElement = elements[0];
let maxDepth = 0;
for (const element of elements) {
let depth = 0;
let current = element;
while (current) {
depth++;
if (current.parentElement) {
current = current.parentElement;
} else {
break;
}
}
if (depth > maxDepth) {
maxDepth = depth;
deepestElement = element;
}
}
return deepestElement;
};
let deepestElement = findDeepestElement(elements);
if (!deepestElement) return null;
const traverseShadowDOM = (element: HTMLElement): HTMLElement => {
let current = element;
let shadowRoot = current.shadowRoot;
let deepest = current;
let depth = 0;
const MAX_SHADOW_DEPTH = 4;
while (shadowRoot && depth < MAX_SHADOW_DEPTH) {
const shadowElement = shadowRoot.elementFromPoint(
x,
y
) as HTMLElement;
if (!shadowElement || shadowElement === current) break;
deepest = shadowElement;
current = shadowElement;
shadowRoot = current.shadowRoot;
depth++;
}
return deepest;
};
const isInFrameset = () => {
let node = deepestElement;
while (node && node.parentElement) {
if (node.tagName === "FRAMESET" || node.tagName === "FRAME") {
return true;
}
node = node.parentElement;
}
return false;
};
if (deepestElement.tagName === "IFRAME") {
let currentIframe = deepestElement as HTMLIFrameElement;
let depth = 0;
const MAX_IFRAME_DEPTH = 4;
while (currentIframe && depth < MAX_IFRAME_DEPTH) {
try {
const iframeRect = currentIframe.getBoundingClientRect();
const iframeX = x - iframeRect.left;
const iframeY = y - iframeRect.top;
const iframeDocument =
currentIframe.contentDocument ||
currentIframe.contentWindow?.document;
if (!iframeDocument) break;
const iframeElement = iframeDocument.elementFromPoint(
iframeX,
iframeY
) as HTMLElement;
if (!iframeElement) break;
deepestElement = traverseShadowDOM(iframeElement);
if (iframeElement.tagName === "IFRAME") {
currentIframe = iframeElement as HTMLIFrameElement;
depth++;
} else {
break;
}
} catch (error) {
console.warn("Cannot access iframe content:", error);
break;
}
}
} else if (deepestElement.tagName === "FRAME" || isInFrameset()) {
const framesToCheck = [];
if (deepestElement.tagName === "FRAME") {
framesToCheck.push(deepestElement as HTMLFrameElement);
}
if (isInFrameset()) {
iframeDoc.querySelectorAll("frame").forEach((frame) => {
framesToCheck.push(frame as HTMLFrameElement);
});
}
let frameDepth = 0;
const MAX_FRAME_DEPTH = 4;
const processFrames = (
frames: HTMLFrameElement[],
currentDepth: number
) => {
if (currentDepth >= MAX_FRAME_DEPTH) return;
for (const frameElement of frames) {
try {
const frameRect = frameElement.getBoundingClientRect();
const frameX = x - frameRect.left;
const frameY = y - frameRect.top;
if (
frameX < 0 ||
frameY < 0 ||
frameX > frameRect.width ||
frameY > frameRect.height
) {
continue;
}
const frameDocument =
frameElement.contentDocument ||
frameElement.contentWindow?.document;
if (!frameDocument) continue;
const frameElementAtPoint = frameDocument.elementFromPoint(
frameX,
frameY
) as HTMLElement;
if (!frameElementAtPoint) continue;
deepestElement = traverseShadowDOM(frameElementAtPoint);
if (frameElementAtPoint.tagName === "FRAME") {
processFrames(
[frameElementAtPoint as HTMLFrameElement],
currentDepth + 1
); );
}
break;
} catch (error) {
console.warn("Cannot access frame content:", error);
continue;
}
}
};
processFrames(framesToCheck, frameDepth);
} else {
deepestElement = traverseShadowDOM(deepestElement);
}
return deepestElement;
};
const el = getDeepestElementFromPoint(coordinates.x, coordinates.y);
if (el) { if (el) {
// Prioritize Link (DO NOT REMOVE) // Prioritize Link (DO NOT REMOVE)
const { parentElement } = el; const { parentElement } = el;
@@ -2779,31 +2447,11 @@ class ClientSelectorGenerator {
parentSelector: string parentSelector: string
): string[] => { ): string[] => {
try { try {
let parentElements: HTMLElement[] = []; // Use XPath evaluation to find parent elements
let parentElements: HTMLElement[] = this.evaluateXPath(
if (parentSelector.includes(">>")) { parentSelector,
const selectorParts = parentSelector iframeDoc
.split(">>")
.map((part) => part.trim());
parentElements = this.evaluateXPath(selectorParts[0], iframeDoc);
for (let i = 1; i < selectorParts.length; i++) {
const newParentElements: HTMLElement[] = [];
for (const element of parentElements) {
if (element.shadowRoot) {
const shadowChildren = this.evaluateXPath(
selectorParts[i],
element.shadowRoot as any
); );
newParentElements.push(...shadowChildren);
}
}
parentElements = newParentElements;
}
} else {
parentElements = this.evaluateXPath(parentSelector, iframeDoc);
}
if (parentElements.length === 0) { if (parentElements.length === 0) {
console.warn("No parent elements found for selector:", parentSelector); console.warn("No parent elements found for selector:", parentSelector);
@@ -2813,14 +2461,20 @@ class ClientSelectorGenerator {
const allChildSelectors = new Set<string>(); const allChildSelectors = new Set<string>();
parentElements.forEach((parentElement) => { parentElements.forEach((parentElement) => {
const otherListElements = parentElements.filter(
(el) => el !== parentElement
);
const childSelectors = this.generateOptimizedChildXPaths( const childSelectors = this.generateOptimizedChildXPaths(
parentElement, parentElement,
parentSelector, parentSelector,
iframeDoc iframeDoc,
otherListElements
); );
childSelectors.forEach((selector) => allChildSelectors.add(selector)); childSelectors.forEach((selector) => allChildSelectors.add(selector));
}); });
// Convert Set back to array and sort for consistency
const childSelectors = Array.from(allChildSelectors).sort(); const childSelectors = Array.from(allChildSelectors).sort();
return childSelectors; return childSelectors;
} catch (error) { } catch (error) {
@@ -2900,7 +2554,8 @@ class ClientSelectorGenerator {
private generateOptimizedChildXPaths( private generateOptimizedChildXPaths(
parentElement: HTMLElement, parentElement: HTMLElement,
listSelector: string, listSelector: string,
document: Document document: Document,
otherListElements: HTMLElement[] = []
): string[] { ): string[] {
const selectors: string[] = []; const selectors: string[] = [];
const processedElements = new Set<HTMLElement>(); const processedElements = new Set<HTMLElement>();
@@ -2916,7 +2571,8 @@ class ClientSelectorGenerator {
descendant, descendant,
listSelector, listSelector,
parentElement, parentElement,
document document,
otherListElements
); );
if (absolutePath) { if (absolutePath) {
@@ -2924,26 +2580,14 @@ class ClientSelectorGenerator {
} }
}); });
const shadowElements = this.getShadowDOMDescendants(parentElement);
shadowElements.forEach((shadowElement) => {
const shadowPath = this.buildOptimizedAbsoluteXPath(
shadowElement,
listSelector,
parentElement,
document
);
if (shadowPath) {
selectors.push(shadowPath);
}
});
return [...new Set(selectors)]; return [...new Set(selectors)];
} }
private generateOptimizedStructuralStep( private generateOptimizedStructuralStep(
element: HTMLElement, element: HTMLElement,
rootElement?: HTMLElement, rootElement?: HTMLElement,
addPositionToAll: boolean = false addPositionToAll: boolean = false,
otherListElements: HTMLElement[] = []
): string { ): string {
const tagName = element.tagName.toLowerCase(); const tagName = element.tagName.toLowerCase();
@@ -2955,7 +2599,10 @@ class ClientSelectorGenerator {
return tagName; return tagName;
} }
const classes = this.normalizeClasses(element.classList).split(" "); const classes = this.getCommonClassesAcrossLists(
element,
otherListElements
);
if (classes.length > 0 && !addPositionToAll) { if (classes.length > 0 && !addPositionToAll) {
const classSelector = classes const classSelector = classes
.map((cls) => `contains(@class, '${cls}')`) .map((cls) => `contains(@class, '${cls}')`)
@@ -2966,7 +2613,9 @@ class ClientSelectorGenerator {
.filter((el) => el !== element) .filter((el) => el !== element)
.some((el) => .some((el) =>
classes.every((cls) => classes.every((cls) =>
this.normalizeClasses((el as HTMLElement).classList).split(" ").includes(cls) this.normalizeClasses((el as HTMLElement).classList)
.split(" ")
.includes(cls)
) )
) )
: false; : false;
@@ -3079,29 +2728,21 @@ class ClientSelectorGenerator {
targetElement: HTMLElement, targetElement: HTMLElement,
listSelector: string, listSelector: string,
listElement: HTMLElement, listElement: HTMLElement,
document: Document document: Document,
otherListElements: HTMLElement[] = []
): string | null { ): string | null {
try { try {
let xpath = listSelector; let xpath = listSelector;
const pathFromList = this.getOptimizedStructuralPath( const pathFromList = this.getOptimizedStructuralPath(
targetElement, targetElement,
listElement listElement,
otherListElements
); );
if (!pathFromList) return null; if (!pathFromList) return null;
const fullXPath = xpath + pathFromList; const fullXPath = xpath + pathFromList;
if (targetElement.tagName.toLowerCase() === "a") {
// Ensure the XPath ends with an anchor selector
if (!fullXPath.includes("/a[") && !fullXPath.endsWith("/a")) {
console.warn(
"Generated XPath for anchor element does not target anchor:",
fullXPath
);
}
}
return fullXPath; return fullXPath;
} catch (error) { } catch (error) {
console.error("Error building optimized absolute XPath:", error); console.error("Error building optimized absolute XPath:", error);
@@ -3112,7 +2753,8 @@ class ClientSelectorGenerator {
// Unified path optimization (works for both light and shadow DOM) // Unified path optimization (works for both light and shadow DOM)
private getOptimizedStructuralPath( private getOptimizedStructuralPath(
targetElement: HTMLElement, targetElement: HTMLElement,
rootElement: HTMLElement rootElement: HTMLElement,
otherListElements: HTMLElement[] = []
): string | null { ): string | null {
if (!this.elementContains(rootElement, targetElement) || targetElement === rootElement) { if (!this.elementContains(rootElement, targetElement) || targetElement === rootElement) {
return null; return null;
@@ -3124,14 +2766,22 @@ class ClientSelectorGenerator {
// Build path from target up to root // Build path from target up to root
while (current && current !== rootElement) { while (current && current !== rootElement) {
// Calculate conflicts for each element in the path // Calculate conflicts for each element in the path
const classes = this.normalizeClasses(current.classList).split(" "); const classes = this.getCommonClassesAcrossLists(
current,
otherListElements
);
const hasConflictingElement = const hasConflictingElement =
classes.length > 0 && rootElement classes.length > 0 && rootElement
? this.queryElementsInScope(rootElement, current.tagName.toLowerCase()) ? this.queryElementsInScope(
rootElement,
current.tagName.toLowerCase()
)
.filter((el) => el !== current) .filter((el) => el !== current)
.some((el) => .some((el) =>
classes.every((cls) => classes.every((cls) =>
this.normalizeClasses((el as HTMLElement).classList).split(" ").includes(cls) this.normalizeClasses((el as HTMLElement).classList)
.split(" ")
.includes(cls)
) )
) )
: false; : false;
@@ -3139,7 +2789,8 @@ class ClientSelectorGenerator {
const pathPart = this.generateOptimizedStructuralStep( const pathPart = this.generateOptimizedStructuralStep(
current, current,
rootElement, rootElement,
hasConflictingElement hasConflictingElement,
otherListElements
); );
if (pathPart) { if (pathPart) {
pathParts.unshift(pathPart); pathParts.unshift(pathPart);
@@ -3155,6 +2806,69 @@ class ClientSelectorGenerator {
return pathParts.length > 0 ? "/" + pathParts.join("/") : null; return pathParts.length > 0 ? "/" + pathParts.join("/") : null;
} }
private getCommonClassesAcrossLists(
targetElement: HTMLElement,
otherListElements: HTMLElement[]
): string[] {
const targetClasses = this.normalizeClasses(targetElement.classList).split(" ").filter(Boolean);
const otherListsKey = otherListElements.map(el => `${el.tagName}-${el.className}`).sort().join('|');
const cacheKey = `${targetElement.tagName}-${targetClasses.sort().join(',')}-${otherListsKey}`;
if (this.classCache.has(cacheKey)) {
return this.classCache.get(cacheKey)!;
}
if (otherListElements.length === 0) {
this.classCache.set(cacheKey, targetClasses);
return targetClasses;
}
const similarElements = otherListElements.flatMap(listEl =>
this.getAllDescendantsIncludingShadow(listEl).filter(child =>
child.tagName === targetElement.tagName
)
);
if (similarElements.length === 0) {
this.classCache.set(cacheKey, targetClasses);
return targetClasses;
}
const exactMatches = similarElements.filter(el => {
const elClasses = this.normalizeClasses(el.classList).split(" ").filter(Boolean);
return targetClasses.length === elClasses.length &&
targetClasses.every(cls => elClasses.includes(cls));
});
if (exactMatches.length > 0) {
this.classCache.set(cacheKey, targetClasses);
return targetClasses;
}
const commonClasses: string[] = [];
for (const targetClass of targetClasses) {
const existsInAllOtherLists = otherListElements.every(listEl => {
const elementsInThisList = this.getAllDescendantsIncludingShadow(listEl).filter(child =>
child.tagName === targetElement.tagName
);
return elementsInThisList.some(el =>
this.normalizeClasses(el.classList).split(" ").includes(targetClass)
);
});
if (existsInAllOtherLists) {
commonClasses.push(targetClass);
}
}
// Cache the result
this.classCache.set(cacheKey, commonClasses);
return commonClasses;
}
// Helper method to check containment (works for both light and shadow DOM) // Helper method to check containment (works for both light and shadow DOM)
private elementContains(container: HTMLElement, element: HTMLElement): boolean { private elementContains(container: HTMLElement, element: HTMLElement): boolean {
// Standard containment check // Standard containment check
@@ -3647,25 +3361,6 @@ class ClientSelectorGenerator {
} }
} }
private getShadowDOMDescendants(element: HTMLElement): HTMLElement[] {
const shadowDescendants: HTMLElement[] = [];
const traverse = (el: HTMLElement) => {
if (el.shadowRoot) {
const shadowElements = Array.from(
el.shadowRoot.querySelectorAll("*")
) as HTMLElement[];
shadowDescendants.push(...shadowElements);
// Recursively check shadow elements for more shadow roots
shadowElements.forEach((shadowEl) => traverse(shadowEl));
}
};
traverse(element);
return shadowDescendants;
}
private getBestSelectorForAction = (action: Action) => { private getBestSelectorForAction = (action: Action) => {
switch (action.type) { switch (action.type) {
case ActionType.Click: case ActionType.Click:
@@ -4024,19 +3719,6 @@ class ClientSelectorGenerator {
xpath += `[${predicates.join(" and ")}]`; xpath += `[${predicates.join(" and ")}]`;
} }
// 6. Post-validate that XPath matches all elements
const matched = document.evaluate(
xpath,
document,
null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
null
);
const matchedSet = new Set<HTMLElement>();
for (let i = 0; i < matched.snapshotLength; i++) {
matchedSet.add(matched.snapshotItem(i) as HTMLElement);
}
return xpath; return xpath;
} }
@@ -4058,19 +3740,27 @@ class ClientSelectorGenerator {
const attrMap: Record<string, string> = {}; const attrMap: Record<string, string> = {};
for (const attr of Array.from(firstEl.attributes)) { for (const attr of Array.from(firstEl.attributes)) {
if (excludeAttrs.includes(attr.name) || !attr.value || attr.value.trim() === '') { if (
excludeAttrs.includes(attr.name) ||
!attr.value ||
attr.value.trim() === ""
) {
continue; continue;
} }
if (attr.name.startsWith('_ngcontent-') || attr.name.startsWith('_nghost-')) { if (
attr.name.startsWith("_ngcontent-") ||
attr.name.startsWith("_nghost-")
) {
continue; continue;
} }
if (attr.name.match(/^(data-reactid|data-react-checksum|ng-reflect-)/) || if (
attr.name.includes('-c') && attr.name.match(/\d+$/)) { attr.name.match(/^(data-reactid|data-react-checksum|ng-reflect-)/) ||
(attr.name.includes("-c") && attr.name.match(/\d+$/))
) {
continue; continue;
} }
attrMap[attr.name] = attr.value; attrMap[attr.name] = attr.value;
} }
@@ -4086,23 +3776,104 @@ class ClientSelectorGenerator {
return attrMap; return attrMap;
} }
/**
* Unified getDeepestElementFromPoint method that combines all features
* from the different implementations in getRect, getElementInformation, and the private method
*/
private getDeepestElementFromPoint( private getDeepestElementFromPoint(
x: number,
y: number,
iframeDoc: Document
): HTMLElement | null {
let elements = iframeDoc.elementsFromPoint(x, y) as HTMLElement[];
if (!elements.length) return null;
const filteredElements = this.filterLogicalElements(elements, x, y);
const targetElements =
filteredElements.length > 0 ? filteredElements : elements;
const visited = new Set<HTMLElement>();
let deepestElement = this.findTrulyDeepestElement(
targetElements,
x,
y,
visited
);
if (!deepestElement) return null;
return deepestElement;
}
/**
* Helper methods used by the unified getDeepestElementFromPoint
*/
private filterLogicalElements(
elements: HTMLElement[], elements: HTMLElement[],
x: number, x: number,
y: number y: number
): HTMLElement | null { ): HTMLElement[] {
if (!elements.length) return null; if (elements.length <= 1) return elements;
const visited = new Set<HTMLElement>(); const elementsWithContent = elements.filter((element) => {
return this.findTrulyDeepestElement(elements, x, y, visited); return this.elementHasRelevantContentAtPoint(element, x, y);
} });
private findTrulyDeepestElement( if (elementsWithContent.length > 0) {
return elementsWithContent;
}
return elements;
}
private elementHasRelevantContentAtPoint(
element: HTMLElement,
x: number,
y: number
): boolean {
const rect = element.getBoundingClientRect();
if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) {
return false;
}
const hasDirectText = Array.from(element.childNodes).some(
(node) => node.nodeType === Node.TEXT_NODE && node.textContent?.trim()
);
if (hasDirectText) {
return true;
}
if (element.tagName === "IMG") {
return true;
}
const contentTags = [
"INPUT",
"BUTTON",
"SELECT",
"TEXTAREA",
"VIDEO",
"AUDIO",
"CANVAS",
"SVG",
];
if (contentTags.includes(element.tagName)) {
return true;
}
const childElements = Array.from(element.children) as HTMLElement[];
return childElements.some(child =>
this.elementHasRelevantContentAtPoint(child, x, y)
);
}
private findTrulyDeepestElement(
elements: HTMLElement[], elements: HTMLElement[],
x: number, x: number,
y: number, y: number,
visited: Set<HTMLElement> visited: Set<HTMLElement>
): HTMLElement | null { ): HTMLElement | null {
let deepestElement: HTMLElement | null = null; let deepestElement: HTMLElement | null = null;
let maxDepth = -1; let maxDepth = -1;
@@ -4111,8 +3882,16 @@ private findTrulyDeepestElement(
visited.add(element); visited.add(element);
if (element.shadowRoot) { if (element.shadowRoot) {
const shadowElements = element.shadowRoot.elementsFromPoint(x, y) as HTMLElement[]; const shadowElements = element.shadowRoot.elementsFromPoint(
const deeper = this.findTrulyDeepestElement(shadowElements, x, y, visited); x,
y
) as HTMLElement[];
const deeper = this.findTrulyDeepestElement(
shadowElements,
x,
y,
visited
);
if (deeper) { if (deeper) {
const depth = this.getElementDepth(deeper); const depth = this.getElementDepth(deeper);
if (depth > maxDepth) { if (depth > maxDepth) {
@@ -4130,19 +3909,21 @@ private findTrulyDeepestElement(
} }
return deepestElement; return deepestElement;
} }
private getElementDepth(element: HTMLElement): number { private getElementDepth(element: HTMLElement): number {
let depth = 0; let depth = 0;
let current: HTMLElement | null = element; let current: HTMLElement | null = element;
while (current && current !== this.lastAnalyzedDocument?.body) { while (current && current !== this.lastAnalyzedDocument?.body) {
depth++; depth++;
current = current.parentElement || (current.getRootNode() as ShadowRoot).host as HTMLElement | null; current =
current.parentElement ||
((current.getRootNode() as ShadowRoot).host as HTMLElement | null);
if (depth > 50) break; if (depth > 50) break;
} }
return depth; return depth;
} }
/** /**
@@ -4156,6 +3937,7 @@ private getElementDepth(element: HTMLElement): number {
this.elementSelectorCache = new WeakMap(); this.elementSelectorCache = new WeakMap();
this.spatialIndex.clear(); this.spatialIndex.clear();
this.lastCachedDocument = null; this.lastCachedDocument = null;
this.classCache.clear();
} }
// Update generateSelector to use instance variables // Update generateSelector to use instance variables