@afixt/test-utils 1.2.0 → 1.2.1

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,95 +1,109 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
2
  import domUtils from '../src/domUtils.js';
3
3
 
4
+ // Polyfill CSS.escape for JSDOM
5
+ if (typeof globalThis.CSS === 'undefined') {
6
+ globalThis.CSS = {};
7
+ }
8
+ if (typeof globalThis.CSS.escape !== 'function') {
9
+ globalThis.CSS.escape = function (value) {
10
+ return String(value).replace(/([^\w-])/g, '\\$1');
11
+ };
12
+ }
13
+
4
14
  describe('domUtils', () => {
5
- beforeEach(() => {
6
- document.body.innerHTML = '';
7
- });
8
-
9
- describe('hasAttr', () => {
10
- it('should return true if element has the attribute', () => {
11
- document.body.innerHTML = `<div id="test" data-test="value"></div>`;
12
- const element = document.getElementById('test');
13
-
14
- expect(domUtils.hasAttr(element, 'data-test')).toBe(true);
15
+ beforeEach(() => {
16
+ document.body.innerHTML = '';
15
17
  });
16
18
 
17
- it('should return false if element does not have the attribute', () => {
18
- document.body.innerHTML = `<div id="test"></div>`;
19
- const element = document.getElementById('test');
20
-
21
- expect(domUtils.hasAttr(element, 'data-test')).toBe(false);
19
+ describe('hasAttr', () => {
20
+ it('should return true if element has the attribute', () => {
21
+ document.body.innerHTML = '<div id="test" data-test="value"></div>';
22
+ const element = document.getElementById('test');
23
+
24
+ expect(domUtils.hasAttr(element, 'data-test')).toBe(true);
25
+ });
26
+
27
+ it('should return false if element does not have the attribute', () => {
28
+ document.body.innerHTML = '<div id="test"></div>';
29
+ const element = document.getElementById('test');
30
+
31
+ expect(domUtils.hasAttr(element, 'data-test')).toBe(false);
32
+ });
22
33
  });
23
- });
24
34
 
25
- describe('attrBegins', () => {
26
- it('should filter elements with attributes that begin with prefix', () => {
27
- document.body.innerHTML = `
35
+ describe('attrBegins', () => {
36
+ it('should filter elements with attributes that begin with prefix', () => {
37
+ document.body.innerHTML = `
28
38
  <div id="div1" data-test="value"></div>
29
39
  <div id="div2" data-other="value"></div>
30
40
  <div id="div3" aria-label="value"></div>
31
41
  `;
32
-
33
- const elements = document.querySelectorAll('div');
34
-
35
- // Filter for data- attributes
36
- const dataElements = domUtils.attrBegins(elements, 'data-');
37
- expect(dataElements.length).toBe(2);
38
- expect(dataElements[0].id).toBe('div1');
39
- expect(dataElements[1].id).toBe('div2');
40
-
41
- // Filter for aria- attributes
42
- const ariaElements = domUtils.attrBegins(elements, 'aria-');
43
- expect(ariaElements.length).toBe(1);
44
- expect(ariaElements[0].id).toBe('div3');
45
- });
46
- });
47
-
48
- describe('containsNoCase', () => {
49
- it('should find text case-insensitively', () => {
50
- document.body.innerHTML = `<div id="test">This is a Test</div>`;
51
- const element = document.getElementById('test');
52
-
53
- expect(domUtils.containsNoCase(element, 'this')).toBe(true);
54
- expect(domUtils.containsNoCase(element, 'TEST')).toBe(true);
55
- expect(domUtils.containsNoCase(element, 'not found')).toBe(false);
42
+
43
+ const elements = document.querySelectorAll('div');
44
+
45
+ // Filter for data- attributes
46
+ const dataElements = domUtils.attrBegins(elements, 'data-');
47
+ expect(dataElements.length).toBe(2);
48
+ expect(dataElements[0].id).toBe('div1');
49
+ expect(dataElements[1].id).toBe('div2');
50
+
51
+ // Filter for aria- attributes
52
+ const ariaElements = domUtils.attrBegins(elements, 'aria-');
53
+ expect(ariaElements.length).toBe(1);
54
+ expect(ariaElements[0].id).toBe('div3');
55
+ });
56
56
  });
57
- });
58
-
59
- describe('getAttributes', () => {
60
- it('should return all attributes as an object', () => {
61
- document.body.innerHTML = `<div id="test" class="container" data-value="123"></div>`;
62
- const element = document.getElementById('test');
63
-
64
- const attrs = domUtils.getAttributes(element);
65
- expect(attrs.id).toBe('test');
66
- expect(attrs.class).toBe('container');
67
- expect(attrs['data-value']).toBe('123');
57
+
58
+ describe('containsNoCase', () => {
59
+ it('should find text case-insensitively', () => {
60
+ document.body.innerHTML = '<div id="test">This is a Test</div>';
61
+ const element = document.getElementById('test');
62
+
63
+ expect(domUtils.containsNoCase(element, 'this')).toBe(true);
64
+ expect(domUtils.containsNoCase(element, 'TEST')).toBe(true);
65
+ expect(domUtils.containsNoCase(element, 'not found')).toBe(false);
66
+ });
68
67
  });
69
68
 
70
- it('should return empty object for null or undefined', () => {
71
- expect(domUtils.getAttributes(null)).toEqual({});
72
- expect(domUtils.getAttributes(undefined)).toEqual({});
69
+ describe('getAttributes', () => {
70
+ it('should return all attributes as an object', () => {
71
+ document.body.innerHTML = '<div id="test" class="container" data-value="123"></div>';
72
+ const element = document.getElementById('test');
73
+
74
+ const attrs = domUtils.getAttributes(element);
75
+ expect(attrs.id).toBe('test');
76
+ expect(attrs.class).toBe('container');
77
+ expect(attrs['data-value']).toBe('123');
78
+ });
79
+
80
+ it('should return empty object for null or undefined', () => {
81
+ expect(domUtils.getAttributes(null)).toEqual({});
82
+ expect(domUtils.getAttributes(undefined)).toEqual({});
83
+ });
73
84
  });
74
- });
75
85
 
76
- describe('getConstructor', () => {
77
- it('should return the constructor name of an element', () => {
78
- document.body.innerHTML = `
86
+ describe('getConstructor', () => {
87
+ it('should return the constructor name of an element', () => {
88
+ document.body.innerHTML = `
79
89
  <div id="div"></div>
80
90
  <button id="button"></button>
81
91
  <input id="input" type="text">
82
92
  `;
83
-
84
- expect(domUtils.getConstructor(document.getElementById('div'))).toBe('HTMLDivElement');
85
- expect(domUtils.getConstructor(document.getElementById('button'))).toBe('HTMLButtonElement');
86
- expect(domUtils.getConstructor(document.getElementById('input'))).toBe('HTMLInputElement');
93
+
94
+ expect(domUtils.getConstructor(document.getElementById('div'))).toBe('HTMLDivElement');
95
+ expect(domUtils.getConstructor(document.getElementById('button'))).toBe(
96
+ 'HTMLButtonElement'
97
+ );
98
+ expect(domUtils.getConstructor(document.getElementById('input'))).toBe(
99
+ 'HTMLInputElement'
100
+ );
101
+ });
87
102
  });
88
- });
89
103
 
90
- describe('getXPath', () => {
91
- it('should generate correct XPath for elements', () => {
92
- document.body.innerHTML = `
104
+ describe('getXPath', () => {
105
+ it('should generate correct XPath for elements', () => {
106
+ document.body.innerHTML = `
93
107
  <div>
94
108
  <section>
95
109
  <article id="target">
@@ -99,232 +113,234 @@ describe('domUtils', () => {
99
113
  </section>
100
114
  </div>
101
115
  `;
102
-
103
- const element = document.getElementById('target');
104
- // The exact XPath depends on document structure but should contain the correct path components
105
- const xpath = domUtils.getXPath(element);
106
-
107
- expect(xpath).toContain('/html');
108
- expect(xpath).toContain('/body');
109
- expect(xpath).toContain('/div');
110
- expect(xpath).toContain('/section');
111
- expect(xpath).toContain('/article[1]');
112
- });
113
116
 
114
- it('should return empty string for null or undefined', () => {
115
- expect(domUtils.getXPath(null)).toBe('');
116
- expect(domUtils.getXPath(undefined)).toBe('');
117
- });
118
- });
119
-
120
- describe('hasFocus', () => {
121
- it('should return true if element has focus', () => {
122
- document.body.innerHTML = `<input id="test" type="text">`;
123
- const element = document.getElementById('test');
124
-
125
- // Focus the element
126
- element.focus();
127
-
128
- expect(domUtils.hasFocus(element)).toBe(true);
117
+ const element = document.getElementById('target');
118
+ // The exact XPath depends on document structure but should contain the correct path components
119
+ const xpath = domUtils.getXPath(element);
120
+
121
+ expect(xpath).toContain('/html');
122
+ expect(xpath).toContain('/body');
123
+ expect(xpath).toContain('/div');
124
+ expect(xpath).toContain('/section');
125
+ expect(xpath).toContain('/article[1]');
126
+ });
127
+
128
+ it('should return empty string for null or undefined', () => {
129
+ expect(domUtils.getXPath(null)).toBe('');
130
+ expect(domUtils.getXPath(undefined)).toBe('');
131
+ });
129
132
  });
130
133
 
131
- it('should return false if element does not have focus', () => {
132
- document.body.innerHTML = `
134
+ describe('hasFocus', () => {
135
+ it('should return true if element has focus', () => {
136
+ document.body.innerHTML = '<input id="test" type="text">';
137
+ const element = document.getElementById('test');
138
+
139
+ // Focus the element
140
+ element.focus();
141
+
142
+ expect(domUtils.hasFocus(element)).toBe(true);
143
+ });
144
+
145
+ it('should return false if element does not have focus', () => {
146
+ document.body.innerHTML = `
133
147
  <input id="test1" type="text">
134
148
  <input id="test2" type="text">
135
149
  `;
136
-
137
- const element1 = document.getElementById('test1');
138
- const element2 = document.getElementById('test2');
139
-
140
- // Focus element1
141
- element1.focus();
142
-
143
- expect(domUtils.hasFocus(element1)).toBe(true);
144
- expect(domUtils.hasFocus(element2)).toBe(false);
145
- });
146
- });
147
-
148
- describe('getAttributesAsString', () => {
149
- it('should return attributes as JSON string when element has attributes', () => {
150
- document.body.innerHTML = `<div id="test" class="container" data-value="123"></div>`;
151
- const element = document.getElementById('test');
152
-
153
- const attrsString = domUtils.getAttributesAsString(element);
154
- const parsed = JSON.parse(attrsString);
155
-
156
- expect(parsed.id).toBe('test');
157
- expect(parsed.class).toBe('container');
158
- expect(parsed['data-value']).toBe('123');
150
+
151
+ const element1 = document.getElementById('test1');
152
+ const element2 = document.getElementById('test2');
153
+
154
+ // Focus element1
155
+ element1.focus();
156
+
157
+ expect(domUtils.hasFocus(element1)).toBe(true);
158
+ expect(domUtils.hasFocus(element2)).toBe(false);
159
+ });
159
160
  });
160
161
 
161
- it('should return false for elements with no attributes', () => {
162
- const element = document.createElement('div');
163
- expect(domUtils.getAttributesAsString(element)).toBe(false);
162
+ describe('getAttributesAsString', () => {
163
+ it('should return attributes as JSON string when element has attributes', () => {
164
+ document.body.innerHTML = '<div id="test" class="container" data-value="123"></div>';
165
+ const element = document.getElementById('test');
166
+
167
+ const attrsString = domUtils.getAttributesAsString(element);
168
+ const parsed = JSON.parse(attrsString);
169
+
170
+ expect(parsed.id).toBe('test');
171
+ expect(parsed.class).toBe('container');
172
+ expect(parsed['data-value']).toBe('123');
173
+ });
174
+
175
+ it('should return false for elements with no attributes', () => {
176
+ const element = document.createElement('div');
177
+ expect(domUtils.getAttributesAsString(element)).toBe(false);
178
+ });
164
179
  });
165
- });
166
180
 
167
- describe('getDocumentSize', () => {
168
- it('should return the size of the document HTML', () => {
169
- const size = domUtils.getDocumentSize();
170
- expect(typeof size).toBe('number');
171
- expect(size).toBeGreaterThan(0);
181
+ describe('getDocumentSize', () => {
182
+ it('should return the size of the document HTML', () => {
183
+ const size = domUtils.getDocumentSize();
184
+ expect(typeof size).toBe('number');
185
+ expect(size).toBeGreaterThan(0);
186
+ });
172
187
  });
173
- });
174
188
 
175
- describe('getElementsWithDuplicateIds', () => {
176
- it('should find elements with duplicate IDs', () => {
177
- document.body.innerHTML = `
189
+ describe('getElementsWithDuplicateIds', () => {
190
+ it('should find elements with duplicate IDs', () => {
191
+ document.body.innerHTML = `
178
192
  <div id="duplicate">First</div>
179
193
  <span id="duplicate">Second</span>
180
194
  <p id="unique">Unique</p>
181
195
  `;
182
-
183
- const duplicates = domUtils.getElementsWithDuplicateIds();
184
- expect(duplicates.length).toBeGreaterThan(0);
185
- expect(duplicates[0].id).toBe('duplicate');
186
- });
187
196
 
188
- it('should return empty array when no duplicate IDs exist', () => {
189
- document.body.innerHTML = `
197
+ const duplicates = domUtils.getElementsWithDuplicateIds();
198
+ expect(duplicates.length).toBeGreaterThan(0);
199
+ expect(duplicates[0].id).toBe('duplicate');
200
+ });
201
+
202
+ it('should return empty array when no duplicate IDs exist', () => {
203
+ document.body.innerHTML = `
190
204
  <div id="first">First</div>
191
205
  <span id="second">Second</span>
192
206
  <p id="third">Third</p>
193
207
  `;
194
-
195
- const duplicates = domUtils.getElementsWithDuplicateIds();
196
- expect(duplicates.length).toBe(0);
197
- });
198
- });
199
-
200
- describe('getOuterHTML', () => {
201
- it('should return the outer HTML of an element', () => {
202
- document.body.innerHTML = `<div id="test" class="container">Content</div>`;
203
- const element = document.getElementById('test');
204
-
205
- const outerHTML = domUtils.getOuterHTML(element);
206
- expect(outerHTML).toBe('<div id="test" class="container">Content</div>');
207
- });
208
- });
209
-
210
- describe('isFullyVisible', () => {
211
- it('should check if element is fully visible in viewport', () => {
212
- document.body.innerHTML = `<div id="test" style="width: 100px; height: 100px;">Test</div>`;
213
- const element = document.getElementById('test');
214
-
215
- // Mock getBoundingClientRect to simulate visible element
216
- element.getBoundingClientRect = () => ({
217
- top: 10,
218
- left: 10,
219
- bottom: 110,
220
- right: 110
221
- });
222
-
223
- // Mock window dimensions
224
- Object.defineProperty(window, 'innerHeight', {
225
- writable: true,
226
- configurable: true,
227
- value: 800
228
- });
229
- Object.defineProperty(window, 'innerWidth', {
230
- writable: true,
231
- configurable: true,
232
- value: 1200
233
- });
234
-
235
- expect(domUtils.isFullyVisible(element)).toBe(true);
236
- });
237
208
 
238
- it('should return false if element is not fully visible', () => {
239
- document.body.innerHTML = `<div id="test" style="width: 100px; height: 100px;">Test</div>`;
240
- const element = document.getElementById('test');
241
-
242
- // Mock getBoundingClientRect to simulate partially visible element
243
- element.getBoundingClientRect = () => ({
244
- top: -50,
245
- left: 10,
246
- bottom: 50,
247
- right: 110
248
- });
249
-
250
- Object.defineProperty(window, 'innerHeight', {
251
- writable: true,
252
- configurable: true,
253
- value: 800
254
- });
255
- Object.defineProperty(window, 'innerWidth', {
256
- writable: true,
257
- configurable: true,
258
- value: 1200
259
- });
260
-
261
- expect(domUtils.isFullyVisible(element)).toBe(false);
209
+ const duplicates = domUtils.getElementsWithDuplicateIds();
210
+ expect(duplicates.length).toBe(0);
211
+ });
262
212
  });
263
213
 
264
- it('should return false when element extends beyond left edge', () => {
265
- const element = document.createElement('div');
266
- document.body.appendChild(element);
267
-
268
- element.getBoundingClientRect = vi.fn().mockReturnValue({
269
- top: 100,
270
- left: -10, // Beyond left edge
271
- bottom: 200,
272
- right: 300
273
- });
214
+ describe('getOuterHTML', () => {
215
+ it('should return the outer HTML of an element', () => {
216
+ document.body.innerHTML = '<div id="test" class="container">Content</div>';
217
+ const element = document.getElementById('test');
274
218
 
275
- expect(domUtils.isFullyVisible(element)).toBe(false);
219
+ const outerHTML = domUtils.getOuterHTML(element);
220
+ expect(outerHTML).toBe('<div id="test" class="container">Content</div>');
221
+ });
276
222
  });
277
223
 
278
- it('should return false when element extends beyond bottom edge', () => {
279
- const element = document.createElement('div');
280
- document.body.appendChild(element);
281
-
282
- vi.stubGlobal('innerHeight', 768);
283
-
284
- element.getBoundingClientRect = vi.fn().mockReturnValue({
285
- top: 700,
286
- left: 100,
287
- bottom: 800, // Beyond bottom edge (768)
288
- right: 300
289
- });
290
-
291
- expect(domUtils.isFullyVisible(element)).toBe(false);
224
+ describe('isFullyVisible', () => {
225
+ it('should check if element is fully visible in viewport', () => {
226
+ document.body.innerHTML =
227
+ '<div id="test" style="width: 100px; height: 100px;">Test</div>';
228
+ const element = document.getElementById('test');
229
+
230
+ // Mock getBoundingClientRect to simulate visible element
231
+ element.getBoundingClientRect = () => ({
232
+ top: 10,
233
+ left: 10,
234
+ bottom: 110,
235
+ right: 110,
236
+ });
237
+
238
+ // Mock window dimensions
239
+ Object.defineProperty(window, 'innerHeight', {
240
+ writable: true,
241
+ configurable: true,
242
+ value: 800,
243
+ });
244
+ Object.defineProperty(window, 'innerWidth', {
245
+ writable: true,
246
+ configurable: true,
247
+ value: 1200,
248
+ });
249
+
250
+ expect(domUtils.isFullyVisible(element)).toBe(true);
251
+ });
252
+
253
+ it('should return false if element is not fully visible', () => {
254
+ document.body.innerHTML =
255
+ '<div id="test" style="width: 100px; height: 100px;">Test</div>';
256
+ const element = document.getElementById('test');
257
+
258
+ // Mock getBoundingClientRect to simulate partially visible element
259
+ element.getBoundingClientRect = () => ({
260
+ top: -50,
261
+ left: 10,
262
+ bottom: 50,
263
+ right: 110,
264
+ });
265
+
266
+ Object.defineProperty(window, 'innerHeight', {
267
+ writable: true,
268
+ configurable: true,
269
+ value: 800,
270
+ });
271
+ Object.defineProperty(window, 'innerWidth', {
272
+ writable: true,
273
+ configurable: true,
274
+ value: 1200,
275
+ });
276
+
277
+ expect(domUtils.isFullyVisible(element)).toBe(false);
278
+ });
279
+
280
+ it('should return false when element extends beyond left edge', () => {
281
+ const element = document.createElement('div');
282
+ document.body.appendChild(element);
283
+
284
+ element.getBoundingClientRect = vi.fn().mockReturnValue({
285
+ top: 100,
286
+ left: -10, // Beyond left edge
287
+ bottom: 200,
288
+ right: 300,
289
+ });
290
+
291
+ expect(domUtils.isFullyVisible(element)).toBe(false);
292
+ });
293
+
294
+ it('should return false when element extends beyond bottom edge', () => {
295
+ const element = document.createElement('div');
296
+ document.body.appendChild(element);
297
+
298
+ vi.stubGlobal('innerHeight', 768);
299
+
300
+ element.getBoundingClientRect = vi.fn().mockReturnValue({
301
+ top: 700,
302
+ left: 100,
303
+ bottom: 800, // Beyond bottom edge (768)
304
+ right: 300,
305
+ });
306
+
307
+ expect(domUtils.isFullyVisible(element)).toBe(false);
308
+ });
309
+
310
+ it('should return false when element extends beyond right edge', () => {
311
+ const element = document.createElement('div');
312
+ document.body.appendChild(element);
313
+
314
+ vi.stubGlobal('innerWidth', 1024);
315
+
316
+ element.getBoundingClientRect = vi.fn().mockReturnValue({
317
+ top: 100,
318
+ left: 900,
319
+ bottom: 200,
320
+ right: 1100, // Beyond right edge (1024)
321
+ });
322
+
323
+ expect(domUtils.isFullyVisible(element)).toBe(false);
324
+ });
292
325
  });
293
326
 
294
- it('should return false when element extends beyond right edge', () => {
295
- const element = document.createElement('div');
296
- document.body.appendChild(element);
297
-
298
- vi.stubGlobal('innerWidth', 1024);
299
-
300
- element.getBoundingClientRect = vi.fn().mockReturnValue({
301
- top: 100,
302
- left: 900,
303
- bottom: 200,
304
- right: 1100 // Beyond right edge (1024)
305
- });
306
-
307
- expect(domUtils.isFullyVisible(element)).toBe(false);
308
- });
309
- });
310
-
311
- describe('getXPath edge cases', () => {
312
- it('should handle elements with multiple siblings of same type', () => {
313
- document.body.innerHTML = `
327
+ describe('getXPath edge cases', () => {
328
+ it('should handle elements with multiple siblings of same type', () => {
329
+ document.body.innerHTML = `
314
330
  <div>
315
331
  <span></span>
316
332
  <span></span>
317
333
  <span id="target"></span>
318
334
  </div>
319
335
  `;
320
- const element = document.getElementById('target');
321
- const xpath = domUtils.getXPath(element);
336
+ const element = document.getElementById('target');
337
+ const xpath = domUtils.getXPath(element);
322
338
 
323
- expect(xpath).toContain('span[3]');
324
- });
339
+ expect(xpath).toContain('span[3]');
340
+ });
325
341
 
326
- it('should handle deeply nested elements', () => {
327
- document.body.innerHTML = `
342
+ it('should handle deeply nested elements', () => {
343
+ document.body.innerHTML = `
328
344
  <div>
329
345
  <section>
330
346
  <article>
@@ -333,56 +349,558 @@ describe('domUtils', () => {
333
349
  </section>
334
350
  </div>
335
351
  `;
336
- const element = document.getElementById('deep');
337
- const xpath = domUtils.getXPath(element);
338
-
339
- expect(xpath).toContain('/div');
340
- expect(xpath).toContain('/section');
341
- expect(xpath).toContain('/article');
342
- expect(xpath).toContain('/p');
352
+ const element = document.getElementById('deep');
353
+ const xpath = domUtils.getXPath(element);
354
+
355
+ expect(xpath).toContain('/div');
356
+ expect(xpath).toContain('/section');
357
+ expect(xpath).toContain('/article');
358
+ expect(xpath).toContain('/p');
359
+ });
343
360
  });
344
- });
345
361
 
346
- describe('hasFocus edge cases', () => {
347
- it('should return false when no element has focus', () => {
348
- const element = document.createElement('div');
349
- document.body.appendChild(element);
362
+ describe('hasFocus edge cases', () => {
363
+ it('should return false when no element has focus', () => {
364
+ const element = document.createElement('div');
365
+ document.body.appendChild(element);
350
366
 
351
- // Ensure nothing has focus
352
- if (document.activeElement) {
353
- document.activeElement.blur?.();
354
- }
367
+ // Ensure nothing has focus
368
+ if (document.activeElement) {
369
+ document.activeElement.blur?.();
370
+ }
355
371
 
356
- expect(domUtils.hasFocus(element)).toBe(false);
372
+ expect(domUtils.hasFocus(element)).toBe(false);
373
+ });
357
374
  });
358
- });
359
375
 
360
- describe('getXPath - non-element nodes', () => {
361
- it('should handle non-element nodes properly', () => {
362
- // Test with text node (nodeType 3)
363
- const textNode = document.createTextNode('test text');
364
- const xpath = domUtils.getXPath(textNode);
376
+ describe('getXPath - non-element nodes', () => {
377
+ it('should handle non-element nodes properly', () => {
378
+ // Test with text node (nodeType 3)
379
+ const textNode = document.createTextNode('test text');
380
+ const xpath = domUtils.getXPath(textNode);
365
381
 
366
- expect(xpath).toBe('');
382
+ expect(xpath).toBe('');
383
+ });
367
384
  });
368
- });
369
385
 
370
- describe('attrBegins - array input', () => {
371
- it('should handle array input correctly', () => {
372
- document.body.innerHTML = `
386
+ describe('attrBegins - array input', () => {
387
+ it('should handle array input correctly', () => {
388
+ document.body.innerHTML = `
373
389
  <div id="div1" data-test="value"></div>
374
390
  <div id="div2" aria-label="value"></div>
375
391
  `;
376
392
 
377
- // Test with array instead of NodeList
378
- const elementsArray = [
379
- document.getElementById('div1'),
380
- document.getElementById('div2')
381
- ];
393
+ // Test with array instead of NodeList
394
+ const elementsArray = [
395
+ document.getElementById('div1'),
396
+ document.getElementById('div2'),
397
+ ];
398
+
399
+ const dataElements = domUtils.attrBegins(elementsArray, 'data-');
400
+ expect(dataElements.length).toBe(1);
401
+ expect(dataElements[0].id).toBe('div1');
402
+ });
403
+ });
404
+
405
+ describe('isIdReferenced', () => {
406
+ it('should return false for null/empty id', () => {
407
+ expect(domUtils.isIdReferenced(null)).toBe(false);
408
+ expect(domUtils.isIdReferenced('')).toBe(false);
409
+ expect(domUtils.isIdReferenced(undefined)).toBe(false);
410
+ });
411
+
412
+ it('should return true when id is referenced by label[for]', () => {
413
+ document.body.innerHTML = `
414
+ <label for="myInput">Name</label>
415
+ <input id="myInput" type="text">
416
+ `;
417
+ expect(domUtils.isIdReferenced('myInput')).toBe(true);
418
+ });
419
+
420
+ it('should return true when id is referenced by aria-labelledby', () => {
421
+ document.body.innerHTML = `
422
+ <span id="lbl">Label text</span>
423
+ <div aria-labelledby="lbl"></div>
424
+ `;
425
+ expect(domUtils.isIdReferenced('lbl')).toBe(true);
426
+ });
427
+
428
+ it('should return true when id is in multi-value aria-labelledby', () => {
429
+ document.body.innerHTML = `
430
+ <span id="lbl1">First</span>
431
+ <span id="lbl2">Second</span>
432
+ <div aria-labelledby="lbl1 lbl2"></div>
433
+ `;
434
+ expect(domUtils.isIdReferenced('lbl2')).toBe(true);
435
+ });
436
+
437
+ it('should return true when id is referenced by aria-describedby', () => {
438
+ document.body.innerHTML = `
439
+ <span id="desc">Description</span>
440
+ <input aria-describedby="desc">
441
+ `;
442
+ expect(domUtils.isIdReferenced('desc')).toBe(true);
443
+ });
444
+
445
+ it('should return true when id is referenced by aria-controls', () => {
446
+ document.body.innerHTML = `
447
+ <div id="panel">Panel content</div>
448
+ <button aria-controls="panel">Toggle</button>
449
+ `;
450
+ expect(domUtils.isIdReferenced('panel')).toBe(true);
451
+ });
452
+
453
+ it('should return true when id is referenced by aria-owns', () => {
454
+ document.body.innerHTML = `
455
+ <div id="owned">Owned element</div>
456
+ <div aria-owns="owned"></div>
457
+ `;
458
+ expect(domUtils.isIdReferenced('owned')).toBe(true);
459
+ });
460
+
461
+ it('should return true when id is referenced by aria-activedescendant', () => {
462
+ document.body.innerHTML = `
463
+ <div id="item1">Item 1</div>
464
+ <div aria-activedescendant="item1"></div>
465
+ `;
466
+ expect(domUtils.isIdReferenced('item1')).toBe(true);
467
+ });
468
+
469
+ it('should return true when id is referenced by aria-errormessage', () => {
470
+ document.body.innerHTML = `
471
+ <span id="err">Error message</span>
472
+ <input aria-errormessage="err">
473
+ `;
474
+ expect(domUtils.isIdReferenced('err')).toBe(true);
475
+ });
476
+
477
+ it('should return true when id is referenced by href fragment', () => {
478
+ document.body.innerHTML = `
479
+ <div id="section1">Section 1</div>
480
+ <a href="#section1">Go to section</a>
481
+ `;
482
+ expect(domUtils.isIdReferenced('section1')).toBe(true);
483
+ });
484
+
485
+ it('should return true when id is referenced by headers attribute', () => {
486
+ document.body.innerHTML = `
487
+ <table>
488
+ <tr><th id="h1">Header 1</th></tr>
489
+ <tr><td headers="h1">Data</td></tr>
490
+ </table>
491
+ `;
492
+ expect(domUtils.isIdReferenced('h1')).toBe(true);
493
+ });
494
+
495
+ it('should return true when id is referenced by input list attribute', () => {
496
+ document.body.innerHTML = `
497
+ <datalist id="opts"><option value="A"></option></datalist>
498
+ <input list="opts">
499
+ `;
500
+ expect(domUtils.isIdReferenced('opts')).toBe(true);
501
+ });
502
+
503
+ it('should return false when id is not referenced', () => {
504
+ document.body.innerHTML = `
505
+ <div id="lonely">Not referenced</div>
506
+ `;
507
+ expect(domUtils.isIdReferenced('lonely')).toBe(false);
508
+ });
509
+
510
+ it('should return true when id is referenced by aria-flowto', () => {
511
+ document.body.innerHTML = `
512
+ <div id="next">Next section</div>
513
+ <div aria-flowto="next">Current</div>
514
+ `;
515
+ expect(domUtils.isIdReferenced('next')).toBe(true);
516
+ });
517
+ });
518
+
519
+ describe('isHiddenFromAT', () => {
520
+ it('should return false for null/undefined', () => {
521
+ expect(domUtils.isHiddenFromAT(null)).toBe(false);
522
+ expect(domUtils.isHiddenFromAT(undefined)).toBe(false);
523
+ });
524
+
525
+ it('should return true when element has aria-hidden="true"', () => {
526
+ const el = document.createElement('div');
527
+ el.setAttribute('aria-hidden', 'true');
528
+ expect(domUtils.isHiddenFromAT(el)).toBe(true);
529
+ });
530
+
531
+ it('should return false when element has aria-hidden="false"', () => {
532
+ const el = document.createElement('div');
533
+ el.setAttribute('aria-hidden', 'false');
534
+ document.body.appendChild(el);
535
+ expect(domUtils.isHiddenFromAT(el)).toBe(false);
536
+ });
537
+
538
+ it('should return true when ancestor has aria-hidden="true"', () => {
539
+ document.body.innerHTML = `
540
+ <div aria-hidden="true">
541
+ <span id="child">Hidden child</span>
542
+ </div>
543
+ `;
544
+ const child = document.getElementById('child');
545
+ expect(domUtils.isHiddenFromAT(child)).toBe(true);
546
+ });
547
+
548
+ it('should return false when no aria-hidden in tree', () => {
549
+ document.body.innerHTML = `
550
+ <div><span id="child">Visible</span></div>
551
+ `;
552
+ const child = document.getElementById('child');
553
+ expect(domUtils.isHiddenFromAT(child)).toBe(false);
554
+ });
555
+ });
556
+
557
+ describe('isEffectivelyInteractive', () => {
558
+ it('should return false when element is disabled', () => {
559
+ const el = document.createElement('button');
560
+ el.disabled = true;
561
+ expect(domUtils.isEffectivelyInteractive(el)).toBe(false);
562
+ });
563
+
564
+ it('should return false when aria-disabled="true"', () => {
565
+ const el = document.createElement('div');
566
+ el.setAttribute('aria-disabled', 'true');
567
+ expect(domUtils.isEffectivelyInteractive(el)).toBe(false);
568
+ });
569
+
570
+ it('should return false when aria-hidden="true"', () => {
571
+ const el = document.createElement('button');
572
+ el.setAttribute('aria-hidden', 'true');
573
+ expect(domUtils.isEffectivelyInteractive(el)).toBe(false);
574
+ });
575
+
576
+ it('should return false when role is "presentation"', () => {
577
+ const el = document.createElement('div');
578
+ el.setAttribute('role', 'presentation');
579
+ expect(domUtils.isEffectivelyInteractive(el)).toBe(false);
580
+ });
581
+
582
+ it('should return false when role is "none"', () => {
583
+ const el = document.createElement('div');
584
+ el.setAttribute('role', 'none');
585
+ expect(domUtils.isEffectivelyInteractive(el)).toBe(false);
586
+ });
587
+
588
+ it('should return true for enabled interactive element', () => {
589
+ const el = document.createElement('button');
590
+ expect(domUtils.isEffectivelyInteractive(el)).toBe(true);
591
+ });
592
+
593
+ it('should return true for element with role="button"', () => {
594
+ const el = document.createElement('div');
595
+ el.setAttribute('role', 'button');
596
+ expect(domUtils.isEffectivelyInteractive(el)).toBe(true);
597
+ });
598
+ });
599
+
600
+ describe('isValidAriaNesting', () => {
601
+ it('should return true for listbox > option', () => {
602
+ const parent = document.createElement('div');
603
+ parent.setAttribute('role', 'listbox');
604
+ const child = document.createElement('div');
605
+ child.setAttribute('role', 'option');
606
+ parent.appendChild(child);
607
+ expect(domUtils.isValidAriaNesting(parent, child)).toBe(true);
608
+ });
609
+
610
+ it('should return true for menu > menuitem', () => {
611
+ const parent = document.createElement('div');
612
+ parent.setAttribute('role', 'menu');
613
+ const child = document.createElement('div');
614
+ child.setAttribute('role', 'menuitem');
615
+ parent.appendChild(child);
616
+ expect(domUtils.isValidAriaNesting(parent, child)).toBe(true);
617
+ });
618
+
619
+ it('should return true for tablist > tab', () => {
620
+ const parent = document.createElement('div');
621
+ parent.setAttribute('role', 'tablist');
622
+ const child = document.createElement('div');
623
+ child.setAttribute('role', 'tab');
624
+ parent.appendChild(child);
625
+ expect(domUtils.isValidAriaNesting(parent, child)).toBe(true);
626
+ });
627
+
628
+ it('should return true for radiogroup > radio', () => {
629
+ const parent = document.createElement('div');
630
+ parent.setAttribute('role', 'radiogroup');
631
+ const child = document.createElement('div');
632
+ child.setAttribute('role', 'radio');
633
+ parent.appendChild(child);
634
+ expect(domUtils.isValidAriaNesting(parent, child)).toBe(true);
635
+ });
636
+
637
+ it('should return false for invalid nesting', () => {
638
+ const parent = document.createElement('div');
639
+ parent.setAttribute('role', 'listbox');
640
+ const child = document.createElement('div');
641
+ child.setAttribute('role', 'button');
642
+ parent.appendChild(child);
643
+ expect(domUtils.isValidAriaNesting(parent, child)).toBe(false);
644
+ });
645
+
646
+ it('should return true for label wrapping input', () => {
647
+ const label = document.createElement('label');
648
+ const input = document.createElement('input');
649
+ label.appendChild(input);
650
+ expect(domUtils.isValidAriaNesting(label, input)).toBe(true);
651
+ });
652
+
653
+ it('should return false when parent has no role', () => {
654
+ const parent = document.createElement('div');
655
+ const child = document.createElement('div');
656
+ child.setAttribute('role', 'option');
657
+ parent.appendChild(child);
658
+ expect(domUtils.isValidAriaNesting(parent, child)).toBe(false);
659
+ });
660
+
661
+ it('should return true for combobox > listbox', () => {
662
+ const parent = document.createElement('div');
663
+ parent.setAttribute('role', 'combobox');
664
+ const child = document.createElement('div');
665
+ child.setAttribute('role', 'listbox');
666
+ parent.appendChild(child);
667
+ expect(domUtils.isValidAriaNesting(parent, child)).toBe(true);
668
+ });
669
+ });
670
+
671
+ describe('hasInteractiveHandler', () => {
672
+ it('should return true for element with onclick', () => {
673
+ const el = document.createElement('div');
674
+ el.setAttribute('onclick', 'doSomething()');
675
+ expect(domUtils.hasInteractiveHandler(el)).toBe(true);
676
+ });
677
+
678
+ it('should return true for element with onmousedown', () => {
679
+ const el = document.createElement('div');
680
+ el.setAttribute('onmousedown', 'handle()');
681
+ expect(domUtils.hasInteractiveHandler(el)).toBe(true);
682
+ });
683
+
684
+ it('should return true for element with onkeydown', () => {
685
+ const el = document.createElement('div');
686
+ el.setAttribute('onkeydown', 'handle()');
687
+ expect(domUtils.hasInteractiveHandler(el)).toBe(true);
688
+ });
689
+
690
+ it('should return true for element with ontouchstart', () => {
691
+ const el = document.createElement('div');
692
+ el.setAttribute('ontouchstart', 'handle()');
693
+ expect(domUtils.hasInteractiveHandler(el)).toBe(true);
694
+ });
695
+
696
+ it('should return true for element with onmouseup', () => {
697
+ const el = document.createElement('div');
698
+ el.setAttribute('onmouseup', 'handle()');
699
+ expect(domUtils.hasInteractiveHandler(el)).toBe(true);
700
+ });
701
+
702
+ it('should return true for element with onkeyup', () => {
703
+ const el = document.createElement('div');
704
+ el.setAttribute('onkeyup', 'handle()');
705
+ expect(domUtils.hasInteractiveHandler(el)).toBe(true);
706
+ });
707
+
708
+ it('should return false for element with no handlers', () => {
709
+ const el = document.createElement('div');
710
+ expect(domUtils.hasInteractiveHandler(el)).toBe(false);
711
+ });
712
+
713
+ it('should return false for element with non-interactive event handlers', () => {
714
+ const el = document.createElement('div');
715
+ el.setAttribute('onload', 'handle()');
716
+ expect(domUtils.hasInteractiveHandler(el)).toBe(false);
717
+ });
718
+ });
719
+
720
+ describe('isWithinNavContext', () => {
721
+ it('should return true when inside nav element', () => {
722
+ document.body.innerHTML = `
723
+ <nav><a id="link" href="#">Link</a></nav>
724
+ `;
725
+ expect(domUtils.isWithinNavContext(document.getElementById('link'))).toBe(true);
726
+ });
727
+
728
+ it('should return true when inside role="navigation"', () => {
729
+ document.body.innerHTML = `
730
+ <div role="navigation"><a id="link" href="#">Link</a></div>
731
+ `;
732
+ expect(domUtils.isWithinNavContext(document.getElementById('link'))).toBe(true);
733
+ });
734
+
735
+ it('should return true when inside role="menu"', () => {
736
+ document.body.innerHTML = `
737
+ <div role="menu"><div role="menuitem" id="item">Item</div></div>
738
+ `;
739
+ expect(domUtils.isWithinNavContext(document.getElementById('item'))).toBe(true);
740
+ });
741
+
742
+ it('should return true when inside role="menubar"', () => {
743
+ document.body.innerHTML = `
744
+ <div role="menubar"><div role="menuitem" id="item">Item</div></div>
745
+ `;
746
+ expect(domUtils.isWithinNavContext(document.getElementById('item'))).toBe(true);
747
+ });
748
+
749
+ it('should return false when not in nav context', () => {
750
+ document.body.innerHTML = `
751
+ <div><a id="link" href="#">Link</a></div>
752
+ `;
753
+ expect(domUtils.isWithinNavContext(document.getElementById('link'))).toBe(false);
754
+ });
755
+ });
756
+
757
+ describe('isLandmark', () => {
758
+ it('should return true for native landmark elements', () => {
759
+ const tags = ['nav', 'main', 'header', 'footer', 'aside', 'section'];
760
+ tags.forEach(tag => {
761
+ const el = document.createElement(tag);
762
+ expect(domUtils.isLandmark(el)).toBe(true);
763
+ });
764
+ });
765
+
766
+ it('should return true for elements with landmark roles', () => {
767
+ const roles = [
768
+ 'navigation',
769
+ 'main',
770
+ 'banner',
771
+ 'contentinfo',
772
+ 'complementary',
773
+ 'region',
774
+ 'search',
775
+ 'form',
776
+ ];
777
+ roles.forEach(role => {
778
+ const el = document.createElement('div');
779
+ el.setAttribute('role', role);
780
+ expect(domUtils.isLandmark(el)).toBe(true);
781
+ });
782
+ });
783
+
784
+ it('should return false for non-landmark elements', () => {
785
+ const el = document.createElement('div');
786
+ expect(domUtils.isLandmark(el)).toBe(false);
787
+ });
788
+
789
+ it('should return false for elements with non-landmark roles', () => {
790
+ const el = document.createElement('div');
791
+ el.setAttribute('role', 'button');
792
+ expect(domUtils.isLandmark(el)).toBe(false);
793
+ });
794
+ });
795
+
796
+ describe('getSemanticContainer', () => {
797
+ it('should return article ancestor', () => {
798
+ document.body.innerHTML = `
799
+ <article><p id="child">Content</p></article>
800
+ `;
801
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
802
+ expect(result.tagName).toBe('ARTICLE');
803
+ });
804
+
805
+ it('should return section ancestor', () => {
806
+ document.body.innerHTML = `
807
+ <section><p id="child">Content</p></section>
808
+ `;
809
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
810
+ expect(result.tagName).toBe('SECTION');
811
+ });
812
+
813
+ it('should return li ancestor', () => {
814
+ document.body.innerHTML = `
815
+ <ul><li><span id="child">Item</span></li></ul>
816
+ `;
817
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
818
+ expect(result.tagName).toBe('LI');
819
+ });
820
+
821
+ it('should return td ancestor', () => {
822
+ document.body.innerHTML = `
823
+ <table><tr><td><span id="child">Cell</span></td></tr></table>
824
+ `;
825
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
826
+ expect(result.tagName).toBe('TD');
827
+ });
828
+
829
+ it('should return element with role attribute', () => {
830
+ document.body.innerHTML = `
831
+ <div role="region"><p id="child">Content</p></div>
832
+ `;
833
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
834
+ expect(result.getAttribute('role')).toBe('region');
835
+ });
836
+
837
+ it('should return null when no semantic container found', () => {
838
+ document.body.innerHTML = `
839
+ <div><p id="child">Content</p></div>
840
+ `;
841
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
842
+ expect(result).toBeNull();
843
+ });
844
+
845
+ it('should return nearest semantic container', () => {
846
+ document.body.innerHTML = `
847
+ <article><section><p id="child">Content</p></section></article>
848
+ `;
849
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
850
+ expect(result.tagName).toBe('SECTION');
851
+ });
852
+ });
382
853
 
383
- const dataElements = domUtils.attrBegins(elementsArray, 'data-');
384
- expect(dataElements.length).toBe(1);
385
- expect(dataElements[0].id).toBe('div1');
854
+ describe('getHeadingLevel', () => {
855
+ it('should return level for native heading elements', () => {
856
+ for (let i = 1; i <= 6; i++) {
857
+ const el = document.createElement('h' + i);
858
+ expect(domUtils.getHeadingLevel(el)).toBe(i);
859
+ }
860
+ });
861
+
862
+ it('should return level from aria-level on role="heading"', () => {
863
+ const el = document.createElement('div');
864
+ el.setAttribute('role', 'heading');
865
+ el.setAttribute('aria-level', '3');
866
+ expect(domUtils.getHeadingLevel(el)).toBe(3);
867
+ });
868
+
869
+ it('should return null for role="heading" without aria-level', () => {
870
+ const el = document.createElement('div');
871
+ el.setAttribute('role', 'heading');
872
+ expect(domUtils.getHeadingLevel(el)).toBeNull();
873
+ });
874
+
875
+ it('should return null for role="heading" with invalid aria-level', () => {
876
+ const el = document.createElement('div');
877
+ el.setAttribute('role', 'heading');
878
+ el.setAttribute('aria-level', 'abc');
879
+ expect(domUtils.getHeadingLevel(el)).toBeNull();
880
+ });
881
+
882
+ it('should return null for role="heading" with level 0', () => {
883
+ const el = document.createElement('div');
884
+ el.setAttribute('role', 'heading');
885
+ el.setAttribute('aria-level', '0');
886
+ expect(domUtils.getHeadingLevel(el)).toBeNull();
887
+ });
888
+
889
+ it('should override native heading level with aria-level', () => {
890
+ const el = document.createElement('h2');
891
+ el.setAttribute('aria-level', '4');
892
+ expect(domUtils.getHeadingLevel(el)).toBe(4);
893
+ });
894
+
895
+ it('should return null for non-heading elements', () => {
896
+ const el = document.createElement('div');
897
+ expect(domUtils.getHeadingLevel(el)).toBeNull();
898
+ });
899
+
900
+ it('should return null for non-heading with aria-level', () => {
901
+ const el = document.createElement('p');
902
+ el.setAttribute('aria-level', '2');
903
+ expect(domUtils.getHeadingLevel(el)).toBeNull();
904
+ });
386
905
  });
387
- });
388
- });
906
+ });