UNPKG

testplane

Version:

Tests framework based on mocha and wdio

398 lines 14.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.captureDomSnapshotInBrowser = void 0; const captureDomSnapshotInBrowser = (selectorOrElementOrOptions, maybeOptions) => { let selector = null; let element = null; let options; if (typeof selectorOrElementOrOptions === "string") { selector = selectorOrElementOrOptions; options = maybeOptions || {}; } else if (selectorOrElementOrOptions && typeof selectorOrElementOrOptions === "object" && "tagName" in selectorOrElementOrOptions) { element = selectorOrElementOrOptions; options = maybeOptions || {}; } else { options = selectorOrElementOrOptions || {}; } const { includeTags = [], includeAttrs = [], excludeTags = [], excludeAttrs = [], truncateText = true, maxTextLength = 100, } = options; const BASE_EXCLUDED_TAGS = new Set([ "HEAD", "LINK", "META", "NOSCRIPT", "SCRIPT", "SLOT", "STYLE", "TEMPLATE", "TITLE", ]); const EXCLUDED_TAGS = new Set(BASE_EXCLUDED_TAGS); if (includeTags) { includeTags.forEach(tag => EXCLUDED_TAGS.delete(tag.toUpperCase())); } if (excludeTags) { excludeTags.forEach(tag => EXCLUDED_TAGS.add(tag.toUpperCase())); } const BASE_USEFUL_ATTRIBUTES = new Set([ "action", "alt", "aria-describedby", "aria-label", "aria-labelledby", "checked", "class", "data-automation", "data-qa", "data-test", "data-test-id", "data-testid", "disabled", "for", "hidden", "href", "id", "method", "name", "open", "placeholder", "readonly", "required", "role", "selected", "src", "tabindex", "title", "type", "value", ]); const USEFUL_ATTRIBUTES = new Set(BASE_USEFUL_ATTRIBUTES); if (includeAttrs) { includeAttrs.forEach(attr => USEFUL_ATTRIBUTES.add(attr.toLowerCase())); } if (excludeAttrs) { excludeAttrs.forEach(attr => USEFUL_ATTRIBUTES.delete(attr.toLowerCase())); } const omittedTags = new Set(); const omittedAttributes = new Set(); let textWasTruncated = false; const INTERACTIVE_TAGS = new Set(["BUTTON", "INPUT", "SELECT", "TEXTAREA", "A"]); const INTERACTIVE_ROLES = new Set([ "button", "checkbox", "combobox", "link", "listbox", "menu", "menubar", "menuitem", "menuitemcheckbox", "menuitemradio", "option", "radio", "radiogroup", "searchbox", "slider", "spinbutton", "switch", "tab", "tablist", "textbox", "tree", "treeitem", ]); const escapeQuotesAndNewlines = (value) => { return value.replace(/"/g, '\\"').replace(/\n/g, "\\n"); }; function isElementVisible(element) { const style = window.getComputedStyle(element); // For SVG elements, check differently since they don't have offsetWidth/offsetHeight if (element.tagName.toLowerCase() === "svg" || element instanceof SVGElement) { return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0"; } const htmlElement = element; return (style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0" && htmlElement.offsetWidth > 0 && htmlElement.offsetHeight > 0); } function hasInteractiveContent(element) { if (INTERACTIVE_TAGS.has(element.tagName)) { return true; } if (element.hasAttribute("onclick") || element.hasAttribute("tabindex") || INTERACTIVE_ROLES.has(element.getAttribute("role") || "")) { return true; } return Array.from(element.children).some(child => hasInteractiveContent(child)); } function hasImportantOrTestAttributes(element, importantAttributes) { const testAttrs = ["data-testid", "data-test-id", "data-test", "data-qa", "data-automation"]; return (importantAttributes.some(attr => element.hasAttribute(attr)) || testAttrs.some(attr => element.hasAttribute(attr))); } function getElementText(element) { let text = ""; for (const node of element.childNodes) { if (node.nodeType === Node.TEXT_NODE) { const textContent = node.textContent?.trim(); if (textContent) { text += textContent + " "; } } } text = text.trim(); if (truncateText && text.length > maxTextLength) { textWasTruncated = true; text = text.substring(0, maxTextLength) + "..."; } return text; } function getElementState(element) { const state = {}; const tagName = element.tagName.toLowerCase(); if (document.activeElement === element && element !== document.body) { state.focused = true; } try { const hoveredElements = Array.from(document.querySelectorAll(":hover")); const deepestHovered = hoveredElements[hoveredElements.length - 1]; if (deepestHovered === element) { state.hover = true; } } catch { /* */ } try { switch (tagName) { case "input": { const inputEl = element; const inputType = inputEl.type.toLowerCase(); if (inputType === "checkbox" || inputType === "radio") { if (inputEl.checked) { state.checked = true; } } else if (inputEl.value) { state.value = inputEl.value; } if (inputEl.checkValidity && !inputEl.checkValidity()) { state.invalid = true; } break; } case "textarea": { const textareaEl = element; if (textareaEl.value) { state.value = textareaEl.value; } if (textareaEl.checkValidity && !textareaEl.checkValidity()) { state.invalid = true; } break; } case "select": { const selectEl = element; const selectedOption = selectEl.options[selectEl.selectedIndex]; if (selectedOption) { state.selected = selectedOption.value; if (selectedOption.text !== selectedOption.value) { state.selectedText = selectedOption.text; } } break; } case "option": { const optionEl = element; if (optionEl.selected) { state.selected = true; } break; } } } catch { /* */ } return state; } function buildElementNode(element, depth = 0) { const tagName = element.tagName.toLowerCase(); if (EXCLUDED_TAGS.has(element.tagName)) { omittedTags.add(tagName); return null; } const hasTestAttrs = hasImportantOrTestAttributes(element, includeAttrs); const isVisible = isElementVisible(element); const directText = getElementText(element); const children = []; if (tagName !== "svg") { for (const child of element.children) { const childNode = buildElementNode(child, depth + 1); if (childNode) { children.push(childNode); } } } const selfClosingTags = new Set(["img", "input", "br", "hr", "meta", "link"]); // Hide empty elements that doesn't have anything interesting if (children.length === 0 && !directText && !selfClosingTags.has(tagName)) { const hasInterestingContent = hasInteractiveContent(element) || hasTestAttrs; // SVGs need special handling, because we omit their children during filtering on our end // For SVGs, children.length is always zero, and we need to check real DOM (element.children) if (tagName === "svg") { if (element.children.length === 0 && !hasInterestingContent) { return null; } } else if (!hasInteractiveContent(element) && !hasTestAttrs && !tagName.includes("-")) { return null; } } return buildCompactElement(element, directText, children, depth, !isVisible); } function buildCompactElement(element, text, children, depth, forceHidden = false) { const tagName = element.tagName.toLowerCase(); const indent = " ".repeat(depth); // Build CSS-like selector: tag.class#id let selector = tagName; const className = element.getAttribute("class"); if (className && USEFUL_ATTRIBUTES.has("class")) { const classes = className.trim().split(/\s+/).filter(Boolean); if (classes.length > 0) { selector += "." + classes.join("."); } } const id = element.id; if (id && USEFUL_ATTRIBUTES.has("id")) { selector += "#" + id; } // Build compact attributes [key=val key2="val with spaces"] const attributes = []; for (const attr of element.attributes) { const attrName = attr.name.toLowerCase(); if (attrName === "class" || attrName === "id") { continue; } if (USEFUL_ATTRIBUTES.has(attrName)) { let value = attr.value; if (value.length > maxTextLength) { textWasTruncated = true; value = value.substring(0, maxTextLength) + "..."; } if ([" ", '"', "="].some(char => value.includes(char))) { value = escapeQuotesAndNewlines(value); attributes.push(`${attrName}="${value}"`); } else if (value === "") { // Boolean attributes like 'required', 'disabled' attributes.push(attrName); } else { attributes.push(`${attrName}=${value}`); } } else { omittedAttributes.add(attrName); } } const elementState = getElementState(element); for (const [key, value] of Object.entries(elementState)) { if (typeof value === "boolean") { attributes.push(`@${key}`); } else { if (value.includes(" ") || value.includes('"')) { const escapedValue = escapeQuotesAndNewlines(value); attributes.push(`@${key}="${escapedValue}"`); } else { attributes.push(`@${key}=${value}`); } } } if (forceHidden) { attributes.push("@hidden"); } let elementLine = selector; if (attributes.length > 0) { elementLine += `[${attributes.join(" ")}]`; } if (text) { const escapedText = escapeQuotesAndNewlines(text); elementLine += ` "${escapedText}"`; } if (children.length > 0) { const childLines = children.join("\n"); // If a line contains ": " string, we should place it in quotes and escape to be parsed correctly const escapedElementLine = elementLine.includes(": ") ? `"${elementLine.replace(/"/g, '\\"')}"` : elementLine; return `${indent}- ${escapedElementLine}:\n${childLines}`; } else { return indent + "- " + elementLine; } } let startElement; if (element) { startElement = element; } else if (selector) { try { startElement = document.querySelector(selector); if (!startElement) { return { snapshot: `# Element not found: ${selector}`, omittedTags: [], omittedAttributes: [], textWasTruncated: false, }; } } catch (error) { return { snapshot: `# Invalid selector: ${selector}`, omittedTags: [], omittedAttributes: [], textWasTruncated: false, }; } } else { startElement = document.body || document.documentElement; if (!startElement) { return { snapshot: "# No elements found", omittedTags: [], omittedAttributes: [], textWasTruncated: false, }; } } const rootNode = buildElementNode(startElement); const compactSnapshot = rootNode ? `${rootNode}` : `# No visible elements found`; return { snapshot: compactSnapshot, omittedTags: Array.from(omittedTags).sort(), omittedAttributes: Array.from(omittedAttributes).sort(), textWasTruncated, }; }; exports.captureDomSnapshotInBrowser = captureDomSnapshotInBrowser; exports.default = (browser) => { const { publicAPI: session } = browser; session.addCommand("unstable_captureDomSnapshot", async function (selectorOrOptions, maybeOptions) { return session.execute(exports.captureDomSnapshotInBrowser, selectorOrOptions, maybeOptions); }); session.addCommand("unstable_captureDomSnapshot", async function (options = {}) { return this.execute(exports.captureDomSnapshotInBrowser, options); }, true); }; //# sourceMappingURL=captureDomSnapshot.js.map