From 752fcb1b17f5d1895f426bc882b4102282bca022 Mon Sep 17 00:00:00 2001 From: Aaryan Choudhary Date: Wed, 28 May 2025 10:50:20 +0530 Subject: [PATCH 1/4] optimization --- lib/core/utils/dq-element.js | 85 ++++++++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 17 deletions(-) diff --git a/lib/core/utils/dq-element.js b/lib/core/utils/dq-element.js index 47bcc64e..7d4b54e8 100644 --- a/lib/core/utils/dq-element.js +++ b/lib/core/utils/dq-element.js @@ -1,4 +1,3 @@ -import getSelector from './get-selector'; import getAncestry from './get-ancestry'; import getXpath from './get-xpath'; import getNodeFromTree from './get-node-from-tree'; @@ -7,30 +6,80 @@ import cache from '../base/cache'; const CACHE_KEY = 'DqElm.RunOptions'; -function truncate(str, maxLength) { - maxLength = maxLength || 300; +function getFullPathSelector(elm) { + if (elm.nodeName === 'BODY') { + return 'BODY'; + } + + if (cache.get('getFullPathSelector') === undefined) { + cache.set('getFullPathSelector', new WeakMap()); + } - if (str.length > maxLength) { - var index = str.indexOf('>'); - str = str.substring(0, index + 1); + // Check cache first + const sourceCache = cache.get('getFullPathSelector'); + if (sourceCache.has(elm)) { + return sourceCache.get(elm); } - return str; + const element = elm; + const names = []; + while (elm.parentElement && elm.nodeName !== 'BODY') { + if (elm.id) { + names.unshift('#' + elm.getAttribute('id')); + break; + } else { + let c = 1, + e = elm; + for (; e.previousElementSibling; e = e.previousElementSibling, c++) {} + names.unshift(elm.nodeName + ':nth-child(' + c + ')'); + } + elm = elm.parentElement; + } + + const selector = names.join('>'); + sourceCache.set(element, selector); + return selector; } function getSource(element) { - if (!element?.outerHTML) { + if (!element) { return ''; } - var source = element.outerHTML; - if (!source && typeof window.XMLSerializer === 'function') { - source = new window.XMLSerializer().serializeToString(element); + + // 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); } - let htmlString = truncate(source || ''); - // Remove unwanted attributes - const regex = /\s*data-percy-[^=]+="[^"]*"/g; - htmlString = htmlString.replace(regex, ''); - return htmlString; + + // 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}>`; + + // Store in cache + sourceCache.set(element, result); + } catch (e) { + // Handle potential errors (like accessing attributes on non-element nodes) + result = `<${tagName || 'unknown'}>`; + } + + return result; } /** @@ -94,7 +143,9 @@ DqElement.prototype = { * @return {String} */ get selector() { - return this.spec.selector || [getSelector(this.element, this._options)]; + return ( + this.spec.selector || [getFullPathSelector(this.element, this._options)] + ); }, /** From 5e611b51f6dd642b2fe2cc3e06d61f946ed96389 Mon Sep 17 00:00:00 2001 From: Aaryan Choudhary Date: Thu, 29 May 2025 00:38:49 +0530 Subject: [PATCH 2/4] selector optimization --- lib/core/utils/dq-element.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/core/utils/dq-element.js b/lib/core/utils/dq-element.js index 7d4b54e8..f1431354 100644 --- a/lib/core/utils/dq-element.js +++ b/lib/core/utils/dq-element.js @@ -24,7 +24,10 @@ function getFullPathSelector(elm) { const element = elm; const names = []; while (elm.parentElement && elm.nodeName !== 'BODY') { - if (elm.id) { + if(sourceCache.has(elm)) { + names.unshift(sourceCache.get(elm)); + break; + } else if (elm.id) { names.unshift('#' + elm.getAttribute('id')); break; } else { From 3e105c7666916cdb5d39bdd2a6653ba2375c49c8 Mon Sep 17 00:00:00 2001 From: Aaryan Choudhary Date: Thu, 29 May 2025 00:45:55 +0530 Subject: [PATCH 3/4] fix --- lib/core/utils/dq-element.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/core/utils/dq-element.js b/lib/core/utils/dq-element.js index f1431354..792c5a83 100644 --- a/lib/core/utils/dq-element.js +++ b/lib/core/utils/dq-element.js @@ -24,7 +24,7 @@ function getFullPathSelector(elm) { const element = elm; const names = []; while (elm.parentElement && elm.nodeName !== 'BODY') { - if(sourceCache.has(elm)) { + if (sourceCache.has(elm)) { names.unshift(sourceCache.get(elm)); break; } else if (elm.id) { @@ -33,7 +33,9 @@ function getFullPathSelector(elm) { } else { let c = 1, e = elm; - for (; e.previousElementSibling; e = e.previousElementSibling, c++) {} + for (; e.previousElementSibling; e = e.previousElementSibling, c++) { + // Increment counter for each previous sibling + } names.unshift(elm.nodeName + ':nth-child(' + c + ')'); } elm = elm.parentElement; From 4b65e770c0bbca856bb220335d6811272632b8f9 Mon Sep 17 00:00:00 2001 From: Aaryan Choudhary Date: Thu, 29 May 2025 01:58:30 +0530 Subject: [PATCH 4/4] selector edge cases fix --- lib/core/utils/dq-element.js | 44 ++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/lib/core/utils/dq-element.js b/lib/core/utils/dq-element.js index 792c5a83..9ce3acf6 100644 --- a/lib/core/utils/dq-element.js +++ b/lib/core/utils/dq-element.js @@ -6,9 +6,25 @@ import cache from '../base/cache'; const CACHE_KEY = 'DqElm.RunOptions'; +/** + * 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) { - if (elm.nodeName === 'BODY') { - return 'BODY'; + if (elm.nodeName === 'HTML' || elm.nodeName === 'BODY') { + return elm.nodeName.toLowerCase(); } if (cache.get('getFullPathSelector') === undefined) { @@ -28,15 +44,29 @@ function getFullPathSelector(elm) { names.unshift(sourceCache.get(elm)); break; } else if (elm.id) { - names.unshift('#' + elm.getAttribute('id')); - break; + // 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, - e = elm; + let c = 1; + let e = elm; for (; e.previousElementSibling; e = e.previousElementSibling, c++) { // Increment counter for each previous sibling } - names.unshift(elm.nodeName + ':nth-child(' + c + ')'); + names.unshift(`${elm.nodeName.toLowerCase()}:nth-child(${c})`); } elm = elm.parentElement; }