@afixt/test-utils 1.2.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +1 -6
- package/BROWSER_TESTING.md +42 -22
- package/CHANGELOG.md +40 -0
- package/CLAUDE.md +10 -9
- package/package.json +1 -1
- package/src/constants.js +438 -1
- package/src/domUtils.js +17 -38
- package/src/formUtils.js +7 -24
- package/src/getAccessibleName.js +20 -56
- package/src/getCSSGeneratedContent.js +2 -0
- package/src/getFocusableElements.js +12 -21
- package/src/getGeneratedContent.js +18 -11
- package/src/getImageText.js +22 -7
- package/src/hasValidAriaRole.js +11 -19
- package/src/index.js +4 -4
- package/src/interactiveRoles.js +2 -19
- package/src/isA11yVisible.js +95 -0
- package/src/isAriaAttributesValid.js +5 -64
- package/src/isFocusable.js +30 -10
- package/src/isHidden.js +44 -8
- package/src/listEventListeners.js +115 -10
- package/src/stringUtils.js +19 -98
- package/src/tableUtils.js +4 -36
- package/src/testContrast.js +54 -0
- package/test/domUtils.test.js +156 -0
- package/test/formUtils.test.js +0 -47
- package/test/getAccessibleName.test.js +39 -0
- package/test/getGeneratedContent.test.js +305 -241
- package/test/getImageText.test.js +158 -99
- package/test/index.test.js +54 -17
- package/test/{isVisible.test.js → isA11yVisible.test.js} +39 -33
- package/test/isFocusable.test.js +265 -272
- package/test/isHidden.test.js +257 -153
- package/test/listEventListeners.test.js +163 -44
- package/test/playwright/css-pseudo-elements.spec.js +3 -13
- package/test/stringUtils.test.js +55 -228
- package/test/testContrast.test.js +104 -2
- package/todo.md +2 -2
- package/src/isVisible.js +0 -103
|
@@ -30,9 +30,9 @@ describe('listEventListeners', () => {
|
|
|
30
30
|
it('should track added event listeners', () => {
|
|
31
31
|
const element = document.createElement('button');
|
|
32
32
|
const listener = vi.fn();
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
element.addEventListener('click', listener);
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
const listeners = getEventListeners(element);
|
|
37
37
|
expect(listeners.click).toBeDefined();
|
|
38
38
|
expect(listeners.click.length).toBe(1);
|
|
@@ -43,10 +43,10 @@ describe('listEventListeners', () => {
|
|
|
43
43
|
const element = document.createElement('div');
|
|
44
44
|
const listener1 = vi.fn();
|
|
45
45
|
const listener2 = vi.fn();
|
|
46
|
-
|
|
46
|
+
|
|
47
47
|
element.addEventListener('click', listener1);
|
|
48
48
|
element.addEventListener('click', listener2);
|
|
49
|
-
|
|
49
|
+
|
|
50
50
|
const listeners = getEventListeners(element);
|
|
51
51
|
expect(listeners.click.length).toBe(2);
|
|
52
52
|
expect(listeners.click[0].listener).toBe(listener1);
|
|
@@ -57,10 +57,10 @@ describe('listEventListeners', () => {
|
|
|
57
57
|
const element = document.createElement('input');
|
|
58
58
|
const clickListener = vi.fn();
|
|
59
59
|
const focusListener = vi.fn();
|
|
60
|
-
|
|
60
|
+
|
|
61
61
|
element.addEventListener('click', clickListener);
|
|
62
62
|
element.addEventListener('focus', focusListener);
|
|
63
|
-
|
|
63
|
+
|
|
64
64
|
const listeners = getEventListeners(element);
|
|
65
65
|
expect(listeners.click).toBeDefined();
|
|
66
66
|
expect(listeners.focus).toBeDefined();
|
|
@@ -72,9 +72,9 @@ describe('listEventListeners', () => {
|
|
|
72
72
|
const element = document.createElement('div');
|
|
73
73
|
const listener = vi.fn();
|
|
74
74
|
const options = { capture: true, once: true };
|
|
75
|
-
|
|
75
|
+
|
|
76
76
|
element.addEventListener('click', listener, options);
|
|
77
|
-
|
|
77
|
+
|
|
78
78
|
const listeners = getEventListeners(element);
|
|
79
79
|
expect(listeners.click[0].options).toEqual(options);
|
|
80
80
|
});
|
|
@@ -82,9 +82,9 @@ describe('listEventListeners', () => {
|
|
|
82
82
|
it('should call original addEventListener', () => {
|
|
83
83
|
const element = document.createElement('button');
|
|
84
84
|
const listener = vi.fn();
|
|
85
|
-
|
|
85
|
+
|
|
86
86
|
element.addEventListener('click', listener);
|
|
87
|
-
|
|
87
|
+
|
|
88
88
|
// Verify the event listener was actually attached by triggering it
|
|
89
89
|
element.click();
|
|
90
90
|
expect(listener).toHaveBeenCalled();
|
|
@@ -95,10 +95,10 @@ describe('listEventListeners', () => {
|
|
|
95
95
|
it('should remove tracked event listeners', () => {
|
|
96
96
|
const element = document.createElement('button');
|
|
97
97
|
const listener = vi.fn();
|
|
98
|
-
|
|
98
|
+
|
|
99
99
|
element.addEventListener('click', listener);
|
|
100
100
|
element.removeEventListener('click', listener);
|
|
101
|
-
|
|
101
|
+
|
|
102
102
|
const listeners = getEventListeners(element);
|
|
103
103
|
expect(listeners.click.length).toBe(0);
|
|
104
104
|
});
|
|
@@ -107,11 +107,11 @@ describe('listEventListeners', () => {
|
|
|
107
107
|
const element = document.createElement('div');
|
|
108
108
|
const listener1 = vi.fn();
|
|
109
109
|
const listener2 = vi.fn();
|
|
110
|
-
|
|
110
|
+
|
|
111
111
|
element.addEventListener('click', listener1);
|
|
112
112
|
element.addEventListener('click', listener2);
|
|
113
113
|
element.removeEventListener('click', listener1);
|
|
114
|
-
|
|
114
|
+
|
|
115
115
|
const listeners = getEventListeners(element);
|
|
116
116
|
expect(listeners.click.length).toBe(1);
|
|
117
117
|
expect(listeners.click[0].listener).toBe(listener2);
|
|
@@ -120,7 +120,7 @@ describe('listEventListeners', () => {
|
|
|
120
120
|
it('should handle removing non-existent listeners gracefully', () => {
|
|
121
121
|
const element = document.createElement('button');
|
|
122
122
|
const listener = vi.fn();
|
|
123
|
-
|
|
123
|
+
|
|
124
124
|
expect(() => {
|
|
125
125
|
element.removeEventListener('click', listener);
|
|
126
126
|
}).not.toThrow();
|
|
@@ -129,10 +129,10 @@ describe('listEventListeners', () => {
|
|
|
129
129
|
it('should call original removeEventListener', () => {
|
|
130
130
|
const element = document.createElement('button');
|
|
131
131
|
const listener = vi.fn();
|
|
132
|
-
|
|
132
|
+
|
|
133
133
|
element.addEventListener('click', listener);
|
|
134
134
|
element.removeEventListener('click', listener);
|
|
135
|
-
|
|
135
|
+
|
|
136
136
|
// Verify the event listener was actually removed by triggering it
|
|
137
137
|
element.click();
|
|
138
138
|
expect(listener).not.toHaveBeenCalled();
|
|
@@ -144,10 +144,10 @@ describe('listEventListeners', () => {
|
|
|
144
144
|
const element = document.createElement('div');
|
|
145
145
|
element.id = 'test-id';
|
|
146
146
|
document.body.appendChild(element);
|
|
147
|
-
|
|
147
|
+
|
|
148
148
|
const result = listEventListeners(document.body);
|
|
149
149
|
element.addEventListener('click', () => {});
|
|
150
|
-
|
|
150
|
+
|
|
151
151
|
const listeners = listEventListeners(document.body);
|
|
152
152
|
expect(listeners[0].xpath).toBe('//*[@id="test-id"]');
|
|
153
153
|
});
|
|
@@ -157,9 +157,9 @@ describe('listEventListeners', () => {
|
|
|
157
157
|
const child = document.createElement('span');
|
|
158
158
|
container.appendChild(child);
|
|
159
159
|
document.body.appendChild(container);
|
|
160
|
-
|
|
160
|
+
|
|
161
161
|
child.addEventListener('click', () => {});
|
|
162
|
-
|
|
162
|
+
|
|
163
163
|
const listeners = listEventListeners(document.body);
|
|
164
164
|
const spanListener = listeners.find(l => l.element === 'span');
|
|
165
165
|
expect(spanListener.xpath).toMatch(/^\/html\[1\]\/body\[1\]\/div\[1\]\/span\[1\]$/);
|
|
@@ -170,14 +170,14 @@ describe('listEventListeners', () => {
|
|
|
170
170
|
const span1 = document.createElement('span');
|
|
171
171
|
const span2 = document.createElement('span');
|
|
172
172
|
const span3 = document.createElement('span');
|
|
173
|
-
|
|
173
|
+
|
|
174
174
|
container.appendChild(span1);
|
|
175
175
|
container.appendChild(span2);
|
|
176
176
|
container.appendChild(span3);
|
|
177
177
|
document.body.appendChild(container);
|
|
178
|
-
|
|
178
|
+
|
|
179
179
|
span2.addEventListener('click', () => {});
|
|
180
|
-
|
|
180
|
+
|
|
181
181
|
const listeners = listEventListeners(document.body);
|
|
182
182
|
const spanListener = listeners.find(l => l.element === 'span');
|
|
183
183
|
expect(spanListener.xpath).toMatch(/span\[2\]/);
|
|
@@ -188,7 +188,7 @@ describe('listEventListeners', () => {
|
|
|
188
188
|
it('should return empty object for elements without listeners', () => {
|
|
189
189
|
const element = document.createElement('div');
|
|
190
190
|
const listeners = getEventListeners(element);
|
|
191
|
-
|
|
191
|
+
|
|
192
192
|
expect(listeners).toEqual({});
|
|
193
193
|
});
|
|
194
194
|
|
|
@@ -196,10 +196,10 @@ describe('listEventListeners', () => {
|
|
|
196
196
|
const element = document.createElement('button');
|
|
197
197
|
const clickListener = vi.fn();
|
|
198
198
|
const focusListener = vi.fn();
|
|
199
|
-
|
|
199
|
+
|
|
200
200
|
element.addEventListener('click', clickListener);
|
|
201
201
|
element.addEventListener('focus', focusListener);
|
|
202
|
-
|
|
202
|
+
|
|
203
203
|
const listeners = getEventListeners(element);
|
|
204
204
|
expect(Object.keys(listeners)).toHaveLength(2);
|
|
205
205
|
expect(listeners.click).toBeDefined();
|
|
@@ -216,13 +216,14 @@ describe('listEventListeners', () => {
|
|
|
216
216
|
it('should list event listeners on root element', () => {
|
|
217
217
|
const listener = vi.fn();
|
|
218
218
|
document.body.addEventListener('click', listener);
|
|
219
|
-
|
|
219
|
+
|
|
220
220
|
const result = listEventListeners(document.body);
|
|
221
221
|
expect(result).toHaveLength(1);
|
|
222
222
|
expect(result[0]).toEqual({
|
|
223
223
|
element: 'body',
|
|
224
224
|
xpath: expect.any(String),
|
|
225
|
-
event: 'click'
|
|
225
|
+
event: 'click',
|
|
226
|
+
bindingType: 'addEventListener',
|
|
226
227
|
});
|
|
227
228
|
});
|
|
228
229
|
|
|
@@ -231,29 +232,30 @@ describe('listEventListeners', () => {
|
|
|
231
232
|
const button = document.createElement('button');
|
|
232
233
|
div.appendChild(button);
|
|
233
234
|
document.body.appendChild(div);
|
|
234
|
-
|
|
235
|
+
|
|
235
236
|
button.addEventListener('click', vi.fn());
|
|
236
|
-
|
|
237
|
+
|
|
237
238
|
const result = listEventListeners(document.body);
|
|
238
239
|
expect(result).toHaveLength(1);
|
|
239
240
|
expect(result[0]).toEqual({
|
|
240
241
|
element: 'button',
|
|
241
242
|
xpath: expect.any(String),
|
|
242
|
-
event: 'click'
|
|
243
|
+
event: 'click',
|
|
244
|
+
bindingType: 'addEventListener',
|
|
243
245
|
});
|
|
244
246
|
});
|
|
245
247
|
|
|
246
248
|
it('should list multiple event types on same element', () => {
|
|
247
249
|
const input = document.createElement('input');
|
|
248
250
|
document.body.appendChild(input);
|
|
249
|
-
|
|
251
|
+
|
|
250
252
|
input.addEventListener('click', vi.fn());
|
|
251
253
|
input.addEventListener('focus', vi.fn());
|
|
252
254
|
input.addEventListener('blur', vi.fn());
|
|
253
|
-
|
|
255
|
+
|
|
254
256
|
const result = listEventListeners(document.body);
|
|
255
257
|
expect(result).toHaveLength(3);
|
|
256
|
-
|
|
258
|
+
|
|
257
259
|
const events = result.map(r => r.event).sort();
|
|
258
260
|
expect(events).toEqual(['blur', 'click', 'focus']);
|
|
259
261
|
});
|
|
@@ -262,18 +264,18 @@ describe('listEventListeners', () => {
|
|
|
262
264
|
const div1 = document.createElement('div');
|
|
263
265
|
const div2 = document.createElement('div');
|
|
264
266
|
const button = document.createElement('button');
|
|
265
|
-
|
|
267
|
+
|
|
266
268
|
div1.appendChild(button);
|
|
267
269
|
document.body.appendChild(div1);
|
|
268
270
|
document.body.appendChild(div2);
|
|
269
|
-
|
|
271
|
+
|
|
270
272
|
div1.addEventListener('mouseover', vi.fn());
|
|
271
273
|
div2.addEventListener('mouseout', vi.fn());
|
|
272
274
|
button.addEventListener('click', vi.fn());
|
|
273
|
-
|
|
275
|
+
|
|
274
276
|
const result = listEventListeners(document.body);
|
|
275
277
|
expect(result).toHaveLength(3);
|
|
276
|
-
|
|
278
|
+
|
|
277
279
|
const elements = result.map(r => r.element).sort();
|
|
278
280
|
expect(elements).toEqual(['button', 'div', 'div']);
|
|
279
281
|
});
|
|
@@ -282,7 +284,7 @@ describe('listEventListeners', () => {
|
|
|
282
284
|
const button = document.createElement('button');
|
|
283
285
|
document.body.appendChild(button);
|
|
284
286
|
button.addEventListener('click', vi.fn());
|
|
285
|
-
|
|
287
|
+
|
|
286
288
|
const result = listEventListeners();
|
|
287
289
|
const buttonListener = result.find(l => l.element === 'button');
|
|
288
290
|
expect(buttonListener).toBeDefined();
|
|
@@ -293,18 +295,135 @@ describe('listEventListeners', () => {
|
|
|
293
295
|
const container = document.createElement('div');
|
|
294
296
|
const innerDiv = document.createElement('div');
|
|
295
297
|
const button = document.createElement('button');
|
|
296
|
-
|
|
298
|
+
|
|
297
299
|
innerDiv.appendChild(button);
|
|
298
300
|
container.appendChild(innerDiv);
|
|
299
301
|
document.body.appendChild(container);
|
|
300
|
-
|
|
302
|
+
|
|
301
303
|
// Add listeners to elements inside and outside container
|
|
302
304
|
button.addEventListener('click', vi.fn());
|
|
303
305
|
document.body.addEventListener('scroll', vi.fn());
|
|
304
|
-
|
|
306
|
+
|
|
305
307
|
const result = listEventListeners(container);
|
|
306
308
|
expect(result).toHaveLength(1);
|
|
307
309
|
expect(result[0].element).toBe('button');
|
|
308
310
|
});
|
|
309
311
|
});
|
|
310
|
-
|
|
312
|
+
|
|
313
|
+
describe('inline attribute detection', () => {
|
|
314
|
+
it('should detect inline onclick attribute', () => {
|
|
315
|
+
const button = document.createElement('button');
|
|
316
|
+
button.setAttribute('onclick', 'alert("hi")');
|
|
317
|
+
document.body.appendChild(button);
|
|
318
|
+
|
|
319
|
+
const result = listEventListeners(document.body);
|
|
320
|
+
expect(result).toHaveLength(1);
|
|
321
|
+
expect(result[0]).toEqual({
|
|
322
|
+
element: 'button',
|
|
323
|
+
xpath: expect.any(String),
|
|
324
|
+
event: 'click',
|
|
325
|
+
bindingType: 'attribute',
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('should detect multiple inline attributes on same element', () => {
|
|
330
|
+
const div = document.createElement('div');
|
|
331
|
+
div.setAttribute('onclick', 'doClick()');
|
|
332
|
+
div.setAttribute('onmouseover', 'doHover()');
|
|
333
|
+
div.setAttribute('onfocus', 'doFocus()');
|
|
334
|
+
document.body.appendChild(div);
|
|
335
|
+
|
|
336
|
+
const result = listEventListeners(document.body);
|
|
337
|
+
expect(result).toHaveLength(3);
|
|
338
|
+
|
|
339
|
+
const events = result.map(r => r.event).sort();
|
|
340
|
+
expect(events).toEqual(['click', 'focus', 'mouseover']);
|
|
341
|
+
result.forEach(r => {
|
|
342
|
+
expect(r.bindingType).toBe('attribute');
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
describe('property-based handler detection', () => {
|
|
348
|
+
it('should detect el.onclick = fn property handler', () => {
|
|
349
|
+
const button = document.createElement('button');
|
|
350
|
+
button.onclick = () => {};
|
|
351
|
+
document.body.appendChild(button);
|
|
352
|
+
|
|
353
|
+
const result = listEventListeners(document.body);
|
|
354
|
+
expect(result).toHaveLength(1);
|
|
355
|
+
expect(result[0]).toEqual({
|
|
356
|
+
element: 'button',
|
|
357
|
+
xpath: expect.any(String),
|
|
358
|
+
event: 'click',
|
|
359
|
+
bindingType: 'property',
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
describe('bindingType field', () => {
|
|
365
|
+
it('should add bindingType addEventListener for tracked listeners', () => {
|
|
366
|
+
const button = document.createElement('button');
|
|
367
|
+
document.body.appendChild(button);
|
|
368
|
+
button.addEventListener('click', vi.fn());
|
|
369
|
+
|
|
370
|
+
const result = listEventListeners(document.body);
|
|
371
|
+
expect(result).toHaveLength(1);
|
|
372
|
+
expect(result[0].bindingType).toBe('addEventListener');
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('should not double-count attribute handler as property handler', () => {
|
|
376
|
+
const button = document.createElement('button');
|
|
377
|
+
button.setAttribute('onclick', 'doClick()');
|
|
378
|
+
document.body.appendChild(button);
|
|
379
|
+
|
|
380
|
+
const result = listEventListeners(document.body);
|
|
381
|
+
const clickEntries = result.filter(r => r.event === 'click');
|
|
382
|
+
expect(clickEntries).toHaveLength(1);
|
|
383
|
+
expect(clickEntries[0].bindingType).toBe('attribute');
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('should detect all three binding types on one element', () => {
|
|
387
|
+
const button = document.createElement('button');
|
|
388
|
+
button.setAttribute('onmouseover', 'doHover()');
|
|
389
|
+
button.onfocus = () => {};
|
|
390
|
+
document.body.appendChild(button);
|
|
391
|
+
button.addEventListener('click', vi.fn());
|
|
392
|
+
|
|
393
|
+
const result = listEventListeners(document.body);
|
|
394
|
+
expect(result).toHaveLength(3);
|
|
395
|
+
|
|
396
|
+
const byEvent = {};
|
|
397
|
+
result.forEach(r => {
|
|
398
|
+
byEvent[r.event] = r.bindingType;
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
expect(byEvent.click).toBe('addEventListener');
|
|
402
|
+
expect(byEvent.mouseover).toBe('attribute');
|
|
403
|
+
expect(byEvent.focus).toBe('property');
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
describe('getEventListeners with inline/property handlers', () => {
|
|
408
|
+
it('should include attribute handlers in getEventListeners result', () => {
|
|
409
|
+
const button = document.createElement('button');
|
|
410
|
+
button.setAttribute('onclick', 'doClick()');
|
|
411
|
+
|
|
412
|
+
const listeners = getEventListeners(button);
|
|
413
|
+
expect(listeners.click).toBeDefined();
|
|
414
|
+
expect(listeners.click.length).toBeGreaterThanOrEqual(1);
|
|
415
|
+
const attrEntry = listeners.click.find(l => l.bindingType === 'attribute');
|
|
416
|
+
expect(attrEntry).toBeDefined();
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('should include property handlers in getEventListeners result', () => {
|
|
420
|
+
const button = document.createElement('button');
|
|
421
|
+
button.onclick = () => {};
|
|
422
|
+
|
|
423
|
+
const listeners = getEventListeners(button);
|
|
424
|
+
expect(listeners.click).toBeDefined();
|
|
425
|
+
const propEntry = listeners.click.find(l => l.bindingType === 'property');
|
|
426
|
+
expect(propEntry).toBeDefined();
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
});
|
|
@@ -18,22 +18,12 @@ test.describe('CSS Pseudo-element Support', () => {
|
|
|
18
18
|
await page.addScriptTag({
|
|
19
19
|
content: `
|
|
20
20
|
// getGeneratedContent: includes textContent alongside pseudo-element content
|
|
21
|
+
// Delegates to getCSSGeneratedContent for pseudo-element retrieval
|
|
21
22
|
function getGeneratedContent(el) {
|
|
22
23
|
if (!el) return false;
|
|
23
|
-
|
|
24
|
-
const beforeStyle = window.getComputedStyle(el, '::before');
|
|
25
|
-
const afterStyle = window.getComputedStyle(el, '::after');
|
|
26
|
-
|
|
27
|
-
let before = beforeStyle.getPropertyValue('content') || '';
|
|
28
|
-
let after = afterStyle.getPropertyValue('content') || '';
|
|
24
|
+
const before = getCSSGeneratedContent(el, 'before') || '';
|
|
29
25
|
const inner = el.textContent || '';
|
|
30
|
-
|
|
31
|
-
before = before.replace(/^["']|["']$/g, '');
|
|
32
|
-
after = after.replace(/^["']|["']$/g, '');
|
|
33
|
-
|
|
34
|
-
if (before === 'none') before = '';
|
|
35
|
-
if (after === 'none') after = '';
|
|
36
|
-
|
|
26
|
+
const after = getCSSGeneratedContent(el, 'after') || '';
|
|
37
27
|
const result = [before, inner, after].filter(Boolean).join(' ').trim();
|
|
38
28
|
return result || false;
|
|
39
29
|
}
|