@afixt/test-utils 1.2.0 → 1.2.2
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/eslint.config.mjs +1 -1
- package/package.json +2 -1
- package/src/constants.js +231 -0
- package/src/cssUtils.js +77 -0
- package/src/domUtils.js +268 -12
- package/src/formUtils.js +175 -0
- package/src/getAccessibleText.js +60 -39
- package/src/getCSSGeneratedContent.js +39 -17
- package/src/index.js +18 -2
- package/src/stringUtils.js +149 -0
- package/src/tableUtils.js +180 -0
- package/src/testContrast.js +35 -1
- package/src/testLang.js +514 -444
- package/test/cssUtils.test.js +248 -0
- package/test/domUtils.test.js +815 -297
- package/test/formUtils.test.js +389 -0
- package/test/getAccessibleText.test.js +93 -0
- package/test/getCSSGeneratedContent.test.js +187 -232
- package/test/hasCSSGeneratedContent.test.js +37 -147
- package/test/playwright/css-pseudo-elements.spec.js +224 -91
- package/test/playwright/fixtures/css-pseudo-elements.html +6 -0
- package/test/stringUtils.test.js +222 -0
- package/test/tableUtils.test.js +340 -0
- package/vitest.config.js +28 -28
- package/test/getCSSGeneratedContent.browser.test.js +0 -125
package/test/domUtils.test.js
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
219
|
+
const outerHTML = domUtils.getOuterHTML(element);
|
|
220
|
+
expect(outerHTML).toBe('<div id="test" class="container">Content</div>');
|
|
221
|
+
});
|
|
276
222
|
});
|
|
277
223
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
321
|
-
|
|
336
|
+
const element = document.getElementById('target');
|
|
337
|
+
const xpath = domUtils.getXPath(element);
|
|
322
338
|
|
|
323
|
-
|
|
324
|
-
|
|
339
|
+
expect(xpath).toContain('span[3]');
|
|
340
|
+
});
|
|
325
341
|
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
367
|
+
// Ensure nothing has focus
|
|
368
|
+
if (document.activeElement) {
|
|
369
|
+
document.activeElement.blur?.();
|
|
370
|
+
}
|
|
355
371
|
|
|
356
|
-
|
|
372
|
+
expect(domUtils.hasFocus(element)).toBe(false);
|
|
373
|
+
});
|
|
357
374
|
});
|
|
358
|
-
});
|
|
359
375
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
382
|
+
expect(xpath).toBe('');
|
|
383
|
+
});
|
|
367
384
|
});
|
|
368
|
-
});
|
|
369
385
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
+
});
|