feat: get deeply nested iframe element info
This commit is contained in:
@@ -24,62 +24,67 @@ export const getElementInformation = async (
|
|||||||
const elementInfo = await page.evaluate(
|
const elementInfo = await page.evaluate(
|
||||||
async ({ x, y }) => {
|
async ({ x, y }) => {
|
||||||
// Helper function to find elements within iframes, handling nested cases
|
// Helper function to find elements within iframes, handling nested cases
|
||||||
const getElementFromIframePoint = (
|
const getDeepestElementFromPoint = (x: number, y: number): HTMLElement | null => {
|
||||||
x: number,
|
// First, get the element at the clicked coordinates in the main document
|
||||||
y: number,
|
let element = document.elementFromPoint(x, y) as HTMLElement;
|
||||||
context: Document = document,
|
if (!element) return null;
|
||||||
iframePath: string[] = []
|
|
||||||
): { element: HTMLElement | null; iframePath: string[] } => {
|
// If it's not an iframe, return the element as is
|
||||||
// First try to get element at the given coordinates
|
if (element.tagName !== 'IFRAME') return element;
|
||||||
let element = context.elementFromPoint(x, y) as HTMLElement;
|
|
||||||
if (!element) return { element: null, iframePath };
|
// Initialize tracking variables for iframe traversal
|
||||||
|
let currentIframe = element as HTMLIFrameElement;
|
||||||
// Check if we found an iframe
|
let deepestElement = element;
|
||||||
if (element.tagName === 'IFRAME') {
|
|
||||||
const iframe = element as HTMLIFrameElement;
|
// Continue traversing while we have a valid iframe
|
||||||
try {
|
while (currentIframe) {
|
||||||
// Make sure we can access the iframe's content
|
try {
|
||||||
if (!iframe.contentDocument) {
|
// Convert the coordinates from main document space to iframe's local space
|
||||||
return { element, iframePath };
|
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);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform coordinates to iframe's space
|
|
||||||
const rect = iframe.getBoundingClientRect();
|
|
||||||
const relativeX = x - rect.left;
|
|
||||||
const relativeY = y - rect.top;
|
|
||||||
|
|
||||||
// Add this iframe to the path
|
|
||||||
const updatedPath = [...iframePath, iframe.id || 'unnamed-iframe'];
|
|
||||||
|
|
||||||
// Recursively search within the iframe
|
|
||||||
const iframeResult = getElementFromIframePoint(
|
|
||||||
relativeX,
|
|
||||||
relativeY,
|
|
||||||
iframe.contentDocument,
|
|
||||||
updatedPath
|
|
||||||
);
|
|
||||||
|
|
||||||
// If we found an element in the iframe, return it
|
|
||||||
if (iframeResult.element) {
|
|
||||||
return iframeResult;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Cannot access iframe content:', e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return deepestElement;
|
||||||
// Return the element we found (either in main document or iframe)
|
|
||||||
return { element, iframePath };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the element and its iframe path
|
// Get the element and its iframe path
|
||||||
const { element: el, iframePath } = getElementFromIframePoint(x, y);
|
const el = getDeepestElementFromPoint(x, y);
|
||||||
|
|
||||||
if (el) {
|
if (el) {
|
||||||
// Handle potential anchor parent
|
// Handle potential anchor parent
|
||||||
const { parentElement } = el;
|
const { parentElement } = el;
|
||||||
const targetElement = parentElement?.tagName === 'A' ? parentElement : el;
|
const targetElement = parentElement?.tagName === 'A' ? parentElement : el;
|
||||||
|
|
||||||
|
const ownerDocument = targetElement.ownerDocument;
|
||||||
|
const frameElement = ownerDocument?.defaultView?.frameElement as HTMLIFrameElement;
|
||||||
|
const isIframeContent = Boolean(frameElement);
|
||||||
|
|
||||||
// Build the element information object
|
// Build the element information object
|
||||||
let info: {
|
let info: {
|
||||||
tagName: string;
|
tagName: string;
|
||||||
@@ -90,14 +95,42 @@ export const getElementInformation = async (
|
|||||||
attributes?: Record<string, string>;
|
attributes?: Record<string, string>;
|
||||||
innerHTML?: string;
|
innerHTML?: string;
|
||||||
outerHTML?: string;
|
outerHTML?: string;
|
||||||
fromIframe?: boolean;
|
isIframeContent?: boolean;
|
||||||
iframePath?: string[];
|
iframeURL?: string;
|
||||||
|
iframeIndex?: number;
|
||||||
|
frameHierarchy?: string[];
|
||||||
} = {
|
} = {
|
||||||
tagName: targetElement?.tagName ?? '',
|
tagName: targetElement?.tagName ?? '',
|
||||||
fromIframe: iframePath.length > 0,
|
isIframeContent: isIframeContent
|
||||||
iframePath: iframePath.length > 0 ? iframePath : undefined
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isIframeContent) {
|
||||||
|
// Include iframe specific information
|
||||||
|
info.iframeURL = frameElement.src;
|
||||||
|
|
||||||
|
// Calculate the frame's position in the hierarchy
|
||||||
|
let currentFrame = frameElement;
|
||||||
|
const frameHierarchy: string[] = [];
|
||||||
|
let frameIndex = 0;
|
||||||
|
|
||||||
|
while (currentFrame) {
|
||||||
|
// Store the frame's identifier (src, id, or index)
|
||||||
|
frameHierarchy.unshift(
|
||||||
|
currentFrame.id ||
|
||||||
|
currentFrame.src ||
|
||||||
|
`iframe[${frameIndex}]`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Move up to parent frame if it exists
|
||||||
|
const parentDoc = currentFrame.ownerDocument;
|
||||||
|
currentFrame = parentDoc?.defaultView?.frameElement as HTMLIFrameElement;
|
||||||
|
frameIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
info.frameHierarchy = frameHierarchy;
|
||||||
|
info.iframeIndex = frameIndex - 1; // Adjust for 0-based index
|
||||||
|
}
|
||||||
|
|
||||||
// Collect element attributes and properties
|
// Collect element attributes and properties
|
||||||
if (targetElement) {
|
if (targetElement) {
|
||||||
// Get all attributes
|
// Get all attributes
|
||||||
@@ -133,8 +166,170 @@ export const getElementInformation = async (
|
|||||||
{ x: coordinates.x, y: coordinates.y }
|
{ x: coordinates.x, y: coordinates.y }
|
||||||
);
|
);
|
||||||
return elementInfo;
|
return elementInfo;
|
||||||
|
} else {
|
||||||
|
const elementInfo = await page.evaluate(
|
||||||
|
async ({ x, y }) => {
|
||||||
|
// Enhanced helper function to get element from point including shadow DOM
|
||||||
|
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;
|
||||||
|
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
|
||||||
|
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);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deepestElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
const originalEl = getDeepestElementFromPoint(x, y);
|
||||||
|
if (originalEl) {
|
||||||
|
let element = originalEl;
|
||||||
|
|
||||||
|
while (element.parentElement) {
|
||||||
|
const parentRect = element.parentElement.getBoundingClientRect();
|
||||||
|
const childRect = element.getBoundingClientRect();
|
||||||
|
|
||||||
|
const fullyContained =
|
||||||
|
parentRect.left <= childRect.left &&
|
||||||
|
parentRect.right >= childRect.right &&
|
||||||
|
parentRect.top <= childRect.top &&
|
||||||
|
parentRect.bottom >= childRect.bottom;
|
||||||
|
|
||||||
|
const significantOverlap =
|
||||||
|
(childRect.width * childRect.height) /
|
||||||
|
(parentRect.width * parentRect.height) > 0.5;
|
||||||
|
|
||||||
|
if (fullyContained && significantOverlap) {
|
||||||
|
element = element.parentElement;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ownerDocument = element.ownerDocument;
|
||||||
|
const frameElement = ownerDocument?.defaultView?.frameElement as HTMLIFrameElement;
|
||||||
|
const isIframeContent = Boolean(frameElement);
|
||||||
|
|
||||||
|
// Build the element information object
|
||||||
|
let info: {
|
||||||
|
tagName: string;
|
||||||
|
hasOnlyText?: boolean;
|
||||||
|
innerText?: string;
|
||||||
|
url?: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
attributes?: Record<string, string>;
|
||||||
|
innerHTML?: string;
|
||||||
|
outerHTML?: string;
|
||||||
|
isIframeContent?: boolean;
|
||||||
|
iframeURL?: string;
|
||||||
|
iframeIndex?: number;
|
||||||
|
frameHierarchy?: string[];
|
||||||
|
} = {
|
||||||
|
tagName: element?.tagName ?? '',
|
||||||
|
isIframeContent: isIframeContent
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isIframeContent) {
|
||||||
|
// Include iframe specific information
|
||||||
|
info.iframeURL = frameElement.src;
|
||||||
|
|
||||||
|
// Calculate the frame's position in the hierarchy
|
||||||
|
let currentFrame = frameElement;
|
||||||
|
const frameHierarchy: string[] = [];
|
||||||
|
let frameIndex = 0;
|
||||||
|
|
||||||
|
while (currentFrame) {
|
||||||
|
// Store the frame's identifier (src, id, or index)
|
||||||
|
frameHierarchy.unshift(
|
||||||
|
currentFrame.id ||
|
||||||
|
currentFrame.src ||
|
||||||
|
`iframe[${frameIndex}]`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Move up to parent frame if it exists
|
||||||
|
const parentDoc = currentFrame.ownerDocument;
|
||||||
|
currentFrame = parentDoc?.defaultView?.frameElement as HTMLIFrameElement;
|
||||||
|
frameIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
info.frameHierarchy = frameHierarchy;
|
||||||
|
info.iframeIndex = frameIndex - 1; // Adjust for 0-based index
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
// Get attributes including those from shadow DOM context
|
||||||
|
info.attributes = Array.from(element.attributes).reduce(
|
||||||
|
(acc, attr) => {
|
||||||
|
acc[attr.name] = attr.value;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, string>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle specific element types
|
||||||
|
if (element.tagName === 'A') {
|
||||||
|
info.url = (element as HTMLAnchorElement).href;
|
||||||
|
info.innerText = element.textContent ?? '';
|
||||||
|
} else if (element.tagName === 'IMG') {
|
||||||
|
info.imageUrl = (element as HTMLImageElement).src;
|
||||||
|
} else {
|
||||||
|
// Handle text content with proper null checking
|
||||||
|
info.hasOnlyText = element.children.length === 0 &&
|
||||||
|
(element.textContent !== null &&
|
||||||
|
element.textContent.trim().length > 0);
|
||||||
|
info.innerText = element.textContent ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
info.innerHTML = element.innerHTML;
|
||||||
|
info.outerHTML = element.outerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
{ x: coordinates.x, y: coordinates.y },
|
||||||
|
);
|
||||||
|
return elementInfo;
|
||||||
}
|
}
|
||||||
// ... rest of the code remains same
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const { message, stack } = error as Error;
|
const { message, stack } = error as Error;
|
||||||
console.error('Error while retrieving selector:', message);
|
console.error('Error while retrieving selector:', message);
|
||||||
|
|||||||
Reference in New Issue
Block a user