@afixt/test-utils 2.1.1 → 2.3.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/BROWSER_TESTING.md +12 -12
- package/CHANGELOG.md +21 -0
- package/CLAUDE.md +1 -1
- package/package.json +1 -1
- package/src/constants.js +1 -0
- package/src/domUtils.js +45 -11
- package/src/formUtils.js +5 -2
- package/src/getAccessibleName.js +37 -51
- package/src/getAccessibleText.js +3 -3
- package/src/getComputedRole.js +187 -122
- package/src/getImageText.js +8 -1
- package/src/index.js +4 -0
- package/src/isA11yVisible.js +13 -7
- package/src/isHidden.js +11 -4
- package/src/shadowDomUtils.js +118 -0
- package/src/stringUtils.js +5 -2
- package/src/testContrast.js +42 -1
- package/test/domUtils.test.js +52 -0
- package/test/formUtils.test.js +39 -0
- package/test/getAccessibleName.test.js +83 -0
- package/test/getComputedRole.test.js +248 -176
- package/test/isA11yVisible.test.js +33 -0
- package/test/isHidden.test.js +18 -0
- package/test/playwright/colon-id-a11y-visible.spec.js +160 -0
- package/test/playwright/fixtures/colon-id-a11y-visible.html +48 -0
- package/test/shadowDomUtils.test.js +248 -0
- package/test/testContrast.test.js +42 -5
- package/todo.md +3 -2
package/test/domUtils.test.js
CHANGED
|
@@ -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', () => {
|
|
@@ -1059,4 +1079,36 @@ describe('domUtils', () => {
|
|
|
1059
1079
|
expect(domUtils.getHeadingLevel(el)).toBeNull();
|
|
1060
1080
|
});
|
|
1061
1081
|
});
|
|
1082
|
+
|
|
1083
|
+
describe('shadow DOM integration', () => {
|
|
1084
|
+
it('isIdReferenced should find label[for] inside a shadow root', () => {
|
|
1085
|
+
const host = document.createElement('div');
|
|
1086
|
+
document.body.appendChild(host);
|
|
1087
|
+
const shadow = host.attachShadow({ mode: 'open' });
|
|
1088
|
+
const label = document.createElement('label');
|
|
1089
|
+
label.setAttribute('for', 'shadow-ref-target');
|
|
1090
|
+
shadow.appendChild(label);
|
|
1091
|
+
|
|
1092
|
+
expect(domUtils.isIdReferenced('shadow-ref-target')).toBe(true);
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
it('getElementsWithDuplicateIds should detect duplicate IDs across shadow roots', () => {
|
|
1096
|
+
// Light DOM element with id="dup"
|
|
1097
|
+
const div1 = document.createElement('div');
|
|
1098
|
+
div1.id = 'dup';
|
|
1099
|
+
document.body.appendChild(div1);
|
|
1100
|
+
|
|
1101
|
+
// Shadow DOM element with same id="dup"
|
|
1102
|
+
const host = document.createElement('div');
|
|
1103
|
+
document.body.appendChild(host);
|
|
1104
|
+
const shadow = host.attachShadow({ mode: 'open' });
|
|
1105
|
+
const div2 = document.createElement('div');
|
|
1106
|
+
div2.id = 'dup';
|
|
1107
|
+
shadow.appendChild(div2);
|
|
1108
|
+
|
|
1109
|
+
const duplicates = domUtils.getElementsWithDuplicateIds();
|
|
1110
|
+
expect(duplicates.length).toBe(1);
|
|
1111
|
+
expect(duplicates[0].id).toBe('dup');
|
|
1112
|
+
});
|
|
1113
|
+
});
|
|
1062
1114
|
});
|
package/test/formUtils.test.js
CHANGED
|
@@ -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>
|
|
@@ -339,4 +357,25 @@ describe('formUtils', () => {
|
|
|
339
357
|
expect(text.trim()).toBe('');
|
|
340
358
|
});
|
|
341
359
|
});
|
|
360
|
+
|
|
361
|
+
describe('shadow DOM integration', () => {
|
|
362
|
+
it('hasExplicitAccessibleName should find aria-labelledby target inside shadow root', () => {
|
|
363
|
+
// Create shadow host with label element inside
|
|
364
|
+
const host = document.createElement('div');
|
|
365
|
+
document.body.appendChild(host);
|
|
366
|
+
const shadow = host.attachShadow({ mode: 'open' });
|
|
367
|
+
const labelEl = document.createElement('span');
|
|
368
|
+
labelEl.id = 'shadow-group-label';
|
|
369
|
+
labelEl.textContent = 'Group Name';
|
|
370
|
+
shadow.appendChild(labelEl);
|
|
371
|
+
|
|
372
|
+
// Create an element in light DOM referencing the shadow label
|
|
373
|
+
const group = document.createElement('div');
|
|
374
|
+
group.setAttribute('role', 'radiogroup');
|
|
375
|
+
group.setAttribute('aria-labelledby', 'shadow-group-label');
|
|
376
|
+
document.body.appendChild(group);
|
|
377
|
+
|
|
378
|
+
expect(formUtils.hasExplicitAccessibleName(group)).toBe(true);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
342
381
|
});
|
|
@@ -467,4 +467,87 @@ 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
|
+
});
|
|
514
|
+
|
|
515
|
+
describe('shadow DOM integration', () => {
|
|
516
|
+
it('should resolve aria-labelledby referencing an element inside a shadow root', () => {
|
|
517
|
+
// Create a shadow host with the label inside
|
|
518
|
+
const host = document.createElement('div');
|
|
519
|
+
document.body.appendChild(host);
|
|
520
|
+
const shadow = host.attachShadow({ mode: 'open' });
|
|
521
|
+
const labelSpan = document.createElement('span');
|
|
522
|
+
labelSpan.id = 'shadow-label';
|
|
523
|
+
labelSpan.textContent = 'Shadow Label';
|
|
524
|
+
shadow.appendChild(labelSpan);
|
|
525
|
+
|
|
526
|
+
// Create the button in light DOM that references the shadow label
|
|
527
|
+
const button = document.createElement('button');
|
|
528
|
+
button.setAttribute('aria-labelledby', 'shadow-label');
|
|
529
|
+
document.body.appendChild(button);
|
|
530
|
+
|
|
531
|
+
expect(getAccessibleName(button)).toBe('Shadow Label');
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('should resolve label[for] inside a shadow root for text inputs', () => {
|
|
535
|
+
// Create a shadow host with the label inside
|
|
536
|
+
const host = document.createElement('div');
|
|
537
|
+
document.body.appendChild(host);
|
|
538
|
+
const shadow = host.attachShadow({ mode: 'open' });
|
|
539
|
+
const label = document.createElement('label');
|
|
540
|
+
label.setAttribute('for', 'shadow-input');
|
|
541
|
+
label.textContent = 'Shadow Input Label';
|
|
542
|
+
shadow.appendChild(label);
|
|
543
|
+
|
|
544
|
+
// Create the input in light DOM
|
|
545
|
+
const input = document.createElement('input');
|
|
546
|
+
input.type = 'text';
|
|
547
|
+
input.id = 'shadow-input';
|
|
548
|
+
document.body.appendChild(input);
|
|
549
|
+
|
|
550
|
+
expect(getAccessibleName(input)).toBe('Shadow Input Label');
|
|
551
|
+
});
|
|
552
|
+
});
|
|
470
553
|
});
|
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
+
});
|