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,50 +205,66 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
} }
function findAllElements(config) { function findAllElements(config) {
// Check if selector contains iframe notation (:>>) // Regular DOM query if no special delimiters
if (!config.selector.includes(':>>')) { if (!config.selector.includes('>>') && !config.selector.includes(':>>')) {
return Array.from(document.querySelectorAll(config.selector)); return Array.from(document.querySelectorAll(config.selector));
} }
// For iframe traversal, split by iframe boundary marker // Split by both types of delimiters
const parts = config.selector.split(':>>').map(s => s.trim()); const parts = config.selector.split(/(?:>>|:>>)/).map(s => s.trim());
const delimiters = config.selector.match(/(?:>>|:>>)/g) || [];
let currentElements = [document]; let currentElements = [document];
for (let i = 0; i < parts.length; i++) { for (let i = 0; i < parts.length; i++) {
const part = parts[i]; const part = parts[i];
const nextElements = []; const nextElements = [];
const isLast = i === parts.length - 1;
const delimiter = delimiters[i] || '';
const isIframeTraversal = delimiter === ':>>';
for (const element of currentElements) { for (const element of currentElements) {
try { try {
let targets; let targets;
if (i === 0) { if (i === 0) {
// First selector is queried from main document // First selector is queried from main document
targets = Array.from(element.querySelectorAll(part)) targets = Array.from(element.querySelectorAll(part))
.filter(el => { .filter(el => {
// Only include iframes if not the last part if (isLast) return true;
if (i === parts.length - 1) return true; // For iframe traversal, only include iframes
return el.tagName === 'IFRAME'; if (isIframeTraversal) return el.tagName === 'IFRAME';
}); // For shadow DOM traversal, only include elements with shadow root
return el.shadowRoot && el.shadowRoot.mode === 'open';
});
} else { } else {
// For subsequent selectors, we need to look inside iframes if (isIframeTraversal) {
// Handle iframe traversal
const iframeDocument = element.contentDocument || element.contentWindow?.document; const iframeDocument = element.contentDocument || element.contentWindow?.document;
if (!iframeDocument) continue; if (!iframeDocument) continue;
targets = Array.from(iframeDocument.querySelectorAll(part)); targets = Array.from(iframeDocument.querySelectorAll(part));
if (!isLast) {
// If this isn't the last part, filter for iframes only targets = targets.filter(el => el.tagName === 'IFRAME');
if (i < parts.length - 1) {
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); nextElements.push(...targets);
} catch (error) { } catch (error) {
// Handle cross-origin iframe access errors console.warn('Cannot access content:', error);
console.warn('Cannot access iframe content:', error);
continue; continue;
} }
} }
if (nextElements.length === 0) return []; if (nextElements.length === 0) return [];
currentElements = nextElements; currentElements = nextElements;
} }
@@ -291,26 +307,27 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
)[0]; )[0];
} }
// Find minimal bounding elements
function getMBEs(elements) { function getMBEs(elements) {
return elements.map((element) => { return elements.map((element) => {
let candidate = element; let candidate = element;
const isUniqueChild = (e) => elements const isUniqueChild = (e) => elements
.filter((elem) => { .filter((elem) => {
// Handle iframe boundaries when checking containment // Handle both iframe and shadow DOM boundaries
const sameDocument = elem.ownerDocument === e.ownerDocument; const sameContext = elem.getRootNode() === e.getRootNode() &&
return sameDocument && e.parentNode?.contains(elem); elem.ownerDocument === e.ownerDocument;
}) return sameContext && e.parentNode?.contains(elem);
.length === 1; })
.length === 1;
while (candidate && isUniqueChild(candidate)) {
candidate = candidate.parentNode; while (candidate && isUniqueChild(candidate)) {
} candidate = candidate.parentNode;
}
return candidate;
return candidate;
}); });
} }
// Main scraping logic remains the same
const seedName = getSeedKey(lists); const seedName = getSeedKey(lists);
const seedElements = findAllElements(lists[seedName]); const seedElements = findAllElements(lists[seedName]);
const MBEs = getMBEs(seedElements); const MBEs = getMBEs(seedElements);
@@ -364,144 +381,207 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
* @returns {Array.<Array.<Object>>} Array of arrays of scraped items, one sub-array per list * @returns {Array.<Array.<Object>>} Array of arrays of scraped items, one sub-array per list
*/ */
window.scrapeList = async function ({ listSelector, fields, limit = 10 }) { window.scrapeList = async function ({ listSelector, fields, limit = 10 }) {
// Helper function to query elements within an iframe // Enhanced query function to handle both iframe and shadow DOM
const queryIframe = (rootElement, selector) => { const queryElement = (rootElement, selector) => {
if (!selector.includes(':>>')) { if (!selector.includes('>>') && !selector.includes(':>>')) {
return rootElement.querySelector(selector); return rootElement.querySelector(selector);
} }
const parts = selector.split(':>>').map(part => part.trim()); const parts = selector.split(/(?:>>|:>>)/).map(part => part.trim());
let currentElement = rootElement; let currentElement = rootElement;
for (let i = 0; i < parts.length; i++) { for (let i = 0; i < parts.length; i++) {
if (!currentElement) return null; if (!currentElement) return null;
// Handle iframe content document // Handle iframe traversal
if (currentElement.tagName === 'IFRAME') { if (currentElement.tagName === 'IFRAME') {
try { try {
const iframeDoc = currentElement.contentDocument || currentElement.contentWindow.document; const iframeDoc = currentElement.contentDocument || currentElement.contentWindow.document;
currentElement = iframeDoc.querySelector(parts[i]); currentElement = iframeDoc.querySelector(parts[i]);
continue; continue;
} catch (e) { } catch (e) {
console.error('Cannot access iframe content:', e); console.warn('Cannot access iframe content:', e);
return null; 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 // Enhanced query all function for both contexts
const queryIframeAll = (rootElement, selector) => { const queryElementAll = (rootElement, selector) => {
if (!selector.includes(':>>')) { if (!selector.includes('>>') && !selector.includes(':>>')) {
return rootElement.querySelectorAll(selector); return rootElement.querySelectorAll(selector);
} }
const parts = selector.split(':>>').map(part => part.trim()); const parts = selector.split(/(?:>>|:>>)/).map(part => part.trim());
let currentElements = [rootElement]; let currentElements = [rootElement];
for (const part of parts) { for (const part of parts) {
const nextElements = []; const nextElements = [];
for (const element of currentElements) { for (const element of currentElements) {
if (element.tagName === 'IFRAME') { // Handle iframe traversal
try { if (element.tagName === 'IFRAME') {
const iframeDoc = element.contentDocument || element.contentWindow.document; try {
nextElements.push(...iframeDoc.querySelectorAll(part)); const iframeDoc = element.contentDocument || element.contentWindow.document;
} catch (e) { nextElements.push(...iframeDoc.querySelectorAll(part));
console.error('Cannot access iframe content:', e); } catch (e) {
continue; console.warn('Cannot access iframe content:', e);
} continue;
} else { }
nextElements.push(...element.querySelectorAll(part)); } else {
} // Regular DOM elements
} if (element.querySelectorAll) {
nextElements.push(...element.querySelectorAll(part));
currentElements = nextElements; }
}
// Shadow DOM elements
return currentElements; 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) { function extractValue(element, attribute) {
if (!element) return null; if (!element) return null;
if (attribute === 'innerText') { // Get context-aware base URL
return element.innerText.trim(); const baseURL = element.ownerDocument?.location?.href || window.location.origin;
} else if (attribute === 'innerHTML') {
return element.innerHTML.trim(); // Check shadow root first
} else if (attribute === 'src' || attribute === 'href') { if (element.shadowRoot) {
const attrValue = element.getAttribute(attribute); const shadowContent = element.shadowRoot.textContent;
return attrValue ? new URL(attrValue, window.location.origin).href : null; if (shadowContent?.trim()) {
} return shadowContent.trim();
return element.getAttribute(attribute); }
}
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) { function findTableAncestor(element) {
let currentElement = element; let currentElement = element;
const MAX_DEPTH = 5; const MAX_DEPTH = 5;
let depth = 0; let depth = 0;
while (currentElement && depth < MAX_DEPTH) { while (currentElement && depth < MAX_DEPTH) {
if (currentElement.tagName === 'TD') { // Handle shadow DOM
return { type: 'TD', element: currentElement }; if (currentElement.getRootNode() instanceof ShadowRoot) {
} else if (currentElement.tagName === 'TR') { currentElement = currentElement.getRootNode().host;
return { type: 'TR', element: currentElement }; continue;
} }
// Handle iframe boundary crossing if (currentElement.tagName === 'TD') {
if (currentElement.tagName === 'IFRAME') { return { type: 'TD', element: currentElement };
try { } else if (currentElement.tagName === 'TR') {
currentElement = currentElement.contentDocument.body; return { type: 'TR', element: currentElement };
} catch (e) { }
return null;
} // Handle iframe crossing
} else { if (currentElement.tagName === 'IFRAME') {
currentElement = currentElement.parentElement; try {
} currentElement = currentElement.contentDocument.body;
depth++; } catch (e) {
} return null;
return null; }
} else {
currentElement = currentElement.parentElement;
}
depth++;
}
return null;
} }
// Helper function to get cell index // Helper function to get cell index
function getCellIndex(td) { function getCellIndex(td) {
let index = 0; if (td.getRootNode() instanceof ShadowRoot) {
let sibling = td; const shadowRoot = td.getRootNode();
while (sibling = sibling.previousElementSibling) { const allCells = Array.from(shadowRoot.querySelectorAll('td'));
index++; return allCells.indexOf(td);
} }
return index;
let index = 0;
let sibling = td;
while (sibling = sibling.previousElementSibling) {
index++;
}
return index;
} }
// Helper function to check for TH elements // Helper function to check for TH elements
function hasThElement(row, tableFields) { function hasThElement(row, tableFields) {
for (const [label, { selector }] of Object.entries(tableFields)) { for (const [_, { selector }] of Object.entries(tableFields)) {
const element = queryIframe(row, selector); const element = queryElement(row, selector);
if (element) { if (element) {
let current = element; let current = element;
while (current && current !== row) { while (current && current !== row) {
if (current.tagName === 'TH') { if (current.getRootNode() instanceof ShadowRoot) {
return true; current = current.getRootNode().host;
} continue;
if (current.tagName === 'IFRAME') { }
try {
current = current.contentDocument.body; if (current.tagName === 'TH') return true;
} catch (e) {
break; if (current.tagName === 'IFRAME') {
} try {
} else { current = current.contentDocument.body;
current = current.parentElement; } catch (e) {
} break;
} }
} } else {
} current = current.parentElement;
return false; }
}
}
}
return false;
} }
// Helper function to filter rows // Helper function to filter rows
@@ -511,7 +591,13 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
return rows; 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 // Class similarity comparison functions
@@ -523,188 +609,231 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
return intersection.size / union.size; return intersection.size / union.size;
} }
// Enhanced similar elements finding with context support
function findSimilarElements(baseElement, similarityThreshold = 0.7) { function findSimilarElements(baseElement, similarityThreshold = 0.7) {
const baseClasses = Array.from(baseElement.classList); const baseClasses = Array.from(baseElement.classList);
if (baseClasses.length === 0) return []; if (baseClasses.length === 0) return [];
// Include elements from all iframes const allElements = [];
const allElements = [];
const iframes = document.getElementsByTagName('iframe'); // Get elements from main document
allElements.push(...document.getElementsByTagName(baseElement.tagName));
// Add elements from main document
allElements.push(...document.getElementsByTagName(baseElement.tagName)); // Get elements from shadow DOM
if (baseElement.getRootNode() instanceof ShadowRoot) {
// Add elements from each iframe const shadowHost = baseElement.getRootNode().host;
for (const iframe of iframes) { allElements.push(...shadowHost.getElementsByTagName(baseElement.tagName));
try { }
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
allElements.push(...iframeDoc.getElementsByTagName(baseElement.tagName)); // Get elements from iframes
} catch (e) { const iframes = document.getElementsByTagName('iframe');
console.error('Cannot access iframe content:', e); for (const iframe of iframes) {
} try {
} const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
allElements.push(...iframeDoc.getElementsByTagName(baseElement.tagName));
return allElements.filter(element => { } catch (e) {
if (element === baseElement) return false; console.warn('Cannot access iframe content:', e);
const similarity = calculateClassSimilarity( }
baseClasses, }
Array.from(element.classList)
); return allElements.filter(element => {
return similarity >= similarityThreshold; if (element === baseElement) return false;
}); const similarity = calculateClassSimilarity(
baseClasses,
Array.from(element.classList)
);
return similarity >= similarityThreshold;
});
} }
// Main scraping logic // Main scraping logic with context support
let containers = queryIframeAll(document, listSelector); let containers = queryElementAll(document, listSelector);
containers = Array.from(containers); containers = Array.from(containers);
if (containers.length === 0) return []; if (containers.length === 0) return [];
if (limit > 1 && containers.length === 1) { if (limit > 1 && containers.length === 1) {
const baseContainer = containers[0]; const baseContainer = containers[0];
const similarContainers = findSimilarElements(baseContainer); const similarContainers = findSimilarElements(baseContainer);
if (similarContainers.length > 0) { if (similarContainers.length > 0) {
const newContainers = similarContainers.filter(container => const newContainers = similarContainers.filter(container =>
!container.matches(listSelector) !container.matches(listSelector)
); );
containers = [...containers, ...newContainers]; containers = [...containers, ...newContainers];
} }
} }
const containerFields = containers.map(() => ({ const containerFields = containers.map(() => ({
tableFields: {}, tableFields: {},
nonTableFields: {} nonTableFields: {}
})); }));
// Classify fields // Classify fields
containers.forEach((container, containerIndex) => { containers.forEach((container, containerIndex) => {
for (const [label, field] of Object.entries(fields)) { for (const [label, field] of Object.entries(fields)) {
const sampleElement = queryIframe(container, field.selector); const sampleElement = queryElement(container, field.selector);
if (sampleElement) { if (sampleElement) {
const ancestor = findTableAncestor(sampleElement); const ancestor = findTableAncestor(sampleElement);
if (ancestor) { if (ancestor) {
containerFields[containerIndex].tableFields[label] = { containerFields[containerIndex].tableFields[label] = {
...field, ...field,
tableContext: ancestor.type, tableContext: ancestor.type,
cellIndex: ancestor.type === 'TD' ? getCellIndex(ancestor.element) : -1 cellIndex: ancestor.type === 'TD' ? getCellIndex(ancestor.element) : -1
}; };
} else {
containerFields[containerIndex].nonTableFields[label] = field;
}
} else { } else {
containerFields[containerIndex].nonTableFields[label] = field; containerFields[containerIndex].nonTableFields[label] = field;
} }
} else {
containerFields[containerIndex].nonTableFields[label] = field;
} }
}
}); });
const tableData = []; const tableData = [];
const nonTableData = []; const nonTableData = [];
// Process table data // Process table data with both iframe and shadow DOM support
for (let containerIndex = 0; containerIndex < containers.length; containerIndex++) { for (let containerIndex = 0; containerIndex < containers.length; containerIndex++) {
const container = containers[containerIndex]; const container = containers[containerIndex];
const { tableFields } = containerFields[containerIndex]; const { tableFields } = containerFields[containerIndex];
if (Object.keys(tableFields).length > 0) { if (Object.keys(tableFields).length > 0) {
const firstField = Object.values(tableFields)[0]; const firstField = Object.values(tableFields)[0];
const firstElement = queryIframe(container, firstField.selector); const firstElement = queryElement(container, firstField.selector);
let tableContext = firstElement; let tableContext = firstElement;
while (tableContext && tableContext.tagName !== 'TABLE' && tableContext !== container) { // Find table context including both iframe and shadow DOM
if (tableContext.tagName === 'IFRAME') { while (tableContext && tableContext.tagName !== 'TABLE' && tableContext !== container) {
try { if (tableContext.getRootNode() instanceof ShadowRoot) {
tableContext = tableContext.contentDocument.body; tableContext = tableContext.getRootNode().host;
} catch (e) { continue;
break; }
}
} else { if (tableContext.tagName === 'IFRAME') {
tableContext = tableContext.parentElement; try {
} tableContext = tableContext.contentDocument.body;
} } catch (e) {
break;
}
} else {
tableContext = tableContext.parentElement;
}
}
if (tableContext) { if (tableContext) {
const rows = Array.from(tableContext.getElementsByTagName('TR')); // Get rows from all contexts
const processedRows = filterRowsBasedOnTag(rows, tableFields); const rows = [];
for (let rowIndex = 0; rowIndex < Math.min(processedRows.length, limit); rowIndex++) { // Get rows from regular DOM
const record = {}; rows.push(...tableContext.getElementsByTagName('TR'));
const currentRow = processedRows[rowIndex];
// Get rows from shadow DOM
for (const [label, { selector, attribute, cellIndex }] of Object.entries(tableFields)) { if (tableContext.shadowRoot) {
let element = null; rows.push(...tableContext.shadowRoot.getElementsByTagName('TR'));
}
if (cellIndex >= 0) {
const td = currentRow.children[cellIndex]; // Get rows from iframes
if (td) { if (tableContext.tagName === 'IFRAME') {
element = queryIframe(td, selector); try {
const iframeDoc = tableContext.contentDocument || tableContext.contentWindow.document;
if (!element && selector.split(">").pop().includes('td:nth-child')) { rows.push(...iframeDoc.getElementsByTagName('TR'));
element = td; } 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) { if (!element) {
const tagOnlySelector = selector.split('.')[0]; const tagOnlySelector = selector.split('.')[0];
element = queryIframe(td, tagOnlySelector); element = queryElement(td, tagOnlySelector);
} }
if (!element) { if (!element) {
let currentElement = td; let currentElement = td;
while (currentElement && currentElement.children.length > 0) { while (currentElement && currentElement.children.length > 0) {
let foundContentChild = false; let foundContentChild = false;
for (const child of currentElement.children) { for (const child of currentElement.children) {
if (extractValue(child, attribute)) { if (extractValue(child, attribute)) {
currentElement = child; currentElement = child;
foundContentChild = true; foundContentChild = true;
break; break;
} }
} }
if (!foundContentChild) break; if (!foundContentChild) break;
} }
element = currentElement; element = currentElement;
} }
} }
} else { } else {
element = queryIframe(currentRow, selector); element = queryElement(currentRow, selector);
} }
if (element) { if (element) {
record[label] = extractValue(element, attribute); record[label] = extractValue(element, attribute);
} }
} }
if (Object.keys(record).length > 0) { if (Object.keys(record).length > 0) {
tableData.push(record); tableData.push(record);
} }
} }
} }
} }
} }
// Process non-table data // Process non-table data with both contexts support
for (let containerIndex = 0; containerIndex < containers.length; containerIndex++) { for (let containerIndex = 0; containerIndex < containers.length; containerIndex++) {
if (nonTableData.length >= limit) break; if (nonTableData.length >= limit) break;
const container = containers[containerIndex]; const container = containers[containerIndex];
const { nonTableFields } = containerFields[containerIndex]; const { nonTableFields } = containerFields[containerIndex];
if (Object.keys(nonTableFields).length > 0) { if (Object.keys(nonTableFields).length > 0) {
const record = {}; const record = {};
for (const [label, { selector, attribute }] of Object.entries(nonTableFields)) { for (const [label, { selector, attribute }] of Object.entries(nonTableFields)) {
const relativeSelector = selector.split(':>>').slice(-1)[0]; // Get the last part of the selector after any context delimiter
const element = queryIframe(container, relativeSelector); const relativeSelector = selector.split(/(?:>>|:>>)/).slice(-1)[0];
const element = queryElement(container, relativeSelector);
if (element) {
record[label] = extractValue(element, attribute); if (element) {
} record[label] = extractValue(element, attribute);
} }
}
if (Object.keys(record).length > 0) {
nonTableData.push(record); if (Object.keys(record).length > 0) {
} nonTableData.push(record);
} }
}
} }
// Merge and limit the results // Merge and limit the results

View File

@@ -403,7 +403,7 @@ export default class Interpreter extends EventEmitter {
await this.options.serializableCallback(scrapeResults); 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); await this.ensureScriptsLoaded(page);
const scrapeResult = await page.evaluate((schemaObj) => window.scrapeSchema(schemaObj), schema); const scrapeResult = await page.evaluate((schemaObj) => window.scrapeSchema(schemaObj), schema);
@@ -663,27 +663,42 @@ export default class Interpreter extends EventEmitter {
if (isApplicable) { if (isApplicable) {
return actionId; return actionId;
} }
} }
} }
private removeIframeSelectors(workflow: Workflow) { private removeShadowSelectors(workflow: Workflow) {
for (let actionId = workflow.length - 1; actionId >= 0; actionId--) { for (let actionId = workflow.length - 1; actionId >= 0; actionId--) {
const step = workflow[actionId]; const step = workflow[actionId];
// Check if step has where and selectors // Check if step has where and selectors
if (step.where && Array.isArray(step.where.selectors)) { if (step.where && Array.isArray(step.where.selectors)) {
// Filter out selectors that contain ">>" // 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; 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) { private async runLoop(p: Page, workflow: Workflow) {
let workflowCopy: Workflow = JSON.parse(JSON.stringify(workflow)); let workflowCopy: Workflow = JSON.parse(JSON.stringify(workflow));
workflowCopy = this.removeIframeSelectors(workflowCopy); workflowCopy = this.removeSpecialSelectors(workflowCopy);
// apply ad-blocker to the current page // apply ad-blocker to the current page
try { try {

View File

@@ -129,11 +129,17 @@ export interface BaseActionInfo {
hasOnlyText: boolean; hasOnlyText: boolean;
} }
interface IframeSelector { interface IframeSelector {
full: string; full: string;
isIframe: boolean; isIframe: boolean;
} }
interface ShadowSelector {
full: string;
mode: string;
}
/** /**
* Holds all the possible css selectors that has been found for an element. * Holds all the possible css selectors that has been found for an element.
* @category Types * @category Types
@@ -149,6 +155,7 @@ export interface Selectors {
accessibilitySelector: string|null; accessibilitySelector: string|null;
formSelector: string|null; formSelector: string|null;
iframeSelector: IframeSelector|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 displaySelector = await this.generateSelector(page, coordinates, ActionType.Click);
const elementInfo = await getElementInformation(page, coordinates, this.listSelector, this.getList); const elementInfo = await getElementInformation(page, coordinates, this.listSelector, this.getList);
if (rect) { 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.getList === true) {
if (this.listSelector !== '') { if (this.listSelector !== '') {
const childSelectors = await getChildSelectors(page, 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 { } else {
this.socket.emit('highlighter', { rect, selector: displaySelector, elementInfo }); this.socket.emit('highlighter', { ...highlighterData });
} }
} else { } 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: { case ActionType.DragAndDrop: {
const selectors = action.selectors; const selectors = action.selectors;
if (selectors?.iframeSelector?.full) { if (selectors?.iframeSelector?.full) {
return 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 // less than 25 characters, and element only has text inside
@@ -80,6 +84,11 @@ export const getBestSelectorForAction = (action: Action) => {
case ActionType.Input: case ActionType.Input:
case ActionType.Keydown: { case ActionType.Keydown: {
const selectors = action.selectors; const selectors = action.selectors;
if (selectors?.shadowSelector?.full) {
return selectors.shadowSelector.full;
}
return ( return (
selectors.testIdSelector ?? selectors.testIdSelector ??
selectors?.id ?? selectors?.id ??

View File

@@ -9,11 +9,11 @@ import { useBrowserSteps, TextStep } from '../../context/browserSteps';
import { useGlobalInfoStore } from '../../context/globalInfo'; import { useGlobalInfoStore } from '../../context/globalInfo';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
interface ElementInfo { interface ElementInfo {
tagName: string; tagName: string;
hasOnlyText?: boolean; hasOnlyText?: boolean;
isIframeContent?: boolean; isIframeContent?: boolean;
isShadowRoot?: boolean;
innerText?: string; innerText?: string;
url?: string; url?: string;
imageUrl?: string; imageUrl?: string;
@@ -125,7 +125,7 @@ export const BrowserWindow = () => {
if (listSelector) { if (listSelector) {
socket?.emit('listSelector', { selector: listSelector }); socket?.emit('listSelector', { selector: listSelector });
const hasValidChildSelectors = Array.isArray(data.childSelectors) && data.childSelectors.length > 0; const hasValidChildSelectors = Array.isArray(data.childSelectors) && data.childSelectors.length > 0;
if (limitMode) { if (limitMode) {
setHighlighterData(null); setHighlighterData(null);
} else if (paginationMode) { } else if (paginationMode) {
@@ -159,18 +159,39 @@ export const BrowserWindow = () => {
) )
); );
setHighlighterData(isValidMixedSelector ? data : null); setHighlighterData(isValidMixedSelector ? data : null);
} else { } else if (data.elementInfo?.isShadowRoot && data.childSelectors) {
// If no valid child in normal mode, clear the highlighter // 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); setHighlighterData(null);
} }
} else { } else {
// Set highlighterData for the initial listSelector selection // Set highlighterData for the initial listSelector selection
setHighlighterData(data); setHighlighterData(data);
} }
} else { } else {
// For non-list steps // For non-list steps
setHighlighterData(data); setHighlighterData(data);
} }
}, [highlighterData, getList, socket, listSelector, paginationMode, paginationType, captureStage]); }, [highlighterData, getList, socket, listSelector, paginationMode, paginationType, captureStage]);
@@ -219,6 +240,7 @@ export const BrowserWindow = () => {
addTextStep('', data, { addTextStep('', data, {
selector: highlighterData.selector, selector: highlighterData.selector,
tag: highlighterData.elementInfo?.tagName, tag: highlighterData.elementInfo?.tagName,
shadow: highlighterData.elementInfo?.isShadowRoot,
attribute attribute
}); });
} else { } else {
@@ -226,7 +248,7 @@ export const BrowserWindow = () => {
setAttributeOptions(options); setAttributeOptions(options);
setSelectedElement({ setSelectedElement({
selector: highlighterData.selector, selector: highlighterData.selector,
info: highlighterData.elementInfo info: highlighterData.elementInfo,
}); });
setShowAttributeModal(true); setShowAttributeModal(true);
} }
@@ -263,6 +285,7 @@ export const BrowserWindow = () => {
selectorObj: { selectorObj: {
selector: highlighterData.selector, selector: highlighterData.selector,
tag: highlighterData.elementInfo?.tagName, tag: highlighterData.elementInfo?.tagName,
shadow: highlighterData.elementInfo?.isShadowRoot,
attribute attribute
} }
}; };
@@ -310,6 +333,7 @@ export const BrowserWindow = () => {
addTextStep('', data, { addTextStep('', data, {
selector: selectedElement.selector, selector: selectedElement.selector,
tag: selectedElement.info?.tagName, tag: selectedElement.info?.tagName,
shadow: selectedElement.info?.isShadowRoot,
attribute: attribute attribute: attribute
}); });
} }
@@ -322,6 +346,7 @@ export const BrowserWindow = () => {
selectorObj: { selectorObj: {
selector: selectedElement.selector, selector: selectedElement.selector,
tag: selectedElement.info?.tagName, tag: selectedElement.info?.tagName,
shadow: selectedElement.info?.isShadowRoot,
attribute: attribute attribute: attribute
} }
}; };

View File

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