UNPKG

web-snaps

Version:

Browser automation with automatic snapshotting.

142 lines (141 loc) 5.2 kB
/** * Extract all HTML from a Playwright Page, including all shadow DOM HTML, even from closed shadow * roots! This requires a browser page that has been started with `withBrowserContext`, * `setupBrowser`, or `initBrowser` from this package. * * @category Internal */ export async function getAllPageHtml(page, storeKey, includeComments = false) { const response = await page.evaluate(({ storeKey, includeComments, }) => { /** Extracted from `@augment-vir/common`. */ function extractErrorMessage(maybeError) { if (!maybeError) { return ''; } if (maybeError instanceof Error) { return maybeError.message; } else if (typeof maybeError === 'object' && 'message' in maybeError) { return String(maybeError.message); } else if (typeof maybeError === 'string') { return maybeError; } else { return JSON.stringify(maybeError); } } try { const dataStore = globalThis[storeKey]; if (!dataStore) { throw new Error('missing data store'); } /** A set of HTML void elements that should not have a closing tag. */ const VOID_ELEMENTS = new Set([ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr', ]); /** * Recursively serializes a DOM Node into its HTML string representation (like * outerHTML). * * @param {Node} node The DOM Node to serialize. * @returns {string} The HTML string for the node. */ function serializeNode(node) { if (node instanceof Element) { const tagName = node.tagName.toLowerCase(); let html = `<${tagName}`; // Add attributes for (const attr of node.attributes) { html += ` ${attr.name}="${attr.value}"`; } // Handle void elements which don't have children or a closing tag if (VOID_ELEMENTS.has(tagName)) { html += '>'; return html; } // Add children for non-void elements html += `>${getInnerHTML(node)}</${tagName}>`; return html; } else if (node instanceof Text) { // For text nodes, escape the content and return it return node.textContent || ''; } else if (node instanceof Comment) { if (includeComments) { return `<!--${node.textContent}-->`; } else { return ''; } } else if (node instanceof DocumentFragment) { return getInnerHTML(node); } else if (node instanceof DocumentType) { return `<!doctype ${node.name}>`; } else { return ''; } } /** * Creates an innerHTML-like string for any given DOM Node without using the * .innerHTML property. * * @param {Node} node The DOM node whose inner HTML should be generated. * @returns {string} A string representing the inner HTML of the node. */ function getInnerHTML(node) { const shadowRoot = dataStore.closedShadows.get(node) || (node instanceof Element && node.shadowRoot) || undefined; const allChildren = [ ...(shadowRoot?.childNodes || []), ...node.childNodes, ]; if (!allChildren.length) { return ''; } let innerHTMLString = ''; // Iterate over all child nodes for (const child of allChildren) { // Serialize each child node and append it to the result innerHTMLString += serializeNode(child); } return innerHTMLString; } return { result: getInnerHTML(document), }; } catch (error) { return { error: extractErrorMessage(error), }; } }, { storeKey, includeComments }); if ('debug' in response) { console.info(response.debug); } if ('error' in response) { throw new Error(response.error); } else { return response.result; } }