From bad7255dc1912ccd3eb5589abd58b81e5166580f Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 20 Mar 2025 00:34:23 +0530 Subject: [PATCH 1/4] feat: avoid overlays and get deepest element --- server/src/workflow-management/selector.ts | 200 ++++++++++++++++----- 1 file changed, 160 insertions(+), 40 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 80ed55f3..34c37759 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -24,10 +24,40 @@ export const getElementInformation = async ( const elementInfo = await page.evaluate( async ({ x, y }) => { const getDeepestElementFromPoint = (x: number, y: number): HTMLElement | null => { - let element = document.elementFromPoint(x, y) as HTMLElement; - if (!element) return null; + let elements = document.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 = element; + let deepestElement = findDeepestElement(elements); + if (!deepestElement) return null; const traverseShadowDOM = (element: HTMLElement): HTMLElement => { let current = element; @@ -50,7 +80,7 @@ export const getElementInformation = async ( }; const isInFrameset = () => { - let node = element; + let node = deepestElement; while (node && node.parentElement) { if (node.tagName === 'FRAMESET' || node.tagName === 'FRAME') { return true; @@ -60,8 +90,8 @@ export const getElementInformation = async ( return false; }; - if (element.tagName === 'IFRAME') { - let currentIframe = element as HTMLIFrameElement; + if (deepestElement.tagName === 'IFRAME') { + let currentIframe = deepestElement as HTMLIFrameElement; let depth = 0; const MAX_IFRAME_DEPTH = 4; @@ -91,11 +121,11 @@ export const getElementInformation = async ( } } } - else if (element.tagName === 'FRAME' || isInFrameset()) { + else if (deepestElement.tagName === 'FRAME' || isInFrameset()) { const framesToCheck = []; - if (element.tagName === 'FRAME') { - framesToCheck.push(element as HTMLFrameElement); + if (deepestElement.tagName === 'FRAME') { + framesToCheck.push(deepestElement as HTMLFrameElement); } if (isInFrameset()) { @@ -145,7 +175,7 @@ export const getElementInformation = async ( processFrames(framesToCheck, frameDepth); } else { - deepestElement = traverseShadowDOM(element); + deepestElement = traverseShadowDOM(deepestElement); } return deepestElement; @@ -277,10 +307,40 @@ export const getElementInformation = async ( const elementInfo = await page.evaluate( async ({ x, y }) => { const getDeepestElementFromPoint = (x: number, y: number): HTMLElement | null => { - let element = document.elementFromPoint(x, y) as HTMLElement; - if (!element) return null; + let elements = document.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 = element; + let deepestElement = findDeepestElement(elements); + if (!deepestElement) return null; const traverseShadowDOM = (element: HTMLElement): HTMLElement => { let current = element; @@ -303,7 +363,7 @@ export const getElementInformation = async ( }; const isInFrameset = () => { - let node = element; + let node = deepestElement; while (node && node.parentElement) { if (node.tagName === 'FRAMESET' || node.tagName === 'FRAME') { return true; @@ -313,8 +373,8 @@ export const getElementInformation = async ( return false; }; - if (element.tagName === 'IFRAME') { - let currentIframe = element as HTMLIFrameElement; + if (deepestElement.tagName === 'IFRAME') { + let currentIframe = deepestElement as HTMLIFrameElement; let depth = 0; const MAX_IFRAME_DEPTH = 4; @@ -344,11 +404,11 @@ export const getElementInformation = async ( } } } - else if (element.tagName === 'FRAME' || isInFrameset()) { + else if (deepestElement.tagName === 'FRAME' || isInFrameset()) { const framesToCheck = []; - if (element.tagName === 'FRAME') { - framesToCheck.push(element as HTMLFrameElement); + if (deepestElement.tagName === 'FRAME') { + framesToCheck.push(deepestElement as HTMLFrameElement); } if (isInFrameset()) { @@ -398,7 +458,7 @@ export const getElementInformation = async ( processFrames(framesToCheck, frameDepth); } else { - deepestElement = traverseShadowDOM(element); + deepestElement = traverseShadowDOM(deepestElement); } return deepestElement; @@ -575,10 +635,40 @@ export const getRect = async (page: Page, coordinates: Coordinates, listSelector const rect = await page.evaluate( async ({ x, y }) => { const getDeepestElementFromPoint = (x: number, y: number): HTMLElement | null => { - let element = document.elementFromPoint(x, y) as HTMLElement; - if (!element) return null; + let elements = document.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 = element; + let deepestElement = findDeepestElement(elements); + if (!deepestElement) return null; const traverseShadowDOM = (element: HTMLElement): HTMLElement => { let current = element; @@ -601,7 +691,7 @@ export const getRect = async (page: Page, coordinates: Coordinates, listSelector }; const isInFrameset = () => { - let node = element; + let node = deepestElement; while (node && node.parentElement) { if (node.tagName === 'FRAMESET' || node.tagName === 'FRAME') { return true; @@ -611,8 +701,8 @@ export const getRect = async (page: Page, coordinates: Coordinates, listSelector return false; }; - if (element.tagName === 'IFRAME') { - let currentIframe = element as HTMLIFrameElement; + if (deepestElement.tagName === 'IFRAME') { + let currentIframe = deepestElement as HTMLIFrameElement; let depth = 0; const MAX_IFRAME_DEPTH = 4; @@ -642,11 +732,11 @@ export const getRect = async (page: Page, coordinates: Coordinates, listSelector } } } - else if (element.tagName === 'FRAME' || isInFrameset()) { + else if (deepestElement.tagName === 'FRAME' || isInFrameset()) { const framesToCheck = []; - if (element.tagName === 'FRAME') { - framesToCheck.push(element as HTMLFrameElement); + if (deepestElement.tagName === 'FRAME') { + framesToCheck.push(deepestElement as HTMLFrameElement); } if (isInFrameset()) { @@ -696,7 +786,7 @@ export const getRect = async (page: Page, coordinates: Coordinates, listSelector processFrames(framesToCheck, frameDepth); } else { - deepestElement = traverseShadowDOM(element); + deepestElement = traverseShadowDOM(deepestElement); } return deepestElement; @@ -769,10 +859,40 @@ export const getRect = async (page: Page, coordinates: Coordinates, listSelector const rect = await page.evaluate( async ({ x, y }) => { const getDeepestElementFromPoint = (x: number, y: number): HTMLElement | null => { - let element = document.elementFromPoint(x, y) as HTMLElement; - if (!element) return null; + let elements = document.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 = element; + let deepestElement = findDeepestElement(elements); + if (!deepestElement) return null; const traverseShadowDOM = (element: HTMLElement): HTMLElement => { let current = element; @@ -795,7 +915,7 @@ export const getRect = async (page: Page, coordinates: Coordinates, listSelector }; const isInFrameset = () => { - let node = element; + let node = deepestElement; while (node && node.parentElement) { if (node.tagName === 'FRAMESET' || node.tagName === 'FRAME') { return true; @@ -805,8 +925,8 @@ export const getRect = async (page: Page, coordinates: Coordinates, listSelector return false; }; - if (element.tagName === 'IFRAME') { - let currentIframe = element as HTMLIFrameElement; + if (deepestElement.tagName === 'IFRAME') { + let currentIframe = deepestElement as HTMLIFrameElement; let depth = 0; const MAX_IFRAME_DEPTH = 4; @@ -836,11 +956,11 @@ export const getRect = async (page: Page, coordinates: Coordinates, listSelector } } } - else if (element.tagName === 'FRAME' || isInFrameset()) { + else if (deepestElement.tagName === 'FRAME' || isInFrameset()) { const framesToCheck = []; - if (element.tagName === 'FRAME') { - framesToCheck.push(element as HTMLFrameElement); + if (deepestElement.tagName === 'FRAME') { + framesToCheck.push(deepestElement as HTMLFrameElement); } if (isInFrameset()) { @@ -890,7 +1010,7 @@ export const getRect = async (page: Page, coordinates: Coordinates, listSelector processFrames(framesToCheck, frameDepth); } else { - deepestElement = traverseShadowDOM(element); + deepestElement = traverseShadowDOM(deepestElement); } return deepestElement; From a5f8f9d64e3d92826b4fe8fb82e45b9624a7e577 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 20 Mar 2025 01:40:51 +0530 Subject: [PATCH 2/4] feat: rm threshold logic --- server/src/workflow-management/selector.ts | 102 --------------------- 1 file changed, 102 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 34c37759..20b56713 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -474,40 +474,6 @@ export const getElementInformation = async ( element = tableParent; } } - - if (element.tagName !== 'TABLE') { - while (element.parentElement) { - if (element.tagName.toLowerCase() === 'body' || - element.tagName.toLowerCase() === 'html') { - break; - } - - const parentRect = element.parentElement.getBoundingClientRect(); - const childRect = element.getBoundingClientRect(); - - const fullyContained = - parentRect.left <= childRect.left && - parentRect.right >= childRect.right && - parentRect.top <= childRect.top && - parentRect.bottom >= childRect.bottom; - - const significantOverlap = - (childRect.width * childRect.height) / - (parentRect.width * parentRect.height) > 0.1; - - if (fullyContained && significantOverlap) { - const nextParent = element.parentElement; - if (nextParent.tagName.toLowerCase() !== 'body' && - nextParent.tagName.toLowerCase() !== 'html') { - element = nextParent; - } else { - break; - } - } else { - break; - } - } - } const ownerDocument = element.ownerDocument; const frameElement = ownerDocument?.defaultView?.frameElement; @@ -1027,40 +993,6 @@ export const getRect = async (page: Page, coordinates: Coordinates, listSelector } } - if (element.tagName !== 'TABLE') { - while (element.parentElement) { - if (element.tagName.toLowerCase() === 'body' || - element.tagName.toLowerCase() === 'html') { - break; - } - - const parentRect = element.parentElement.getBoundingClientRect(); - const childRect = element.getBoundingClientRect(); - - const fullyContained = - parentRect.left <= childRect.left && - parentRect.right >= childRect.right && - parentRect.top <= childRect.top && - parentRect.bottom >= childRect.bottom; - - const significantOverlap = - (childRect.width * childRect.height) / - (parentRect.width * parentRect.height) > 0.1; - - if (fullyContained && significantOverlap) { - const nextParent = element.parentElement; - if (nextParent.tagName.toLowerCase() !== 'body' && - nextParent.tagName.toLowerCase() !== 'html') { - element = nextParent; - } else { - break; - } - } else { - break; - } - } - } - const rectangle = element?.getBoundingClientRect(); if (rectangle) { const createRectObject = (rect: DOMRect) => ({ @@ -2377,40 +2309,6 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates } } - if (element.tagName !== 'TABLE') { - while (element.parentElement) { - if (element.tagName.toLowerCase() === 'body' || - element.tagName.toLowerCase() === 'html') { - break; - } - - const parentRect = element.parentElement.getBoundingClientRect(); - const childRect = element.getBoundingClientRect(); - - const fullyContained = - parentRect.left <= childRect.left && - parentRect.right >= childRect.right && - parentRect.top <= childRect.top && - parentRect.bottom >= childRect.bottom; - - const significantOverlap = - (childRect.width * childRect.height) / - (parentRect.width * parentRect.height) > 0.1; - - if (fullyContained && significantOverlap) { - const nextParent = element.parentElement; - if (nextParent.tagName.toLowerCase() !== 'body' && - nextParent.tagName.toLowerCase() !== 'html') { - element = nextParent; - } else { - break; - } - } else { - break; - } - } - } - const generalSelector = getSelectorPath(element); return { generalSelector }; }, coordinates); From fe67193fd6823504b6bcce416a611cfdc0317a9f Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 20 Mar 2025 14:34:48 +0530 Subject: [PATCH 3/4] feat: improve dom parsing for capture list --- server/src/workflow-management/selector.ts | 150 ++++++++++++++++----- 1 file changed, 120 insertions(+), 30 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 20b56713..79df5e31 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -1532,10 +1532,40 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { } const getDeepestElementFromPoint = (x: number, y: number): HTMLElement | null => { - let element = document.elementFromPoint(x, y) as HTMLElement; - if (!element) return null; + let elements = document.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 = element; + let deepestElement = findDeepestElement(elements); + if (!deepestElement) return null; const traverseShadowDOM = (element: HTMLElement): HTMLElement => { let current = element; @@ -1558,7 +1588,7 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { }; const isInFrameset = () => { - let node = element; + let node = deepestElement; while (node && node.parentElement) { if (node.tagName === 'FRAMESET' || node.tagName === 'FRAME') { return true; @@ -1568,8 +1598,8 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { return false; }; - if (element.tagName === 'IFRAME') { - let currentIframe = element as HTMLIFrameElement; + if (deepestElement.tagName === 'IFRAME') { + let currentIframe = deepestElement as HTMLIFrameElement; let depth = 0; const MAX_IFRAME_DEPTH = 4; @@ -1599,11 +1629,11 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { } } } - else if (element.tagName === 'FRAME' || isInFrameset()) { + else if (deepestElement.tagName === 'FRAME' || isInFrameset()) { const framesToCheck = []; - if (element.tagName === 'FRAME') { - framesToCheck.push(element as HTMLFrameElement); + if (deepestElement.tagName === 'FRAME') { + framesToCheck.push(deepestElement as HTMLFrameElement); } if (isInFrameset()) { @@ -1653,7 +1683,7 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { processFrames(framesToCheck, frameDepth); } else { - deepestElement = traverseShadowDOM(element); + deepestElement = traverseShadowDOM(deepestElement); } return deepestElement; @@ -1978,10 +2008,40 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates if (!listSelector) { const selectors = await page.evaluate(({ x, y }: { x: number, y: number }) => { const getDeepestElementFromPoint = (x: number, y: number): HTMLElement | null => { - let element = document.elementFromPoint(x, y) as HTMLElement; - if (!element) return null; + let elements = document.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 = element; + let deepestElement = findDeepestElement(elements); + if (!deepestElement) return null; const traverseShadowDOM = (element: HTMLElement): HTMLElement => { let current = element; @@ -2004,7 +2064,7 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates }; const isInFrameset = () => { - let node = element; + let node = deepestElement; while (node && node.parentElement) { if (node.tagName === 'FRAMESET' || node.tagName === 'FRAME') { return true; @@ -2014,8 +2074,8 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates return false; }; - if (element.tagName === 'IFRAME') { - let currentIframe = element as HTMLIFrameElement; + if (deepestElement.tagName === 'IFRAME') { + let currentIframe = deepestElement as HTMLIFrameElement; let depth = 0; const MAX_IFRAME_DEPTH = 4; @@ -2045,11 +2105,11 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates } } } - else if (element.tagName === 'FRAME' || isInFrameset()) { + else if (deepestElement.tagName === 'FRAME' || isInFrameset()) { const framesToCheck = []; - if (element.tagName === 'FRAME') { - framesToCheck.push(element as HTMLFrameElement); + if (deepestElement.tagName === 'FRAME') { + framesToCheck.push(deepestElement as HTMLFrameElement); } if (isInFrameset()) { @@ -2099,7 +2159,7 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates processFrames(framesToCheck, frameDepth); } else { - deepestElement = traverseShadowDOM(element); + deepestElement = traverseShadowDOM(deepestElement); } return deepestElement; @@ -2318,10 +2378,40 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates // When we have a list selector, we need special handling while maintaining shadow DOM and frame support const selectors = await page.evaluate(({ x, y }: { x: number, y: number }) => { const getDeepestElementFromPoint = (x: number, y: number): HTMLElement | null => { - let element = document.elementFromPoint(x, y) as HTMLElement; - if (!element) return null; + let elements = document.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 = element; + let deepestElement = findDeepestElement(elements); + if (!deepestElement) return null; const traverseShadowDOM = (element: HTMLElement): HTMLElement => { let current = element; @@ -2344,7 +2434,7 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates }; const isInFrameset = () => { - let node = element; + let node = deepestElement; while (node && node.parentElement) { if (node.tagName === 'FRAMESET' || node.tagName === 'FRAME') { return true; @@ -2354,8 +2444,8 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates return false; }; - if (element.tagName === 'IFRAME') { - let currentIframe = element as HTMLIFrameElement; + if (deepestElement.tagName === 'IFRAME') { + let currentIframe = deepestElement as HTMLIFrameElement; let depth = 0; const MAX_IFRAME_DEPTH = 4; @@ -2385,11 +2475,11 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates } } } - else if (element.tagName === 'FRAME' || isInFrameset()) { + else if (deepestElement.tagName === 'FRAME' || isInFrameset()) { const framesToCheck = []; - if (element.tagName === 'FRAME') { - framesToCheck.push(element as HTMLFrameElement); + if (deepestElement.tagName === 'FRAME') { + framesToCheck.push(deepestElement as HTMLFrameElement); } if (isInFrameset()) { @@ -2439,7 +2529,7 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates processFrames(framesToCheck, frameDepth); } else { - deepestElement = traverseShadowDOM(element); + deepestElement = traverseShadowDOM(deepestElement); } return deepestElement; From 7303859efd5b3017e30b877787fd9c1b733a8e88 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 20 Mar 2025 15:46:17 +0530 Subject: [PATCH 4/4] feat: add nth-child for sibling sharing one same class --- server/src/workflow-management/selector.ts | 122 ++++++++------------- 1 file changed, 43 insertions(+), 79 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 79df5e31..90f22154 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -2201,25 +2201,18 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates if (element.parentElement) { const siblings = Array.from(element.parentElement.children); - const identicalSiblings = siblings.filter(sibling => { - if (sibling === element) return false; - - let siblingSelector = sibling.tagName.toLowerCase(); - const siblingClassName = typeof sibling.className === 'string' ? sibling.className : ''; - if (siblingClassName) { - const siblingClasses = siblingClassName.split(/\s+/).filter(Boolean); - const validSiblingClasses = siblingClasses.filter(cls => !cls.startsWith('!') && !cls.includes(':')); - if (validSiblingClasses.length > 0) { - siblingSelector += '.' + validSiblingClasses.map(cls => CSS.escape(cls)).join('.'); - } - } - - return siblingSelector === baseSelector; - }); - if (identicalSiblings.length > 0) { + const elementClasses = Array.from(element.classList || []); + + const similarSiblings = siblings.filter(sibling => { + if (sibling === element) return false; + const siblingClasses = Array.from(sibling.classList || []); + return siblingClasses.some(cls => elementClasses.includes(cls)); + }); + + if (similarSiblings.length > 0) { const position = siblings.indexOf(element) + 1; - return `${baseSelector}:nth-child(${position})`; + selector += `:nth-child(${position})`; } } @@ -2244,23 +2237,16 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates if (element.parentElement) { const siblings = Array.from(element.parentElement.children); - const identicalSiblings = siblings.filter(sibling => { + + const elementClasses = Array.from(element.classList || []); + + const similarSiblings = siblings.filter(sibling => { if (sibling === element) return false; - - let siblingSelector = sibling.tagName.toLowerCase(); - const siblingClassName = typeof sibling.className === 'string' ? sibling.className : ''; - if (siblingClassName) { - const siblingClasses = siblingClassName.split(/\s+/).filter(Boolean); - const validSiblingClasses = siblingClasses.filter(cls => !cls.startsWith('!') && !cls.includes(':')); - if (validSiblingClasses.length > 0) { - siblingSelector += '.' + validSiblingClasses.map(cls => CSS.escape(cls)).join('.'); - } - } - - return siblingSelector === selector; + const siblingClasses = Array.from(sibling.classList || []); + return siblingClasses.some(cls => elementClasses.includes(cls)); }); - - if (identicalSiblings.length > 0) { + + if (similarSiblings.length > 0) { const position = siblings.indexOf(element) + 1; selector += `:nth-child(${position})`; } @@ -2571,25 +2557,18 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates if (element.parentElement) { const siblings = Array.from(element.parentElement.children); - const identicalSiblings = siblings.filter(sibling => { + + const elementClasses = Array.from(element.classList || []); + + const similarSiblings = siblings.filter(sibling => { if (sibling === element) return false; - - let siblingSelector = sibling.tagName.toLowerCase(); - const siblingClassName = typeof sibling.className === 'string' ? sibling.className : ''; - if (siblingClassName) { - const siblingClasses = siblingClassName.split(/\s+/).filter(Boolean); - const validSiblingClasses = siblingClasses.filter(cls => !cls.startsWith('!') && !cls.includes(':')); - if (validSiblingClasses.length > 0) { - siblingSelector += '.' + validSiblingClasses.map(cls => CSS.escape(cls)).join('.'); - } - } - - return siblingSelector === baseSelector; + const siblingClasses = Array.from(sibling.classList || []); + return siblingClasses.some(cls => elementClasses.includes(cls)); }); - - if (identicalSiblings.length > 0) { + + if (similarSiblings.length > 0) { const position = siblings.indexOf(element) + 1; - return `${baseSelector}:nth-child(${position})`; + selector += `:nth-child(${position})`; } } @@ -2614,23 +2593,16 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates if (element.parentElement) { const siblings = Array.from(element.parentElement.children); - const identicalSiblings = siblings.filter(sibling => { + + const elementClasses = Array.from(element.classList || []); + + const similarSiblings = siblings.filter(sibling => { if (sibling === element) return false; - - let siblingSelector = sibling.tagName.toLowerCase(); - const siblingClassName = typeof sibling.className === 'string' ? sibling.className : ''; - if (siblingClassName) { - const siblingClasses = siblingClassName.split(/\s+/).filter(Boolean); - const validSiblingClasses = siblingClasses.filter(cls => !cls.startsWith('!') && !cls.includes(':')); - if (validSiblingClasses.length > 0) { - siblingSelector += '.' + validSiblingClasses.map(cls => CSS.escape(cls)).join('.'); - } - } - - return siblingSelector === selector; + const siblingClasses = Array.from(sibling.classList || []); + return siblingClasses.some(cls => elementClasses.includes(cls)); }); - - if (identicalSiblings.length > 0) { + + if (similarSiblings.length > 0) { const position = siblings.indexOf(element) + 1; selector += `:nth-child(${position})`; } @@ -2769,25 +2741,17 @@ export const getChildSelectors = async (page: Page, parentSelector: string): Pro } if (element.parentElement) { - // Look for identical siblings const siblings = Array.from(element.parentElement.children); - const identicalSiblings = siblings.filter(sibling => { + + const elementClasses = Array.from(element.classList || []); + + const similarSiblings = siblings.filter(sibling => { if (sibling === element) return false; - - let siblingSelector = sibling.tagName.toLowerCase(); - const siblingClassName = typeof sibling.className === 'string' ? sibling.className : ''; - if (siblingClassName) { - const siblingClasses = siblingClassName.split(/\s+/).filter(Boolean); - const validSiblingClasses = siblingClasses.filter(cls => !cls.startsWith('!') && !cls.includes(':')); - if (validSiblingClasses.length > 0) { - siblingSelector += '.' + validSiblingClasses.map(cls => CSS.escape(cls)).join('.'); - } - } - - return siblingSelector === selector; + const siblingClasses = Array.from(sibling.classList || []); + return siblingClasses.some(cls => elementClasses.includes(cls)); }); - - if (identicalSiblings.length > 0) { + + if (similarSiblings.length > 0) { const position = siblings.indexOf(element) + 1; selector += `:nth-child(${position})`; }