From 414ff5cd437495a8205e6e865323e95f98a70e44 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 28 Jan 2025 13:40:55 +0530 Subject: [PATCH 1/9] feat: better shadow selector generation --- server/src/workflow-management/classes/Generator.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 6e36f287e..73088283a 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -353,6 +353,9 @@ export class WorkflowGenerator { const selector = await this.generateSelector(page, coordinates, ActionType.Click); logger.log('debug', `Element's selector: ${selector}`); + const tempCheck = await getSelectors(page, coordinates) + console.log("SPECIFIC SELECTORS: ", tempCheck); + const elementInfo = await getElementInformation(page, coordinates, '', false); console.log("Element info: ", elementInfo); From cdb550c77fc1d23e46154b7b1c5e0387756ef03e Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 28 Jan 2025 13:43:24 +0530 Subject: [PATCH 2/9] feat: revert generator changes --- server/src/workflow-management/classes/Generator.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 73088283a..6e36f287e 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -353,9 +353,6 @@ export class WorkflowGenerator { const selector = await this.generateSelector(page, coordinates, ActionType.Click); logger.log('debug', `Element's selector: ${selector}`); - const tempCheck = await getSelectors(page, coordinates) - console.log("SPECIFIC SELECTORS: ", tempCheck); - const elementInfo = await getElementInformation(page, coordinates, '', false); console.log("Element info: ", elementInfo); From f0f4141cc4b093b3ce53bd71f50a5372bcb97a3a Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 28 Jan 2025 13:43:41 +0530 Subject: [PATCH 3/9] feat: better shadow selector generation --- server/src/workflow-management/selector.ts | 146 +++++++++------------ 1 file changed, 63 insertions(+), 83 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 8a9096ec6..c469fdeba 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -1340,48 +1340,52 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { } }; - // Helper function to generate selectors for shadow DOM elements - const genSelectorForShadowDOM = (element: HTMLElement) => { - // Get complete path up to document root - const getShadowPath = (el: HTMLElement) => { - const path = []; - let current = el; - let depth = 0; - const MAX_DEPTH = 4; - - while (current && depth < MAX_DEPTH) { - const rootNode = current.getRootNode(); - if (rootNode instanceof ShadowRoot) { - path.unshift({ - host: rootNode.host as HTMLElement, - root: rootNode, - element: current - }); - current = rootNode.host as HTMLElement; - depth++; - } else { - break; - } + const getShadowPath = (element: HTMLElement) => { + const path = []; + let current = element; + let depth = 0; + const MAX_DEPTH = 4; + + while (current && depth < MAX_DEPTH) { + const rootNode = current.getRootNode(); + if (rootNode instanceof ShadowRoot) { + path.unshift({ + host: rootNode.host as HTMLElement, + root: rootNode, + element: current + }); + current = rootNode.host as HTMLElement; + depth++; + } else { + break; } - return path; - }; + } + return path; + }; + + const genSelectors = (element: HTMLElement | null) => { + if (element == null) { + return null; + } + const href = element.getAttribute('href'); const shadowPath = getShadowPath(element); - if (shadowPath.length === 0) return null; - try { + const generateShadowAwareSelector = (elementOptions = {}) => { + if (shadowPath.length === 0) { + return finder(element, elementOptions); + } + const selectorParts: string[] = []; - // Generate selector for each shadow DOM boundary shadowPath.forEach((context, index) => { - // Get selector for the host element const hostSelector = finder(context.host, { root: index === 0 ? document.body : (shadowPath[index - 1].root as unknown as Element) }); - - // For the last context, get selector for target element + if (index === shadowPath.length - 1) { const elementSelector = finder(element, { + ...elementOptions, root: context.root as unknown as Element }); selectorParts.push(`${hostSelector} >> ${elementSelector}`); @@ -1389,72 +1393,52 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { selectorParts.push(hostSelector); } }); + + return selectorParts.join(' >> '); + }; - return { - fullSelector: selectorParts.join(' >> '), - mode: shadowPath[shadowPath.length - 1].root.mode - }; - } catch (e) { - console.warn('Error generating shadow DOM selector:', e); - return null; - } - }; - - const genSelectors = (element: HTMLElement | null) => { - if (element == null) { - return null; - } - - const href = element.getAttribute('href'); let generalSelector = null; try { - generalSelector = finder(element); + generalSelector = generateShadowAwareSelector(); } catch (e) { } let attrSelector = null; try { - attrSelector = finder(element, { attr: () => true }); + attrSelector = generateShadowAwareSelector({ attr: () => true }); } catch (e) { } - const iframeSelector = genSelectorForIframe(element); - const shadowSelector = genSelectorForShadowDOM(element); - - const hrefSelector = genSelectorForAttributes(element, ['href']); - const formSelector = genSelectorForAttributes(element, [ - 'name', - 'placeholder', - 'for', - ]); - const accessibilitySelector = genSelectorForAttributes(element, [ - 'aria-label', - 'alt', - 'title', - ]); - - const testIdSelector = genSelectorForAttributes(element, [ - 'data-testid', - 'data-test-id', - 'data-testing', - 'data-test', - 'data-qa', - 'data-cy', - ]); + + const hrefSelector = generateShadowAwareSelector({ + attr: genValidAttributeFilter(element, ['href']) + }); + const formSelector = generateShadowAwareSelector({ + attr: genValidAttributeFilter(element, ['name', 'placeholder', 'for']) + }); + const accessibilitySelector = generateShadowAwareSelector({ + attr: genValidAttributeFilter(element, ['aria-label', 'alt', 'title']) + }); + + const testIdSelector = generateShadowAwareSelector({ + attr: genValidAttributeFilter(element, [ + 'data-testid', + 'data-test-id', + 'data-testing', + 'data-test', + 'data-qa', + 'data-cy', + ]) + }); // We won't use an id selector if the id is invalid (starts with a number) let idSelector = null; try { - idSelector = - isAttributesDefined(element, ['id']) && - !isCharacterNumber(element.id?.[0]) - ? // Certain apps don't have unique ids (ex. youtube) - finder(element, { - attr: (name) => name === 'id', - }) - : null; + idSelector = isAttributesDefined(element, ['id']) && !isCharacterNumber(element.id?.[0]) + ? generateShadowAwareSelector({ attr: (name: string) => name === 'id' }) + : null; } catch (e) { } @@ -1473,10 +1457,6 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { full: iframeSelector.fullSelector, isIframe: iframeSelector.isFrameContent, } : null, - shadowSelector: shadowSelector ? { - full: shadowSelector.fullSelector, - mode: shadowSelector.mode - } : null }; } From e2546245cf038da4b082995ede51a365090c61aa Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 28 Jan 2025 13:44:19 +0530 Subject: [PATCH 4/9] feat: rm shadowSelector logic --- server/src/workflow-management/utils.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/server/src/workflow-management/utils.ts b/server/src/workflow-management/utils.ts index 0804aa78c..1c5411548 100644 --- a/server/src/workflow-management/utils.ts +++ b/server/src/workflow-management/utils.ts @@ -17,10 +17,6 @@ export const getBestSelectorForAction = (action: Action) => { if (selectors?.iframeSelector?.full) { return selectors.iframeSelector.full; } - - if (selectors?.shadowSelector?.full) { - return selectors.shadowSelector.full; - } // less than 25 characters, and element only has text inside const textSelector = From 22a3dc3589ba8cfc78010d0e1cadbcdb4150d494 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 28 Jan 2025 14:37:16 +0530 Subject: [PATCH 5/9] feat: rm iframe selector logic --- server/src/workflow-management/utils.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/server/src/workflow-management/utils.ts b/server/src/workflow-management/utils.ts index 1c5411548..4ac729f7a 100644 --- a/server/src/workflow-management/utils.ts +++ b/server/src/workflow-management/utils.ts @@ -13,11 +13,6 @@ export const getBestSelectorForAction = (action: Action) => { case ActionType.DragAndDrop: { const selectors = action.selectors; - - if (selectors?.iframeSelector?.full) { - return selectors.iframeSelector.full; - } - // less than 25 characters, and element only has text inside const textSelector = selectors?.text?.length != null && From 58a893c37cb7e176de5e7699e24cfdb43b412fc1 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 28 Jan 2025 14:38:05 +0530 Subject: [PATCH 6/9] feat: better iframe selector generation --- server/src/workflow-management/selector.ts | 127 +++++++++++++++------ 1 file changed, 95 insertions(+), 32 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index c469fdeba..1689ae836 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -762,6 +762,22 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { One, } + type ShadowBoundary = { + type: 'shadow'; + host: HTMLElement; + root: ShadowRoot; + element: HTMLElement; + }; + + type IframeBoundary = { + type: 'iframe'; + frame: HTMLIFrameElement; + document: Document; + element: HTMLElement; + }; + + type Boundary = ShadowBoundary | IframeBoundary; + type Options = { root: Element; idName: (name: string) => boolean; @@ -1340,8 +1356,8 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { } }; - const getShadowPath = (element: HTMLElement) => { - const path = []; + const getBoundaryPath = (element: HTMLElement): Boundary[] => { + const path: Boundary[] = []; let current = element; let depth = 0; const MAX_DEPTH = 4; @@ -1350,15 +1366,31 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { const rootNode = current.getRootNode(); if (rootNode instanceof ShadowRoot) { path.unshift({ + type: 'shadow', host: rootNode.host as HTMLElement, root: rootNode, element: current }); current = rootNode.host as HTMLElement; depth++; - } else { - break; + continue; } + + const ownerDocument = current.ownerDocument; + const frameElement = ownerDocument?.defaultView?.frameElement as HTMLIFrameElement; + if (frameElement) { + path.unshift({ + type: 'iframe', + frame: frameElement, + document: ownerDocument, + element: current + }); + current = frameElement; + depth++; + continue; + } + + break; } return path; }; @@ -1369,60 +1401,91 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { } const href = element.getAttribute('href'); - const shadowPath = getShadowPath(element); + const boundaryPath = getBoundaryPath(element); + + const getRootElement = (index: number): Element => { + if (index === 0) { + return document.body; + } + + const previousBoundary = boundaryPath[index - 1]; + if (!previousBoundary) { + return document.body; + } + + if (previousBoundary.type === 'shadow') { + return previousBoundary.root as unknown as Element; + } + return previousBoundary.document.body as Element; + }; - const generateShadowAwareSelector = (elementOptions = {}) => { - if (shadowPath.length === 0) { + const generateBoundaryAwareSelector = (elementOptions = {}) => { + if (boundaryPath.length === 0) { return finder(element, elementOptions); } const selectorParts: string[] = []; - shadowPath.forEach((context, index) => { - const hostSelector = finder(context.host, { - root: index === 0 ? document.body : (shadowPath[index - 1].root as unknown as Element) - }); + boundaryPath.forEach((context, index) => { + const root = getRootElement(index); - if (index === shadowPath.length - 1) { - const elementSelector = finder(element, { - ...elementOptions, - root: context.root as unknown as Element - }); - selectorParts.push(`${hostSelector} >> ${elementSelector}`); + if (context.type === 'shadow') { + const hostSelector = finder(context.host, { root }); + + if (index === boundaryPath.length - 1) { + const elementSelector = finder(element, { + ...elementOptions, + root: context.root as unknown as Element + }); + selectorParts.push(`${hostSelector} >> ${elementSelector}`); + } else { + selectorParts.push(hostSelector); + } } else { - selectorParts.push(hostSelector); + const frameSelector = finder(context.frame, { root }); + + if (index === boundaryPath.length - 1) { + const elementSelector = finder(element, { + ...elementOptions, + root: context.document.body as Element + }); + selectorParts.push(`${frameSelector} :>> ${elementSelector}`); + } else { + selectorParts.push(frameSelector); + } } }); - return selectorParts.join(' >> '); + const lastBoundary = boundaryPath[boundaryPath.length - 1]; + const delimiter = lastBoundary.type === 'shadow' ? ' >> ' : ' :>> '; + return selectorParts.join(delimiter); }; - let generalSelector = null; try { - generalSelector = generateShadowAwareSelector(); + generalSelector = generateBoundaryAwareSelector(); } catch (e) { } let attrSelector = null; try { - attrSelector = generateShadowAwareSelector({ attr: () => true }); + attrSelector = generateBoundaryAwareSelector({ attr: () => true }); } catch (e) { } - const iframeSelector = genSelectorForIframe(element); + // const iframeSelector = genSelectorForIframe(element); - const hrefSelector = generateShadowAwareSelector({ + const hrefSelector = generateBoundaryAwareSelector({ attr: genValidAttributeFilter(element, ['href']) }); - const formSelector = generateShadowAwareSelector({ + const formSelector = generateBoundaryAwareSelector({ attr: genValidAttributeFilter(element, ['name', 'placeholder', 'for']) }); - const accessibilitySelector = generateShadowAwareSelector({ + const accessibilitySelector = generateBoundaryAwareSelector({ attr: genValidAttributeFilter(element, ['aria-label', 'alt', 'title']) }); - const testIdSelector = generateShadowAwareSelector({ + const testIdSelector = generateBoundaryAwareSelector({ attr: genValidAttributeFilter(element, [ 'data-testid', 'data-test-id', @@ -1437,7 +1500,7 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { let idSelector = null; try { idSelector = isAttributesDefined(element, ['id']) && !isCharacterNumber(element.id?.[0]) - ? generateShadowAwareSelector({ attr: (name: string) => name === 'id' }) + ? generateBoundaryAwareSelector({ attr: (name: string) => name === 'id' }) : null; } catch (e) { } @@ -1453,10 +1516,10 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { hrefSelector, accessibilitySelector, formSelector, - iframeSelector: iframeSelector ? { - full: iframeSelector.fullSelector, - isIframe: iframeSelector.isFrameContent, - } : null, + // iframeSelector: iframeSelector ? { + // full: iframeSelector.fullSelector, + // isIframe: iframeSelector.isFrameContent, + // } : null, }; } From 8b82d33827cd4b3000b41f60a4a6e1f54c0ffd51 Mon Sep 17 00:00:00 2001 From: Rohit Date: Sat, 1 Feb 2025 12:19:44 +0530 Subject: [PATCH 7/9] feat: rm iframe selector generation logic --- server/src/workflow-management/selector.ts | 66 ---------------------- 1 file changed, 66 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 1689ae836..861de6151 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -1289,72 +1289,6 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { return deepestElement; }; - - const genSelectorForIframe = (element: HTMLElement) => { - // Helper function to get the complete iframe path up to document root - const getIframePath = (el: HTMLElement) => { - const path = []; - let current = el; - let depth = 0; - const MAX_DEPTH = 4; - - while (current && depth < MAX_DEPTH) { - // Get the owner document of the current element - const ownerDocument = current.ownerDocument; - - // Check if this document belongs to an iframe - const frameElement = ownerDocument?.defaultView?.frameElement as HTMLIFrameElement; - - if (frameElement) { - path.unshift({ - frame: frameElement, - document: ownerDocument, - element: current - }); - // Move up to the parent document's element (the iframe) - current = frameElement; - depth++; - } else { - break; - } - } - return path; - }; - - const iframePath = getIframePath(element); - if (iframePath.length === 0) return null; - - try { - const selectorParts: string[] = []; - - // Generate selector for each iframe boundary - iframePath.forEach((context, index) => { - // Get selector for the iframe element - const frameSelector = finder(context.frame, { - root: index === 0 ? document.body : - (iframePath[index - 1].document.body as Element) - }); - - // For the last context, get selector for target element - if (index === iframePath.length - 1) { - const elementSelector = finder(element, { - root: context.document.body as Element - }); - selectorParts.push(`${frameSelector} :>> ${elementSelector}`); - } else { - selectorParts.push(frameSelector); - } - }); - - return { - fullSelector: selectorParts.join(' :>> '), - isFrameContent: true - }; - } catch (e) { - console.warn('Error generating iframe selector:', e); - return null; - } - }; const getBoundaryPath = (element: HTMLElement): Boundary[] => { const path: Boundary[] = []; From 83afef7c8e8df7fc920d1059883edeea984e12e4 Mon Sep 17 00:00:00 2001 From: Rohit Date: Sat, 1 Feb 2025 12:24:05 +0530 Subject: [PATCH 8/9] feat: add error handling for cross-origin iframe --- server/src/workflow-management/selector.ts | 31 +++++++++++++++------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 861de6151..ced843519 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -1313,15 +1313,28 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { const ownerDocument = current.ownerDocument; const frameElement = ownerDocument?.defaultView?.frameElement as HTMLIFrameElement; if (frameElement) { - path.unshift({ - type: 'iframe', - frame: frameElement, - document: ownerDocument, - element: current - }); - current = frameElement; - depth++; - continue; + try { + // Check if we can access the iframe's origin + const iframeOrigin = new URL(frameElement.src).origin; + const currentOrigin = window.location.origin; + if (iframeOrigin !== currentOrigin) { + console.warn(`Skipping cross-origin iframe: ${iframeOrigin}`); + break; + } + + path.unshift({ + type: 'iframe', + frame: frameElement, + document: ownerDocument, + element: current + }); + current = frameElement; + depth++; + continue; + } catch (error) { + console.warn('Cannot access iframe origin:', error); + break; + } } break; From 1a8cfbc4e6fdff7ebad76dfb93252667b5eff881 Mon Sep 17 00:00:00 2001 From: Rohit Date: Sat, 1 Feb 2025 12:34:42 +0530 Subject: [PATCH 9/9] feat: revamp iframe shadow selector logic --- server/src/workflow-management/selector.ts | 126 ++++++++++++--------- 1 file changed, 72 insertions(+), 54 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index ced843519..4db85725f 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -5,6 +5,22 @@ import logger from "../logger"; type Workflow = WorkflowFile["workflow"]; +type ShadowBoundary = { + type: 'shadow'; + host: HTMLElement; + root: ShadowRoot; + element: HTMLElement; +}; + +type IframeBoundary = { + type: 'iframe'; + frame: HTMLIFrameElement; + document: Document; + element: HTMLElement; +}; + +type Boundary = ShadowBoundary | IframeBoundary; + /** * Checks the basic info about an element and returns a {@link BaseActionInfo} object. * If the element is not found, returns undefined. @@ -762,22 +778,6 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { One, } - type ShadowBoundary = { - type: 'shadow'; - host: HTMLElement; - root: ShadowRoot; - element: HTMLElement; - }; - - type IframeBoundary = { - type: 'iframe'; - frame: HTMLIFrameElement; - document: Document; - element: HTMLElement; - }; - - type Boundary = ShadowBoundary | IframeBoundary; - type Options = { root: Element; idName: (name: string) => boolean; @@ -1547,14 +1547,6 @@ interface SelectorResult { */ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates, listSelector: string): Promise => { - interface DOMContext { - type: 'iframe' | 'shadow'; - element: HTMLElement; - container: HTMLIFrameElement | ShadowRoot; - host?: HTMLElement; - document?: Document; - } - try { if (!listSelector) { const selectors = await page.evaluate(({ x, y }: { x: number, y: number }) => { @@ -1648,8 +1640,8 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates } - function getContextPath(element: HTMLElement): DOMContext[] { - const path: DOMContext[] = []; + function getContextPath(element: HTMLElement): Boundary[] { + const path: Boundary[] = []; let current = element; let depth = 0; const MAX_DEPTH = 4; @@ -1660,9 +1652,9 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates if (rootNode instanceof ShadowRoot) { path.unshift({ type: 'shadow', - element: current, - container: rootNode, - host: rootNode.host as HTMLElement + host: rootNode.host as HTMLElement, + root: rootNode, + element: current }); current = rootNode.host as HTMLElement; depth++; @@ -1674,15 +1666,28 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates const frameElement = ownerDocument?.defaultView?.frameElement as HTMLIFrameElement; if (frameElement) { - path.unshift({ - type: 'iframe', - element: current, - container: frameElement, - document: ownerDocument - }); - current = frameElement; - depth++; - continue; + try { + // Check if we can access the iframe's origin + const iframeOrigin = new URL(frameElement.src).origin; + const currentOrigin = window.location.origin; + if (iframeOrigin !== currentOrigin) { + console.warn(`Skipping cross-origin iframe: ${iframeOrigin}`); + break; + } + + path.unshift({ + type: 'iframe', + frame: frameElement, + document: ownerDocument, + element: current + }); + current = frameElement; + depth++; + continue; + } catch (error) { + console.warn('Cannot access iframe origin:', error); + break; + } } break; @@ -1701,7 +1706,7 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates contextPath.forEach((context, index) => { const containerSelector = getNonUniqueSelector( - context.type === 'shadow' ? context.host! : context.container as HTMLElement + context.type === 'shadow' ? context.host! : context.element as HTMLElement ); if (index === contextPath.length - 1) { @@ -1888,8 +1893,8 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates } // Get complete context path (both iframe and shadow DOM) - function getContextPath(element: HTMLElement): DOMContext[] { - const path: DOMContext[] = []; + function getContextPath(element: HTMLElement): Boundary[] { + const path: Boundary[] = []; let current = element; let depth = 0; const MAX_DEPTH = 4; @@ -1900,9 +1905,9 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates if (rootNode instanceof ShadowRoot) { path.unshift({ type: 'shadow', - element: current, - container: rootNode, - host: rootNode.host as HTMLElement + host: rootNode.host as HTMLElement, + root: rootNode, + element: current }); current = rootNode.host as HTMLElement; depth++; @@ -1914,15 +1919,28 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates const frameElement = ownerDocument?.defaultView?.frameElement as HTMLIFrameElement; if (frameElement) { - path.unshift({ - type: 'iframe', - element: current, - container: frameElement, - document: ownerDocument - }); - current = frameElement; - depth++; - continue; + try { + // Check if we can access the iframe's origin + const iframeOrigin = new URL(frameElement.src).origin; + const currentOrigin = window.location.origin; + if (iframeOrigin !== currentOrigin) { + console.warn(`Skipping cross-origin iframe: ${iframeOrigin}`); + break; + } + + path.unshift({ + type: 'iframe', + frame: frameElement, + document: ownerDocument, + element: current + }); + current = frameElement; + depth++; + continue; + } catch (error) { + console.warn('Cannot access iframe origin:', error); + break; + } } break; @@ -1941,7 +1959,7 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates contextPath.forEach((context, index) => { const containerSelector = getNonUniqueSelector( - context.type === 'shadow' ? context.host! : context.container as HTMLElement + context.type === 'shadow' ? context.host! : context.element as HTMLElement ); if (index === contextPath.length - 1) {