feat: multi-list common classes, normalizing classes
This commit is contained in:
@@ -2447,31 +2447,11 @@ class ClientSelectorGenerator {
|
||||
parentSelector: string
|
||||
): string[] => {
|
||||
try {
|
||||
let parentElements: HTMLElement[] = [];
|
||||
|
||||
if (parentSelector.includes(">>")) {
|
||||
const selectorParts = parentSelector
|
||||
.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);
|
||||
}
|
||||
// Use XPath evaluation to find parent elements
|
||||
let parentElements: HTMLElement[] = this.evaluateXPath(
|
||||
parentSelector,
|
||||
iframeDoc
|
||||
);
|
||||
|
||||
if (parentElements.length === 0) {
|
||||
console.warn("No parent elements found for selector:", parentSelector);
|
||||
@@ -2481,14 +2461,20 @@ class ClientSelectorGenerator {
|
||||
const allChildSelectors = new Set<string>();
|
||||
|
||||
parentElements.forEach((parentElement) => {
|
||||
const otherListElements = parentElements.filter(
|
||||
(el) => el !== parentElement
|
||||
);
|
||||
|
||||
const childSelectors = this.generateOptimizedChildXPaths(
|
||||
parentElement,
|
||||
parentSelector,
|
||||
iframeDoc
|
||||
iframeDoc,
|
||||
otherListElements
|
||||
);
|
||||
childSelectors.forEach((selector) => allChildSelectors.add(selector));
|
||||
});
|
||||
|
||||
// Convert Set back to array and sort for consistency
|
||||
const childSelectors = Array.from(allChildSelectors).sort();
|
||||
return childSelectors;
|
||||
} catch (error) {
|
||||
@@ -2568,7 +2554,8 @@ class ClientSelectorGenerator {
|
||||
private generateOptimizedChildXPaths(
|
||||
parentElement: HTMLElement,
|
||||
listSelector: string,
|
||||
document: Document
|
||||
document: Document,
|
||||
otherListElements: HTMLElement[] = []
|
||||
): string[] {
|
||||
const selectors: string[] = [];
|
||||
const processedElements = new Set<HTMLElement>();
|
||||
@@ -2584,7 +2571,8 @@ class ClientSelectorGenerator {
|
||||
descendant,
|
||||
listSelector,
|
||||
parentElement,
|
||||
document
|
||||
document,
|
||||
otherListElements
|
||||
);
|
||||
|
||||
if (absolutePath) {
|
||||
@@ -2592,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)];
|
||||
}
|
||||
|
||||
private generateOptimizedStructuralStep(
|
||||
element: HTMLElement,
|
||||
rootElement?: HTMLElement,
|
||||
addPositionToAll: boolean = false
|
||||
addPositionToAll: boolean = false,
|
||||
otherListElements: HTMLElement[] = []
|
||||
): string {
|
||||
const tagName = element.tagName.toLowerCase();
|
||||
|
||||
@@ -2623,7 +2599,10 @@ class ClientSelectorGenerator {
|
||||
return tagName;
|
||||
}
|
||||
|
||||
const classes = Array.from(element.classList);
|
||||
const classes = this.getCommonClassesAcrossLists(
|
||||
element,
|
||||
otherListElements
|
||||
);
|
||||
if (classes.length > 0 && !addPositionToAll) {
|
||||
const classSelector = classes
|
||||
.map((cls) => `contains(@class, '${cls}')`)
|
||||
@@ -2634,7 +2613,9 @@ class ClientSelectorGenerator {
|
||||
.filter((el) => el !== element)
|
||||
.some((el) =>
|
||||
classes.every((cls) =>
|
||||
(el as HTMLElement).classList.contains(cls)
|
||||
this.normalizeClasses((el as HTMLElement).classList)
|
||||
.split(" ")
|
||||
.includes(cls)
|
||||
)
|
||||
)
|
||||
: false;
|
||||
@@ -2747,29 +2728,21 @@ class ClientSelectorGenerator {
|
||||
targetElement: HTMLElement,
|
||||
listSelector: string,
|
||||
listElement: HTMLElement,
|
||||
document: Document
|
||||
document: Document,
|
||||
otherListElements: HTMLElement[] = []
|
||||
): string | null {
|
||||
try {
|
||||
let xpath = listSelector;
|
||||
const pathFromList = this.getOptimizedStructuralPath(
|
||||
targetElement,
|
||||
listElement
|
||||
listElement,
|
||||
otherListElements
|
||||
);
|
||||
|
||||
if (!pathFromList) return null;
|
||||
|
||||
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;
|
||||
} catch (error) {
|
||||
console.error("Error building optimized absolute XPath:", error);
|
||||
@@ -2780,7 +2753,8 @@ class ClientSelectorGenerator {
|
||||
// Unified path optimization (works for both light and shadow DOM)
|
||||
private getOptimizedStructuralPath(
|
||||
targetElement: HTMLElement,
|
||||
rootElement: HTMLElement
|
||||
rootElement: HTMLElement,
|
||||
otherListElements: HTMLElement[] = []
|
||||
): string | null {
|
||||
if (!this.elementContains(rootElement, targetElement) || targetElement === rootElement) {
|
||||
return null;
|
||||
@@ -2792,14 +2766,22 @@ class ClientSelectorGenerator {
|
||||
// Build path from target up to root
|
||||
while (current && current !== rootElement) {
|
||||
// Calculate conflicts for each element in the path
|
||||
const classes = Array.from(current.classList);
|
||||
const classes = this.getCommonClassesAcrossLists(
|
||||
current,
|
||||
otherListElements
|
||||
);
|
||||
const hasConflictingElement =
|
||||
classes.length > 0 && rootElement
|
||||
? this.queryElementsInScope(rootElement, current.tagName.toLowerCase())
|
||||
? this.queryElementsInScope(
|
||||
rootElement,
|
||||
current.tagName.toLowerCase()
|
||||
)
|
||||
.filter((el) => el !== current)
|
||||
.some((el) =>
|
||||
classes.every((cls) =>
|
||||
(el as HTMLElement).classList.contains(cls)
|
||||
this.normalizeClasses((el as HTMLElement).classList)
|
||||
.split(" ")
|
||||
.includes(cls)
|
||||
)
|
||||
)
|
||||
: false;
|
||||
@@ -2807,7 +2789,8 @@ class ClientSelectorGenerator {
|
||||
const pathPart = this.generateOptimizedStructuralStep(
|
||||
current,
|
||||
rootElement,
|
||||
hasConflictingElement
|
||||
hasConflictingElement,
|
||||
otherListElements
|
||||
);
|
||||
if (pathPart) {
|
||||
pathParts.unshift(pathPart);
|
||||
@@ -2823,6 +2806,69 @@ class ClientSelectorGenerator {
|
||||
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)
|
||||
private elementContains(container: HTMLElement, element: HTMLElement): boolean {
|
||||
// Standard containment check
|
||||
@@ -3315,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) => {
|
||||
switch (action.type) {
|
||||
case ActionType.Click:
|
||||
@@ -3675,6 +3702,7 @@ class ClientSelectorGenerator {
|
||||
const commonAttributes = this.getCommonAttributes(elements, [
|
||||
"id",
|
||||
"style",
|
||||
"class"
|
||||
]);
|
||||
for (const [attr, value] of Object.entries(commonAttributes)) {
|
||||
predicates.push(`@${attr}='${value}'`);
|
||||
@@ -3691,19 +3719,6 @@ class ClientSelectorGenerator {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -3725,7 +3740,28 @@ class ClientSelectorGenerator {
|
||||
const attrMap: Record<string, string> = {};
|
||||
|
||||
for (const attr of Array.from(firstEl.attributes)) {
|
||||
if (excludeAttrs.includes(attr.name)) continue;
|
||||
if (
|
||||
excludeAttrs.includes(attr.name) ||
|
||||
!attr.value ||
|
||||
attr.value.trim() === ""
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
attr.name.startsWith("_ngcontent-") ||
|
||||
attr.name.startsWith("_nghost-")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
attr.name.match(/^(data-reactid|data-react-checksum|ng-reflect-)/) ||
|
||||
(attr.name.includes("-c") && attr.name.match(/\d+$/))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
attrMap[attr.name] = attr.value;
|
||||
}
|
||||
|
||||
@@ -3902,6 +3938,7 @@ class ClientSelectorGenerator {
|
||||
this.elementSelectorCache = new WeakMap();
|
||||
this.spatialIndex.clear();
|
||||
this.lastCachedDocument = null;
|
||||
this.classCache.clear();
|
||||
}
|
||||
|
||||
// Update generateSelector to use instance variables
|
||||
|
||||
Reference in New Issue
Block a user