Skip to content

optimization #139

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 103 additions & 17 deletions lib/core/utils/dq-element.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -7,30 +6,115 @@ import cache from '../base/cache';

const CACHE_KEY = 'DqElm.RunOptions';

function truncate(str, maxLength) {
maxLength = maxLength || 300;
/**
* 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 === 'HTML' || elm.nodeName === 'BODY') {
return elm.nodeName.toLowerCase();
}

if (str.length > maxLength) {
var index = str.indexOf('>');
str = str.substring(0, index + 1);
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;
}

return str;
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);
}

// Compute value if not cached
const tagName = element.nodeName?.toLowerCase();
if (!tagName) {
return '';
}
let htmlString = truncate(source || '');
// Remove unwanted attributes
const regex = /\s*data-percy-[^=]+="[^"]*"/g;
htmlString = htmlString.replace(regex, '');
return htmlString;

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;
}

/**
Expand Down Expand Up @@ -94,7 +178,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)]
);
},

/**
Expand Down
Loading