Skip to content
Merged
Changes from 1 commit
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
125 changes: 124 additions & 1 deletion lib/core/utils/dq-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 '';
}
Expand Down Expand Up @@ -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);
}
}
}

Expand All @@ -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)];
},

Expand Down
Loading