diff --git a/maxun-core/src/browserSide/scraper.js b/maxun-core/src/browserSide/scraper.js index 846f68e9..d9765f3a 100644 --- a/maxun-core/src/browserSide/scraper.js +++ b/maxun-core/src/browserSide/scraper.js @@ -205,50 +205,66 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3, } function findAllElements(config) { - // Check if selector contains iframe notation (:>>) - if (!config.selector.includes(':>>')) { - return Array.from(document.querySelectorAll(config.selector)); + // Regular DOM query if no special delimiters + if (!config.selector.includes('>>') && !config.selector.includes(':>>')) { + return Array.from(document.querySelectorAll(config.selector)); } - // For iframe traversal, split by iframe boundary marker - const parts = config.selector.split(':>>').map(s => s.trim()); + // Split by both types of delimiters + const parts = config.selector.split(/(?:>>|:>>)/).map(s => s.trim()); + const delimiters = config.selector.match(/(?:>>|:>>)/g) || []; let currentElements = [document]; - + for (let i = 0; i < parts.length; i++) { const part = parts[i]; const nextElements = []; - + const isLast = i === parts.length - 1; + const delimiter = delimiters[i] || ''; + const isIframeTraversal = delimiter === ':>>'; + for (const element of currentElements) { try { let targets; + if (i === 0) { - // First selector is queried from main document - targets = Array.from(element.querySelectorAll(part)) - .filter(el => { - // Only include iframes if not the last part - if (i === parts.length - 1) return true; - return el.tagName === 'IFRAME'; - }); + // First selector is queried from main document + targets = Array.from(element.querySelectorAll(part)) + .filter(el => { + if (isLast) return true; + // For iframe traversal, only include iframes + if (isIframeTraversal) return el.tagName === 'IFRAME'; + // For shadow DOM traversal, only include elements with shadow root + return el.shadowRoot && el.shadowRoot.mode === 'open'; + }); } else { - // For subsequent selectors, we need to look inside iframes + if (isIframeTraversal) { + // Handle iframe traversal const iframeDocument = element.contentDocument || element.contentWindow?.document; if (!iframeDocument) continue; - + targets = Array.from(iframeDocument.querySelectorAll(part)); - - // If this isn't the last part, filter for iframes only - if (i < parts.length - 1) { - targets = targets.filter(el => el.tagName === 'IFRAME'); + if (!isLast) { + targets = targets.filter(el => el.tagName === 'IFRAME'); } + } else { + // Handle shadow DOM traversal + const shadowRoot = element.shadowRoot; + if (!shadowRoot || shadowRoot.mode !== 'open') continue; + + targets = Array.from(shadowRoot.querySelectorAll(part)); + if (!isLast) { + targets = targets.filter(el => el.shadowRoot && el.shadowRoot.mode === 'open'); + } + } } + nextElements.push(...targets); } catch (error) { - // Handle cross-origin iframe access errors - console.warn('Cannot access iframe content:', error); + console.warn('Cannot access content:', error); continue; } } - + if (nextElements.length === 0) return []; currentElements = nextElements; } @@ -291,26 +307,27 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3, )[0]; } + // Find minimal bounding elements function getMBEs(elements) { return elements.map((element) => { - let candidate = element; - const isUniqueChild = (e) => elements - .filter((elem) => { - // Handle iframe boundaries when checking containment - const sameDocument = elem.ownerDocument === e.ownerDocument; - return sameDocument && e.parentNode?.contains(elem); - }) - .length === 1; - - while (candidate && isUniqueChild(candidate)) { - candidate = candidate.parentNode; - } - - return candidate; + let candidate = element; + const isUniqueChild = (e) => elements + .filter((elem) => { + // Handle both iframe and shadow DOM boundaries + const sameContext = elem.getRootNode() === e.getRootNode() && + elem.ownerDocument === e.ownerDocument; + return sameContext && e.parentNode?.contains(elem); + }) + .length === 1; + + while (candidate && isUniqueChild(candidate)) { + candidate = candidate.parentNode; + } + + return candidate; }); } - // Main scraping logic remains the same const seedName = getSeedKey(lists); const seedElements = findAllElements(lists[seedName]); const MBEs = getMBEs(seedElements); @@ -364,144 +381,207 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3, * @returns {Array.>} Array of arrays of scraped items, one sub-array per list */ window.scrapeList = async function ({ listSelector, fields, limit = 10 }) { - // Helper function to query elements within an iframe - const queryIframe = (rootElement, selector) => { - if (!selector.includes(':>>')) { - return rootElement.querySelector(selector); - } + // Enhanced query function to handle both iframe and shadow DOM + const queryElement = (rootElement, selector) => { + if (!selector.includes('>>') && !selector.includes(':>>')) { + return rootElement.querySelector(selector); + } - const parts = selector.split(':>>').map(part => part.trim()); - let currentElement = rootElement; + const parts = selector.split(/(?:>>|:>>)/).map(part => part.trim()); + let currentElement = rootElement; - for (let i = 0; i < parts.length; i++) { - if (!currentElement) return null; + for (let i = 0; i < parts.length; i++) { + if (!currentElement) return null; - // Handle iframe content document - if (currentElement.tagName === 'IFRAME') { - try { - const iframeDoc = currentElement.contentDocument || currentElement.contentWindow.document; - currentElement = iframeDoc.querySelector(parts[i]); - continue; - } catch (e) { - console.error('Cannot access iframe content:', e); - return null; - } - } + // Handle iframe traversal + if (currentElement.tagName === 'IFRAME') { + try { + const iframeDoc = currentElement.contentDocument || currentElement.contentWindow.document; + currentElement = iframeDoc.querySelector(parts[i]); + continue; + } catch (e) { + console.warn('Cannot access iframe content:', e); + return null; + } + } - currentElement = currentElement.querySelector(parts[i]); - } + // Try regular DOM first + let nextElement = currentElement.querySelector(parts[i]); - return currentElement; + // Try shadow DOM if not found + if (!nextElement && currentElement.shadowRoot) { + nextElement = currentElement.shadowRoot.querySelector(parts[i]); + } + + // Check children's shadow roots if still not found + if (!nextElement) { + const children = Array.from(currentElement.children || []); + for (const child of children) { + if (child.shadowRoot) { + nextElement = child.shadowRoot.querySelector(parts[i]); + if (nextElement) break; + } + } + } + + currentElement = nextElement; + } + + return currentElement; }; - // Helper function to query all matching elements within iframes - const queryIframeAll = (rootElement, selector) => { - if (!selector.includes(':>>')) { - return rootElement.querySelectorAll(selector); - } + // Enhanced query all function for both contexts + const queryElementAll = (rootElement, selector) => { + if (!selector.includes('>>') && !selector.includes(':>>')) { + return rootElement.querySelectorAll(selector); + } - const parts = selector.split(':>>').map(part => part.trim()); - let currentElements = [rootElement]; - - for (const part of parts) { - const nextElements = []; - - for (const element of currentElements) { - if (element.tagName === 'IFRAME') { - try { - const iframeDoc = element.contentDocument || element.contentWindow.document; - nextElements.push(...iframeDoc.querySelectorAll(part)); - } catch (e) { - console.error('Cannot access iframe content:', e); - continue; - } - } else { - nextElements.push(...element.querySelectorAll(part)); - } - } - - currentElements = nextElements; - } - - return currentElements; + const parts = selector.split(/(?:>>|:>>)/).map(part => part.trim()); + let currentElements = [rootElement]; + + for (const part of parts) { + const nextElements = []; + + for (const element of currentElements) { + // Handle iframe traversal + if (element.tagName === 'IFRAME') { + try { + const iframeDoc = element.contentDocument || element.contentWindow.document; + nextElements.push(...iframeDoc.querySelectorAll(part)); + } catch (e) { + console.warn('Cannot access iframe content:', e); + continue; + } + } else { + // Regular DOM elements + if (element.querySelectorAll) { + nextElements.push(...element.querySelectorAll(part)); + } + + // Shadow DOM elements + if (element.shadowRoot) { + nextElements.push(...element.shadowRoot.querySelectorAll(part)); + } + + // Check children's shadow roots + const children = Array.from(element.children || []); + for (const child of children) { + if (child.shadowRoot) { + nextElements.push(...child.shadowRoot.querySelectorAll(part)); + } + } + } + } + + currentElements = nextElements; + } + + return currentElements; }; - // Helper function to extract values from elements + // Enhanced value extraction with context awareness function extractValue(element, attribute) { - if (!element) return null; - - if (attribute === 'innerText') { - return element.innerText.trim(); - } else if (attribute === 'innerHTML') { - return element.innerHTML.trim(); - } else if (attribute === 'src' || attribute === 'href') { - const attrValue = element.getAttribute(attribute); - return attrValue ? new URL(attrValue, window.location.origin).href : null; - } - return element.getAttribute(attribute); + if (!element) return null; + + // Get context-aware base URL + const baseURL = element.ownerDocument?.location?.href || window.location.origin; + + // Check shadow root first + if (element.shadowRoot) { + const shadowContent = element.shadowRoot.textContent; + if (shadowContent?.trim()) { + return shadowContent.trim(); + } + } + + if (attribute === 'innerText') { + return element.innerText.trim(); + } else if (attribute === 'innerHTML') { + return element.innerHTML.trim(); + } else if (attribute === 'src' || attribute === 'href') { + const attrValue = element.getAttribute(attribute); + return attrValue ? new URL(attrValue, baseURL).href : null; + } + return element.getAttribute(attribute); } - // Helper function to find table ancestor elements + // Enhanced table ancestor finding with context support function findTableAncestor(element) { - let currentElement = element; - const MAX_DEPTH = 5; - let depth = 0; - - while (currentElement && depth < MAX_DEPTH) { - if (currentElement.tagName === 'TD') { - return { type: 'TD', element: currentElement }; - } else if (currentElement.tagName === 'TR') { - return { type: 'TR', element: currentElement }; - } - - // Handle iframe boundary crossing - if (currentElement.tagName === 'IFRAME') { - try { - currentElement = currentElement.contentDocument.body; - } catch (e) { - return null; - } - } else { - currentElement = currentElement.parentElement; - } - depth++; - } - return null; + let currentElement = element; + const MAX_DEPTH = 5; + let depth = 0; + + while (currentElement && depth < MAX_DEPTH) { + // Handle shadow DOM + if (currentElement.getRootNode() instanceof ShadowRoot) { + currentElement = currentElement.getRootNode().host; + continue; + } + + if (currentElement.tagName === 'TD') { + return { type: 'TD', element: currentElement }; + } else if (currentElement.tagName === 'TR') { + return { type: 'TR', element: currentElement }; + } + + // Handle iframe crossing + if (currentElement.tagName === 'IFRAME') { + try { + currentElement = currentElement.contentDocument.body; + } catch (e) { + return null; + } + } else { + currentElement = currentElement.parentElement; + } + depth++; + } + return null; } // Helper function to get cell index function getCellIndex(td) { - let index = 0; - let sibling = td; - while (sibling = sibling.previousElementSibling) { - index++; - } - return index; + if (td.getRootNode() instanceof ShadowRoot) { + const shadowRoot = td.getRootNode(); + const allCells = Array.from(shadowRoot.querySelectorAll('td')); + return allCells.indexOf(td); + } + + let index = 0; + let sibling = td; + while (sibling = sibling.previousElementSibling) { + index++; + } + return index; } // Helper function to check for TH elements function hasThElement(row, tableFields) { - for (const [label, { selector }] of Object.entries(tableFields)) { - const element = queryIframe(row, selector); - if (element) { - let current = element; - while (current && current !== row) { - if (current.tagName === 'TH') { - return true; - } - if (current.tagName === 'IFRAME') { - try { - current = current.contentDocument.body; - } catch (e) { - break; - } - } else { - current = current.parentElement; - } - } - } - } - return false; + for (const [_, { selector }] of Object.entries(tableFields)) { + const element = queryElement(row, selector); + if (element) { + let current = element; + while (current && current !== row) { + if (current.getRootNode() instanceof ShadowRoot) { + current = current.getRootNode().host; + continue; + } + + if (current.tagName === 'TH') return true; + + if (current.tagName === 'IFRAME') { + try { + current = current.contentDocument.body; + } catch (e) { + break; + } + } else { + current = current.parentElement; + } + } + } + } + return false; } // Helper function to filter rows @@ -511,7 +591,13 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3, return rows; } } - return rows.filter(row => row.getElementsByTagName('TH').length === 0); + // Include shadow DOM in TH search + return rows.filter(row => { + const directTH = row.getElementsByTagName('TH').length === 0; + const shadowTH = row.shadowRoot ? + row.shadowRoot.querySelector('th') === null : true; + return directTH && shadowTH; + }); } // Class similarity comparison functions @@ -523,188 +609,231 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3, return intersection.size / union.size; } + // Enhanced similar elements finding with context support function findSimilarElements(baseElement, similarityThreshold = 0.7) { - const baseClasses = Array.from(baseElement.classList); - if (baseClasses.length === 0) return []; - - // Include elements from all iframes - const allElements = []; - const iframes = document.getElementsByTagName('iframe'); - - // Add elements from main document - allElements.push(...document.getElementsByTagName(baseElement.tagName)); - - // Add elements from each iframe - for (const iframe of iframes) { - try { - const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; - allElements.push(...iframeDoc.getElementsByTagName(baseElement.tagName)); - } catch (e) { - console.error('Cannot access iframe content:', e); - } - } - - return allElements.filter(element => { - if (element === baseElement) return false; - const similarity = calculateClassSimilarity( - baseClasses, - Array.from(element.classList) - ); - return similarity >= similarityThreshold; - }); + const baseClasses = Array.from(baseElement.classList); + if (baseClasses.length === 0) return []; + + const allElements = []; + + // Get elements from main document + allElements.push(...document.getElementsByTagName(baseElement.tagName)); + + // Get elements from shadow DOM + if (baseElement.getRootNode() instanceof ShadowRoot) { + const shadowHost = baseElement.getRootNode().host; + allElements.push(...shadowHost.getElementsByTagName(baseElement.tagName)); + } + + // Get elements from iframes + const iframes = document.getElementsByTagName('iframe'); + for (const iframe of iframes) { + try { + const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; + allElements.push(...iframeDoc.getElementsByTagName(baseElement.tagName)); + } catch (e) { + console.warn('Cannot access iframe content:', e); + } + } + + return allElements.filter(element => { + if (element === baseElement) return false; + const similarity = calculateClassSimilarity( + baseClasses, + Array.from(element.classList) + ); + return similarity >= similarityThreshold; + }); } - // Main scraping logic - let containers = queryIframeAll(document, listSelector); + // Main scraping logic with context support + let containers = queryElementAll(document, listSelector); containers = Array.from(containers); if (containers.length === 0) return []; if (limit > 1 && containers.length === 1) { - const baseContainer = containers[0]; - const similarContainers = findSimilarElements(baseContainer); - - if (similarContainers.length > 0) { - const newContainers = similarContainers.filter(container => - !container.matches(listSelector) - ); - containers = [...containers, ...newContainers]; - } + const baseContainer = containers[0]; + const similarContainers = findSimilarElements(baseContainer); + + if (similarContainers.length > 0) { + const newContainers = similarContainers.filter(container => + !container.matches(listSelector) + ); + containers = [...containers, ...newContainers]; + } } const containerFields = containers.map(() => ({ - tableFields: {}, - nonTableFields: {} + tableFields: {}, + nonTableFields: {} })); // Classify fields containers.forEach((container, containerIndex) => { - for (const [label, field] of Object.entries(fields)) { - const sampleElement = queryIframe(container, field.selector); - - if (sampleElement) { - const ancestor = findTableAncestor(sampleElement); - if (ancestor) { - containerFields[containerIndex].tableFields[label] = { - ...field, - tableContext: ancestor.type, - cellIndex: ancestor.type === 'TD' ? getCellIndex(ancestor.element) : -1 - }; - } else { - containerFields[containerIndex].nonTableFields[label] = field; - } + for (const [label, field] of Object.entries(fields)) { + const sampleElement = queryElement(container, field.selector); + + if (sampleElement) { + const ancestor = findTableAncestor(sampleElement); + if (ancestor) { + containerFields[containerIndex].tableFields[label] = { + ...field, + tableContext: ancestor.type, + cellIndex: ancestor.type === 'TD' ? getCellIndex(ancestor.element) : -1 + }; } else { containerFields[containerIndex].nonTableFields[label] = field; } + } else { + containerFields[containerIndex].nonTableFields[label] = field; } + } }); const tableData = []; const nonTableData = []; - // Process table data + // Process table data with both iframe and shadow DOM support for (let containerIndex = 0; containerIndex < containers.length; containerIndex++) { - const container = containers[containerIndex]; - const { tableFields } = containerFields[containerIndex]; + const container = containers[containerIndex]; + const { tableFields } = containerFields[containerIndex]; - if (Object.keys(tableFields).length > 0) { - const firstField = Object.values(tableFields)[0]; - const firstElement = queryIframe(container, firstField.selector); - let tableContext = firstElement; - - while (tableContext && tableContext.tagName !== 'TABLE' && tableContext !== container) { - if (tableContext.tagName === 'IFRAME') { - try { - tableContext = tableContext.contentDocument.body; - } catch (e) { - break; - } - } else { - tableContext = tableContext.parentElement; - } - } + if (Object.keys(tableFields).length > 0) { + const firstField = Object.values(tableFields)[0]; + const firstElement = queryElement(container, firstField.selector); + let tableContext = firstElement; + + // Find table context including both iframe and shadow DOM + while (tableContext && tableContext.tagName !== 'TABLE' && tableContext !== container) { + if (tableContext.getRootNode() instanceof ShadowRoot) { + tableContext = tableContext.getRootNode().host; + continue; + } + + if (tableContext.tagName === 'IFRAME') { + try { + tableContext = tableContext.contentDocument.body; + } catch (e) { + break; + } + } else { + tableContext = tableContext.parentElement; + } + } - if (tableContext) { - const rows = Array.from(tableContext.getElementsByTagName('TR')); - const processedRows = filterRowsBasedOnTag(rows, tableFields); - - for (let rowIndex = 0; rowIndex < Math.min(processedRows.length, limit); rowIndex++) { - const record = {}; - const currentRow = processedRows[rowIndex]; - - for (const [label, { selector, attribute, cellIndex }] of Object.entries(tableFields)) { - let element = null; - - if (cellIndex >= 0) { - const td = currentRow.children[cellIndex]; - if (td) { - element = queryIframe(td, selector); - - if (!element && selector.split(">").pop().includes('td:nth-child')) { - element = td; - } + if (tableContext) { + // Get rows from all contexts + const rows = []; + + // Get rows from regular DOM + rows.push(...tableContext.getElementsByTagName('TR')); + + // Get rows from shadow DOM + if (tableContext.shadowRoot) { + rows.push(...tableContext.shadowRoot.getElementsByTagName('TR')); + } + + // Get rows from iframes + if (tableContext.tagName === 'IFRAME') { + try { + const iframeDoc = tableContext.contentDocument || tableContext.contentWindow.document; + rows.push(...iframeDoc.getElementsByTagName('TR')); + } catch (e) { + console.warn('Cannot access iframe rows:', e); + } + } + + const processedRows = filterRowsBasedOnTag(rows, tableFields); + + for (let rowIndex = 0; rowIndex < Math.min(processedRows.length, limit); rowIndex++) { + const record = {}; + const currentRow = processedRows[rowIndex]; + + for (const [label, { selector, attribute, cellIndex }] of Object.entries(tableFields)) { + let element = null; + + if (cellIndex >= 0) { + // Get TD element considering both contexts + let td = currentRow.children[cellIndex]; + + // Check shadow DOM for td + if (!td && currentRow.shadowRoot) { + const shadowCells = currentRow.shadowRoot.children; + if (shadowCells && shadowCells.length > cellIndex) { + td = shadowCells[cellIndex]; + } + } + + if (td) { + element = queryElement(td, selector); + + if (!element && selector.split(/(?:>>|:>>)/).pop().includes('td:nth-child')) { + element = td; + } - if (!element) { - const tagOnlySelector = selector.split('.')[0]; - element = queryIframe(td, tagOnlySelector); - } - - if (!element) { - let currentElement = td; - while (currentElement && currentElement.children.length > 0) { - let foundContentChild = false; - for (const child of currentElement.children) { - if (extractValue(child, attribute)) { - currentElement = child; - foundContentChild = true; - break; - } - } - if (!foundContentChild) break; - } - element = currentElement; - } - } - } else { - element = queryIframe(currentRow, selector); - } - - if (element) { - record[label] = extractValue(element, attribute); - } - } + if (!element) { + const tagOnlySelector = selector.split('.')[0]; + element = queryElement(td, tagOnlySelector); + } + + if (!element) { + let currentElement = td; + while (currentElement && currentElement.children.length > 0) { + let foundContentChild = false; + for (const child of currentElement.children) { + if (extractValue(child, attribute)) { + currentElement = child; + foundContentChild = true; + break; + } + } + if (!foundContentChild) break; + } + element = currentElement; + } + } + } else { + element = queryElement(currentRow, selector); + } + + if (element) { + record[label] = extractValue(element, attribute); + } + } - if (Object.keys(record).length > 0) { - tableData.push(record); - } - } - } - } + if (Object.keys(record).length > 0) { + tableData.push(record); + } + } + } + } } - // Process non-table data + // Process non-table data with both contexts support for (let containerIndex = 0; containerIndex < containers.length; containerIndex++) { - if (nonTableData.length >= limit) break; + if (nonTableData.length >= limit) break; - const container = containers[containerIndex]; - const { nonTableFields } = containerFields[containerIndex]; + const container = containers[containerIndex]; + const { nonTableFields } = containerFields[containerIndex]; - if (Object.keys(nonTableFields).length > 0) { - const record = {}; + if (Object.keys(nonTableFields).length > 0) { + const record = {}; - for (const [label, { selector, attribute }] of Object.entries(nonTableFields)) { - const relativeSelector = selector.split(':>>').slice(-1)[0]; - const element = queryIframe(container, relativeSelector); - - if (element) { - record[label] = extractValue(element, attribute); - } - } - - if (Object.keys(record).length > 0) { - nonTableData.push(record); - } - } + for (const [label, { selector, attribute }] of Object.entries(nonTableFields)) { + // Get the last part of the selector after any context delimiter + const relativeSelector = selector.split(/(?:>>|:>>)/).slice(-1)[0]; + const element = queryElement(container, relativeSelector); + + if (element) { + record[label] = extractValue(element, attribute); + } + } + + if (Object.keys(record).length > 0) { + nonTableData.push(record); + } + } } // Merge and limit the results diff --git a/maxun-core/src/interpret.ts b/maxun-core/src/interpret.ts index 58d0df42..e09ac5d5 100644 --- a/maxun-core/src/interpret.ts +++ b/maxun-core/src/interpret.ts @@ -403,7 +403,7 @@ export default class Interpreter extends EventEmitter { await this.options.serializableCallback(scrapeResults); }, - scrapeSchema: async (schema: Record) => { + scrapeSchema: async (schema: Record) => { await this.ensureScriptsLoaded(page); const scrapeResult = await page.evaluate((schemaObj) => window.scrapeSchema(schemaObj), schema); @@ -663,27 +663,42 @@ export default class Interpreter extends EventEmitter { if (isApplicable) { return actionId; } - } + } } - private removeIframeSelectors(workflow: Workflow) { + private removeShadowSelectors(workflow: Workflow) { for (let actionId = workflow.length - 1; actionId >= 0; actionId--) { const step = workflow[actionId]; // Check if step has where and selectors if (step.where && Array.isArray(step.where.selectors)) { // Filter out selectors that contain ">>" - step.where.selectors = step.where.selectors.filter(selector => !selector.includes(':>>')); + step.where.selectors = step.where.selectors.filter(selector => !selector.includes('>>')); } } return workflow; } + private removeSpecialSelectors(workflow: Workflow) { + for (let actionId = workflow.length - 1; actionId >= 0; actionId--) { + const step = workflow[actionId]; + + if (step.where && Array.isArray(step.where.selectors)) { + // Filter out if selector has EITHER ":>>" OR ">>" + step.where.selectors = step.where.selectors.filter(selector => + !(selector.includes(':>>') || selector.includes('>>')) + ); + } + } + + return workflow; + } + private async runLoop(p: Page, workflow: Workflow) { let workflowCopy: Workflow = JSON.parse(JSON.stringify(workflow)); - workflowCopy = this.removeIframeSelectors(workflowCopy); + workflowCopy = this.removeSpecialSelectors(workflowCopy); // apply ad-blocker to the current page try { diff --git a/server/src/types/index.ts b/server/src/types/index.ts index a2673d1d..75aac802 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -129,11 +129,17 @@ export interface BaseActionInfo { hasOnlyText: boolean; } + interface IframeSelector { full: string; isIframe: boolean; } +interface ShadowSelector { + full: string; + mode: string; +} + /** * Holds all the possible css selectors that has been found for an element. * @category Types @@ -149,6 +155,7 @@ export interface Selectors { accessibilitySelector: string|null; formSelector: string|null; iframeSelector: IframeSelector|null; + shadowSelector: ShadowSelector|null; } /** diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 609541de..d1bccbe4 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -730,15 +730,26 @@ export class WorkflowGenerator { const displaySelector = await this.generateSelector(page, coordinates, ActionType.Click); const elementInfo = await getElementInformation(page, coordinates, this.listSelector, this.getList); if (rect) { + const highlighterData = { + rect, + selector: displaySelector, + elementInfo, + // Include shadow DOM specific information + shadowInfo: elementInfo?.isShadowRoot ? { + mode: elementInfo.shadowRootMode, + content: elementInfo.shadowRootContent + } : null + }; + if (this.getList === true) { if (this.listSelector !== '') { const childSelectors = await getChildSelectors(page, this.listSelector || ''); - this.socket.emit('highlighter', { rect, selector: displaySelector, elementInfo, childSelectors }) + this.socket.emit('highlighter', { ...highlighterData, childSelectors }) } else { - this.socket.emit('highlighter', { rect, selector: displaySelector, elementInfo }); + this.socket.emit('highlighter', { ...highlighterData }); } } else { - this.socket.emit('highlighter', { rect, selector: displaySelector, elementInfo }); + this.socket.emit('highlighter', { ...highlighterData }); } } } diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index e6337049..82464e63 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -23,53 +23,68 @@ export const getElementInformation = async ( if (!getList || listSelector !== '') { const elementInfo = await page.evaluate( async ({ x, y }) => { - // Helper function to find elements within iframes, handling nested cases const getDeepestElementFromPoint = (x: number, y: number): HTMLElement | null => { // First, get the element at the clicked coordinates in the main document let element = document.elementFromPoint(x, y) as HTMLElement; if (!element) return null; - - // If it's not an iframe, return the element as is - if (element.tagName !== 'IFRAME') return element; - - // Initialize tracking variables for iframe traversal - let currentIframe = element as HTMLIFrameElement; + + // Track the deepest element found let deepestElement = element; - - // Continue traversing while we have a valid iframe - while (currentIframe) { + + // Function to traverse shadow DOM + const traverseShadowDOM = (element: HTMLElement): HTMLElement => { + let current = element; + let shadowRoot = current.shadowRoot; + let deepest = current; + + while (shadowRoot) { + const shadowElement = shadowRoot.elementFromPoint(x, y) as HTMLElement; + if (!shadowElement || shadowElement === current) break; + + deepest = shadowElement; + current = shadowElement; + shadowRoot = current.shadowRoot; + } + + return deepest; + }; + + // Handle iframe traversal + if (element.tagName === 'IFRAME') { + let currentIframe = element as HTMLIFrameElement; + + while (currentIframe) { try { - // Convert the coordinates from main document space to iframe's local space - const iframeRect = currentIframe.getBoundingClientRect(); - const iframeX = x - iframeRect.left; - const iframeY = y - iframeRect.top; - - // Get the iframe's document object - this gives us access to the iframe's content - const iframeDocument = currentIframe.contentDocument || currentIframe.contentWindow?.document; - if (!iframeDocument) break; - - // Find the element at the transformed coordinates within the iframe - const iframeElement = iframeDocument.elementFromPoint(iframeX, iframeY) as HTMLElement; - - // If no element found or it's the same as current, stop traversing - if (!iframeElement) break; - - // Update our tracking of the deepest element - deepestElement = iframeElement; - - // If we found another iframe, continue traversing through it - if (iframeElement.tagName === 'IFRAME') { - currentIframe = iframeElement as HTMLIFrameElement; - } else { - // If it's not an iframe, we've reached the deepest level - break; - } - } catch (error) { - // Handle potential cross-origin security restrictions - console.warn('Cannot access iframe content:', error); + // Convert coordinates to iframe's local space + const iframeRect = currentIframe.getBoundingClientRect(); + const iframeX = x - iframeRect.left; + const iframeY = y - iframeRect.top; + + const iframeDocument = currentIframe.contentDocument || currentIframe.contentWindow?.document; + if (!iframeDocument) break; + + const iframeElement = iframeDocument.elementFromPoint(iframeX, iframeY) as HTMLElement; + if (!iframeElement) break; + + // Update deepest element and check for shadow DOM + deepestElement = traverseShadowDOM(iframeElement); + + // Continue traversing if we found another iframe + if (iframeElement.tagName === 'IFRAME') { + currentIframe = iframeElement as HTMLIFrameElement; + } else { break; + } + } catch (error) { + console.warn('Cannot access iframe content:', error); + break; } + } + } else { + // If not an iframe, check for shadow DOM + deepestElement = traverseShadowDOM(element); } + return deepestElement; }; @@ -81,11 +96,15 @@ export const getElementInformation = async ( const { parentElement } = el; const targetElement = parentElement?.tagName === 'A' ? parentElement : el; + // Get containing context information const ownerDocument = targetElement.ownerDocument; const frameElement = ownerDocument?.defaultView?.frameElement as HTMLIFrameElement; const isIframeContent = Boolean(frameElement); + + // Get the containing shadow root if any + const containingShadowRoot = targetElement.getRootNode() as ShadowRoot; + const isShadowRoot = containingShadowRoot instanceof ShadowRoot; - // Build the element information object let info: { tagName: string; hasOnlyText?: boolean; @@ -99,9 +118,13 @@ export const getElementInformation = async ( iframeURL?: string; iframeIndex?: number; frameHierarchy?: string[]; + isShadowRoot?: boolean; + shadowRootMode?: string; + shadowRootContent?: string; } = { tagName: targetElement?.tagName ?? '', - isIframeContent: isIframeContent + isIframeContent, + isShadowRoot }; if (isIframeContent) { @@ -131,9 +154,14 @@ export const getElementInformation = async ( info.iframeIndex = frameIndex - 1; // Adjust for 0-based index } + if (isShadowRoot) { + // Include shadow root specific information + info.shadowRootMode = containingShadowRoot.mode; + info.shadowRootContent = containingShadowRoot.innerHTML; + } + // Collect element attributes and properties if (targetElement) { - // Get all attributes info.attributes = Array.from(targetElement.attributes).reduce( (acc, attr) => { acc[attr.name] = attr.value; @@ -142,7 +170,6 @@ export const getElementInformation = async ( {} as Record ); - // Handle specific element types if (targetElement.tagName === 'A') { info.url = (targetElement as HTMLAnchorElement).href; info.innerText = targetElement.textContent ?? ''; @@ -150,8 +177,8 @@ export const getElementInformation = async ( info.imageUrl = (targetElement as HTMLImageElement).src; } else { info.hasOnlyText = targetElement.children.length === 0 && - (targetElement.textContent !== null && - targetElement.textContent.trim().length > 0); + (targetElement.textContent !== null && + targetElement.textContent.trim().length > 0); info.innerText = targetElement.textContent ?? ''; } @@ -174,48 +201,64 @@ export const getElementInformation = async ( // First, get the element at the clicked coordinates in the main document let element = document.elementFromPoint(x, y) as HTMLElement; if (!element) return null; - - // If it's not an iframe, return the element as is - if (element.tagName !== 'IFRAME') return element; - - // Initialize tracking variables for iframe traversal - let currentIframe = element as HTMLIFrameElement; + + // Track the deepest element found let deepestElement = element; - - // Continue traversing while we have a valid iframe - while (currentIframe) { - try { - // Convert the coordinates from main document space to iframe's local space + + // Function to traverse shadow DOM + const traverseShadowDOM = (element: HTMLElement): HTMLElement => { + let current = element; + let shadowRoot = current.shadowRoot; + let deepest = current; + + while (shadowRoot) { + const shadowElement = shadowRoot.elementFromPoint(x, y) as HTMLElement; + if (!shadowElement || shadowElement === current) break; + + deepest = shadowElement; + current = shadowElement; + shadowRoot = current.shadowRoot; + } + + return deepest; + }; + + // Handle iframe traversal + if (element.tagName === 'IFRAME') { + let currentIframe = element as HTMLIFrameElement; + + while (currentIframe) { + try { + // Convert coordinates to iframe's local space const iframeRect = currentIframe.getBoundingClientRect(); const iframeX = x - iframeRect.left; const iframeY = y - iframeRect.top; - - // Get the iframe's document object - this gives us access to the iframe's content + const iframeDocument = currentIframe.contentDocument || currentIframe.contentWindow?.document; if (!iframeDocument) break; - - // Find the element at the transformed coordinates within the iframe + const iframeElement = iframeDocument.elementFromPoint(iframeX, iframeY) as HTMLElement; - - // If no element found or it's the same as current, stop traversing if (!iframeElement) break; - - // Update our tracking of the deepest element - deepestElement = iframeElement; - - // If we found another iframe, continue traversing through it + + // Update deepest element and check for shadow DOM + deepestElement = traverseShadowDOM(iframeElement); + + // Continue traversing if we found another iframe if (iframeElement.tagName === 'IFRAME') { - currentIframe = iframeElement as HTMLIFrameElement; + currentIframe = iframeElement as HTMLIFrameElement; } else { - // If it's not an iframe, we've reached the deepest level - break; + break; } - } catch (error) { - // Handle potential cross-origin security restrictions + } catch (error) { console.warn('Cannot access iframe content:', error); break; + } } + } else { + // If not an iframe, check for shadow DOM + deepestElement = traverseShadowDOM(element); } + return deepestElement; }; @@ -268,7 +311,9 @@ export const getElementInformation = async ( const frameElement = ownerDocument?.defaultView?.frameElement as HTMLIFrameElement; const isIframeContent = Boolean(frameElement); - // Build the element information object + const containingShadowRoot = element.getRootNode() as ShadowRoot; + const isShadowRoot = containingShadowRoot instanceof ShadowRoot; + let info: { tagName: string; hasOnlyText?: boolean; @@ -282,9 +327,13 @@ export const getElementInformation = async ( iframeURL?: string; iframeIndex?: number; frameHierarchy?: string[]; + isShadowRoot?: boolean; + shadowRootMode?: string; + shadowRootContent?: string; } = { - tagName: element?.tagName ?? '', - isIframeContent: isIframeContent + tagName: element?.tagName ?? '', + isIframeContent, + isShadowRoot }; if (isIframeContent) { @@ -312,6 +361,12 @@ export const getElementInformation = async ( info.frameHierarchy = frameHierarchy; info.iframeIndex = frameIndex - 1; // Adjust for 0-based index + }; + + if (isShadowRoot) { + // Include shadow root specific information + info.shadowRootMode = containingShadowRoot.mode; + info.shadowRootContent = containingShadowRoot.innerHTML; } if (element) { @@ -357,15 +412,6 @@ export const getElementInformation = async ( } }; -/** - * Returns a {@link Rectangle} object representing - * the coordinates, width, height and corner points of the element. - * If an element is not found, returns null. - * @param page The page instance. - * @param coordinates Coordinates of an element. - * @category WorkflowManagement-Selectors - * @returns {Promise} - */ export const getRect = async (page: Page, coordinates: Coordinates, listSelector: string, getList: boolean) => { try { if (!getList || listSelector !== '') { @@ -377,44 +423,63 @@ export const getRect = async (page: Page, coordinates: Coordinates, listSelector let element = document.elementFromPoint(x, y) as HTMLElement; if (!element) return null; - // If it's not an iframe, return the element as is - if (element.tagName !== 'IFRAME') return element; - - // Initialize tracking variables for iframe traversal - let currentIframe = element as HTMLIFrameElement; + // Track the deepest element found let deepestElement = element; - // Continue traversing while we have a valid iframe - while (currentIframe) { - try { - // Convert coordinates from main document space to iframe's local space - const iframeRect = currentIframe.getBoundingClientRect(); - const iframeX = x - iframeRect.left; - const iframeY = y - iframeRect.top; + // Function to traverse shadow DOM + const traverseShadowDOM = (element: HTMLElement): HTMLElement => { + let current = element; + let shadowRoot = current.shadowRoot; + let deepest = current; - // Get the iframe's document object - const iframeDocument = currentIframe.contentDocument || currentIframe.contentWindow?.document; - if (!iframeDocument) break; + while (shadowRoot) { + const shadowElement = shadowRoot.elementFromPoint(x, y) as HTMLElement; + if (!shadowElement || shadowElement === current) break; + + deepest = shadowElement; + current = shadowElement; + shadowRoot = current.shadowRoot; + } - // Find the element at the transformed coordinates within the iframe - const iframeElement = iframeDocument.elementFromPoint(iframeX, iframeY) as HTMLElement; - if (!iframeElement) break; + return deepest; + }; - // Update our tracking of the deepest element - deepestElement = iframeElement; + // Handle iframe traversal + if (element.tagName === 'IFRAME') { + let currentIframe = element as HTMLIFrameElement; - // If we found another iframe, continue traversing through it - if (iframeElement.tagName === 'IFRAME') { - currentIframe = iframeElement as HTMLIFrameElement; - } else { + while (currentIframe) { + try { + // Convert coordinates to iframe's local space + const iframeRect = currentIframe.getBoundingClientRect(); + const iframeX = x - iframeRect.left; + const iframeY = y - iframeRect.top; + + const iframeDocument = currentIframe.contentDocument || currentIframe.contentWindow?.document; + if (!iframeDocument) break; + + const iframeElement = iframeDocument.elementFromPoint(iframeX, iframeY) as HTMLElement; + if (!iframeElement) break; + + // Update deepest element and check for shadow DOM + deepestElement = traverseShadowDOM(iframeElement); + + // Continue traversing if we found another iframe + if (iframeElement.tagName === 'IFRAME') { + currentIframe = iframeElement as HTMLIFrameElement; + } else { + break; + } + } catch (error) { + console.warn('Cannot access iframe content:', error); break; } - } catch (error) { - // Handle potential cross-origin security restrictions - console.warn('Cannot access iframe content:', error); - break; } + } else { + // If not an iframe, check for shadow DOM + deepestElement = traverseShadowDOM(element); } + return deepestElement; }; @@ -481,50 +546,68 @@ export const getRect = async (page: Page, coordinates: Coordinates, listSelector } else { const rect = await page.evaluate( async ({ x, y }) => { - // Same getDeepestElementFromPoint function as above const getDeepestElementFromPoint = (x: number, y: number): HTMLElement | null => { // First, get the element at the clicked coordinates in the main document let element = document.elementFromPoint(x, y) as HTMLElement; if (!element) return null; - // If it's not an iframe, return the element as is - if (element.tagName !== 'IFRAME') return element; - - // Initialize tracking variables for iframe traversal - let currentIframe = element as HTMLIFrameElement; + // Track the deepest element found let deepestElement = element; - // Continue traversing while we have a valid iframe - while (currentIframe) { - try { - // Convert coordinates from main document space to iframe's local space - const iframeRect = currentIframe.getBoundingClientRect(); - const iframeX = x - iframeRect.left; - const iframeY = y - iframeRect.top; + // Function to traverse shadow DOM + const traverseShadowDOM = (element: HTMLElement): HTMLElement => { + let current = element; + let shadowRoot = current.shadowRoot; + let deepest = current; - // Get the iframe's document object - const iframeDocument = currentIframe.contentDocument || currentIframe.contentWindow?.document; - if (!iframeDocument) break; + while (shadowRoot) { + const shadowElement = shadowRoot.elementFromPoint(x, y) as HTMLElement; + if (!shadowElement || shadowElement === current) break; + + deepest = shadowElement; + current = shadowElement; + shadowRoot = current.shadowRoot; + } - // Find the element at the transformed coordinates within the iframe - const iframeElement = iframeDocument.elementFromPoint(iframeX, iframeY) as HTMLElement; - if (!iframeElement) break; + return deepest; + }; - // Update our tracking of the deepest element - deepestElement = iframeElement; + // Handle iframe traversal + if (element.tagName === 'IFRAME') { + let currentIframe = element as HTMLIFrameElement; - // If we found another iframe, continue traversing through it - if (iframeElement.tagName === 'IFRAME') { - currentIframe = iframeElement as HTMLIFrameElement; - } else { + while (currentIframe) { + try { + // Convert coordinates to iframe's local space + const iframeRect = currentIframe.getBoundingClientRect(); + const iframeX = x - iframeRect.left; + const iframeY = y - iframeRect.top; + + const iframeDocument = currentIframe.contentDocument || currentIframe.contentWindow?.document; + if (!iframeDocument) break; + + const iframeElement = iframeDocument.elementFromPoint(iframeX, iframeY) as HTMLElement; + if (!iframeElement) break; + + // Update deepest element and check for shadow DOM + deepestElement = traverseShadowDOM(iframeElement); + + // Continue traversing if we found another iframe + if (iframeElement.tagName === 'IFRAME') { + currentIframe = iframeElement as HTMLIFrameElement; + } else { + break; + } + } catch (error) { + console.warn('Cannot access iframe content:', error); break; } - } catch (error) { - // Handle potential cross-origin security restrictions - console.warn('Cannot access iframe content:', error); - break; } + } else { + // If not an iframe, check for shadow DOM + deepestElement = traverseShadowDOM(element); } + return deepestElement; }; @@ -637,7 +720,6 @@ export const getRect = async (page: Page, coordinates: Coordinates, listSelector } }; - /** * Returns the best and unique css {@link Selectors} for the element on the page. * Internally uses a finder function from https://github.com/antonmedv/finder/blob/master/finder.ts @@ -1109,48 +1191,76 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { } const getDeepestElementFromPoint = (x: number, y: number): HTMLElement | null => { - // First, get the element at the specified coordinates in the main document + // Helper function to traverse shadow DOM + const traverseShadowDOM = (element: HTMLElement, depth: number = 0): HTMLElement => { + const MAX_SHADOW_DEPTH = 4; + let current = element; + let deepest = current; + + while (current && depth < MAX_SHADOW_DEPTH) { + const shadowRoot = current.shadowRoot; + if (!shadowRoot) break; + + const shadowElement = shadowRoot.elementFromPoint(x, y) as HTMLElement; + if (!shadowElement || shadowElement === current) break; + + deepest = shadowElement; + current = shadowElement; + depth++; + } + + return deepest; + }; + + // Start with the element at the specified coordinates let element = document.elementFromPoint(x, y) as HTMLElement; if (!element) return null; - - // Check if the element is an iframe - if (element.tagName !== 'IFRAME') return element; - - let currentIframe = element as HTMLIFrameElement; + + // Initialize tracking variables let deepestElement = element; let depth = 0; - const MAX_DEPTH = 4; // Limit the depth of nested iframes to prevent infinite loops - - while (currentIframe && depth < MAX_DEPTH) { + const MAX_IFRAME_DEPTH = 4; + + // First check if the initial element has a shadow root + deepestElement = traverseShadowDOM(element); + + // If it's an iframe, traverse through iframe hierarchy + if (deepestElement.tagName === 'IFRAME') { + let currentIframe = deepestElement as HTMLIFrameElement; + + while (currentIframe && depth < MAX_IFRAME_DEPTH) { try { - // Convert coordinates from main document to iframe's coordinate system - const iframeRect = currentIframe.getBoundingClientRect(); - const iframeX = x - iframeRect.left; - const iframeY = y - iframeRect.top; - - // Access the iframe's content document and get the element at the transformed coordinates - const iframeDoc = currentIframe.contentDocument || currentIframe.contentWindow?.document; - if (!iframeDoc) break; - - const iframeElement = iframeDoc.elementFromPoint(iframeX, iframeY) as HTMLElement; - if (!iframeElement) break; - - // If the element found is another iframe, continue traversing - if (iframeElement.tagName === 'IFRAME') { - deepestElement = iframeElement; - currentIframe = iframeElement as HTMLIFrameElement; - depth++; - } else { - // If it's not an iframe, we've found our deepest element - deepestElement = iframeElement; - break; - } - } catch (error) { - // Handle potential security errors when accessing cross-origin iframes - console.warn('Cannot access iframe content:', error); + // Convert coordinates to iframe's local space + const iframeRect = currentIframe.getBoundingClientRect(); + const iframeX = x - iframeRect.left; + const iframeY = y - iframeRect.top; + + // Access iframe's document + const iframeDoc = currentIframe.contentDocument || currentIframe.contentWindow?.document; + if (!iframeDoc) break; + + // Get element at transformed coordinates in iframe + const iframeElement = iframeDoc.elementFromPoint(iframeX, iframeY) as HTMLElement; + if (!iframeElement) break; + + // Check for shadow DOM within iframe + const shadowResult = traverseShadowDOM(iframeElement); + deepestElement = shadowResult; + + // If we found another iframe, continue traversing + if (shadowResult.tagName === 'IFRAME') { + currentIframe = shadowResult as HTMLIFrameElement; + depth++; + } else { break; + } + } catch (error) { + console.warn('Cannot access iframe content:', error); + break; } + } } + return deepestElement; }; @@ -1220,6 +1330,66 @@ 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; + } + } + return path; + }; + + const shadowPath = getShadowPath(element); + if (shadowPath.length === 0) return null; + + try { + 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, { + root: context.root as unknown as Element + }); + selectorParts.push(`${hostSelector} >> ${elementSelector}`); + } else { + selectorParts.push(hostSelector); + } + }); + + 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; @@ -1239,7 +1409,9 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { } catch (e) { } + const iframeSelector = genSelectorForIframe(element); + const shadowSelector = genSelectorForShadowDOM(element); const hrefSelector = genSelectorForAttributes(element, ['href']); const formSelector = genSelectorForAttributes(element, [ @@ -1290,10 +1462,16 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { iframeSelector: iframeSelector ? { full: iframeSelector.fullSelector, isIframe: iframeSelector.isFrameContent, + } : null, + shadowSelector: shadowSelector ? { + full: shadowSelector.fullSelector, + mode: shadowSelector.mode } : null }; } + + function genAttributeSet(element: HTMLElement, attributes: string[]) { return new Set( attributes.filter((attr) => { @@ -1334,6 +1512,7 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { } const hoveredElement = getDeepestElementFromPoint(x, y) as HTMLElement; + if ( hoveredElement != null && !hoveredElement.closest('#overlay-controls') != null @@ -1368,61 +1547,83 @@ interface SelectorResult { */ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates, listSelector: string): Promise => { - interface IframeContext { - frame: HTMLIFrameElement; - document: Document; + 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 }) => { const getDeepestElementFromPoint = (x: number, y: number): HTMLElement | null => { - // First, get the element at the specified coordinates in the main document + // Function to traverse shadow DOM + const traverseShadowDOM = (element: HTMLElement): HTMLElement => { + let current = element; + let deepest = current; + let shadowRoot = current.shadowRoot; + + while (shadowRoot) { + const shadowElement = shadowRoot.elementFromPoint(x, y) as HTMLElement; + if (!shadowElement || shadowElement === current) break; + + deepest = shadowElement; + current = shadowElement; + shadowRoot = current.shadowRoot; + } + + return deepest; + }; + + // Start with the element at coordinates let element = document.elementFromPoint(x, y) as HTMLElement; if (!element) return null; - - // Check if the element is an iframe - if (element.tagName !== 'IFRAME') return element; - - let currentIframe = element as HTMLIFrameElement; + let deepestElement = element; let depth = 0; - const MAX_DEPTH = 4; // Limit the depth of nested iframes to prevent infinite loops - - while (currentIframe && depth < MAX_DEPTH) { - try { - // Convert coordinates from main document to iframe's coordinate system - const iframeRect = currentIframe.getBoundingClientRect(); - const iframeX = x - iframeRect.left; - const iframeY = y - iframeRect.top; - - // Access the iframe's content document and get the element at the transformed coordinates - const iframeDoc = currentIframe.contentDocument || currentIframe.contentWindow?.document; - if (!iframeDoc) break; - - const iframeElement = iframeDoc.elementFromPoint(iframeX, iframeY) as HTMLElement; - if (!iframeElement) break; - - // If the element found is another iframe, continue traversing - if (iframeElement.tagName === 'IFRAME') { - deepestElement = iframeElement; - currentIframe = iframeElement as HTMLIFrameElement; - depth++; - } else { - // If it's not an iframe, we've found our deepest element - deepestElement = iframeElement; + const MAX_DEPTH = 4; + + // Handle iframe traversal + if (element.tagName === 'IFRAME') { + let currentIframe = element as HTMLIFrameElement; + + while (currentIframe && depth < MAX_DEPTH) { + try { + const iframeRect = currentIframe.getBoundingClientRect(); + const iframeX = x - iframeRect.left; + const iframeY = y - iframeRect.top; + + const iframeDoc = currentIframe.contentDocument || currentIframe.contentWindow?.document; + if (!iframeDoc) break; + + const iframeElement = iframeDoc.elementFromPoint(iframeX, iframeY) as HTMLElement; + if (!iframeElement) break; + + // Check for shadow DOM within iframe + deepestElement = traverseShadowDOM(iframeElement); + + if (deepestElement.tagName === 'IFRAME') { + currentIframe = deepestElement as HTMLIFrameElement; + depth++; + } else { + break; + } + } catch (error) { + console.warn('Cannot access iframe content:', error); break; } - } catch (error) { - // Handle potential security errors when accessing cross-origin iframes - console.warn('Cannot access iframe content:', error); - break; } + } else { + // If not an iframe, check for shadow DOM + deepestElement = traverseShadowDOM(element); } + return deepestElement; }; + // Basic selector generation function getNonUniqueSelector(element: HTMLElement): string { let selector = element.tagName.toLowerCase(); @@ -1446,76 +1647,94 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates return selector; } - function getIframePath(element: HTMLElement): IframeContext[] { - const path: IframeContext[] = []; + + function getContextPath(element: HTMLElement): DOMContext[] { + const path: DOMContext[] = []; let current = element; let depth = 0; const MAX_DEPTH = 4; while (current && depth < MAX_DEPTH) { - // Get the owning document and its frame element + // Check for shadow DOM + const rootNode = current.getRootNode(); + if (rootNode instanceof ShadowRoot) { + path.unshift({ + type: 'shadow', + element: current, + container: rootNode, + host: rootNode.host as HTMLElement + }); + current = rootNode.host as HTMLElement; + depth++; + continue; + } + + // Check for iframe const ownerDocument = current.ownerDocument; const frameElement = ownerDocument?.defaultView?.frameElement as HTMLIFrameElement; if (frameElement) { path.unshift({ - frame: frameElement, - document: ownerDocument, - element: current + type: 'iframe', + element: current, + container: frameElement, + document: ownerDocument }); current = frameElement; depth++; - } else { - break; + continue; } + + break; } + return path; } function getSelectorPath(element: HTMLElement | null): string { if (!element) return ''; - // Check for iframe path first - const iframePath = getIframePath(element); - if (iframePath.length > 0) { + // Get the complete context path + const contextPath = getContextPath(element); + if (contextPath.length > 0) { const selectorParts: string[] = []; - // Build complete iframe path - iframePath.forEach((context, index) => { - const frameSelector = getNonUniqueSelector(context.frame); + contextPath.forEach((context, index) => { + const containerSelector = getNonUniqueSelector( + context.type === 'shadow' ? context.host! : context.container as HTMLElement + ); - if (index === iframePath.length - 1) { - // For deepest iframe context, include target element + if (index === contextPath.length - 1) { const elementSelector = getNonUniqueSelector(element); - selectorParts.push(`${frameSelector} :>> ${elementSelector}`); + const delimiter = context.type === 'shadow' ? ' >> ' : ' :>> '; + selectorParts.push(`${containerSelector}${delimiter}${elementSelector}`); } else { - // For intermediate iframe boundaries - selectorParts.push(frameSelector); + selectorParts.push(containerSelector); } }); - return selectorParts.join(' :>> '); + return selectorParts.join(contextPath[0].type === 'shadow' ? ' >> ' : ' :>> '); } - // Regular DOM path generation remains the same + // Regular DOM path generation const path: string[] = []; let currentElement = element; - let depth = 0; const MAX_DEPTH = 2; + let depth = 0; while (currentElement && currentElement !== document.body && depth < MAX_DEPTH) { const selector = getNonUniqueSelector(currentElement); path.unshift(selector); - const parentElement = currentElement.parentElement; - if (!parentElement) break; - currentElement = parentElement; + if (!currentElement.parentElement) break; + currentElement = currentElement.parentElement; depth++; } return path.join(' > '); } + // Main logic to get element and generate selector const originalEl = getDeepestElementFromPoint(x, y); if (!originalEl) return null; @@ -1562,62 +1781,90 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates } } } - // } const generalSelector = getSelectorPath(element); - return { - generalSelector, - }; + return { generalSelector }; }, coordinates); + return selectors || { generalSelector: '' }; } else { + // When we have a list selector, we need special handling while maintaining shadow DOM support const selectors = await page.evaluate(({ x, y }: { x: number, y: number }) => { const getDeepestElementFromPoint = (x: number, y: number): HTMLElement | null => { - // First, get the element at the specified coordinates in the main document + // Helper function to traverse shadow DOM + const traverseShadowDOM = (element: HTMLElement, depth: number = 0): HTMLElement => { + const MAX_SHADOW_DEPTH = 4; + let current = element; + let deepest = current; + + while (current && depth < MAX_SHADOW_DEPTH) { + const shadowRoot = current.shadowRoot; + if (!shadowRoot) break; + + const shadowElement = shadowRoot.elementFromPoint(x, y) as HTMLElement; + if (!shadowElement || shadowElement === current) break; + + deepest = shadowElement; + current = shadowElement; + depth++; + } + + return deepest; + }; + + // Start with the element at the specified coordinates let element = document.elementFromPoint(x, y) as HTMLElement; if (!element) return null; - - // Check if the element is an iframe - if (element.tagName !== 'IFRAME') return element; - - let currentIframe = element as HTMLIFrameElement; + + // Initialize tracking variables let deepestElement = element; let depth = 0; - const MAX_DEPTH = 4; // Limit the depth of nested iframes to prevent infinite loops - - while (currentIframe && depth < MAX_DEPTH) { - try { - // Convert coordinates from main document to iframe's coordinate system - const iframeRect = currentIframe.getBoundingClientRect(); - const iframeX = x - iframeRect.left; - const iframeY = y - iframeRect.top; - - // Access the iframe's content document and get the element at the transformed coordinates - const iframeDoc = currentIframe.contentDocument || currentIframe.contentWindow?.document; - if (!iframeDoc) break; - - const iframeElement = iframeDoc.elementFromPoint(iframeX, iframeY) as HTMLElement; - if (!iframeElement) break; - - // If the element found is another iframe, continue traversing - if (iframeElement.tagName === 'IFRAME') { - deepestElement = iframeElement; - currentIframe = iframeElement as HTMLIFrameElement; - depth++; - } else { - // If it's not an iframe, we've found our deepest element - deepestElement = iframeElement; + const MAX_IFRAME_DEPTH = 4; + + // First check if the initial element has a shadow root + deepestElement = traverseShadowDOM(element); + + // If it's an iframe, traverse through iframe hierarchy + if (deepestElement.tagName === 'IFRAME') { + let currentIframe = deepestElement as HTMLIFrameElement; + + while (currentIframe && depth < MAX_IFRAME_DEPTH) { + try { + // Convert coordinates to iframe's local space + const iframeRect = currentIframe.getBoundingClientRect(); + const iframeX = x - iframeRect.left; + const iframeY = y - iframeRect.top; + + // Access iframe's document + const iframeDoc = currentIframe.contentDocument || currentIframe.contentWindow?.document; + if (!iframeDoc) break; + + // Get element at transformed coordinates in iframe + const iframeElement = iframeDoc.elementFromPoint(iframeX, iframeY) as HTMLElement; + if (!iframeElement) break; + + // Check for shadow DOM within iframe + const shadowResult = traverseShadowDOM(iframeElement); + deepestElement = shadowResult; + + // If we found another iframe, continue traversing + if (shadowResult.tagName === 'IFRAME') { + currentIframe = shadowResult as HTMLIFrameElement; + depth++; + } else { + break; + } + } catch (error) { + console.warn('Cannot access iframe content:', error); break; } - } catch (error) { - // Handle potential security errors when accessing cross-origin iframes - console.warn('Cannot access iframe content:', error); - break; } } + return deepestElement; }; + // Generate basic selector from element's tag and classes function getNonUniqueSelector(element: HTMLElement): string { let selector = element.tagName.toLowerCase(); @@ -1628,9 +1875,9 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates } if (element.className) { - const classes = element.className.split(/\s+/).filter((cls: string) => Boolean(cls)); + const classes = element.className.split(/\s+/).filter(Boolean); if (classes.length > 0) { - const validClasses = classes.filter((cls: string) => !cls.startsWith('!') && !cls.includes(':')); + const validClasses = classes.filter(cls => !cls.startsWith('!') && !cls.includes(':')); if (validClasses.length > 0) { selector += '.' + validClasses.map(cls => CSS.escape(cls)).join('.'); } @@ -1640,70 +1887,87 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates return selector; } - function getIframePath(element: HTMLElement): IframeContext[] { - const path: IframeContext[] = []; + // Get complete context path (both iframe and shadow DOM) + function getContextPath(element: HTMLElement): DOMContext[] { + const path: DOMContext[] = []; let current = element; let depth = 0; const MAX_DEPTH = 4; while (current && depth < MAX_DEPTH) { - // Get the owning document and its frame element + // Check for shadow DOM + const rootNode = current.getRootNode(); + if (rootNode instanceof ShadowRoot) { + path.unshift({ + type: 'shadow', + element: current, + container: rootNode, + host: rootNode.host as HTMLElement + }); + current = rootNode.host as HTMLElement; + depth++; + continue; + } + + // Check for iframe const ownerDocument = current.ownerDocument; const frameElement = ownerDocument?.defaultView?.frameElement as HTMLIFrameElement; if (frameElement) { path.unshift({ - frame: frameElement, - document: ownerDocument, - element: current + type: 'iframe', + element: current, + container: frameElement, + document: ownerDocument }); current = frameElement; depth++; - } else { - break; + continue; } + + break; } + return path; } function getSelectorPath(element: HTMLElement | null): string { if (!element) return ''; - // Check for iframe path first - const iframePath = getIframePath(element); - if (iframePath.length > 0) { + // Get the complete context path + const contextPath = getContextPath(element); + if (contextPath.length > 0) { const selectorParts: string[] = []; - // Build complete iframe path - iframePath.forEach((context, index) => { - const frameSelector = getNonUniqueSelector(context.frame); + contextPath.forEach((context, index) => { + const containerSelector = getNonUniqueSelector( + context.type === 'shadow' ? context.host! : context.container as HTMLElement + ); - if (index === iframePath.length - 1) { - // For deepest iframe context, include target element + if (index === contextPath.length - 1) { const elementSelector = getNonUniqueSelector(element); - selectorParts.push(`${frameSelector} :>> ${elementSelector}`); + const delimiter = context.type === 'shadow' ? ' >> ' : ' :>> '; + selectorParts.push(`${containerSelector}${delimiter}${elementSelector}`); } else { - // For intermediate iframe boundaries - selectorParts.push(frameSelector); + selectorParts.push(containerSelector); } }); - return selectorParts.join(' :>> '); + return selectorParts.join(contextPath[0].type === 'shadow' ? ' >> ' : ' :>> '); } - // Regular DOM path generation remains the same + // Regular DOM path generation const path: string[] = []; let currentElement = element; - let depth = 0; const MAX_DEPTH = 2; + let depth = 0; while (currentElement && currentElement !== document.body && depth < MAX_DEPTH) { const selector = getNonUniqueSelector(currentElement); path.unshift(selector); - const parentElement = currentElement.parentElement; - if (!parentElement) break; - currentElement = parentElement; + if (!currentElement.parentElement) break; + currentElement = currentElement.parentElement; depth++; } @@ -1711,18 +1975,16 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates } const originalEl = getDeepestElementFromPoint(x, y); - if (!originalEl) return null; + if (!originalEl) return { generalSelector: '' }; let element = originalEl; const generalSelector = getSelectorPath(element); - return { - generalSelector, - }; - }, coordinates); - return selectors || { generalSelector: '' }; - } + return { generalSelector }; + }, coordinates); + return selectors || { generalSelector: '' }; + } } catch (error) { console.error('Error in getNonUniqueSelectors:', error); return { generalSelector: '' }; @@ -1760,33 +2022,46 @@ export const getChildSelectors = async (page: Page, parentSelector: string): Pro function getSelectorPath(element: HTMLElement): string { if (!element || !element.parentElement) return ''; - const parentSelector = getNonUniqueSelector(element.parentElement); const elementSelector = getNonUniqueSelector(element); + + // Check for shadow DOM context + const rootNode = element.getRootNode(); + if (rootNode instanceof ShadowRoot) { + const hostSelector = getNonUniqueSelector(rootNode.host as HTMLElement); + return `${hostSelector} >> ${elementSelector}`; + } - // Check if element is in an iframe + // Check for iframe context const ownerDocument = element.ownerDocument; const frameElement = ownerDocument?.defaultView?.frameElement as HTMLIFrameElement; - if (frameElement) { const frameSelector = getNonUniqueSelector(frameElement); return `${frameSelector} :>> ${elementSelector}`; } + // Regular DOM context + const parentSelector = getNonUniqueSelector(element.parentElement); return `${parentSelector} > ${elementSelector}`; } - function getIframeChildren(element: HTMLElement): HTMLElement[] { + + // Function to get all children from special contexts + function getSpecialContextChildren(element: HTMLElement): HTMLElement[] { const children: HTMLElement[] = []; - // Find all iframe elements - const iframes = Array.from(element.querySelectorAll('iframe')) as HTMLIFrameElement[]; + // Get shadow DOM children + const shadowRoot = element.shadowRoot; + if (shadowRoot) { + const shadowElements = Array.from(shadowRoot.querySelectorAll('*')) as HTMLElement[]; + children.push(...shadowElements); + } + // Get iframe children + const iframes = Array.from(element.querySelectorAll('iframe')) as HTMLIFrameElement[]; for (const iframe of iframes) { try { - // Access iframe's document const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document; if (iframeDoc) { - // Get all elements in the iframe const iframeElements = Array.from(iframeDoc.querySelectorAll('*')) as HTMLElement[]; children.push(...iframeElements); } @@ -1799,7 +2074,7 @@ export const getChildSelectors = async (page: Page, parentSelector: string): Pro return children; } - // Function to recursively get all descendant selectors + // Function to recursively get all descendant selectors including shadow DOM and iframes function getAllDescendantSelectors(element: HTMLElement): string[] { let selectors: string[] = []; @@ -1809,63 +2084,68 @@ export const getChildSelectors = async (page: Page, parentSelector: string): Pro const childPath = getSelectorPath(child); if (childPath) { selectors.push(childPath); - // Recursively process regular DOM descendants + + // Process regular descendants selectors = selectors.concat(getAllDescendantSelectors(child)); - // Check for iframes in this child - const iframeChildren = getIframeChildren(child); - for (const iframeChild of iframeChildren) { - try { - const iframePath = getSelectorPath(iframeChild); - if (iframePath) { - selectors.push(iframePath); - // Recursively process iframe descendants - selectors = selectors.concat(getAllDescendantSelectors(iframeChild)); - } - } catch (error) { - console.warn('Error processing iframe child:', error); - continue; + // Process special context children (shadow DOM and iframes) + const specialChildren = getSpecialContextChildren(child); + for (const specialChild of specialChildren) { + const specialPath = getSelectorPath(specialChild); + if (specialPath) { + selectors.push(specialPath); + selectors = selectors.concat(getAllDescendantSelectors(specialChild)); } } } } - // Handle direct iframe children of the current element - const iframeChildren = getIframeChildren(element); - for (const iframeChild of iframeChildren) { - try { - const iframePath = getSelectorPath(iframeChild); - if (iframePath) { - selectors.push(iframePath); - selectors = selectors.concat(getAllDescendantSelectors(iframeChild)); - } - } catch (error) { - console.warn('Error processing direct iframe child:', error); - continue; + // Handle direct special context children + const specialChildren = getSpecialContextChildren(element); + for (const specialChild of specialChildren) { + const specialPath = getSelectorPath(specialChild); + if (specialPath) { + selectors.push(specialPath); + selectors = selectors.concat(getAllDescendantSelectors(specialChild)); } } return selectors; } - const selectorParts = parentSelector.split(':>>').map(part => part.trim()); + // Handle both shadow DOM and iframe parent selectors let parentElements: HTMLElement[] = []; - - // Handle iframe traversal if needed - if (selectorParts.length > 1) { - // Start with the initial iframe elements + + // Check for special context traversal in parent selector + if (parentSelector.includes('>>') || parentSelector.includes(':>>')) { + // Split by both types of delimiters + const selectorParts = parentSelector.split(/(?:>>|:>>)/).map(part => part.trim()); + + // Start with initial elements parentElements = Array.from(document.querySelectorAll(selectorParts[0])) as HTMLElement[]; - // Traverse through iframe parts + // Traverse through parts for (let i = 1; i < selectorParts.length; i++) { const newParentElements: HTMLElement[] = []; + for (const element of parentElements) { + // Check for shadow DOM + if (element.shadowRoot) { + const shadowChildren = Array.from( + element.shadowRoot.querySelectorAll(selectorParts[i]) + ) as HTMLElement[]; + newParentElements.push(...shadowChildren); + } + + // Check for iframe if (element.tagName === 'IFRAME') { try { const iframeDoc = (element as HTMLIFrameElement).contentDocument || (element as HTMLIFrameElement).contentWindow?.document; if (iframeDoc) { - const iframeChildren = Array.from(iframeDoc.querySelectorAll(selectorParts[i])) as HTMLElement[]; + const iframeChildren = Array.from( + iframeDoc.querySelectorAll(selectorParts[i]) + ) as HTMLElement[]; newParentElements.push(...iframeChildren); } } catch (error) { @@ -1874,6 +2154,7 @@ export const getChildSelectors = async (page: Page, parentSelector: string): Pro } } } + parentElements = newParentElements; } } else { @@ -1886,10 +2167,10 @@ export const getChildSelectors = async (page: Page, parentSelector: string): Pro // Process each parent element and its descendants parentElements.forEach((parentElement) => { const descendantSelectors = getAllDescendantSelectors(parentElement); - descendantSelectors.forEach((selector) => allChildSelectors.add(selector)); // Add selectors to the set + descendantSelectors.forEach((selector) => allChildSelectors.add(selector)); }); - return Array.from(allChildSelectors); // Convert the set back to an array + return Array.from(allChildSelectors); }, parentSelector); return childSelectors || []; diff --git a/server/src/workflow-management/utils.ts b/server/src/workflow-management/utils.ts index 53006c78..03d07211 100644 --- a/server/src/workflow-management/utils.ts +++ b/server/src/workflow-management/utils.ts @@ -13,8 +13,12 @@ export const getBestSelectorForAction = (action: Action) => { case ActionType.DragAndDrop: { const selectors = action.selectors; + 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 @@ -80,6 +84,11 @@ export const getBestSelectorForAction = (action: Action) => { case ActionType.Input: case ActionType.Keydown: { const selectors = action.selectors; + + if (selectors?.shadowSelector?.full) { + return selectors.shadowSelector.full; + } + return ( selectors.testIdSelector ?? selectors?.id ?? diff --git a/src/components/organisms/BrowserWindow.tsx b/src/components/organisms/BrowserWindow.tsx index 66d7798d..5a49a89f 100644 --- a/src/components/organisms/BrowserWindow.tsx +++ b/src/components/organisms/BrowserWindow.tsx @@ -9,11 +9,11 @@ import { useBrowserSteps, TextStep } from '../../context/browserSteps'; import { useGlobalInfoStore } from '../../context/globalInfo'; import { useTranslation } from 'react-i18next'; - interface ElementInfo { tagName: string; hasOnlyText?: boolean; isIframeContent?: boolean; + isShadowRoot?: boolean; innerText?: string; url?: string; imageUrl?: string; @@ -125,7 +125,7 @@ export const BrowserWindow = () => { if (listSelector) { socket?.emit('listSelector', { selector: listSelector }); const hasValidChildSelectors = Array.isArray(data.childSelectors) && data.childSelectors.length > 0; - + if (limitMode) { setHighlighterData(null); } else if (paginationMode) { @@ -159,18 +159,39 @@ export const BrowserWindow = () => { ) ); setHighlighterData(isValidMixedSelector ? data : null); - } else { - // If no valid child in normal mode, clear the highlighter + } else if (data.elementInfo?.isShadowRoot && data.childSelectors) { + // New case: Handle pure Shadow DOM elements + // Check if the selector matches any shadow root child selectors + const isShadowChild = data.childSelectors.some(childSelector => + data.selector.includes('>>') && // Shadow DOM uses >> for piercing + childSelector.split('>>').some(part => + data.selector.includes(part.trim()) + ) + ); + setHighlighterData(isShadowChild ? data : null); + } else if (data.selector.includes('>>') && hasValidChildSelectors) { + // New case: Handle mixed DOM cases + // Split the selector into parts and check each against child selectors + const selectorParts = data.selector.split('>>').map(part => part.trim()); + const isValidMixedSelector = selectorParts.some(part => + // Now we know data.childSelectors is defined + data.childSelectors!.some(childSelector => + childSelector.includes(part) + ) + ); + setHighlighterData(isValidMixedSelector ? data : null); + } else { + // if !valid child in normal mode, clear the highlighter setHighlighterData(null); - } - } else { + } + } else { // Set highlighterData for the initial listSelector selection setHighlighterData(data); - } - } else { + } + } else { // For non-list steps setHighlighterData(data); - } + } }, [highlighterData, getList, socket, listSelector, paginationMode, paginationType, captureStage]); @@ -219,6 +240,7 @@ export const BrowserWindow = () => { addTextStep('', data, { selector: highlighterData.selector, tag: highlighterData.elementInfo?.tagName, + shadow: highlighterData.elementInfo?.isShadowRoot, attribute }); } else { @@ -226,7 +248,7 @@ export const BrowserWindow = () => { setAttributeOptions(options); setSelectedElement({ selector: highlighterData.selector, - info: highlighterData.elementInfo + info: highlighterData.elementInfo, }); setShowAttributeModal(true); } @@ -263,6 +285,7 @@ export const BrowserWindow = () => { selectorObj: { selector: highlighterData.selector, tag: highlighterData.elementInfo?.tagName, + shadow: highlighterData.elementInfo?.isShadowRoot, attribute } }; @@ -310,6 +333,7 @@ export const BrowserWindow = () => { addTextStep('', data, { selector: selectedElement.selector, tag: selectedElement.info?.tagName, + shadow: selectedElement.info?.isShadowRoot, attribute: attribute }); } @@ -322,6 +346,7 @@ export const BrowserWindow = () => { selectorObj: { selector: selectedElement.selector, tag: selectedElement.info?.tagName, + shadow: selectedElement.info?.isShadowRoot, attribute: attribute } }; diff --git a/src/context/browserSteps.tsx b/src/context/browserSteps.tsx index dd211199..fd311a35 100644 --- a/src/context/browserSteps.tsx +++ b/src/context/browserSteps.tsx @@ -32,6 +32,7 @@ export interface SelectorObject { selector: string; tag?: string; attribute?: string; + shadow?: boolean; [key: string]: any; }