diff --git a/lib/core/utils/dq-element.js b/lib/core/utils/dq-element.js index 47bcc64e..93498e91 100644 --- a/lib/core/utils/dq-element.js +++ b/lib/core/utils/dq-element.js @@ -18,6 +18,154 @@ function truncate(str, maxLength) { return str; } +/** + * Escapes a string for use in CSS selectors + * @param {String} str - The string to escape + * @returns {String} The escaped string + */ +function escapeCSSSelector(str) { + // Use the CSS.escape method if available + if (window.CSS && window.CSS.escape) { + return window.CSS.escape(str); + } + // Simple fallback for browsers that don't support CSS.escape + return str + .replace(/[!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, '\\$&') + .replace(/^\d/, '\\3$& '); +} +function generateSelectorWithShadow(elm) { + const selectors = getShadowSelector(elm); + if (typeof selectors === 'string') { + return selectors; + } else { + // merge selectors of an array with , + return selectors.join(',').replace(/,$/, ''); + } +} + +function getShadowSelector(elm) { + if (!elm) { + return ''; + } + let doc = (elm.getRootNode && elm.getRootNode()) || document; + // Not a DOCUMENT_FRAGMENT - shadow DOM + if (doc.nodeType !== 11) { + return getFullPathSelector(elm); + } + + const stack = []; + while (doc.nodeType === 11) { + if (!doc.host) { + return ''; + } + stack.unshift({ elm, doc }); + elm = doc.host; + doc = elm.getRootNode(); + } + + stack.unshift({ elm, doc }); + return stack.map(item => getFullPathSelector(item.elm)); +} + +function getFullPathSelector(elm) { + if (elm.nodeName === 'HTML' || elm.nodeName === 'BODY') { + return elm.nodeName.toLowerCase(); + } + + if (cache.get('getFullPathSelector') === undefined) { + cache.set('getFullPathSelector', new WeakMap()); + } + + // Check cache first + const sourceCache = cache.get('getFullPathSelector'); + if (sourceCache.has(elm)) { + return sourceCache.get(elm); + } + + const element = elm; + const names = []; + while (elm.parentElement && elm.nodeName !== 'BODY') { + if (sourceCache.has(elm)) { + names.unshift(sourceCache.get(elm)); + break; + } else if (elm.id) { + // Check if the ID is unique in the document before using it + const escapedId = escapeCSSSelector(elm.getAttribute('id')); + const elementsWithSameId = document.querySelectorAll(`#${escapedId}`); + if (elementsWithSameId.length === 1) { + // ID is unique, safe to use + names.unshift('#' + escapedId); + break; + } else { + // ID is not unique, fallback to position-based selector + let c = 1; + let e = elm; + for (; e.previousElementSibling; e = e.previousElementSibling, c++) { + // Increment counter for each previous sibling + } + names.unshift(`${elm.nodeName.toLowerCase()}:nth-child(${c})`); + } + } else { + let c = 1; + let e = elm; + for (; e.previousElementSibling; e = e.previousElementSibling, c++) { + // Increment counter for each previous sibling + } + names.unshift(`${elm.nodeName.toLowerCase()}:nth-child(${c})`); + } + elm = elm.parentElement; + } + + const selector = names.join('>'); + sourceCache.set(element, selector); + return selector; +} + +function getSourceOpt(element) { + if (!element) { + return ''; + } + + // Initialize cache if needed + if (cache.get('getSourceEfficient') === undefined) { + cache.set('getSourceEfficient', new WeakMap()); + } + + // Check cache first + const sourceCache = cache.get('getSourceEfficient'); + if (sourceCache.has(element)) { + return sourceCache.get(element); + } + + // Compute value if not cached + const tagName = element.nodeName?.toLowerCase(); + if (!tagName) { + return ''; + } + + let result; + try { + const attributes = Array.from(element.attributes || []) + .filter(attr => !attr.name.startsWith('data-percy-')) + .map(attr => `${attr.name}="${attr.value}"`) + .join(' '); + const closingTag = element.children.length ? false : true; + if (closingTag) { + result = `<${tagName} ${attributes}>${element.textContent}`; + } else { + result = attributes ? `<${tagName} ${attributes}>` : `<${tagName}>`; + } + result = truncate(result, 300); // Truncate to 300 characters + // Store in cache + sourceCache.set(element, result); + } catch (e) { + // Handle potential errors (like accessing attributes on non-element nodes) + result = `<${tagName || 'unknown'}>`; + } + + return result; +} + function getSource(element) { if (!element?.outerHTML) { return ''; @@ -84,7 +232,11 @@ function DqElement(elm, options = null, spec = {}) { this.source = null; // TODO: es-modules_audit if (!axe._audit.noHtml) { - this.source = this.spec.source ?? getSource(this._element); + if (axe._cache.get('runTypeAOpt')) { + this.source = this.spec.source ?? getSourceOpt(this._element); + } else { + this.source = this.spec.source ?? getSource(this._element); + } } } @@ -94,6 +246,9 @@ DqElement.prototype = { * @return {String} */ get selector() { + if (axe._cache.get('runTypeAOpt')) { + return this.spec.selector || [generateSelectorWithShadow(this.element)]; + } return this.spec.selector || [getSelector(this.element, this._options)]; },