@afixt/test-utils 1.2.1 → 1.2.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afixt/test-utils",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Various utilities for accessibility testing",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -5,9 +5,16 @@ const { isEmpty } = require('./stringUtils.js');
5
5
  * Traverses the DOM subtree collecting text from text nodes, img alt attributes,
6
6
  * and input[type="image"] alt attributes.
7
7
  * @param {Element} el - The DOM element.
8
+ * @param {Object} [options] - Configuration options.
9
+ * @param {boolean} [options.visibleOnly=false] - If true, return only visually rendered text,
10
+ * skipping aria-label, img alt, input[type="image"] alt, style, script, and
11
+ * aria-hidden="true" elements. Useful for WCAG 2.5.3 Label in Name comparisons.
8
12
  * @returns {string} The accessible text.
9
13
  */
10
- function getAccessibleText(el) {
14
+ function getAccessibleText(el, options) {
15
+ const opts = options || {};
16
+ const visibleOnly = opts.visibleOnly || false;
17
+
11
18
  if (!el || !(el instanceof Element)) {
12
19
  return '';
13
20
  }
@@ -16,31 +23,32 @@ function getAccessibleText(el) {
16
23
  return '';
17
24
  }
18
25
 
19
- // Check for aria-label first (highest priority)
20
- if (el.hasAttribute('aria-label')) {
21
- const ariaLabel = el.getAttribute('aria-label').trim();
22
- if (ariaLabel) {
23
- return ariaLabel;
26
+ if (!visibleOnly) {
27
+ // Check for aria-label first (highest priority)
28
+ if (el.hasAttribute('aria-label')) {
29
+ const ariaLabel = el.getAttribute('aria-label').trim();
30
+ if (ariaLabel) {
31
+ return ariaLabel;
32
+ }
24
33
  }
25
- }
26
34
 
27
- // Check for img alt text when the element itself is an img
28
- if (el.tagName.toLowerCase() === 'img' && el.hasAttribute('alt')) {
29
- return el.getAttribute('alt').trim();
30
- }
35
+ // Check for img alt text when the element itself is an img
36
+ if (el.tagName.toLowerCase() === 'img' && el.hasAttribute('alt')) {
37
+ return el.getAttribute('alt').trim();
38
+ }
31
39
 
32
- // Check for input[type="image"] alt text when the element itself is one
33
- if (
34
- el.tagName.toLowerCase() === 'input' &&
35
- el.getAttribute('type') === 'image' &&
36
- el.hasAttribute('alt')
37
- ) {
38
- return el.getAttribute('alt').trim();
40
+ // Check for input[type="image"] alt text when the element itself is one
41
+ if (
42
+ el.tagName.toLowerCase() === 'input' &&
43
+ el.getAttribute('type') === 'image' &&
44
+ el.hasAttribute('alt')
45
+ ) {
46
+ return el.getAttribute('alt').trim();
47
+ }
39
48
  }
40
49
 
41
- // Collect accessible text from the subtree, including text nodes
42
- // and alt text from embedded images
43
- const parts = collectSubtreeText(el);
50
+ // Collect text from the subtree
51
+ const parts = collectSubtreeText(el, visibleOnly);
44
52
  return parts.join(' ').replace(/\s+/g, ' ').trim();
45
53
  }
46
54
 
@@ -48,9 +56,10 @@ function getAccessibleText(el) {
48
56
  * Recursively collect accessible text parts from an element's subtree.
49
57
  * Handles text nodes, img alt text, and input[type="image"] alt text.
50
58
  * @param {Node} node - The DOM node to traverse.
59
+ * @param {boolean} [visibleOnly=false] - If true, skip non-visible content.
51
60
  * @returns {string[]} Array of text parts found in the subtree.
52
61
  */
53
- function collectSubtreeText(node) {
62
+ function collectSubtreeText(node, visibleOnly) {
54
63
  const parts = [];
55
64
 
56
65
  for (let child = node.firstChild; child; child = child.nextSibling) {
@@ -62,30 +71,42 @@ function collectSubtreeText(node) {
62
71
  } else if (child.nodeType === Node.ELEMENT_NODE) {
63
72
  const tag = child.tagName.toLowerCase();
64
73
 
65
- // img with non-empty alt contributes its alt text
66
- if (tag === 'img' && child.hasAttribute('alt')) {
67
- const alt = child.getAttribute('alt').trim();
68
- if (alt) {
69
- parts.push(alt);
74
+ // In visibleOnly mode, skip non-rendered content
75
+ if (visibleOnly) {
76
+ if (tag === 'style' || tag === 'script') {
77
+ continue;
78
+ }
79
+ if (child.getAttribute('aria-hidden') === 'true') {
80
+ continue;
70
81
  }
71
- continue;
72
82
  }
73
83
 
74
- // input[type="image"] with non-empty alt contributes its alt text
75
- if (
76
- tag === 'input' &&
77
- child.getAttribute('type') === 'image' &&
78
- child.hasAttribute('alt')
79
- ) {
80
- const alt = child.getAttribute('alt').trim();
81
- if (alt) {
82
- parts.push(alt);
84
+ if (!visibleOnly) {
85
+ // img with non-empty alt contributes its alt text
86
+ if (tag === 'img' && child.hasAttribute('alt')) {
87
+ const alt = child.getAttribute('alt').trim();
88
+ if (alt) {
89
+ parts.push(alt);
90
+ }
91
+ continue;
92
+ }
93
+
94
+ // input[type="image"] with non-empty alt contributes its alt text
95
+ if (
96
+ tag === 'input' &&
97
+ child.getAttribute('type') === 'image' &&
98
+ child.hasAttribute('alt')
99
+ ) {
100
+ const alt = child.getAttribute('alt').trim();
101
+ if (alt) {
102
+ parts.push(alt);
103
+ }
104
+ continue;
83
105
  }
84
- continue;
85
106
  }
86
107
 
87
108
  // Recurse into other element children
88
- parts.push(...collectSubtreeText(child));
109
+ parts.push(...collectSubtreeText(child, visibleOnly));
89
110
  }
90
111
  }
91
112
 
@@ -300,6 +300,99 @@ describe('getAccessibleText', () => {
300
300
  });
301
301
  });
302
302
 
303
+ describe('visibleOnly option', () => {
304
+ it('should skip aria-label when visibleOnly is true', () => {
305
+ const div = document.createElement('div');
306
+ div.setAttribute('aria-label', 'Accessible Label');
307
+ div.textContent = 'Visual Text';
308
+ document.body.appendChild(div);
309
+
310
+ expect(getAccessibleText(div, { visibleOnly: true })).toBe('Visual Text');
311
+ });
312
+
313
+ it('should skip img alt text on the element itself when visibleOnly is true', () => {
314
+ const img = document.createElement('img');
315
+ img.setAttribute('alt', 'Image description');
316
+ img.setAttribute('src', 'test.jpg');
317
+ document.body.appendChild(img);
318
+
319
+ expect(getAccessibleText(img, { visibleOnly: true })).toBe('');
320
+ });
321
+
322
+ it('should skip input[type="image"] alt when visibleOnly is true', () => {
323
+ const input = document.createElement('input');
324
+ input.setAttribute('type', 'image');
325
+ input.setAttribute('alt', 'Search');
326
+ document.body.appendChild(input);
327
+
328
+ expect(getAccessibleText(input, { visibleOnly: true })).toBe('');
329
+ });
330
+
331
+ it('should skip child img alt text when visibleOnly is true', () => {
332
+ const container = document.createElement('div');
333
+ container.innerHTML = 'Click <img src="icon.png" alt="here"> to continue';
334
+ document.body.appendChild(container);
335
+
336
+ const result = getAccessibleText(container, { visibleOnly: true });
337
+ expect(result).toContain('Click');
338
+ expect(result).not.toContain('here');
339
+ expect(result).toContain('to continue');
340
+ });
341
+
342
+ it('should skip style element content when visibleOnly is true', () => {
343
+ const container = document.createElement('div');
344
+ container.innerHTML = '<style>.cls-1{fill:#09141d;}</style>Visible text';
345
+ document.body.appendChild(container);
346
+
347
+ expect(getAccessibleText(container, { visibleOnly: true })).toBe('Visible text');
348
+ });
349
+
350
+ it('should skip script element content when visibleOnly is true', () => {
351
+ const container = document.createElement('div');
352
+ container.innerHTML = '<script>var x = 1;</script>Visible text';
353
+ document.body.appendChild(container);
354
+
355
+ expect(getAccessibleText(container, { visibleOnly: true })).toBe('Visible text');
356
+ });
357
+
358
+ it('should skip aria-hidden="true" elements when visibleOnly is true', () => {
359
+ const container = document.createElement('div');
360
+ container.innerHTML = 'Visible <span aria-hidden="true">Hidden</span> text';
361
+ document.body.appendChild(container);
362
+
363
+ expect(getAccessibleText(container, { visibleOnly: true })).toBe('Visible text');
364
+ });
365
+
366
+ it('should return empty for SVG-only element with style when visibleOnly is true', () => {
367
+ const link = document.createElement('a');
368
+ link.setAttribute('aria-label', 'Code Enforcement');
369
+ link.innerHTML =
370
+ '<svg><defs><style>.cls-1{fill:#09141d;}</style></defs><path d="M25,49"></path></svg>';
371
+ document.body.appendChild(link);
372
+
373
+ expect(getAccessibleText(link, { visibleOnly: true })).toBe('');
374
+ });
375
+
376
+ it('should still include style/script content when visibleOnly is false (default)', () => {
377
+ const container = document.createElement('div');
378
+ container.innerHTML = '<style>.cls{color:red;}</style>Visible';
379
+ document.body.appendChild(container);
380
+
381
+ const result = getAccessibleText(container);
382
+ expect(result).toContain('.cls{color:red;}');
383
+ expect(result).toContain('Visible');
384
+ });
385
+
386
+ it('should behave normally when options are not provided', () => {
387
+ const div = document.createElement('div');
388
+ div.setAttribute('aria-label', 'Label');
389
+ div.textContent = 'Text';
390
+ document.body.appendChild(div);
391
+
392
+ expect(getAccessibleText(div)).toBe('Label');
393
+ });
394
+ });
395
+
303
396
  describe('special cases', () => {
304
397
  it('should handle elements with only child elements and no text', () => {
305
398
  const container = document.createElement('div');