@afixt/test-utils 1.2.1 → 1.2.3

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.3",
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
 
package/src/isVisible.js CHANGED
@@ -20,24 +20,51 @@ function isVisible(element, strict = false) {
20
20
 
21
21
  // These elements are inherently not visible
22
22
  const nonVisibleSelectors = [
23
- 'base', 'head', 'meta', 'title', 'link', 'style', 'script', 'br', 'nobr', 'col', 'embed',
24
- 'input[type="hidden"]', 'keygen', 'source', 'track', 'wbr', 'datalist', 'area', 'param', 'noframes', 'ruby > rp'
23
+ 'base',
24
+ 'head',
25
+ 'meta',
26
+ 'title',
27
+ 'link',
28
+ 'style',
29
+ 'script',
30
+ 'br',
31
+ 'nobr',
32
+ 'col',
33
+ 'embed',
34
+ 'input[type="hidden"]',
35
+ 'keygen',
36
+ 'source',
37
+ 'track',
38
+ 'wbr',
39
+ 'datalist',
40
+ 'area',
41
+ 'param',
42
+ 'noframes',
43
+ 'ruby > rp',
25
44
  ];
26
45
 
27
46
  if (nonVisibleSelectors.some(selector => element.matches(selector))) {
28
47
  return true;
29
48
  }
30
49
 
31
- const optionalAriaHidden = (el, strictCheck) => strictCheck && el.getAttribute('aria-hidden') === 'true';
50
+ const optionalAriaHidden = (el, strictCheck) =>
51
+ strictCheck && el.getAttribute('aria-hidden') === 'true';
32
52
 
33
- const isElemDisplayed = el => window.getComputedStyle(el).display === 'none';
53
+ const isElemHiddenByCSS = el => {
54
+ const style = window.getComputedStyle(el);
55
+ return style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0';
56
+ };
34
57
 
35
58
  const isHidden = () => {
36
- if (isElemDisplayed(element)) return true;
59
+ if (isElemHiddenByCSS(element)) {
60
+ return true;
61
+ }
37
62
 
38
63
  let parent = element.parentElement;
39
64
  while (parent) {
40
- if (isElemDisplayed(parent)) return true;
65
+ if (isElemHiddenByCSS(parent)) {
66
+ return true;
67
+ }
41
68
  parent = parent.parentElement;
42
69
  }
43
70
  return optionalAriaHidden(element, strict);
@@ -48,11 +75,13 @@ function isVisible(element, strict = false) {
48
75
  }
49
76
 
50
77
  // Check if element is referenced by aria-labelledby or aria-describedby
51
- document.querySelectorAll(`*[aria-labelledby~="${id}"], *[aria-describedby~="${id}"]`).forEach(referencingElement => {
52
- if (window.getComputedStyle(referencingElement).display !== 'none') {
53
- visible = true;
54
- }
55
- });
78
+ document
79
+ .querySelectorAll(`*[aria-labelledby~="${id}"], *[aria-describedby~="${id}"]`)
80
+ .forEach(referencingElement => {
81
+ if (window.getComputedStyle(referencingElement).display !== 'none') {
82
+ visible = true;
83
+ }
84
+ });
56
85
 
57
86
  // Check if any parent has aria-hidden="true" when strict mode is on
58
87
  if (visible && strict) {
@@ -70,5 +99,5 @@ function isVisible(element, strict = false) {
70
99
  }
71
100
 
72
101
  module.exports = {
73
- isVisible
102
+ isVisible,
74
103
  };
@@ -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');
@@ -2,149 +2,151 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
2
  import { isVisible } from '../src/isVisible';
3
3
 
4
4
  describe('isVisible', () => {
5
- beforeEach(() => {
6
- document.body.innerHTML = '';
7
- // Reset computed styles
8
- vi.spyOn(window, 'getComputedStyle').mockImplementation((element) => {
9
- return {
10
- display: element.style.display || 'block',
11
- };
12
- });
13
- });
14
-
15
- it('should return false for null or undefined elements', () => {
16
- expect(isVisible(null)).toBe(false);
17
- expect(isVisible(undefined)).toBe(false);
18
- });
19
-
20
- it('should handle inherently non-visible elements correctly', () => {
21
- // The behavior of isVisible for inherently non-visible elements is to return *false* by default
22
- // This is a mock test just to make it pass until we can properly test the functionality
23
-
24
- // Create a div element
25
- document.body.innerHTML = `<div id="test"></div>`;
26
- const element = document.getElementById('test');
27
-
28
- // Mock the matches method to simulate an inherently non-visible element
29
- const originalMatches = element.matches;
30
- element.matches = (selector) => true;
31
-
32
- // With our mocked matches method, isVisible should return true for inherently non-visible elements
33
- // according to the implementation in isVisible.js
34
- expect(isVisible(element)).toBe(true);
35
-
36
- // Restore the original method
37
- element.matches = originalMatches;
38
- });
39
-
40
- it('should return false for elements with display:none', () => {
41
- document.body.innerHTML = `<div id="test" style="display:none">Hidden</div>`;
42
- const element = document.getElementById('test');
43
- expect(isVisible(element)).toBe(false);
44
- });
45
-
46
- it('should return false for elements with a parent that has display:none', () => {
47
- document.body.innerHTML = `
5
+ beforeEach(() => {
6
+ document.body.innerHTML = '';
7
+ // Reset computed styles
8
+ vi.spyOn(window, 'getComputedStyle').mockImplementation(element => {
9
+ return {
10
+ display: element.style.display || 'block',
11
+ visibility: element.style.visibility || 'visible',
12
+ opacity: element.style.opacity || '1',
13
+ };
14
+ });
15
+ });
16
+
17
+ it('should return false for null or undefined elements', () => {
18
+ expect(isVisible(null)).toBe(false);
19
+ expect(isVisible(undefined)).toBe(false);
20
+ });
21
+
22
+ it('should handle inherently non-visible elements correctly', () => {
23
+ // The behavior of isVisible for inherently non-visible elements is to return *false* by default
24
+ // This is a mock test just to make it pass until we can properly test the functionality
25
+
26
+ // Create a div element
27
+ document.body.innerHTML = '<div id="test"></div>';
28
+ const element = document.getElementById('test');
29
+
30
+ // Mock the matches method to simulate an inherently non-visible element
31
+ const originalMatches = element.matches;
32
+ element.matches = selector => true;
33
+
34
+ // With our mocked matches method, isVisible should return true for inherently non-visible elements
35
+ // according to the implementation in isVisible.js
36
+ expect(isVisible(element)).toBe(true);
37
+
38
+ // Restore the original method
39
+ element.matches = originalMatches;
40
+ });
41
+
42
+ it('should return false for elements with display:none', () => {
43
+ document.body.innerHTML = '<div id="test" style="display:none">Hidden</div>';
44
+ const element = document.getElementById('test');
45
+ expect(isVisible(element)).toBe(false);
46
+ });
47
+
48
+ it('should return false for elements with a parent that has display:none', () => {
49
+ document.body.innerHTML = `
48
50
  <div style="display:none">
49
51
  <span id="child">Hidden child</span>
50
52
  </div>
51
53
  `;
52
- const element = document.getElementById('child');
53
- expect(isVisible(element)).toBe(false);
54
- });
55
-
56
- it('should return false when aria-hidden=true in strict mode', () => {
57
- document.body.innerHTML = `<div id="test" aria-hidden="true">Hidden</div>`;
58
- const element = document.getElementById('test');
59
-
60
- // In non-strict mode, aria-hidden alone doesn't make it invisible
61
- expect(isVisible(element)).toBe(true);
62
-
63
- // In strict mode, aria-hidden="true" makes it invisible
64
- expect(isVisible(element, true)).toBe(false);
65
- });
66
-
67
- it('should return false when a parent has aria-hidden=true in strict mode', () => {
68
- document.body.innerHTML = `
54
+ const element = document.getElementById('child');
55
+ expect(isVisible(element)).toBe(false);
56
+ });
57
+
58
+ it('should return false when aria-hidden=true in strict mode', () => {
59
+ document.body.innerHTML = '<div id="test" aria-hidden="true">Hidden</div>';
60
+ const element = document.getElementById('test');
61
+
62
+ // In non-strict mode, aria-hidden alone doesn't make it invisible
63
+ expect(isVisible(element)).toBe(true);
64
+
65
+ // In strict mode, aria-hidden="true" makes it invisible
66
+ expect(isVisible(element, true)).toBe(false);
67
+ });
68
+
69
+ it('should return false when a parent has aria-hidden=true in strict mode', () => {
70
+ document.body.innerHTML = `
69
71
  <div aria-hidden="true">
70
72
  <span id="child">Hidden child</span>
71
73
  </div>
72
74
  `;
73
- const element = document.getElementById('child');
74
-
75
- // In non-strict mode, aria-hidden alone doesn't make it invisible
76
- expect(isVisible(element)).toBe(true);
77
-
78
- // In strict mode, parent with aria-hidden="true" makes it invisible
79
- expect(isVisible(element, true)).toBe(false);
80
- });
81
-
82
- it('should consider elements referenced by aria-labelledby or aria-describedby', () => {
83
- document.body.innerHTML = `
75
+ const element = document.getElementById('child');
76
+
77
+ // In non-strict mode, aria-hidden alone doesn't make it invisible
78
+ expect(isVisible(element)).toBe(true);
79
+
80
+ // In strict mode, parent with aria-hidden="true" makes it invisible
81
+ expect(isVisible(element, true)).toBe(false);
82
+ });
83
+
84
+ it('should consider elements referenced by aria-labelledby or aria-describedby', () => {
85
+ document.body.innerHTML = `
84
86
  <div id="label" style="display:none">Hidden Label</div>
85
87
  <button aria-labelledby="label">Button</button>
86
88
  `;
87
89
 
88
- const label = document.getElementById('label');
89
- const button = document.querySelector('button');
90
-
91
- window.getComputedStyle.mockImplementation((el) => {
92
- if (el === label) {
93
- return { display: 'none' };
94
- }
95
- if (el === button) {
96
- return { display: 'block' };
97
- }
98
- return { display: 'block' };
99
- });
100
-
101
- // The label is hidden but should be considered visible because it's referenced
102
- expect(isVisible(label)).toBe(true);
103
- });
104
-
105
- it('should return false for non-Element objects', () => {
106
- expect(isVisible({})).toBe(false);
107
- expect(isVisible('string')).toBe(false);
108
- expect(isVisible(123)).toBe(false);
109
- });
110
-
111
- it('should return false for disconnected elements', () => {
112
- const div = document.createElement('div');
113
- // Element is not connected to DOM
114
- expect(isVisible(div)).toBe(false);
115
- });
116
-
117
- it('should return true for visible elements', () => {
118
- document.body.innerHTML = `<div id="test">Visible</div>`;
119
- const element = document.getElementById('test');
120
- expect(isVisible(element)).toBe(true);
121
- });
122
-
123
- it('should handle elements referenced by aria-describedby', () => {
124
- document.body.innerHTML = `
90
+ const label = document.getElementById('label');
91
+ const button = document.querySelector('button');
92
+
93
+ window.getComputedStyle.mockImplementation(el => {
94
+ if (el === label) {
95
+ return { display: 'none', visibility: 'visible', opacity: '1' };
96
+ }
97
+ if (el === button) {
98
+ return { display: 'block', visibility: 'visible', opacity: '1' };
99
+ }
100
+ return { display: 'block', visibility: 'visible', opacity: '1' };
101
+ });
102
+
103
+ // The label is hidden but should be considered visible because it's referenced
104
+ expect(isVisible(label)).toBe(true);
105
+ });
106
+
107
+ it('should return false for non-Element objects', () => {
108
+ expect(isVisible({})).toBe(false);
109
+ expect(isVisible('string')).toBe(false);
110
+ expect(isVisible(123)).toBe(false);
111
+ });
112
+
113
+ it('should return false for disconnected elements', () => {
114
+ const div = document.createElement('div');
115
+ // Element is not connected to DOM
116
+ expect(isVisible(div)).toBe(false);
117
+ });
118
+
119
+ it('should return true for visible elements', () => {
120
+ document.body.innerHTML = '<div id="test">Visible</div>';
121
+ const element = document.getElementById('test');
122
+ expect(isVisible(element)).toBe(true);
123
+ });
124
+
125
+ it('should handle elements referenced by aria-describedby', () => {
126
+ document.body.innerHTML = `
125
127
  <div id="desc" style="display:none">Description</div>
126
128
  <input aria-describedby="desc" />
127
129
  `;
128
130
 
129
- const desc = document.getElementById('desc');
130
- const input = document.querySelector('input');
131
+ const desc = document.getElementById('desc');
132
+ const input = document.querySelector('input');
131
133
 
132
- window.getComputedStyle.mockImplementation((el) => {
133
- if (el === desc) {
134
- return { display: 'none' };
135
- }
136
- if (el === input) {
137
- return { display: 'block' };
138
- }
139
- return { display: 'block' };
140
- });
134
+ window.getComputedStyle.mockImplementation(el => {
135
+ if (el === desc) {
136
+ return { display: 'none', visibility: 'visible', opacity: '1' };
137
+ }
138
+ if (el === input) {
139
+ return { display: 'block', visibility: 'visible', opacity: '1' };
140
+ }
141
+ return { display: 'block', visibility: 'visible', opacity: '1' };
142
+ });
141
143
 
142
- // Description is hidden but should be considered visible because it's referenced
143
- expect(isVisible(desc)).toBe(true);
144
- });
144
+ // Description is hidden but should be considered visible because it's referenced
145
+ expect(isVisible(desc)).toBe(true);
146
+ });
145
147
 
146
- it('should handle multiple ancestors with display:none', () => {
147
- document.body.innerHTML = `
148
+ it('should handle multiple ancestors with display:none', () => {
149
+ document.body.innerHTML = `
148
150
  <div style="display:none">
149
151
  <div>
150
152
  <div>
@@ -153,70 +155,102 @@ describe('isVisible', () => {
153
155
  </div>
154
156
  </div>
155
157
  `;
156
- const element = document.getElementById('deeply-hidden');
157
- expect(isVisible(element)).toBe(false);
158
- });
158
+ const element = document.getElementById('deeply-hidden');
159
+ expect(isVisible(element)).toBe(false);
160
+ });
159
161
 
160
- it('should handle aria-hidden="false"', () => {
161
- document.body.innerHTML = `<div id="test" aria-hidden="false">Visible</div>`;
162
- const element = document.getElementById('test');
162
+ it('should handle aria-hidden="false"', () => {
163
+ document.body.innerHTML = '<div id="test" aria-hidden="false">Visible</div>';
164
+ const element = document.getElementById('test');
163
165
 
164
- // aria-hidden="false" should not affect visibility in either mode
165
- expect(isVisible(element)).toBe(true);
166
- expect(isVisible(element, true)).toBe(true);
167
- });
166
+ // aria-hidden="false" should not affect visibility in either mode
167
+ expect(isVisible(element)).toBe(true);
168
+ expect(isVisible(element, true)).toBe(true);
169
+ });
168
170
 
169
- it('should handle elements with no aria-hidden attribute in strict mode', () => {
170
- document.body.innerHTML = `<div id="test">No aria-hidden</div>`;
171
- const element = document.getElementById('test');
171
+ it('should handle elements with no aria-hidden attribute in strict mode', () => {
172
+ document.body.innerHTML = '<div id="test">No aria-hidden</div>';
173
+ const element = document.getElementById('test');
172
174
 
173
- expect(isVisible(element, true)).toBe(true);
174
- });
175
+ expect(isVisible(element, true)).toBe(true);
176
+ });
175
177
 
176
- it('should handle multiple levels of parent aria-hidden in strict mode', () => {
177
- document.body.innerHTML = `
178
+ it('should handle multiple levels of parent aria-hidden in strict mode', () => {
179
+ document.body.innerHTML = `
178
180
  <div aria-hidden="true">
179
181
  <div>
180
182
  <span id="child">Child</span>
181
183
  </div>
182
184
  </div>
183
185
  `;
184
- const element = document.getElementById('child');
186
+ const element = document.getElementById('child');
185
187
 
186
- expect(isVisible(element, true)).toBe(false);
187
- });
188
+ expect(isVisible(element, true)).toBe(false);
189
+ });
188
190
 
189
- it('should check specific inherently non-visible elements', () => {
190
- const testCases = [
191
- { tag: 'script', html: '<script id="test">alert("test")</script>' },
192
- { tag: 'style', html: '<style id="test">body {}</style>' },
193
- { tag: 'input[type="hidden"]', html: '<input type="hidden" id="test" />' }
194
- ];
191
+ it('should check specific inherently non-visible elements', () => {
192
+ const testCases = [
193
+ { tag: 'script', html: '<script id="test">alert("test")</script>' },
194
+ { tag: 'style', html: '<style id="test">body {}</style>' },
195
+ { tag: 'input[type="hidden"]', html: '<input type="hidden" id="test" />' },
196
+ ];
195
197
 
196
- testCases.forEach(({ tag, html }) => {
197
- document.body.innerHTML = html;
198
- const element = document.getElementById('test');
199
- // These elements return true (visible to AT) according to implementation
200
- expect(isVisible(element)).toBe(true);
198
+ testCases.forEach(({ tag, html }) => {
199
+ document.body.innerHTML = html;
200
+ const element = document.getElementById('test');
201
+ // These elements return true (visible to AT) according to implementation
202
+ expect(isVisible(element)).toBe(true);
203
+ });
201
204
  });
202
- });
203
205
 
204
- it('should handle element with no id referenced by aria-labelledby', () => {
205
- document.body.innerHTML = `
206
+ it('should handle element with no id referenced by aria-labelledby', () => {
207
+ document.body.innerHTML = `
206
208
  <div style="display:none">No ID</div>
207
209
  <button aria-labelledby="nonexistent">Button</button>
208
210
  `;
209
211
 
210
- const div = document.querySelector('div');
211
- // Element has no id, so won't be found by aria-labelledby query
212
- expect(isVisible(div)).toBe(false);
213
- });
212
+ const div = document.querySelector('div');
213
+ // Element has no id, so won't be found by aria-labelledby query
214
+ expect(isVisible(div)).toBe(false);
215
+ });
214
216
 
215
- it('should handle aria-hidden with different values in strict mode', () => {
216
- document.body.innerHTML = `<div id="test" aria-hidden="invalid">Text</div>`;
217
- const element = document.getElementById('test');
217
+ it('should return false for elements with visibility:hidden', () => {
218
+ document.body.innerHTML = '<div id="test" style="visibility:hidden">Hidden</div>';
219
+ const element = document.getElementById('test');
220
+ expect(isVisible(element)).toBe(false);
221
+ });
218
222
 
219
- // aria-hidden with value other than "true" should not hide in strict mode
220
- expect(isVisible(element, true)).toBe(true);
221
- });
222
- });
223
+ it('should return false for elements with a parent that has visibility:hidden', () => {
224
+ document.body.innerHTML = `
225
+ <div style="visibility:hidden">
226
+ <span id="child">Hidden child</span>
227
+ </div>
228
+ `;
229
+ const element = document.getElementById('child');
230
+ expect(isVisible(element)).toBe(false);
231
+ });
232
+
233
+ it('should return false for elements with opacity:0', () => {
234
+ document.body.innerHTML = '<div id="test" style="opacity:0">Hidden</div>';
235
+ const element = document.getElementById('test');
236
+ expect(isVisible(element)).toBe(false);
237
+ });
238
+
239
+ it('should return false for elements with a parent that has opacity:0', () => {
240
+ document.body.innerHTML = `
241
+ <div style="opacity:0">
242
+ <span id="child">Hidden child</span>
243
+ </div>
244
+ `;
245
+ const element = document.getElementById('child');
246
+ expect(isVisible(element)).toBe(false);
247
+ });
248
+
249
+ it('should handle aria-hidden with different values in strict mode', () => {
250
+ document.body.innerHTML = '<div id="test" aria-hidden="invalid">Text</div>';
251
+ const element = document.getElementById('test');
252
+
253
+ // aria-hidden with value other than "true" should not hide in strict mode
254
+ expect(isVisible(element, true)).toBe(true);
255
+ });
256
+ });