diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 3c88d879..2146560b 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; @@ -275,10 +305,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; @@ -301,7 +361,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; @@ -311,8 +371,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; @@ -342,11 +402,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()) { @@ -396,7 +456,7 @@ export const getElementInformation = async ( processFrames(framesToCheck, frameDepth); } else { - deepestElement = traverseShadowDOM(element); + deepestElement = traverseShadowDOM(deepestElement); } return deepestElement; @@ -412,40 +472,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; @@ -573,10 +599,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; @@ -599,7 +655,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; @@ -609,8 +665,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; @@ -640,11 +696,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()) { @@ -694,7 +750,7 @@ export const getRect = async (page: Page, coordinates: Coordinates, listSelector processFrames(framesToCheck, frameDepth); } else { - deepestElement = traverseShadowDOM(element); + deepestElement = traverseShadowDOM(deepestElement); } return deepestElement; @@ -766,10 +822,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; @@ -792,7 +878,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; @@ -802,8 +888,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; @@ -833,11 +919,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()) { @@ -887,7 +973,7 @@ export const getRect = async (page: Page, coordinates: Coordinates, listSelector processFrames(framesToCheck, frameDepth); } else { - deepestElement = traverseShadowDOM(element); + deepestElement = traverseShadowDOM(deepestElement); } return deepestElement; @@ -904,40 +990,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) => ({ @@ -1477,10 +1529,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; @@ -1503,7 +1585,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; @@ -1513,8 +1595,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; @@ -1544,11 +1626,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()) { @@ -1598,7 +1680,7 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { processFrames(framesToCheck, frameDepth); } else { - deepestElement = traverseShadowDOM(element); + deepestElement = traverseShadowDOM(deepestElement); } return deepestElement; @@ -1922,10 +2004,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; @@ -1948,7 +2060,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; @@ -1958,8 +2070,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; @@ -1989,11 +2101,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()) { @@ -2043,7 +2155,7 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates processFrames(framesToCheck, frameDepth); } else { - deepestElement = traverseShadowDOM(element); + deepestElement = traverseShadowDOM(deepestElement); } return deepestElement; @@ -2085,25 +2197,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})`; } } @@ -2128,23 +2233,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})`; } @@ -2253,40 +2351,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); @@ -2296,10 +2360,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; @@ -2322,7 +2416,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; @@ -2332,8 +2426,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; @@ -2363,11 +2457,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()) { @@ -2417,7 +2511,7 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates processFrames(framesToCheck, frameDepth); } else { - deepestElement = traverseShadowDOM(element); + deepestElement = traverseShadowDOM(deepestElement); } return deepestElement; @@ -2459,25 +2553,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})`; } } @@ -2502,23 +2589,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})`; } @@ -2657,25 +2737,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})`; }