From 90443cde8e101a5dde691129af6a1a7451569aa5 Mon Sep 17 00:00:00 2001 From: Rohit Date: Sat, 26 Jul 2025 16:28:19 +0530 Subject: [PATCH] feat: multi-list common classes, normalizing classes --- src/helpers/clientSelectorGenerator.ts | 225 ++++++++++++++----------- 1 file changed, 131 insertions(+), 94 deletions(-) diff --git a/src/helpers/clientSelectorGenerator.ts b/src/helpers/clientSelectorGenerator.ts index f00cc42f..9d4cc4ff 100644 --- a/src/helpers/clientSelectorGenerator.ts +++ b/src/helpers/clientSelectorGenerator.ts @@ -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(); 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(); @@ -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(); - 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 = {}; 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