From 6e3cbe07a07b94df548dab08d6b28179a31eae42 Mon Sep 17 00:00:00 2001 From: Aaryan Choudhary Date: Thu, 19 Jun 2025 12:01:16 +0530 Subject: [PATCH 1/3] Dq element optimization --- lib/core/utils/dq-element.js | 125 ++++++++++++++++++++++++++++++++++- 1 file changed, 124 insertions(+), 1 deletion(-) diff --git a/lib/core/utils/dq-element.js b/lib/core/utils/dq-element.js index 47bcc64e..02fc3be8 100644 --- a/lib/core/utils/dq-element.js +++ b/lib/core/utils/dq-element.js @@ -18,7 +18,121 @@ 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 getFullPathSelector(elm) { + console.log('Get Full Path Selector Optimization'); + 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) { + console.log('Get Source optimization'); + 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(' '); + + 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) { + console.log('Get Source'); if (!element?.outerHTML) { return ''; } @@ -84,7 +198,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 +212,11 @@ DqElement.prototype = { * @return {String} */ get selector() { + if (axe._cache.get('runTypeAOpt')) { + return ( + this.spec.selector || [getFullPathSelector(this.element, this._options)] + ); + } return this.spec.selector || [getSelector(this.element, this._options)]; }, From 57364b5b76a0841444ce1c1ce6576b06b9c8b583 Mon Sep 17 00:00:00 2001 From: Aaryan Choudhary <76112295+Aaryan430@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:25:20 +0530 Subject: [PATCH 2/3] Update dq-element.js --- lib/core/utils/dq-element.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/core/utils/dq-element.js b/lib/core/utils/dq-element.js index 02fc3be8..4c5463c1 100644 --- a/lib/core/utils/dq-element.js +++ b/lib/core/utils/dq-element.js @@ -35,7 +35,6 @@ function escapeCSSSelector(str) { } function getFullPathSelector(elm) { - console.log('Get Full Path Selector Optimization'); if (elm.nodeName === 'HTML' || elm.nodeName === 'BODY') { return elm.nodeName.toLowerCase(); } @@ -90,7 +89,6 @@ function getFullPathSelector(elm) { } function getSourceOpt(element) { - console.log('Get Source optimization'); if (!element) { return ''; } @@ -132,7 +130,6 @@ function getSourceOpt(element) { } function getSource(element) { - console.log('Get Source'); if (!element?.outerHTML) { return ''; } From 15804f91ddf12fbfbfb5e3fb90f6c28f7aa7b669 Mon Sep 17 00:00:00 2001 From: Aaryan Choudhary Date: Tue, 24 Jun 2025 16:17:17 +0530 Subject: [PATCH 3/3] resolved comments --- lib/core/utils/dq-element.js | 45 ++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/lib/core/utils/dq-element.js b/lib/core/utils/dq-element.js index 4c5463c1..93498e91 100644 --- a/lib/core/utils/dq-element.js +++ b/lib/core/utils/dq-element.js @@ -33,6 +33,39 @@ function escapeCSSSelector(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') { @@ -116,8 +149,12 @@ function getSourceOpt(element) { .filter(attr => !attr.name.startsWith('data-percy-')) .map(attr => `${attr.name}="${attr.value}"`) .join(' '); - - result = attributes ? `<${tagName} ${attributes}>` : `<${tagName}>`; + 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); @@ -210,9 +247,7 @@ DqElement.prototype = { */ get selector() { if (axe._cache.get('runTypeAOpt')) { - return ( - this.spec.selector || [getFullPathSelector(this.element, this._options)] - ); + return this.spec.selector || [generateSelectorWithShadow(this.element)]; } return this.spec.selector || [getSelector(this.element, this._options)]; },