Merge branch 'develop' into iframe
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
@@ -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 ??
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface SelectorObject {
|
||||
selector: string;
|
||||
tag?: string;
|
||||
attribute?: string;
|
||||
shadow?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user