@afixt/test-utils 2.4.0 → 2.6.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.
@@ -300,6 +300,32 @@ describe('getAccessibleText', () => {
300
300
  });
301
301
  });
302
302
 
303
+ describe('child role="img" with aria-label in subtree', () => {
304
+ it('should get aria-label from child elements with role="img"', () => {
305
+ const container = document.createElement('div');
306
+ container.innerHTML = '<div role="img" aria-label="Company Logo"></div>';
307
+ document.body.appendChild(container);
308
+
309
+ expect(getAccessibleText(container)).toBe('Company Logo');
310
+ });
311
+
312
+ it('should skip role="img" elements with empty aria-label', () => {
313
+ const container = document.createElement('div');
314
+ container.innerHTML = 'Label <div role="img" aria-label=""></div>';
315
+ document.body.appendChild(container);
316
+
317
+ expect(getAccessibleText(container)).toBe('Label');
318
+ });
319
+
320
+ it('should skip role="img" elements without aria-label', () => {
321
+ const container = document.createElement('div');
322
+ container.innerHTML = 'Label <div role="img"></div>';
323
+ document.body.appendChild(container);
324
+
325
+ expect(getAccessibleText(container)).toBe('Label');
326
+ });
327
+ });
328
+
303
329
  describe('visibleOnly option', () => {
304
330
  it('should skip aria-label when visibleOnly is true', () => {
305
331
  const div = document.createElement('div');
@@ -522,4 +548,52 @@ describe('getAccessibleText', () => {
522
548
  expect(result).toBe('Valid Text');
523
549
  });
524
550
  });
551
+
552
+ describe('shadow DOM traversal', () => {
553
+ it('should extract text from inside an open shadow root', () => {
554
+ const host = document.createElement('div');
555
+ document.body.appendChild(host);
556
+ const shadow = host.attachShadow({ mode: 'open' });
557
+ const inner = document.createElement('span');
558
+ inner.textContent = 'Shadow text';
559
+ shadow.appendChild(inner);
560
+
561
+ expect(getAccessibleText(host)).toBe('Shadow text');
562
+ });
563
+
564
+ it('should extract text from nested shadow roots', () => {
565
+ const host1 = document.createElement('div');
566
+ document.body.appendChild(host1);
567
+ const shadow1 = host1.attachShadow({ mode: 'open' });
568
+
569
+ const host2 = document.createElement('div');
570
+ shadow1.appendChild(host2);
571
+ const shadow2 = host2.attachShadow({ mode: 'open' });
572
+
573
+ const inner = document.createElement('span');
574
+ inner.textContent = 'Deeply nested text';
575
+ shadow2.appendChild(inner);
576
+
577
+ expect(getAccessibleText(host1)).toBe('Deeply nested text');
578
+ });
579
+
580
+ it('should prefer aria-label over shadow DOM text', () => {
581
+ const host = document.createElement('div');
582
+ host.setAttribute('aria-label', 'Accessible Label');
583
+ document.body.appendChild(host);
584
+ const shadow = host.attachShadow({ mode: 'open' });
585
+ const inner = document.createElement('span');
586
+ inner.textContent = 'Shadow text';
587
+ shadow.appendChild(inner);
588
+
589
+ expect(getAccessibleText(host)).toBe('Accessible Label');
590
+ });
591
+
592
+ it('should return empty string for custom element with no content and no shadow root', () => {
593
+ const el = document.createElement('my-widget');
594
+ document.body.appendChild(el);
595
+
596
+ expect(getAccessibleText(el)).toBe('');
597
+ });
598
+ });
525
599
  });
@@ -2,159 +2,278 @@ import { describe, it, expect, beforeEach } from 'vitest';
2
2
  import { hasValidAriaRole } from '../src/hasValidAriaRole.js';
3
3
 
4
4
  describe('hasValidAriaRole', () => {
5
- beforeEach(() => {
6
- document.body.innerHTML = '';
7
- });
8
-
9
- it('should return true for an element with a valid ARIA role', () => {
10
- // Arrange
11
- const element = document.createElement('div');
12
- element.setAttribute('role', 'button');
13
- document.body.appendChild(element);
14
-
15
- // Act
16
- const result = hasValidAriaRole(element);
17
-
18
- // Assert
19
- expect(result).toBe(true);
20
- });
21
-
22
- it('should return false for an element with an invalid ARIA role', () => {
23
- // Arrange
24
- const element = document.createElement('div');
25
- element.setAttribute('role', 'invalid-role');
26
- document.body.appendChild(element);
27
-
28
- // Act
29
- const result = hasValidAriaRole(element);
30
-
31
- // Assert
32
- expect(result).toBe(false);
33
- });
34
-
35
- it('should return false for an element without a role attribute', () => {
36
- // Arrange
37
- const element = document.createElement('div');
38
- document.body.appendChild(element);
39
-
40
- // Act
41
- const result = hasValidAriaRole(element);
42
-
43
- // Assert
44
- expect(result).toBe(false);
45
- });
46
-
47
- it('should return false for null or non-element input', () => {
48
- // Act & Assert
49
- expect(hasValidAriaRole(null)).toBe(false);
50
- expect(hasValidAriaRole(undefined)).toBe(false);
51
- expect(hasValidAriaRole({})).toBe(false);
52
- expect(hasValidAriaRole('')).toBe(false);
53
- });
54
-
55
- it('should correctly handle elements with multiple roles (should use the first one)', () => {
56
- // Arrange
57
- const validMultipleRoles = document.createElement('div');
58
- validMultipleRoles.setAttribute('role', 'button presentation');
59
-
60
- const invalidMultipleRoles = document.createElement('div');
61
- invalidMultipleRoles.setAttribute('role', 'invalid-role button');
62
-
63
- document.body.appendChild(validMultipleRoles);
64
- document.body.appendChild(invalidMultipleRoles);
65
-
66
- // Act & Assert
67
- expect(hasValidAriaRole(validMultipleRoles)).toBe(true);
68
- expect(hasValidAriaRole(invalidMultipleRoles)).toBe(false);
69
- });
70
-
71
- it('should handle roles with extra whitespace', () => {
72
- // Arrange
73
- const element = document.createElement('div');
74
- element.setAttribute('role', ' button ');
75
- document.body.appendChild(element);
76
-
77
- // Act
78
- const result = hasValidAriaRole(element);
79
-
80
- // Assert
81
- expect(result).toBe(true);
82
- });
83
-
84
- it('should recognize various valid roles', () => {
85
- // Test a sample of different valid roles
86
- const validRoles = ['alert', 'button', 'checkbox', 'dialog', 'navigation', 'main', 'region'];
87
-
88
- validRoles.forEach(role => {
89
- // Arrange
90
- const element = document.createElement('div');
91
- element.setAttribute('role', role);
92
- document.body.appendChild(element);
93
-
94
- // Act & Assert
95
- expect(hasValidAriaRole(element)).toBe(true);
5
+ beforeEach(() => {
6
+ document.body.innerHTML = '';
96
7
  });
97
- });
98
-
99
- it('should recognize all widget roles', () => {
100
- const widgetRoles = [
101
- 'alertdialog', 'gridcell', 'link', 'log', 'marquee', 'menuitem',
102
- 'menuitemcheckbox', 'menuitemradio', 'option', 'progressbar', 'radio',
103
- 'scrollbar', 'searchbox', 'slider', 'spinbutton', 'status', 'switch',
104
- 'tab', 'tabpanel', 'textbox', 'tooltip', 'treeitem'
105
- ];
106
-
107
- widgetRoles.forEach(role => {
108
- const element = document.createElement('div');
109
- element.setAttribute('role', role);
110
- expect(hasValidAriaRole(element)).toBe(true);
8
+
9
+ it('should return true for an element with a valid ARIA role', () => {
10
+ // Arrange
11
+ const element = document.createElement('div');
12
+ element.setAttribute('role', 'button');
13
+ document.body.appendChild(element);
14
+
15
+ // Act
16
+ const result = hasValidAriaRole(element);
17
+
18
+ // Assert
19
+ expect(result).toBe(true);
20
+ });
21
+
22
+ it('should return false for an element with an invalid ARIA role', () => {
23
+ // Arrange
24
+ const element = document.createElement('div');
25
+ element.setAttribute('role', 'invalid-role');
26
+ document.body.appendChild(element);
27
+
28
+ // Act
29
+ const result = hasValidAriaRole(element);
30
+
31
+ // Assert
32
+ expect(result).toBe(false);
33
+ });
34
+
35
+ it('should return false for an element without a role attribute', () => {
36
+ // Arrange
37
+ const element = document.createElement('div');
38
+ document.body.appendChild(element);
39
+
40
+ // Act
41
+ const result = hasValidAriaRole(element);
42
+
43
+ // Assert
44
+ expect(result).toBe(false);
111
45
  });
112
- });
113
-
114
- it('should recognize all composite widget roles', () => {
115
- const compositeRoles = [
116
- 'combobox', 'grid', 'listbox', 'menu', 'menubar', 'radiogroup',
117
- 'tablist', 'tree', 'treegrid'
118
- ];
119
-
120
- compositeRoles.forEach(role => {
121
- const element = document.createElement('div');
122
- element.setAttribute('role', role);
123
- expect(hasValidAriaRole(element)).toBe(true);
46
+
47
+ it('should return false for null or non-element input', () => {
48
+ // Act & Assert
49
+ expect(hasValidAriaRole(null)).toBe(false);
50
+ expect(hasValidAriaRole(undefined)).toBe(false);
51
+ expect(hasValidAriaRole({})).toBe(false);
52
+ expect(hasValidAriaRole('')).toBe(false);
124
53
  });
125
- });
126
-
127
- it('should recognize all document structure roles', () => {
128
- const structureRoles = [
129
- 'article', 'cell', 'columnheader', 'definition', 'directory', 'document',
130
- 'feed', 'figure', 'group', 'heading', 'img', 'list', 'listitem', 'math',
131
- 'none', 'note', 'presentation', 'row', 'rowgroup', 'rowheader',
132
- 'separator', 'table', 'term', 'toolbar'
133
- ];
134
-
135
- structureRoles.forEach(role => {
136
- const element = document.createElement('div');
137
- element.setAttribute('role', role);
138
- expect(hasValidAriaRole(element)).toBe(true);
54
+
55
+ it('should correctly handle elements with multiple roles (should use the first one)', () => {
56
+ // Arrange
57
+ const validMultipleRoles = document.createElement('div');
58
+ validMultipleRoles.setAttribute('role', 'button presentation');
59
+
60
+ const invalidMultipleRoles = document.createElement('div');
61
+ invalidMultipleRoles.setAttribute('role', 'invalid-role button');
62
+
63
+ document.body.appendChild(validMultipleRoles);
64
+ document.body.appendChild(invalidMultipleRoles);
65
+
66
+ // Act & Assert
67
+ expect(hasValidAriaRole(validMultipleRoles)).toBe(true);
68
+ expect(hasValidAriaRole(invalidMultipleRoles)).toBe(false);
139
69
  });
140
- });
141
-
142
- it('should recognize all landmark roles', () => {
143
- const landmarkRoles = [
144
- 'application', 'banner', 'complementary', 'contentinfo', 'form',
145
- 'search'
146
- ];
147
-
148
- landmarkRoles.forEach(role => {
149
- const element = document.createElement('div');
150
- element.setAttribute('role', role);
151
- expect(hasValidAriaRole(element)).toBe(true);
70
+
71
+ it('should handle roles with extra whitespace', () => {
72
+ // Arrange
73
+ const element = document.createElement('div');
74
+ element.setAttribute('role', ' button ');
75
+ document.body.appendChild(element);
76
+
77
+ // Act
78
+ const result = hasValidAriaRole(element);
79
+
80
+ // Assert
81
+ expect(result).toBe(true);
82
+ });
83
+
84
+ it('should recognize various valid roles', () => {
85
+ // Test a sample of different valid roles
86
+ const validRoles = [
87
+ 'alert',
88
+ 'button',
89
+ 'checkbox',
90
+ 'dialog',
91
+ 'navigation',
92
+ 'main',
93
+ 'region',
94
+ ];
95
+
96
+ validRoles.forEach(role => {
97
+ // Arrange
98
+ const element = document.createElement('div');
99
+ element.setAttribute('role', role);
100
+ document.body.appendChild(element);
101
+
102
+ // Act & Assert
103
+ expect(hasValidAriaRole(element)).toBe(true);
104
+ });
105
+ });
106
+
107
+ it('should recognize all widget roles', () => {
108
+ const widgetRoles = [
109
+ 'alertdialog',
110
+ 'gridcell',
111
+ 'link',
112
+ 'log',
113
+ 'marquee',
114
+ 'menuitem',
115
+ 'menuitemcheckbox',
116
+ 'menuitemradio',
117
+ 'option',
118
+ 'progressbar',
119
+ 'radio',
120
+ 'scrollbar',
121
+ 'searchbox',
122
+ 'slider',
123
+ 'spinbutton',
124
+ 'status',
125
+ 'switch',
126
+ 'tab',
127
+ 'tabpanel',
128
+ 'textbox',
129
+ 'tooltip',
130
+ 'treeitem',
131
+ ];
132
+
133
+ widgetRoles.forEach(role => {
134
+ const element = document.createElement('div');
135
+ element.setAttribute('role', role);
136
+ expect(hasValidAriaRole(element)).toBe(true);
137
+ });
138
+ });
139
+
140
+ it('should recognize all composite widget roles', () => {
141
+ const compositeRoles = [
142
+ 'combobox',
143
+ 'grid',
144
+ 'listbox',
145
+ 'menu',
146
+ 'menubar',
147
+ 'radiogroup',
148
+ 'tablist',
149
+ 'tree',
150
+ 'treegrid',
151
+ ];
152
+
153
+ compositeRoles.forEach(role => {
154
+ const element = document.createElement('div');
155
+ element.setAttribute('role', role);
156
+ expect(hasValidAriaRole(element)).toBe(true);
157
+ });
158
+ });
159
+
160
+ it('should recognize all document structure roles', () => {
161
+ const structureRoles = [
162
+ 'article',
163
+ 'cell',
164
+ 'columnheader',
165
+ 'definition',
166
+ 'directory',
167
+ 'document',
168
+ 'feed',
169
+ 'figure',
170
+ 'group',
171
+ 'heading',
172
+ 'img',
173
+ 'list',
174
+ 'listitem',
175
+ 'math',
176
+ 'none',
177
+ 'note',
178
+ 'presentation',
179
+ 'row',
180
+ 'rowgroup',
181
+ 'rowheader',
182
+ 'separator',
183
+ 'table',
184
+ 'term',
185
+ 'toolbar',
186
+ ];
187
+
188
+ structureRoles.forEach(role => {
189
+ const element = document.createElement('div');
190
+ element.setAttribute('role', role);
191
+ expect(hasValidAriaRole(element)).toBe(true);
192
+ });
193
+ });
194
+
195
+ it('should recognize all landmark roles', () => {
196
+ const landmarkRoles = [
197
+ 'application',
198
+ 'banner',
199
+ 'complementary',
200
+ 'contentinfo',
201
+ 'form',
202
+ 'search',
203
+ ];
204
+
205
+ landmarkRoles.forEach(role => {
206
+ const element = document.createElement('div');
207
+ element.setAttribute('role', role);
208
+ expect(hasValidAriaRole(element)).toBe(true);
209
+ });
210
+ });
211
+
212
+ it('should recognize timer role', () => {
213
+ const element = document.createElement('div');
214
+ element.setAttribute('role', 'timer');
215
+ expect(hasValidAriaRole(element)).toBe(true);
216
+ });
217
+
218
+ it('should recognize WAI-ARIA Graphics Module roles', () => {
219
+ const graphicsRoles = ['graphics-document', 'graphics-object', 'graphics-symbol'];
220
+
221
+ graphicsRoles.forEach(role => {
222
+ const element = document.createElement('svg');
223
+ element.setAttribute('role', role);
224
+ expect(hasValidAriaRole(element)).toBe(true);
225
+ });
226
+ });
227
+
228
+ it('should recognize DPub-ARIA roles', () => {
229
+ const dpubRoles = [
230
+ 'doc-abstract',
231
+ 'doc-acknowledgments',
232
+ 'doc-afterword',
233
+ 'doc-appendix',
234
+ 'doc-backlink',
235
+ 'doc-biblioentry',
236
+ 'doc-bibliography',
237
+ 'doc-biblioref',
238
+ 'doc-chapter',
239
+ 'doc-colophon',
240
+ 'doc-conclusion',
241
+ 'doc-cover',
242
+ 'doc-credit',
243
+ 'doc-credits',
244
+ 'doc-dedication',
245
+ 'doc-endnote',
246
+ 'doc-endnotes',
247
+ 'doc-epigraph',
248
+ 'doc-epilogue',
249
+ 'doc-errata',
250
+ 'doc-example',
251
+ 'doc-footnote',
252
+ 'doc-foreword',
253
+ 'doc-glossary',
254
+ 'doc-glossref',
255
+ 'doc-index',
256
+ 'doc-introduction',
257
+ 'doc-noteref',
258
+ 'doc-notice',
259
+ 'doc-pagebreak',
260
+ 'doc-pagefooter',
261
+ 'doc-pageheader',
262
+ 'doc-pagelist',
263
+ 'doc-part',
264
+ 'doc-preface',
265
+ 'doc-prologue',
266
+ 'doc-pullquote',
267
+ 'doc-qna',
268
+ 'doc-subtitle',
269
+ 'doc-tip',
270
+ 'doc-toc',
271
+ ];
272
+
273
+ dpubRoles.forEach(role => {
274
+ const element = document.createElement('div');
275
+ element.setAttribute('role', role);
276
+ expect(hasValidAriaRole(element)).toBe(true);
277
+ });
152
278
  });
153
- });
154
-
155
- it('should recognize timer role', () => {
156
- const element = document.createElement('div');
157
- element.setAttribute('role', 'timer');
158
- expect(hasValidAriaRole(element)).toBe(true);
159
- });
160
- });
279
+ });
@@ -73,6 +73,7 @@ describe('index.js exports', () => {
73
73
  it('should export focus management utilities', () => {
74
74
  expect(utils.getFocusableElements).toBeDefined();
75
75
  expect(utils.isFocusable).toBeDefined();
76
+ expect(utils.detectFocusTrap).toBeDefined();
76
77
  });
77
78
 
78
79
  it('should export role computation utilities', () => {
@@ -145,6 +146,7 @@ describe('index.js exports', () => {
145
146
  'isA11yVisible',
146
147
  'hasParent',
147
148
  'getFocusableElements',
149
+ 'detectFocusTrap',
148
150
  'getComputedRole',
149
151
  'getImageText',
150
152
  'testContrast',
@@ -183,6 +185,7 @@ describe('index.js exports', () => {
183
185
  'hasAttribute',
184
186
  'getFocusableElements',
185
187
  'isFocusable',
188
+ 'detectFocusTrap',
186
189
  'getComputedRole',
187
190
  'getImageText',
188
191
  'testContrast',
@@ -3,6 +3,8 @@ import {
3
3
  deepGetElementById,
4
4
  deepQuerySelector,
5
5
  deepQuerySelectorAll,
6
+ getDeepTextContent,
7
+ hasCustomElementDescendant,
6
8
  } from '../src/shadowDomUtils.js';
7
9
 
8
10
  describe('shadowDomUtils', () => {
@@ -245,4 +247,100 @@ describe('shadowDomUtils', () => {
245
247
  expect(results).toHaveLength(0);
246
248
  });
247
249
  });
250
+
251
+ // =========================================================================
252
+ // getDeepTextContent
253
+ // =========================================================================
254
+ describe('getDeepTextContent', () => {
255
+ it('should return empty string for null', () => {
256
+ expect(getDeepTextContent(null)).toBe('');
257
+ });
258
+
259
+ it('should return text from light DOM', () => {
260
+ document.body.innerHTML = '<div>Hello world</div>';
261
+ const el = document.querySelector('div');
262
+ expect(getDeepTextContent(el)).toBe('Hello world');
263
+ });
264
+
265
+ it('should return text from inside an open shadow root', () => {
266
+ const host = document.createElement('div');
267
+ document.body.appendChild(host);
268
+ const shadow = host.attachShadow({ mode: 'open' });
269
+ const inner = document.createElement('span');
270
+ inner.textContent = 'Shadow content';
271
+ shadow.appendChild(inner);
272
+
273
+ expect(getDeepTextContent(host)).toBe('Shadow content');
274
+ });
275
+
276
+ it('should return text from nested shadow roots', () => {
277
+ const host1 = document.createElement('div');
278
+ document.body.appendChild(host1);
279
+ const shadow1 = host1.attachShadow({ mode: 'open' });
280
+
281
+ const host2 = document.createElement('div');
282
+ shadow1.appendChild(host2);
283
+ const shadow2 = host2.attachShadow({ mode: 'open' });
284
+
285
+ const inner = document.createElement('span');
286
+ inner.textContent = 'Deeply nested';
287
+ shadow2.appendChild(inner);
288
+
289
+ expect(getDeepTextContent(host1)).toBe('Deeply nested');
290
+ });
291
+
292
+ it('should return empty string for element with no text', () => {
293
+ document.body.innerHTML = '<div><img src="test.png"></div>';
294
+ const el = document.querySelector('div');
295
+ expect(getDeepTextContent(el)).toBe('');
296
+ });
297
+
298
+ it('should combine text from shadow and light DOM', () => {
299
+ const host = document.createElement('div');
300
+ host.textContent = 'Light text';
301
+ document.body.appendChild(host);
302
+ const shadow = host.attachShadow({ mode: 'open' });
303
+ const inner = document.createElement('span');
304
+ inner.textContent = 'Shadow text';
305
+ shadow.appendChild(inner);
306
+
307
+ const result = getDeepTextContent(host);
308
+ expect(result).toContain('Shadow text');
309
+ expect(result).toContain('Light text');
310
+ });
311
+ });
312
+
313
+ // =========================================================================
314
+ // hasCustomElementDescendant
315
+ // =========================================================================
316
+ describe('hasCustomElementDescendant', () => {
317
+ it('should return false for null', () => {
318
+ expect(hasCustomElementDescendant(null)).toBe(false);
319
+ });
320
+
321
+ it('should return false for element with no custom elements', () => {
322
+ document.body.innerHTML = '<div><span>Text</span></div>';
323
+ const el = document.querySelector('div');
324
+ expect(hasCustomElementDescendant(el)).toBe(false);
325
+ });
326
+
327
+ it('should return true when the element itself is a custom element', () => {
328
+ const el = document.createElement('my-component');
329
+ document.body.appendChild(el);
330
+ expect(hasCustomElementDescendant(el)).toBe(true);
331
+ });
332
+
333
+ it('should return true when a descendant is a custom element', () => {
334
+ document.body.innerHTML = '<div><store-selector-addon></store-selector-addon></div>';
335
+ const el = document.querySelector('div');
336
+ expect(hasCustomElementDescendant(el)).toBe(true);
337
+ });
338
+
339
+ it('should return false for elements with hyphens in attributes but not tag names', () => {
340
+ document.body.innerHTML =
341
+ '<div data-my-attr="value"><span class="my-class">Text</span></div>';
342
+ const el = document.querySelector('div');
343
+ expect(hasCustomElementDescendant(el)).toBe(false);
344
+ });
345
+ });
248
346
  });