diff --git a/src/components/recorder/DOMBrowserRenderer.tsx b/src/components/recorder/DOMBrowserRenderer.tsx index 7fcafdeb4..a212f14b0 100644 --- a/src/components/recorder/DOMBrowserRenderer.tsx +++ b/src/components/recorder/DOMBrowserRenderer.tsx @@ -908,6 +908,7 @@ export const DOMBrowserRenderer: React.FC = ({ rebuild(snapshotData.snapshot, { doc: iframeDoc, mirror: mirror, + hackCss: false, cache: { stylesWithHoverClass: new Map() }, afterAppend: (node) => { if (node.nodeType === Node.TEXT_NODE && node.textContent) { diff --git a/src/helpers/clientSelectorGenerator.ts b/src/helpers/clientSelectorGenerator.ts index 385aa8ecb..d4e5051cf 100644 --- a/src/helpers/clientSelectorGenerator.ts +++ b/src/helpers/clientSelectorGenerator.ts @@ -555,36 +555,23 @@ class ClientSelectorGenerator { */ private isMeaningfulElement(element: HTMLElement): boolean { const tagName = element.tagName.toLowerCase(); - - // Fast path for common meaningful elements - if (["a", "img", "input", "button", "select"].includes(tagName)) { - return true; + + if (tagName === "img") { + return element.hasAttribute("src"); } - const text = (element.textContent || "").trim(); - const hasHref = element.hasAttribute("href"); - const hasSrc = element.hasAttribute("src"); - - // Quick checks first - if (text.length > 0 || hasHref || hasSrc) { + if (tagName === "a" && element.hasAttribute("href")) { return true; } - const isCustomElement = tagName.includes("-"); + if (element.children.length > 0) { + return false; + } - // For custom elements, be more lenient about what's considered meaningful - if (isCustomElement) { - const hasChildren = element.children.length > 0; - const hasSignificantAttributes = Array.from(element.attributes).some( - (attr) => !["class", "style", "id"].includes(attr.name.toLowerCase()) - ); + const text = (element.textContent || "").trim(); - return ( - hasChildren || - hasSignificantAttributes || - element.hasAttribute("role") || - element.hasAttribute("aria-label") - ); + if (text.length > 0) { + return true; } return false; @@ -2561,12 +2548,9 @@ class ClientSelectorGenerator { const MAX_MEANINGFUL_ELEMENTS = 300; const MAX_NODES_TO_CHECK = 1200; - const MAX_DEPTH = 12; + const MAX_DEPTH = 20; let nodesChecked = 0; - let adjustedMaxDepth = MAX_DEPTH; - const elementDensityThreshold = 50; - const depths: number[] = [0]; let queueIndex = 0; @@ -2576,14 +2560,10 @@ class ClientSelectorGenerator { queueIndex++; nodesChecked++; - if (currentDepth <= 3 && meaningfulDescendants.length > elementDensityThreshold) { - adjustedMaxDepth = Math.max(6, adjustedMaxDepth - 2); - } - if ( nodesChecked > MAX_NODES_TO_CHECK || meaningfulDescendants.length >= MAX_MEANINGFUL_ELEMENTS || - currentDepth > adjustedMaxDepth + currentDepth > MAX_DEPTH ) { break; } @@ -2592,7 +2572,7 @@ class ClientSelectorGenerator { meaningfulDescendants.push(element); } - if (currentDepth >= adjustedMaxDepth) { + if (currentDepth >= MAX_DEPTH) { continue; } @@ -2607,7 +2587,7 @@ class ClientSelectorGenerator { } } - if (element.shadowRoot && currentDepth < adjustedMaxDepth - 1) { + if (element.shadowRoot && currentDepth < MAX_DEPTH - 1) { const shadowChildren = element.shadowRoot.children; const shadowLimit = Math.min(shadowChildren.length, 20); for (let i = 0; i < shadowLimit; i++) { @@ -2716,22 +2696,46 @@ class ClientSelectorGenerator { } if (!addPositionToAll) { - const meaningfulAttrs = ["role", "type", "name", "src", "aria-label"]; + const meaningfulAttrs = ["role", "type"]; for (const attrName of meaningfulAttrs) { if (element.hasAttribute(attrName)) { const value = element.getAttribute(attrName)!.replace(/'/g, "\\'"); - return `${tagName}[@${attrName}='${value}']`; + const isCommonAttribute = this.isAttributeCommonAcrossLists( + element, + attrName, + value, + otherListElements + ); + if (isCommonAttribute) { + return `${tagName}[@${attrName}='${value}']`; + } } } } const testId = element.getAttribute("data-testid"); if (testId && !addPositionToAll) { - return `${tagName}[@data-testid='${testId}']`; + const isCommon = this.isAttributeCommonAcrossLists( + element, + "data-testid", + testId, + otherListElements + ); + if (isCommon) { + return `${tagName}[@data-testid='${testId}']`; + } } if (element.id && !element.id.match(/^\d/) && !addPositionToAll) { - return `${tagName}[@id='${element.id}']`; + const isCommon = this.isAttributeCommonAcrossLists( + element, + "id", + element.id, + otherListElements + ); + if (isCommon) { + return `${tagName}[@id='${element.id}']`; + } } if (!addPositionToAll) { @@ -2742,7 +2746,15 @@ class ClientSelectorGenerator { attr.name !== "data-mx-id" && attr.value ) { - return `${tagName}[@${attr.name}='${attr.value}']`; + const isCommon = this.isAttributeCommonAcrossLists( + element, + attr.name, + attr.value, + otherListElements + ); + if (isCommon) { + return `${tagName}[@${attr.name}='${attr.value}']`; + } } } } @@ -2906,12 +2918,70 @@ class ClientSelectorGenerator { const result = pathParts.length > 0 ? "/" + pathParts.join("/") : null; this.pathCache.set(targetElement, result); - + return result; } + private isAttributeCommonAcrossLists( + targetElement: HTMLElement, + attrName: string, + attrValue: string, + otherListElements: HTMLElement[] + ): boolean { + if (otherListElements.length === 0) { + return true; + } + + const targetPath = this.getElementPath(targetElement); + + for (const otherListElement of otherListElements) { + const correspondingElement = this.findCorrespondingElement( + otherListElement, + targetPath + ); + if (correspondingElement) { + const otherValue = correspondingElement.getAttribute(attrName); + if (otherValue !== attrValue) { + return false; + } + } + } + + return true; + } + + private getElementPath(element: HTMLElement): number[] { + const path: number[] = []; + let current: HTMLElement | null = element; + + while (current && current.parentElement) { + const siblings = Array.from(current.parentElement.children); + path.unshift(siblings.indexOf(current)); + current = current.parentElement; + } + + return path; + } + + private findCorrespondingElement( + rootElement: HTMLElement, + path: number[] + ): HTMLElement | null { + let current: HTMLElement = rootElement; + + for (const index of path) { + const children = Array.from(current.children); + if (index >= children.length) { + return null; + } + current = children[index] as HTMLElement; + } + + return current; + } + private getCommonClassesAcrossLists( - targetElement: HTMLElement, + targetElement: HTMLElement, otherListElements: HTMLElement[] ): string[] { if (otherListElements.length === 0) { @@ -3919,9 +3989,48 @@ class ClientSelectorGenerator { ); if (!deepestElement) return null; + if (!this.isMeaningfulElementCached(deepestElement)) { + const atomicChild = this.findAtomicChildAtPoint(deepestElement, x, y); + if (atomicChild) { + return atomicChild; + } + } + return deepestElement; } + private findAtomicChildAtPoint( + parent: HTMLElement, + x: number, + y: number + ): HTMLElement | null { + const stack: HTMLElement[] = [parent]; + const visited = new Set(); + + while (stack.length > 0) { + const element = stack.pop()!; + if (visited.has(element)) continue; + visited.add(element); + + if (element !== parent && this.isMeaningfulElementCached(element)) { + const rect = element.getBoundingClientRect(); + if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) { + return element; + } + } + + for (let i = element.children.length - 1; i >= 0; i--) { + const child = element.children[i] as HTMLElement; + const rect = child.getBoundingClientRect(); + if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) { + stack.push(child); + } + } + } + + return null; + } + /** * Helper methods used by the unified getDeepestElementFromPoint */