Merge branch 'develop' into iframe

This commit is contained in:
Rohit
2025-01-05 00:05:47 +05:30
committed by GitHub
8 changed files with 1151 additions and 673 deletions

View File

@@ -205,46 +205,62 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
}
function findAllElements(config) {
// Check if selector contains iframe notation (:>>)
if (!config.selector.includes(':>>')) {
// 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';
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) {
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;
}
}
@@ -291,14 +307,16 @@ 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);
// 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;
@@ -310,7 +328,6 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
});
}
// Main scraping logic remains the same
const seedName = getSeedKey(lists);
const seedElements = findAllElements(lists[seedName]);
const MBEs = getMBEs(seedElements);
@@ -364,60 +381,96 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
* @returns {Array.<Array.<Object>>} 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(':>>')) {
// 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());
const parts = selector.split(/(?:>>|:>>)/).map(part => part.trim());
let currentElement = rootElement;
for (let i = 0; i < parts.length; i++) {
if (!currentElement) return null;
// Handle iframe content document
// Handle iframe traversal
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);
console.warn('Cannot access iframe content:', e);
return null;
}
}
currentElement = currentElement.querySelector(parts[i]);
// Try regular DOM first
let nextElement = currentElement.querySelector(parts[i]);
// 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(':>>')) {
// 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());
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.error('Cannot access iframe content:', 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;
@@ -426,35 +479,52 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
return currentElements;
};
// Helper function to extract values from elements
// Enhanced value extraction with context awareness
function extractValue(element, 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, window.location.origin).href : null;
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) {
// 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 boundary crossing
// Handle iframe crossing
if (currentElement.tagName === 'IFRAME') {
try {
currentElement = currentElement.contentDocument.body;
@@ -471,6 +541,12 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
// Helper function to get cell index
function getCellIndex(td) {
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) {
@@ -481,14 +557,18 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
// Helper function to check for TH elements
function hasThElement(row, tableFields) {
for (const [label, { selector }] of Object.entries(tableFields)) {
const element = queryIframe(row, selector);
for (const [_, { selector }] of Object.entries(tableFields)) {
const element = queryElement(row, selector);
if (element) {
let current = element;
while (current && current !== row) {
if (current.tagName === 'TH') {
return true;
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;
@@ -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,24 +609,30 @@ 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
// Get elements from main document
allElements.push(...document.getElementsByTagName(baseElement.tagName));
// Add elements from each iframe
// 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.error('Cannot access iframe content:', e);
console.warn('Cannot access iframe content:', e);
}
}
@@ -554,8 +646,8 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
});
}
// 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 [];
@@ -580,7 +672,7 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
// Classify fields
containers.forEach((container, containerIndex) => {
for (const [label, field] of Object.entries(fields)) {
const sampleElement = queryIframe(container, field.selector);
const sampleElement = queryElement(container, field.selector);
if (sampleElement) {
const ancestor = findTableAncestor(sampleElement);
@@ -602,17 +694,23 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
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];
if (Object.keys(tableFields).length > 0) {
const firstField = Object.values(tableFields)[0];
const firstElement = queryIframe(container, firstField.selector);
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;
@@ -625,7 +723,27 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
}
if (tableContext) {
const rows = Array.from(tableContext.getElementsByTagName('TR'));
// 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++) {
@@ -636,17 +754,27 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
let element = null;
if (cellIndex >= 0) {
const td = currentRow.children[cellIndex];
if (td) {
element = queryIframe(td, selector);
// Get TD element considering both contexts
let td = currentRow.children[cellIndex];
if (!element && selector.split(">").pop().includes('td:nth-child')) {
// 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);
element = queryElement(td, tagOnlySelector);
}
if (!element) {
@@ -666,7 +794,7 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
}
}
} else {
element = queryIframe(currentRow, selector);
element = queryElement(currentRow, selector);
}
if (element) {
@@ -682,7 +810,7 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
}
}
// 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;
@@ -693,8 +821,9 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
const record = {};
for (const [label, { selector, attribute }] of Object.entries(nonTableFields)) {
const relativeSelector = selector.split(':>>').slice(-1)[0];
const element = queryIframe(container, relativeSelector);
// 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);

View File

@@ -403,7 +403,7 @@ export default class Interpreter extends EventEmitter {
await this.options.serializableCallback(scrapeResults);
},
scrapeSchema: async (schema: Record<string, { selector: string; tag: string, attribute: string; }>) => {
scrapeSchema: async (schema: Record<string, { selector: string; tag: string, attribute: string; shadow: string}>) => {
await this.ensureScriptsLoaded(page);
const scrapeResult = await page.evaluate((schemaObj) => window.scrapeSchema(schemaObj), schema);
@@ -666,14 +666,29 @@ export default class Interpreter extends EventEmitter {
}
}
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('>>'))
);
}
}
@@ -683,7 +698,7 @@ export default class Interpreter extends EventEmitter {
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 {

View File

@@ -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;
}
/**

View File

@@ -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 });
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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 ??

View File

@@ -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;
@@ -159,8 +159,29 @@ export const BrowserWindow = () => {
)
);
setHighlighterData(isValidMixedSelector ? data : null);
} 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 no valid child in normal mode, clear the highlighter
// if !valid child in normal mode, clear the highlighter
setHighlighterData(null);
}
} else {
@@ -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
}
};

View File

@@ -32,6 +32,7 @@ export interface SelectorObject {
selector: string;
tag?: string;
attribute?: string;
shadow?: boolean;
[key: string]: any;
}