Skip to content

Commit 89ebc61

Browse files
Merge pull request #141 from browserstack/AXE-1684-onestream-optimization
OneStream optimization
2 parents beccabe + 15804f9 commit 89ebc61

File tree

1 file changed

+156
-1
lines changed

1 file changed

+156
-1
lines changed

lib/core/utils/dq-element.js

Lines changed: 156 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,154 @@ function truncate(str, maxLength) {
1818
return str;
1919
}
2020

21+
/**
22+
* Escapes a string for use in CSS selectors
23+
* @param {String} str - The string to escape
24+
* @returns {String} The escaped string
25+
*/
26+
function escapeCSSSelector(str) {
27+
// Use the CSS.escape method if available
28+
if (window.CSS && window.CSS.escape) {
29+
return window.CSS.escape(str);
30+
}
31+
// Simple fallback for browsers that don't support CSS.escape
32+
return str
33+
.replace(/[!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, '\\$&')
34+
.replace(/^\d/, '\\3$& ');
35+
}
36+
function generateSelectorWithShadow(elm) {
37+
const selectors = getShadowSelector(elm);
38+
if (typeof selectors === 'string') {
39+
return selectors;
40+
} else {
41+
// merge selectors of an array with ,
42+
return selectors.join(',').replace(/,$/, '');
43+
}
44+
}
45+
46+
function getShadowSelector(elm) {
47+
if (!elm) {
48+
return '';
49+
}
50+
let doc = (elm.getRootNode && elm.getRootNode()) || document;
51+
// Not a DOCUMENT_FRAGMENT - shadow DOM
52+
if (doc.nodeType !== 11) {
53+
return getFullPathSelector(elm);
54+
}
55+
56+
const stack = [];
57+
while (doc.nodeType === 11) {
58+
if (!doc.host) {
59+
return '';
60+
}
61+
stack.unshift({ elm, doc });
62+
elm = doc.host;
63+
doc = elm.getRootNode();
64+
}
65+
66+
stack.unshift({ elm, doc });
67+
return stack.map(item => getFullPathSelector(item.elm));
68+
}
69+
70+
function getFullPathSelector(elm) {
71+
if (elm.nodeName === 'HTML' || elm.nodeName === 'BODY') {
72+
return elm.nodeName.toLowerCase();
73+
}
74+
75+
if (cache.get('getFullPathSelector') === undefined) {
76+
cache.set('getFullPathSelector', new WeakMap());
77+
}
78+
79+
// Check cache first
80+
const sourceCache = cache.get('getFullPathSelector');
81+
if (sourceCache.has(elm)) {
82+
return sourceCache.get(elm);
83+
}
84+
85+
const element = elm;
86+
const names = [];
87+
while (elm.parentElement && elm.nodeName !== 'BODY') {
88+
if (sourceCache.has(elm)) {
89+
names.unshift(sourceCache.get(elm));
90+
break;
91+
} else if (elm.id) {
92+
// Check if the ID is unique in the document before using it
93+
const escapedId = escapeCSSSelector(elm.getAttribute('id'));
94+
const elementsWithSameId = document.querySelectorAll(`#${escapedId}`);
95+
if (elementsWithSameId.length === 1) {
96+
// ID is unique, safe to use
97+
names.unshift('#' + escapedId);
98+
break;
99+
} else {
100+
// ID is not unique, fallback to position-based selector
101+
let c = 1;
102+
let e = elm;
103+
for (; e.previousElementSibling; e = e.previousElementSibling, c++) {
104+
// Increment counter for each previous sibling
105+
}
106+
names.unshift(`${elm.nodeName.toLowerCase()}:nth-child(${c})`);
107+
}
108+
} else {
109+
let c = 1;
110+
let e = elm;
111+
for (; e.previousElementSibling; e = e.previousElementSibling, c++) {
112+
// Increment counter for each previous sibling
113+
}
114+
names.unshift(`${elm.nodeName.toLowerCase()}:nth-child(${c})`);
115+
}
116+
elm = elm.parentElement;
117+
}
118+
119+
const selector = names.join('>');
120+
sourceCache.set(element, selector);
121+
return selector;
122+
}
123+
124+
function getSourceOpt(element) {
125+
if (!element) {
126+
return '';
127+
}
128+
129+
// Initialize cache if needed
130+
if (cache.get('getSourceEfficient') === undefined) {
131+
cache.set('getSourceEfficient', new WeakMap());
132+
}
133+
134+
// Check cache first
135+
const sourceCache = cache.get('getSourceEfficient');
136+
if (sourceCache.has(element)) {
137+
return sourceCache.get(element);
138+
}
139+
140+
// Compute value if not cached
141+
const tagName = element.nodeName?.toLowerCase();
142+
if (!tagName) {
143+
return '';
144+
}
145+
146+
let result;
147+
try {
148+
const attributes = Array.from(element.attributes || [])
149+
.filter(attr => !attr.name.startsWith('data-percy-'))
150+
.map(attr => `${attr.name}="${attr.value}"`)
151+
.join(' ');
152+
const closingTag = element.children.length ? false : true;
153+
if (closingTag) {
154+
result = `<${tagName} ${attributes}>${element.textContent}</${tagName}>`;
155+
} else {
156+
result = attributes ? `<${tagName} ${attributes}>` : `<${tagName}>`;
157+
}
158+
result = truncate(result, 300); // Truncate to 300 characters
159+
// Store in cache
160+
sourceCache.set(element, result);
161+
} catch (e) {
162+
// Handle potential errors (like accessing attributes on non-element nodes)
163+
result = `<${tagName || 'unknown'}>`;
164+
}
165+
166+
return result;
167+
}
168+
21169
function getSource(element) {
22170
if (!element?.outerHTML) {
23171
return '';
@@ -84,7 +232,11 @@ function DqElement(elm, options = null, spec = {}) {
84232
this.source = null;
85233
// TODO: es-modules_audit
86234
if (!axe._audit.noHtml) {
87-
this.source = this.spec.source ?? getSource(this._element);
235+
if (axe._cache.get('runTypeAOpt')) {
236+
this.source = this.spec.source ?? getSourceOpt(this._element);
237+
} else {
238+
this.source = this.spec.source ?? getSource(this._element);
239+
}
88240
}
89241
}
90242

@@ -94,6 +246,9 @@ DqElement.prototype = {
94246
* @return {String}
95247
*/
96248
get selector() {
249+
if (axe._cache.get('runTypeAOpt')) {
250+
return this.spec.selector || [generateSelectorWithShadow(this.element)];
251+
}
97252
return this.spec.selector || [getSelector(this.element, this._options)];
98253
},
99254

0 commit comments

Comments
 (0)