feat: multi-list common classes, normalizing classes
This commit is contained in:
@@ -2447,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);
|
||||||
@@ -2481,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) {
|
||||||
@@ -2568,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>();
|
||||||
@@ -2584,7 +2571,8 @@ class ClientSelectorGenerator {
|
|||||||
descendant,
|
descendant,
|
||||||
listSelector,
|
listSelector,
|
||||||
parentElement,
|
parentElement,
|
||||||
document
|
document,
|
||||||
|
otherListElements
|
||||||
);
|
);
|
||||||
|
|
||||||
if (absolutePath) {
|
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)];
|
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();
|
||||||
|
|
||||||
@@ -2623,7 +2599,10 @@ class ClientSelectorGenerator {
|
|||||||
return tagName;
|
return tagName;
|
||||||
}
|
}
|
||||||
|
|
||||||
const classes = Array.from(element.classList);
|
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}')`)
|
||||||
@@ -2634,7 +2613,9 @@ class ClientSelectorGenerator {
|
|||||||
.filter((el) => el !== element)
|
.filter((el) => el !== element)
|
||||||
.some((el) =>
|
.some((el) =>
|
||||||
classes.every((cls) =>
|
classes.every((cls) =>
|
||||||
(el as HTMLElement).classList.contains(cls)
|
this.normalizeClasses((el as HTMLElement).classList)
|
||||||
|
.split(" ")
|
||||||
|
.includes(cls)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
: false;
|
: false;
|
||||||
@@ -2747,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);
|
||||||
@@ -2780,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;
|
||||||
@@ -2792,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 = Array.from(current.classList);
|
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) =>
|
||||||
(el as HTMLElement).classList.contains(cls)
|
this.normalizeClasses((el as HTMLElement).classList)
|
||||||
|
.split(" ")
|
||||||
|
.includes(cls)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
: false;
|
: false;
|
||||||
@@ -2807,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);
|
||||||
@@ -2823,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
|
||||||
@@ -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) => {
|
private getBestSelectorForAction = (action: Action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ActionType.Click:
|
case ActionType.Click:
|
||||||
@@ -3675,6 +3702,7 @@ class ClientSelectorGenerator {
|
|||||||
const commonAttributes = this.getCommonAttributes(elements, [
|
const commonAttributes = this.getCommonAttributes(elements, [
|
||||||
"id",
|
"id",
|
||||||
"style",
|
"style",
|
||||||
|
"class"
|
||||||
]);
|
]);
|
||||||
for (const [attr, value] of Object.entries(commonAttributes)) {
|
for (const [attr, value] of Object.entries(commonAttributes)) {
|
||||||
predicates.push(`@${attr}='${value}'`);
|
predicates.push(`@${attr}='${value}'`);
|
||||||
@@ -3691,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3725,7 +3740,28 @@ 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)) 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;
|
attrMap[attr.name] = attr.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3902,6 +3938,7 @@ class ClientSelectorGenerator {
|
|||||||
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
|
||||||
|
|||||||
Reference in New Issue
Block a user