Skip to content
Merged
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
157 changes: 156 additions & 1 deletion lib/core/utils/dq-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,154 @@ 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 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') {
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(' ');
const closingTag = element.children.length ? false : true;
if (closingTag) {
result = `<${tagName} ${attributes}>${element.textContent}</${tagName}>`;
} else {
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 +232,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 +246,9 @@ DqElement.prototype = {
* @return {String}
*/
get selector() {
if (axe._cache.get('runTypeAOpt')) {
return this.spec.selector || [generateSelectorWithShadow(this.element)];
}
return this.spec.selector || [getSelector(this.element, this._options)];
},

Expand Down
Loading