@afixt/test-utils 2.1.0 → 2.2.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.
@@ -1,5 +1,6 @@
1
1
  const { NON_VISIBLE_SELECTORS } = require('./constants.js');
2
2
  const isHidden = require('./isHidden.js');
3
+ const { cssEscape } = require('./domUtils.js');
3
4
 
4
5
  /**
5
6
  * Checks if an element is visible to assistive technologies (AT).
@@ -67,8 +68,11 @@ function isA11yVisible(element, strict = false) {
67
68
  }
68
69
 
69
70
  // Check if element is referenced by aria-labelledby or aria-describedby
71
+ // Use CSS.escape(id) to handle IDs containing special characters like colons
72
+ // (e.g. Radix UI generates IDs like "radix-:rj:-tab-account")
73
+ const escapedId = id ? cssEscape(id) : '';
70
74
  document
71
- .querySelectorAll(`*[aria-labelledby~="${id}"], *[aria-describedby~="${id}"]`)
75
+ .querySelectorAll(`*[aria-labelledby~="${escapedId}"], *[aria-describedby~="${escapedId}"]`)
72
76
  .forEach(referencingElement => {
73
77
  if (window.getComputedStyle(referencingElement).display !== 'none') {
74
78
  visible = true;
package/src/isHidden.js CHANGED
@@ -18,15 +18,22 @@ const isHidden = (element, options = {}) => {
18
18
 
19
19
  const { checkAriaHidden = false, checkOpacity = false, checkDimensions = false } = options;
20
20
 
21
- // Check the hidden attribute
22
- if (element.hasAttribute('hidden')) {
21
+ // Check the hidden attribute — but not hidden="until-found", which is
22
+ // visually hidden but still findable by Find-in-Page and accessible to AT.
23
+ // In real browsers, hidden="until-found" uses content-visibility:hidden
24
+ // rather than display:none.
25
+ const hiddenAttr = element.getAttribute('hidden');
26
+ const isUntilFound = hiddenAttr === 'until-found';
27
+ if (hiddenAttr !== null && !isUntilFound) {
23
28
  return true;
24
29
  }
25
30
 
26
- // Use computed style to catch CSS class/stylesheet rules
31
+ // Use computed style to catch CSS class/stylesheet rules.
32
+ // Skip display:none check for hidden="until-found" because JSDOM
33
+ // incorrectly applies display:none (real browsers don't).
27
34
  const style = window.getComputedStyle(element);
28
35
 
29
- if (style.display === 'none') {
36
+ if (style.display === 'none' && !isUntilFound) {
30
37
  return true;
31
38
  }
32
39
 
@@ -2,9 +2,12 @@ const { GENERIC_TITLES, GENERIC_LINK_TEXT } = require('./constants.js');
2
2
 
3
3
  const stringUtils = (function () {
4
4
  /**
5
- * Check if a string is empty or only contains whitespace.
5
+ * Check if a string is empty or only contains standard whitespace.
6
+ * NOTE: For accessibility name/label checks, prefer {@link isEmptyOrWhitespace}
7
+ * which also catches non-breaking spaces (\u00A0), zero-width spaces (\u200B),
8
+ * and other invisible Unicode characters that do not constitute meaningful text.
6
9
  * @param {string} str - The string to check.
7
- * @returns {boolean} Whether the string is empty.
10
+ * @returns {boolean} Whether the string is empty or whitespace-only.
8
11
  */
9
12
  function isEmpty(str) {
10
13
  return !str || str.trim().length === 0;
@@ -45,6 +45,40 @@ function hasBackgroundImage(el) {
45
45
  return false;
46
46
  }
47
47
 
48
+ /**
49
+ * Check if an element or any of its ancestors has a CSS filter or mix-blend-mode
50
+ * that would alter effective colors in ways that cannot be computed from CSS values alone.
51
+ * @param {Element} el - The element to check
52
+ * @returns {boolean} True if the element or an ancestor has a color-altering CSS property
53
+ */
54
+ function hasEffectColorModifier(el) {
55
+ if (!el || el.nodeType === 9 || !window.getComputedStyle) {
56
+ return false;
57
+ }
58
+
59
+ let current = el;
60
+ while (current && current.nodeType !== 9) {
61
+ const styles = window.getComputedStyle(current);
62
+ if (!styles) {
63
+ return false;
64
+ }
65
+
66
+ const filter = styles.getPropertyValue('filter');
67
+ if (filter && filter !== 'none') {
68
+ return true;
69
+ }
70
+
71
+ const mixBlendMode = styles.getPropertyValue('mix-blend-mode');
72
+ if (mixBlendMode && mixBlendMode !== 'normal') {
73
+ return true;
74
+ }
75
+
76
+ current = current.parentElement;
77
+ }
78
+
79
+ return false;
80
+ }
81
+
48
82
  /**
49
83
  * Get the computed background color for an element.
50
84
  * @param {Element} el - the element to be tested
@@ -290,7 +324,13 @@ function testContrast(el, options = { level: 'AA' }) {
290
324
  // Skip elements with a background image on the element itself or a visible ancestor.
291
325
  // Contrast cannot be reliably tested against background images.
292
326
  if (hasBackgroundImage(el)) {
293
- return true;
327
+ return null;
328
+ }
329
+
330
+ // Skip elements affected by CSS filters or mix-blend-mode (self or ancestor).
331
+ // These alter effective colors in ways that cannot be computed from CSS values alone.
332
+ if (hasEffectColorModifier(el)) {
333
+ return null;
294
334
  }
295
335
 
296
336
  const styles = window.getComputedStyle(el);
@@ -400,6 +440,7 @@ function testContrast(el, options = { level: 'AA' }) {
400
440
  module.exports = {
401
441
  testContrast,
402
442
  hasBackgroundImage,
443
+ hasEffectColorModifier,
403
444
  getComputedBackgroundColor,
404
445
  luminance,
405
446
  parseRGB,
@@ -552,6 +552,26 @@ describe('domUtils', () => {
552
552
  const child = document.getElementById('child');
553
553
  expect(domUtils.isHiddenFromAT(child)).toBe(false);
554
554
  });
555
+
556
+ it('should return true when element has inert attribute', () => {
557
+ document.body.innerHTML = '<div inert><span id="child">Inert child</span></div>';
558
+ const child = document.getElementById('child');
559
+ expect(domUtils.isHiddenFromAT(child)).toBe(true);
560
+ });
561
+
562
+ it('should return true when element itself is inert', () => {
563
+ const el = document.createElement('div');
564
+ el.setAttribute('inert', '');
565
+ document.body.appendChild(el);
566
+ expect(domUtils.isHiddenFromAT(el)).toBe(true);
567
+ });
568
+
569
+ it('should return true when element has content-visibility hidden', () => {
570
+ document.body.innerHTML = '<div id="target">Content</div>';
571
+ const el = document.getElementById('target');
572
+ el.style.contentVisibility = 'hidden';
573
+ expect(domUtils.isHiddenFromAT(el)).toBe(true);
574
+ });
555
575
  });
556
576
 
557
577
  describe('isEffectivelyInteractive', () => {
@@ -120,6 +120,24 @@ describe('formUtils', () => {
120
120
  expect(formUtils.hasExplicitAccessibleName(el)).toBe(false);
121
121
  });
122
122
 
123
+ it('should return true when aria-labelledby points to sr-only element', () => {
124
+ document.body.innerHTML = `
125
+ <span id="lbl" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)">SR-only Label</span>
126
+ <div id="target" aria-labelledby="lbl"></div>
127
+ `;
128
+ const target = document.getElementById('target');
129
+ expect(formUtils.hasExplicitAccessibleName(target)).toBe(true);
130
+ });
131
+
132
+ it('should return false when aria-labelledby points to aria-hidden="true" element', () => {
133
+ document.body.innerHTML = `
134
+ <span id="lbl" aria-hidden="true">Hidden Label</span>
135
+ <div id="target" aria-labelledby="lbl"></div>
136
+ `;
137
+ const target = document.getElementById('target');
138
+ expect(formUtils.hasExplicitAccessibleName(target)).toBe(false);
139
+ });
140
+
123
141
  it('should handle multiple ids in aria-labelledby', () => {
124
142
  document.body.innerHTML = `
125
143
  <span id="lbl1"></span>
@@ -467,4 +467,48 @@ describe('getAccessibleName', () => {
467
467
  });
468
468
  });
469
469
  });
470
+
471
+ describe('whitespace-only accessible name handling', () => {
472
+ it('should ignore aria-label with only non-breaking spaces and use text content instead', () => {
473
+ document.body.innerHTML = '<button aria-label="\u00A0\u00A0">Click</button>';
474
+ const button = document.querySelector('button');
475
+ // NBSP-only aria-label is treated as empty; falls through to text content
476
+ expect(getAccessibleName(button)).toBe('Click');
477
+ });
478
+
479
+ it('should ignore aria-label with only zero-width spaces and use text content instead', () => {
480
+ document.body.innerHTML = '<button aria-label="\u200B\u200B">Click</button>';
481
+ const button = document.querySelector('button');
482
+ expect(getAccessibleName(button)).toBe('Click');
483
+ });
484
+
485
+ it('should return false when aria-label is NBSP-only and element has no other name source', () => {
486
+ document.body.innerHTML = '<div role="button" aria-label="\u00A0"></div>';
487
+ const div = document.querySelector('[role="button"]');
488
+ expect(getAccessibleName(div)).toBe(false);
489
+ });
490
+ });
491
+
492
+ describe('visibility handling for aria-labelledby referenced elements', () => {
493
+ it('should return accessible name for display:none element referenced by aria-labelledby', () => {
494
+ document.body.innerHTML = `
495
+ <li style="display:none">
496
+ <button id="expand-btn">Expand subnavigation</button>
497
+ </li>
498
+ <ul aria-labelledby="expand-btn">Items</ul>
499
+ `;
500
+ const button = document.getElementById('expand-btn');
501
+ expect(getAccessibleName(button)).toBe('Expand subnavigation');
502
+ });
503
+
504
+ it('should return false for truly hidden element not referenced by aria-labelledby', () => {
505
+ document.body.innerHTML = `
506
+ <div aria-hidden="true">
507
+ <button id="hidden-btn">Hidden button</button>
508
+ </div>
509
+ `;
510
+ const button = document.getElementById('hidden-btn');
511
+ expect(getAccessibleName(button)).toBe(false);
512
+ });
513
+ });
470
514
  });
@@ -2,179 +2,251 @@ import { describe, it, expect, beforeEach } from 'vitest';
2
2
  import { getComputedRole, roleMapping } from '../src/getComputedRole.js';
3
3
 
4
4
  describe('getComputedRole', () => {
5
- beforeEach(() => {
6
- document.body.innerHTML = '';
7
- });
8
-
9
- it('should return undefined for null or undefined element', () => {
10
- expect(getComputedRole(null)).toBeUndefined();
11
- expect(getComputedRole(undefined)).toBeUndefined();
12
- });
13
-
14
- it('should return the role attribute value if present', () => {
15
- // Arrange
16
- const element = document.createElement('div');
17
- element.setAttribute('role', 'button');
18
- document.body.appendChild(element);
19
-
20
- // Act
21
- const result = getComputedRole(element);
22
-
23
- // Assert
24
- expect(result).toBe('button');
25
- });
26
-
27
- it('should return the correct role for basic elements with simple string mapping', () => {
28
- // Test cases for elements with simple string mapping
29
- const testCases = [
30
- { tag: 'div', expectedRole: 'group' },
31
- { tag: 'p', expectedRole: 'text' },
32
- { tag: 'ul', expectedRole: 'list' },
33
- { tag: 'h1', expectedRole: 'heading' },
34
- { tag: 'button', expectedRole: 'button' },
35
- { tag: 'nav', expectedRole: 'navigation' },
36
- { tag: 'header', expectedRole: 'banner' },
37
- { tag: 'footer', expectedRole: 'contentinfo' },
38
- { tag: 'main', expectedRole: 'main' }
39
- ];
40
-
41
- testCases.forEach(({ tag, expectedRole }) => {
42
- // Arrange
43
- const element = document.createElement(tag);
44
- document.body.appendChild(element);
45
-
46
- // Act
47
- const result = getComputedRole(element);
48
-
49
- // Assert
50
- expect(result).toBe(expectedRole);
51
- });
52
- });
53
-
54
- it('should handle link elements (a) correctly based on href attribute', () => {
55
- // Arrange
56
- const linkWithHref = document.createElement('a');
57
- linkWithHref.setAttribute('href', 'https://example.com');
58
-
59
- const linkWithoutHref = document.createElement('a');
60
-
61
- document.body.appendChild(linkWithHref);
62
- document.body.appendChild(linkWithoutHref);
63
-
64
- // Act
65
- const withHrefResult = getComputedRole(linkWithHref);
66
- const withoutHrefResult = getComputedRole(linkWithoutHref);
67
-
68
- // Assert
69
- expect(withHrefResult).toBe('link');
70
- expect(withoutHrefResult).toBe('text');
71
- });
72
-
73
- it('should handle image elements based on alt attribute', () => {
74
- // Arrange
75
- const imgWithAlt = document.createElement('img');
76
- imgWithAlt.setAttribute('alt', 'Description of image');
77
-
78
- const imgWithEmptyAlt = document.createElement('img');
79
- imgWithEmptyAlt.setAttribute('alt', '');
80
-
81
- document.body.appendChild(imgWithAlt);
82
- document.body.appendChild(imgWithEmptyAlt);
83
-
84
- // Act
85
- const withAltResult = getComputedRole(imgWithAlt);
86
- const withEmptyAltResult = getComputedRole(imgWithEmptyAlt);
87
-
88
- // Assert
89
- expect(withAltResult).toBe('image');
90
- expect(withEmptyAltResult).toBe('presentation');
91
- });
92
-
93
- it('should handle various input types correctly', () => {
94
- const inputTypes = [
95
- { type: 'button', expectedRole: 'button' },
96
- { type: 'checkbox', expectedRole: 'checkbox' },
97
- { type: 'hidden', expectedRole: 'noRole' },
98
- { type: 'image', expectedRole: 'button' },
99
- { type: 'number', expectedRole: 'spinbutton' },
100
- { type: 'radio', expectedRole: 'radio' },
101
- { type: 'range', expectedRole: 'slider' },
102
- { type: 'reset', expectedRole: 'button' },
103
- { type: 'submit', expectedRole: 'button' },
104
- { type: 'password', expectedRole: 'textbox' },
105
- { type: 'text', expectedRole: 'textbox' },
106
- { type: 'search', expectedRole: 'searchbox' }
107
- ];
108
-
109
- inputTypes.forEach(({ type, expectedRole }) => {
110
- // Arrange
111
- const input = document.createElement('input');
112
- input.setAttribute('type', type);
113
- document.body.appendChild(input);
114
-
115
- // Act
116
- const result = getComputedRole(input);
117
-
118
- // Assert
119
- expect(result).toBe(expectedRole);
120
- });
121
- });
122
-
123
- it('should handle elements with no role mapping', () => {
124
- // Create a custom element that's not in the roleMapping
125
- const customElement = document.createElement('custom-element');
126
- document.body.appendChild(customElement);
127
-
128
- // Act
129
- const result = getComputedRole(customElement);
130
-
131
- // Assert
132
- expect(result).toBeUndefined();
133
- });
134
-
135
- it('should prioritize role attribute over computed role', () => {
136
- // Arrange
137
- const element = document.createElement('button'); // Inherent role is 'button'
138
- element.setAttribute('role', 'menuitem'); // Override with custom role
139
- document.body.appendChild(element);
140
-
141
- // Act
142
- const result = getComputedRole(element);
143
-
144
- // Assert
145
- expect(result).toBe('menuitem'); // Should use the explicit role
146
- });
147
-
148
- it('should handle empty or whitespace role attribute', () => {
149
- // Arrange
150
- const emptyRoleElement = document.createElement('div');
151
- emptyRoleElement.setAttribute('role', '');
152
-
153
- const whitespaceRoleElement = document.createElement('div');
154
- whitespaceRoleElement.setAttribute('role', ' ');
155
-
156
- document.body.appendChild(emptyRoleElement);
157
- document.body.appendChild(whitespaceRoleElement);
158
-
159
- // Act
160
- const emptyResult = getComputedRole(emptyRoleElement);
161
- const whitespaceResult = getComputedRole(whitespaceRoleElement);
162
-
163
- // Assert
164
- // The implementation returns the fallback 'group' role for div when the role attribute is empty
165
- expect(emptyResult).toBe('group');
166
- expect(whitespaceResult).toBe(' '); // Whitespace is preserved
167
- });
168
-
169
- it('should handle case insensitivity for tag names', () => {
170
- // Arrange - create element with uppercase tag
171
- const upperElement = document.createElement('DIV');
172
- document.body.appendChild(upperElement);
173
-
174
- // Act
175
- const result = getComputedRole(upperElement);
176
-
177
- // Assert
178
- expect(result).toBe('group'); // Should match 'div' in the mapping
179
- });
180
- });
5
+ beforeEach(() => {
6
+ document.body.innerHTML = '';
7
+ });
8
+
9
+ it('should return undefined for null or undefined element', () => {
10
+ expect(getComputedRole(null)).toBeUndefined();
11
+ expect(getComputedRole(undefined)).toBeUndefined();
12
+ });
13
+
14
+ it('should return the role attribute value if present', () => {
15
+ // Arrange
16
+ const element = document.createElement('div');
17
+ element.setAttribute('role', 'button');
18
+ document.body.appendChild(element);
19
+
20
+ // Act
21
+ const result = getComputedRole(element);
22
+
23
+ // Assert
24
+ expect(result).toBe('button');
25
+ });
26
+
27
+ it('should return the correct role for basic elements with simple string mapping', () => {
28
+ // Test cases for elements with simple string mapping
29
+ const testCases = [
30
+ { tag: 'div', expectedRole: 'group' },
31
+ { tag: 'p', expectedRole: 'text' },
32
+ { tag: 'ul', expectedRole: 'list' },
33
+ { tag: 'h1', expectedRole: 'heading' },
34
+ { tag: 'button', expectedRole: 'button' },
35
+ { tag: 'nav', expectedRole: 'navigation' },
36
+ { tag: 'header', expectedRole: 'banner' },
37
+ { tag: 'footer', expectedRole: 'contentinfo' },
38
+ { tag: 'main', expectedRole: 'main' },
39
+ ];
40
+
41
+ testCases.forEach(({ tag, expectedRole }) => {
42
+ // Arrange
43
+ const element = document.createElement(tag);
44
+ document.body.appendChild(element);
45
+
46
+ // Act
47
+ const result = getComputedRole(element);
48
+
49
+ // Assert
50
+ expect(result).toBe(expectedRole);
51
+ });
52
+ });
53
+
54
+ it('should handle link elements (a) correctly based on href attribute', () => {
55
+ // Arrange
56
+ const linkWithHref = document.createElement('a');
57
+ linkWithHref.setAttribute('href', 'https://example.com');
58
+
59
+ const linkWithoutHref = document.createElement('a');
60
+
61
+ document.body.appendChild(linkWithHref);
62
+ document.body.appendChild(linkWithoutHref);
63
+
64
+ // Act
65
+ const withHrefResult = getComputedRole(linkWithHref);
66
+ const withoutHrefResult = getComputedRole(linkWithoutHref);
67
+
68
+ // Assert
69
+ expect(withHrefResult).toBe('link');
70
+ expect(withoutHrefResult).toBe('text');
71
+ });
72
+
73
+ it('should handle image elements based on alt attribute', () => {
74
+ // Arrange
75
+ const imgWithAlt = document.createElement('img');
76
+ imgWithAlt.setAttribute('alt', 'Description of image');
77
+
78
+ const imgWithEmptyAlt = document.createElement('img');
79
+ imgWithEmptyAlt.setAttribute('alt', '');
80
+
81
+ document.body.appendChild(imgWithAlt);
82
+ document.body.appendChild(imgWithEmptyAlt);
83
+
84
+ // Act
85
+ const withAltResult = getComputedRole(imgWithAlt);
86
+ const withEmptyAltResult = getComputedRole(imgWithEmptyAlt);
87
+
88
+ // Assert
89
+ expect(withAltResult).toBe('image');
90
+ expect(withEmptyAltResult).toBe('presentation');
91
+ });
92
+
93
+ it('should handle various input types correctly', () => {
94
+ const inputTypes = [
95
+ { type: 'button', expectedRole: 'button' },
96
+ { type: 'checkbox', expectedRole: 'checkbox' },
97
+ { type: 'hidden', expectedRole: 'noRole' },
98
+ { type: 'image', expectedRole: 'button' },
99
+ { type: 'number', expectedRole: 'spinbutton' },
100
+ { type: 'radio', expectedRole: 'radio' },
101
+ { type: 'range', expectedRole: 'slider' },
102
+ { type: 'reset', expectedRole: 'button' },
103
+ { type: 'submit', expectedRole: 'button' },
104
+ { type: 'password', expectedRole: 'textbox' },
105
+ { type: 'text', expectedRole: 'textbox' },
106
+ { type: 'search', expectedRole: 'searchbox' },
107
+ ];
108
+
109
+ inputTypes.forEach(({ type, expectedRole }) => {
110
+ // Arrange
111
+ const input = document.createElement('input');
112
+ input.setAttribute('type', type);
113
+ document.body.appendChild(input);
114
+
115
+ // Act
116
+ const result = getComputedRole(input);
117
+
118
+ // Assert
119
+ expect(result).toBe(expectedRole);
120
+ });
121
+ });
122
+
123
+ it('should handle elements with no role mapping', () => {
124
+ // Create a custom element that's not in the roleMapping
125
+ const customElement = document.createElement('custom-element');
126
+ document.body.appendChild(customElement);
127
+
128
+ // Act
129
+ const result = getComputedRole(customElement);
130
+
131
+ // Assert
132
+ expect(result).toBeUndefined();
133
+ });
134
+
135
+ it('should prioritize role attribute over computed role', () => {
136
+ // Arrange
137
+ const element = document.createElement('button'); // Inherent role is 'button'
138
+ element.setAttribute('role', 'menuitem'); // Override with custom role
139
+ document.body.appendChild(element);
140
+
141
+ // Act
142
+ const result = getComputedRole(element);
143
+
144
+ // Assert
145
+ expect(result).toBe('menuitem'); // Should use the explicit role
146
+ });
147
+
148
+ it('should handle empty or whitespace role attribute', () => {
149
+ // Arrange
150
+ const emptyRoleElement = document.createElement('div');
151
+ emptyRoleElement.setAttribute('role', '');
152
+
153
+ const whitespaceRoleElement = document.createElement('div');
154
+ whitespaceRoleElement.setAttribute('role', ' ');
155
+
156
+ document.body.appendChild(emptyRoleElement);
157
+ document.body.appendChild(whitespaceRoleElement);
158
+
159
+ // Act
160
+ const emptyResult = getComputedRole(emptyRoleElement);
161
+ const whitespaceResult = getComputedRole(whitespaceRoleElement);
162
+
163
+ // Assert
164
+ // The implementation returns the fallback 'group' role for div when the role attribute is empty
165
+ expect(emptyResult).toBe('group');
166
+ expect(whitespaceResult).toBe(' '); // Whitespace is preserved
167
+ });
168
+
169
+ it('should return gridcell for td inside role="grid"', () => {
170
+ document.body.innerHTML = `
171
+ <table role="grid">
172
+ <tr><td id="target">Cell</td></tr>
173
+ </table>
174
+ `;
175
+ const td = document.getElementById('target');
176
+ expect(getComputedRole(td)).toBe('gridcell');
177
+ });
178
+
179
+ it('should return cell for td inside regular table', () => {
180
+ document.body.innerHTML = `
181
+ <table>
182
+ <tr><td id="target">Cell</td></tr>
183
+ </table>
184
+ `;
185
+ const td = document.getElementById('target');
186
+ expect(getComputedRole(td)).toBe('cell');
187
+ });
188
+
189
+ it('should return gridcell for td inside role="treegrid"', () => {
190
+ document.body.innerHTML = `
191
+ <table role="treegrid">
192
+ <tr><td id="target">Cell</td></tr>
193
+ </table>
194
+ `;
195
+ const td = document.getElementById('target');
196
+ expect(getComputedRole(td)).toBe('gridcell');
197
+ });
198
+
199
+ it('should return generic for header inside article', () => {
200
+ document.body.innerHTML = `
201
+ <article>
202
+ <header id="target">Article Header</header>
203
+ </article>
204
+ `;
205
+ const header = document.getElementById('target');
206
+ expect(getComputedRole(header)).toBe('generic');
207
+ });
208
+
209
+ it('should return generic for header inside section', () => {
210
+ document.body.innerHTML = `
211
+ <section>
212
+ <header id="target">Section Header</header>
213
+ </section>
214
+ `;
215
+ const header = document.getElementById('target');
216
+ expect(getComputedRole(header)).toBe('generic');
217
+ });
218
+
219
+ it('should return banner for header that is direct child of body', () => {
220
+ document.body.innerHTML = '<header id="target">Page Header</header>';
221
+ const header = document.getElementById('target');
222
+ expect(getComputedRole(header)).toBe('banner');
223
+ });
224
+
225
+ it('should return generic for footer inside article', () => {
226
+ document.body.innerHTML = `
227
+ <article>
228
+ <footer id="target">Article Footer</footer>
229
+ </article>
230
+ `;
231
+ const footer = document.getElementById('target');
232
+ expect(getComputedRole(footer)).toBe('generic');
233
+ });
234
+
235
+ it('should return contentinfo for footer that is direct child of body', () => {
236
+ document.body.innerHTML = '<footer id="target">Page Footer</footer>';
237
+ const footer = document.getElementById('target');
238
+ expect(getComputedRole(footer)).toBe('contentinfo');
239
+ });
240
+
241
+ it('should handle case insensitivity for tag names', () => {
242
+ // Arrange - create element with uppercase tag
243
+ const upperElement = document.createElement('DIV');
244
+ document.body.appendChild(upperElement);
245
+
246
+ // Act
247
+ const result = getComputedRole(upperElement);
248
+
249
+ // Assert
250
+ expect(result).toBe('group'); // Should match 'div' in the mapping
251
+ });
252
+ });