@afixt/test-utils 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +2 -1
- package/package.json +1 -1
- package/src/cssUtils.js +116 -0
- package/test/cssUtils.test.js +252 -0
package/package.json
CHANGED
package/src/cssUtils.js
CHANGED
|
@@ -3,6 +3,53 @@
|
|
|
3
3
|
* @module cssUtils
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const { hasCSSGeneratedContent } = require('./hasCSSGeneratedContent.js');
|
|
7
|
+
|
|
8
|
+
const ICON_FONT_CLASS_PREFIXES = ['icon-', 'i-'];
|
|
9
|
+
const ICON_FONT_CLASSES = [
|
|
10
|
+
'fa',
|
|
11
|
+
'fas',
|
|
12
|
+
'far',
|
|
13
|
+
'fal',
|
|
14
|
+
'fad',
|
|
15
|
+
'fab',
|
|
16
|
+
'glyphicon',
|
|
17
|
+
'material-icons',
|
|
18
|
+
'material-symbols-outlined',
|
|
19
|
+
'material-symbols-rounded',
|
|
20
|
+
'dashicons',
|
|
21
|
+
'bi',
|
|
22
|
+
'octicon',
|
|
23
|
+
'feather',
|
|
24
|
+
'fi',
|
|
25
|
+
'typcn',
|
|
26
|
+
'wi',
|
|
27
|
+
'zmdi',
|
|
28
|
+
'icofont',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if a single class name matches known icon font patterns.
|
|
33
|
+
* @param {string} cls - The class name to check
|
|
34
|
+
* @returns {boolean} True if the class matches an icon font pattern
|
|
35
|
+
*/
|
|
36
|
+
function matchesIconFontPattern(cls) {
|
|
37
|
+
if (ICON_FONT_CLASSES.includes(cls)) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
for (const prefix of ICON_FONT_CLASS_PREFIXES) {
|
|
41
|
+
if (cls.startsWith(prefix)) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
for (const iconClass of ICON_FONT_CLASSES) {
|
|
46
|
+
if (cls.startsWith(iconClass + '-')) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
6
53
|
const cssUtils = {
|
|
7
54
|
/**
|
|
8
55
|
* Check if an element's background is transparent/none.
|
|
@@ -29,12 +76,81 @@ const cssUtils = {
|
|
|
29
76
|
return style.borderStyle === 'none' || parseFloat(style.borderWidth) === 0;
|
|
30
77
|
},
|
|
31
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Check if the element or any descendant has a class matching common icon font patterns.
|
|
81
|
+
* @param {HTMLElement} element - The element to check
|
|
82
|
+
* @returns {boolean} True if an icon font class is found
|
|
83
|
+
*/
|
|
84
|
+
hasIconFontClass(element) {
|
|
85
|
+
if (!element) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const elements = [element, ...element.querySelectorAll('*')];
|
|
90
|
+
for (const el of elements) {
|
|
91
|
+
if (el.classList) {
|
|
92
|
+
for (const cls of el.classList) {
|
|
93
|
+
if (matchesIconFontPattern(cls)) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if the element has any visual indicator beyond text styling,
|
|
104
|
+
* such as CSS-generated content, icon font classes, background images on children,
|
|
105
|
+
* or inline img/svg elements.
|
|
106
|
+
* @param {HTMLElement} element - The element to check
|
|
107
|
+
* @returns {boolean} True if the element has a visual indicator
|
|
108
|
+
*/
|
|
109
|
+
hasVisualIndicator(element) {
|
|
110
|
+
if (!element) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check for CSS-generated content on the element itself
|
|
115
|
+
if (hasCSSGeneratedContent(element)) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Check for icon font classes on element or descendants
|
|
120
|
+
if (cssUtils.hasIconFontClass(element)) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check descendants for background-image, <img>, or <svg>
|
|
125
|
+
const descendants = element.querySelectorAll('*');
|
|
126
|
+
for (const child of descendants) {
|
|
127
|
+
const tagName = child.tagName ? child.tagName.toLowerCase() : '';
|
|
128
|
+
if (tagName === 'img' || tagName === 'svg') {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const childStyle = window.getComputedStyle(child);
|
|
133
|
+
if (childStyle.backgroundImage && childStyle.backgroundImage !== 'none') {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
// skip if getComputedStyle fails
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return false;
|
|
142
|
+
},
|
|
143
|
+
|
|
32
144
|
/**
|
|
33
145
|
* Check if an element looks like regular text (no underline, background, or border).
|
|
34
146
|
* @param {HTMLElement} element - The element to check
|
|
35
147
|
* @returns {boolean} True if element looks like regular text
|
|
36
148
|
*/
|
|
37
149
|
looksLikeText(element) {
|
|
150
|
+
if (cssUtils.hasVisualIndicator(element)) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
38
154
|
const style = window.getComputedStyle(element);
|
|
39
155
|
|
|
40
156
|
const noUnderline = !style.textDecoration.toLowerCase().includes('underline');
|
package/test/cssUtils.test.js
CHANGED
|
@@ -245,4 +245,256 @@ describe('cssUtils', () => {
|
|
|
245
245
|
vi.restoreAllMocks();
|
|
246
246
|
});
|
|
247
247
|
});
|
|
248
|
+
|
|
249
|
+
describe('hasIconFontClass', () => {
|
|
250
|
+
it('should return false for element with no classes', () => {
|
|
251
|
+
const el = document.createElement('span');
|
|
252
|
+
document.body.appendChild(el);
|
|
253
|
+
expect(cssUtils.hasIconFontClass(el)).toBe(false);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should return true for element with "fa" class', () => {
|
|
257
|
+
const el = document.createElement('i');
|
|
258
|
+
el.className = 'fa';
|
|
259
|
+
document.body.appendChild(el);
|
|
260
|
+
expect(cssUtils.hasIconFontClass(el)).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should return true for element with "fa-circle" class (hyphenated prefix)', () => {
|
|
264
|
+
const el = document.createElement('i');
|
|
265
|
+
el.className = 'fa-circle';
|
|
266
|
+
document.body.appendChild(el);
|
|
267
|
+
expect(cssUtils.hasIconFontClass(el)).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should return true for child element with "material-icons" class', () => {
|
|
271
|
+
const el = document.createElement('button');
|
|
272
|
+
const child = document.createElement('span');
|
|
273
|
+
child.className = 'material-icons';
|
|
274
|
+
el.appendChild(child);
|
|
275
|
+
document.body.appendChild(el);
|
|
276
|
+
expect(cssUtils.hasIconFontClass(el)).toBe(true);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should return true for child with "icon-" prefixed class', () => {
|
|
280
|
+
const el = document.createElement('a');
|
|
281
|
+
const child = document.createElement('span');
|
|
282
|
+
child.className = 'icon-home';
|
|
283
|
+
el.appendChild(child);
|
|
284
|
+
document.body.appendChild(el);
|
|
285
|
+
expect(cssUtils.hasIconFontClass(el)).toBe(true);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should return false for element with unrelated class', () => {
|
|
289
|
+
const el = document.createElement('div');
|
|
290
|
+
el.className = 'container main-wrapper';
|
|
291
|
+
document.body.appendChild(el);
|
|
292
|
+
expect(cssUtils.hasIconFontClass(el)).toBe(false);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe('hasVisualIndicator', () => {
|
|
297
|
+
it('should return false for plain text element', () => {
|
|
298
|
+
const el = document.createElement('span');
|
|
299
|
+
el.textContent = 'Click here';
|
|
300
|
+
document.body.appendChild(el);
|
|
301
|
+
|
|
302
|
+
vi.spyOn(window, 'getComputedStyle').mockImplementation(() => ({
|
|
303
|
+
getPropertyValue: () => 'none',
|
|
304
|
+
}));
|
|
305
|
+
|
|
306
|
+
expect(cssUtils.hasVisualIndicator(el)).toBe(false);
|
|
307
|
+
vi.restoreAllMocks();
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('should return true when element has CSS-generated content', () => {
|
|
311
|
+
const el = document.createElement('span');
|
|
312
|
+
document.body.appendChild(el);
|
|
313
|
+
|
|
314
|
+
vi.spyOn(window, 'getComputedStyle').mockImplementation((_el, pseudo) => {
|
|
315
|
+
if (pseudo === '::before') {
|
|
316
|
+
return { getPropertyValue: () => '"\\f00c"' };
|
|
317
|
+
}
|
|
318
|
+
return { getPropertyValue: () => 'none' };
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
expect(cssUtils.hasVisualIndicator(el)).toBe(true);
|
|
322
|
+
vi.restoreAllMocks();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should return true when child has icon font class', () => {
|
|
326
|
+
const el = document.createElement('button');
|
|
327
|
+
const icon = document.createElement('i');
|
|
328
|
+
icon.className = 'fas fa-check';
|
|
329
|
+
el.appendChild(icon);
|
|
330
|
+
document.body.appendChild(el);
|
|
331
|
+
|
|
332
|
+
vi.spyOn(window, 'getComputedStyle').mockImplementation(() => ({
|
|
333
|
+
getPropertyValue: () => 'none',
|
|
334
|
+
backgroundImage: 'none',
|
|
335
|
+
}));
|
|
336
|
+
|
|
337
|
+
expect(cssUtils.hasVisualIndicator(el)).toBe(true);
|
|
338
|
+
vi.restoreAllMocks();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('should return true when child has background-image', () => {
|
|
342
|
+
const el = document.createElement('a');
|
|
343
|
+
const child = document.createElement('span');
|
|
344
|
+
el.appendChild(child);
|
|
345
|
+
document.body.appendChild(el);
|
|
346
|
+
|
|
347
|
+
vi.spyOn(window, 'getComputedStyle').mockImplementation((_el, pseudo) => {
|
|
348
|
+
if (pseudo) {
|
|
349
|
+
return { getPropertyValue: () => 'none' };
|
|
350
|
+
}
|
|
351
|
+
if (_el === child) {
|
|
352
|
+
return {
|
|
353
|
+
getPropertyValue: () => 'none',
|
|
354
|
+
backgroundImage: 'url(icon.png)',
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
getPropertyValue: () => 'none',
|
|
359
|
+
backgroundImage: 'none',
|
|
360
|
+
};
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
expect(cssUtils.hasVisualIndicator(el)).toBe(true);
|
|
364
|
+
vi.restoreAllMocks();
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('should return true when child is an <img> element', () => {
|
|
368
|
+
const el = document.createElement('a');
|
|
369
|
+
const img = document.createElement('img');
|
|
370
|
+
img.src = 'icon.png';
|
|
371
|
+
el.appendChild(img);
|
|
372
|
+
document.body.appendChild(el);
|
|
373
|
+
|
|
374
|
+
vi.spyOn(window, 'getComputedStyle').mockImplementation(() => ({
|
|
375
|
+
getPropertyValue: () => 'none',
|
|
376
|
+
backgroundImage: 'none',
|
|
377
|
+
}));
|
|
378
|
+
|
|
379
|
+
expect(cssUtils.hasVisualIndicator(el)).toBe(true);
|
|
380
|
+
vi.restoreAllMocks();
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('should return true when child is an <svg> element', () => {
|
|
384
|
+
const el = document.createElement('button');
|
|
385
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
386
|
+
el.appendChild(svg);
|
|
387
|
+
document.body.appendChild(el);
|
|
388
|
+
|
|
389
|
+
vi.spyOn(window, 'getComputedStyle').mockImplementation(() => ({
|
|
390
|
+
getPropertyValue: () => 'none',
|
|
391
|
+
backgroundImage: 'none',
|
|
392
|
+
}));
|
|
393
|
+
|
|
394
|
+
expect(cssUtils.hasVisualIndicator(el)).toBe(true);
|
|
395
|
+
vi.restoreAllMocks();
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
describe('looksLikeText - visual indicator integration', () => {
|
|
400
|
+
it('should return false for element with Font Awesome icon child', () => {
|
|
401
|
+
const el = document.createElement('a');
|
|
402
|
+
const icon = document.createElement('i');
|
|
403
|
+
icon.className = 'fas fa-arrow-right';
|
|
404
|
+
el.appendChild(icon);
|
|
405
|
+
el.appendChild(document.createTextNode(' Next'));
|
|
406
|
+
document.body.appendChild(el);
|
|
407
|
+
|
|
408
|
+
vi.spyOn(window, 'getComputedStyle').mockImplementation((_el, pseudo) => {
|
|
409
|
+
if (pseudo) {
|
|
410
|
+
return { getPropertyValue: () => 'none' };
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
textDecoration: 'none',
|
|
414
|
+
backgroundColor: 'rgba(0, 0, 0, 0)',
|
|
415
|
+
backgroundImage: 'none',
|
|
416
|
+
borderStyle: 'none',
|
|
417
|
+
borderWidth: '0px',
|
|
418
|
+
getPropertyValue: () => 'none',
|
|
419
|
+
};
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
expect(cssUtils.looksLikeText(el)).toBe(false);
|
|
423
|
+
vi.restoreAllMocks();
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('should return false for element with CSS pseudo-element content', () => {
|
|
427
|
+
const el = document.createElement('a');
|
|
428
|
+
el.textContent = 'Submit';
|
|
429
|
+
document.body.appendChild(el);
|
|
430
|
+
|
|
431
|
+
vi.spyOn(window, 'getComputedStyle').mockImplementation((_el, pseudo) => {
|
|
432
|
+
if (pseudo === '::before') {
|
|
433
|
+
return { getPropertyValue: () => '"\\f00c"' };
|
|
434
|
+
}
|
|
435
|
+
if (pseudo === '::after') {
|
|
436
|
+
return { getPropertyValue: () => 'none' };
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
textDecoration: 'none',
|
|
440
|
+
backgroundColor: 'rgba(0, 0, 0, 0)',
|
|
441
|
+
backgroundImage: 'none',
|
|
442
|
+
borderStyle: 'none',
|
|
443
|
+
borderWidth: '0px',
|
|
444
|
+
getPropertyValue: () => 'none',
|
|
445
|
+
};
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
expect(cssUtils.looksLikeText(el)).toBe(false);
|
|
449
|
+
vi.restoreAllMocks();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('should return false for element with inline <svg> icon child', () => {
|
|
453
|
+
const el = document.createElement('button');
|
|
454
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
455
|
+
el.appendChild(svg);
|
|
456
|
+
el.appendChild(document.createTextNode('Save'));
|
|
457
|
+
document.body.appendChild(el);
|
|
458
|
+
|
|
459
|
+
vi.spyOn(window, 'getComputedStyle').mockImplementation((_el, pseudo) => {
|
|
460
|
+
if (pseudo) {
|
|
461
|
+
return { getPropertyValue: () => 'none' };
|
|
462
|
+
}
|
|
463
|
+
return {
|
|
464
|
+
textDecoration: 'none',
|
|
465
|
+
backgroundColor: 'rgba(0, 0, 0, 0)',
|
|
466
|
+
backgroundImage: 'none',
|
|
467
|
+
borderStyle: 'none',
|
|
468
|
+
borderWidth: '0px',
|
|
469
|
+
getPropertyValue: () => 'none',
|
|
470
|
+
};
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
expect(cssUtils.looksLikeText(el)).toBe(false);
|
|
474
|
+
vi.restoreAllMocks();
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('should still return true for plain text element with no visual indicators', () => {
|
|
478
|
+
const el = document.createElement('span');
|
|
479
|
+
el.textContent = 'Just text';
|
|
480
|
+
document.body.appendChild(el);
|
|
481
|
+
|
|
482
|
+
vi.spyOn(window, 'getComputedStyle').mockImplementation((_el, pseudo) => {
|
|
483
|
+
if (pseudo) {
|
|
484
|
+
return { getPropertyValue: () => 'none' };
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
textDecoration: 'none',
|
|
488
|
+
backgroundColor: 'rgba(0, 0, 0, 0)',
|
|
489
|
+
backgroundImage: 'none',
|
|
490
|
+
borderStyle: 'none',
|
|
491
|
+
borderWidth: '0px',
|
|
492
|
+
getPropertyValue: () => 'none',
|
|
493
|
+
};
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
expect(cssUtils.looksLikeText(el)).toBe(true);
|
|
497
|
+
vi.restoreAllMocks();
|
|
498
|
+
});
|
|
499
|
+
});
|
|
248
500
|
});
|