@afixt/test-utils 2.3.0 → 2.5.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.
@@ -73,12 +73,28 @@ function collectSubtreeText(node, visibleOnly) {
73
73
 
74
74
  // In visibleOnly mode, skip non-rendered content
75
75
  if (visibleOnly) {
76
- if (tag === 'style' || tag === 'script') {
76
+ if (tag === 'style' || tag === 'script' || tag === 'noscript') {
77
77
  continue;
78
78
  }
79
79
  if (child.getAttribute('aria-hidden') === 'true') {
80
80
  continue;
81
81
  }
82
+ if (child.hasAttribute('hidden')) {
83
+ continue;
84
+ }
85
+ // Stop at nested widget containers — their content is not
86
+ // part of the parent element's visible label
87
+ const childRole = child.getAttribute('role');
88
+ if (
89
+ childRole === 'menu' ||
90
+ childRole === 'menubar' ||
91
+ childRole === 'listbox' ||
92
+ childRole === 'tree' ||
93
+ childRole === 'grid' ||
94
+ childRole === 'tablist'
95
+ ) {
96
+ continue;
97
+ }
82
98
  }
83
99
 
84
100
  if (!visibleOnly) {
@@ -103,6 +119,15 @@ function collectSubtreeText(node, visibleOnly) {
103
119
  }
104
120
  continue;
105
121
  }
122
+
123
+ // Elements with role="img" contribute their aria-label
124
+ if (child.getAttribute('role') === 'img' && child.hasAttribute('aria-label')) {
125
+ const ariaLabel = child.getAttribute('aria-label').trim();
126
+ if (ariaLabel) {
127
+ parts.push(ariaLabel);
128
+ }
129
+ continue;
130
+ }
106
131
  }
107
132
 
108
133
  // Recurse into other element children
package/src/index.js CHANGED
@@ -45,6 +45,7 @@ const hasAttribute = require('./hasAttribute.js');
45
45
  // Focus management
46
46
  const getFocusableElements = require('./getFocusableElements.js');
47
47
  const isFocusable = require('./isFocusable.js');
48
+ const detectFocusTrap = require('./detectFocusTrap.js');
48
49
 
49
50
  // Role computation
50
51
  const getComputedRole = require('./getComputedRole.js');
@@ -114,6 +115,7 @@ module.exports = {
114
115
  ...hasAttribute,
115
116
  ...getFocusableElements,
116
117
  ...isFocusable,
118
+ ...detectFocusTrap,
117
119
  ...getComputedRole,
118
120
  getImageText,
119
121
  ...testContrast,
@@ -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
  });