Skip to content
Merged
Changes from 2 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
122 changes: 121 additions & 1 deletion lib/core/utils/dq-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,117 @@ 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) {
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(' ');

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 '';
Expand Down Expand Up @@ -84,7 +195,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 +209,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