@afixt/test-utils 1.3.0 → 2.0.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.
- 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 +13 -71
- 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 +54 -98
- package/src/tableUtils.js +4 -36
- package/test/domUtils.test.js +156 -0
- package/test/formUtils.test.js +0 -47
- 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 +115 -228
- package/todo.md +2 -2
- package/src/isVisible.js +0 -103
package/test/stringUtils.test.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
2
|
import stringUtils from '../src/stringUtils.js';
|
|
3
3
|
|
|
4
4
|
describe('stringUtils', () => {
|
|
@@ -152,151 +152,27 @@ describe('stringUtils', () => {
|
|
|
152
152
|
});
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
-
describe('
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
div.textContent = 'Hello World';
|
|
159
|
-
|
|
160
|
-
expect(stringUtils.getAllText(div)).toBe('Hello World');
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('should extract text from nested elements', () => {
|
|
164
|
-
const div = document.createElement('div');
|
|
165
|
-
const span = document.createElement('span');
|
|
166
|
-
span.textContent = 'Hello';
|
|
167
|
-
const text = document.createTextNode(' World');
|
|
168
|
-
|
|
169
|
-
div.appendChild(span);
|
|
170
|
-
div.appendChild(text);
|
|
171
|
-
|
|
172
|
-
expect(stringUtils.getAllText(div)).toBe('Hello World');
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('should extract aria-label attributes', () => {
|
|
176
|
-
const div = document.createElement('div');
|
|
177
|
-
const button = document.createElement('button');
|
|
178
|
-
button.setAttribute('aria-label', 'Close dialog');
|
|
179
|
-
button.textContent = 'X';
|
|
180
|
-
|
|
181
|
-
div.appendChild(button);
|
|
182
|
-
|
|
183
|
-
const result = stringUtils.getAllText(div);
|
|
184
|
-
expect(result).toContain('Close dialog');
|
|
185
|
-
expect(result).toContain('X');
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('should extract alt attributes from images', () => {
|
|
189
|
-
const div = document.createElement('div');
|
|
190
|
-
const img = document.createElement('img');
|
|
191
|
-
img.setAttribute('alt', 'Profile picture');
|
|
192
|
-
img.setAttribute('src', 'profile.jpg');
|
|
193
|
-
|
|
194
|
-
div.appendChild(img);
|
|
195
|
-
|
|
196
|
-
expect(stringUtils.getAllText(div)).toBe('Profile picture');
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('should handle mixed content with text, aria-labels, and alt text', () => {
|
|
200
|
-
const div = document.createElement('div');
|
|
201
|
-
|
|
202
|
-
// Add some text
|
|
203
|
-
const textNode = document.createTextNode('Welcome ');
|
|
204
|
-
div.appendChild(textNode);
|
|
205
|
-
|
|
206
|
-
// Add element with aria-label
|
|
207
|
-
const button = document.createElement('button');
|
|
208
|
-
button.setAttribute('aria-label', 'Help');
|
|
209
|
-
button.textContent = '?';
|
|
210
|
-
div.appendChild(button);
|
|
211
|
-
|
|
212
|
-
// Add some more text
|
|
213
|
-
const moreText = document.createTextNode(' to our site ');
|
|
214
|
-
div.appendChild(moreText);
|
|
215
|
-
|
|
216
|
-
// Add image with alt text
|
|
217
|
-
const img = document.createElement('img');
|
|
218
|
-
img.setAttribute('alt', 'Company logo');
|
|
219
|
-
div.appendChild(img);
|
|
220
|
-
|
|
221
|
-
const result = stringUtils.getAllText(div);
|
|
222
|
-
expect(result).toContain('Welcome');
|
|
223
|
-
expect(result).toContain('Help');
|
|
224
|
-
expect(result).toContain('?');
|
|
225
|
-
expect(result).toContain('to our site');
|
|
226
|
-
expect(result).toContain('Company logo');
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it('should handle empty elements', () => {
|
|
230
|
-
const div = document.createElement('div');
|
|
231
|
-
expect(stringUtils.getAllText(div)).toBe('');
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it('should trim whitespace from text nodes', () => {
|
|
235
|
-
const div = document.createElement('div');
|
|
236
|
-
const textNode = document.createTextNode(' Hello World ');
|
|
237
|
-
div.appendChild(textNode);
|
|
238
|
-
|
|
239
|
-
expect(stringUtils.getAllText(div)).toBe('Hello World');
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it('should handle elements with only whitespace', () => {
|
|
243
|
-
const div = document.createElement('div');
|
|
244
|
-
const textNode = document.createTextNode(' ');
|
|
245
|
-
div.appendChild(textNode);
|
|
246
|
-
|
|
247
|
-
// Should use textContent.trim() as fallback for empty text
|
|
248
|
-
expect(stringUtils.getAllText(div)).toBe('');
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
it('should handle deeply nested structures', () => {
|
|
252
|
-
const div = document.createElement('div');
|
|
253
|
-
const section = document.createElement('section');
|
|
254
|
-
const article = document.createElement('article');
|
|
255
|
-
const p = document.createElement('p');
|
|
256
|
-
|
|
257
|
-
p.textContent = 'Deep content';
|
|
258
|
-
article.appendChild(p);
|
|
259
|
-
section.appendChild(article);
|
|
260
|
-
div.appendChild(section);
|
|
261
|
-
|
|
262
|
-
expect(stringUtils.getAllText(div)).toBe('Deep content');
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it('should handle images without alt attributes', () => {
|
|
266
|
-
const div = document.createElement('div');
|
|
267
|
-
const img = document.createElement('img');
|
|
268
|
-
img.setAttribute('src', 'image.jpg');
|
|
269
|
-
// No alt attribute
|
|
270
|
-
|
|
271
|
-
const text = document.createTextNode('Some text');
|
|
272
|
-
div.appendChild(text);
|
|
273
|
-
div.appendChild(img);
|
|
274
|
-
|
|
275
|
-
expect(stringUtils.getAllText(div)).toBe('Some text');
|
|
155
|
+
describe('hasText', () => {
|
|
156
|
+
afterEach(() => {
|
|
157
|
+
document.body.innerHTML = '';
|
|
276
158
|
});
|
|
277
159
|
|
|
278
|
-
it('should
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
button.textContent = 'Click me';
|
|
282
|
-
// No aria-label attribute
|
|
283
|
-
|
|
284
|
-
div.appendChild(button);
|
|
285
|
-
|
|
286
|
-
expect(stringUtils.getAllText(div)).toBe('Click me');
|
|
160
|
+
it('should return false for null or undefined', () => {
|
|
161
|
+
expect(stringUtils.hasText(null)).toBe(false);
|
|
162
|
+
expect(stringUtils.hasText(undefined)).toBe(false);
|
|
287
163
|
});
|
|
288
|
-
});
|
|
289
164
|
|
|
290
|
-
describe('hasText', () => {
|
|
291
165
|
it('should return true for elements with text content', () => {
|
|
292
166
|
const div = document.createElement('div');
|
|
293
167
|
div.textContent = 'Hello World';
|
|
168
|
+
document.body.appendChild(div);
|
|
294
169
|
|
|
295
170
|
expect(stringUtils.hasText(div)).toBe(true);
|
|
296
171
|
});
|
|
297
172
|
|
|
298
173
|
it('should return false for empty elements', () => {
|
|
299
174
|
const div = document.createElement('div');
|
|
175
|
+
document.body.appendChild(div);
|
|
300
176
|
|
|
301
177
|
expect(stringUtils.hasText(div)).toBe(false);
|
|
302
178
|
});
|
|
@@ -304,16 +180,15 @@ describe('stringUtils', () => {
|
|
|
304
180
|
it('should return false for elements with only whitespace', () => {
|
|
305
181
|
const div = document.createElement('div');
|
|
306
182
|
div.textContent = ' \n\t ';
|
|
183
|
+
document.body.appendChild(div);
|
|
307
184
|
|
|
308
185
|
expect(stringUtils.hasText(div)).toBe(false);
|
|
309
186
|
});
|
|
310
187
|
|
|
311
188
|
it('should return true for elements with aria-label', () => {
|
|
312
189
|
const div = document.createElement('div');
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
div.appendChild(button);
|
|
190
|
+
div.setAttribute('aria-label', 'Close');
|
|
191
|
+
document.body.appendChild(div);
|
|
317
192
|
|
|
318
193
|
expect(stringUtils.hasText(div)).toBe(true);
|
|
319
194
|
});
|
|
@@ -322,8 +197,8 @@ describe('stringUtils', () => {
|
|
|
322
197
|
const div = document.createElement('div');
|
|
323
198
|
const img = document.createElement('img');
|
|
324
199
|
img.setAttribute('alt', 'Profile picture');
|
|
325
|
-
|
|
326
200
|
div.appendChild(img);
|
|
201
|
+
document.body.appendChild(div);
|
|
327
202
|
|
|
328
203
|
expect(stringUtils.hasText(div)).toBe(true);
|
|
329
204
|
});
|
|
@@ -332,8 +207,8 @@ describe('stringUtils', () => {
|
|
|
332
207
|
const div = document.createElement('div');
|
|
333
208
|
const span = document.createElement('span');
|
|
334
209
|
span.textContent = 'Nested text';
|
|
335
|
-
|
|
336
210
|
div.appendChild(span);
|
|
211
|
+
document.body.appendChild(div);
|
|
337
212
|
|
|
338
213
|
expect(stringUtils.hasText(div)).toBe(true);
|
|
339
214
|
});
|
|
@@ -342,9 +217,9 @@ describe('stringUtils', () => {
|
|
|
342
217
|
const div = document.createElement('div');
|
|
343
218
|
const span = document.createElement('span');
|
|
344
219
|
const p = document.createElement('p');
|
|
345
|
-
|
|
346
220
|
div.appendChild(span);
|
|
347
221
|
span.appendChild(p);
|
|
222
|
+
document.body.appendChild(div);
|
|
348
223
|
|
|
349
224
|
expect(stringUtils.hasText(div)).toBe(false);
|
|
350
225
|
});
|
|
@@ -361,6 +236,7 @@ describe('stringUtils', () => {
|
|
|
361
236
|
const input = document.createElement('input');
|
|
362
237
|
input.type = 'text';
|
|
363
238
|
input.value = '';
|
|
239
|
+
document.body.appendChild(input);
|
|
364
240
|
|
|
365
241
|
expect(stringUtils.hasText(input)).toBe(false);
|
|
366
242
|
});
|
|
@@ -382,48 +258,6 @@ describe('stringUtils', () => {
|
|
|
382
258
|
});
|
|
383
259
|
});
|
|
384
260
|
|
|
385
|
-
describe('getAllText edge cases', () => {
|
|
386
|
-
it('should handle text nodes with empty nodeValue but non-empty textContent', () => {
|
|
387
|
-
const div = document.createElement('div');
|
|
388
|
-
const textNode = document.createTextNode(' ');
|
|
389
|
-
div.appendChild(textNode);
|
|
390
|
-
|
|
391
|
-
// This tests the else branch where nodeValue.trim() is empty
|
|
392
|
-
const result = stringUtils.getAllText(div);
|
|
393
|
-
expect(typeof result).toBe('string');
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
it('should handle mixed content with whitespace text nodes', () => {
|
|
397
|
-
const div = document.createElement('div');
|
|
398
|
-
div.innerHTML = ' <span>Text</span> ';
|
|
399
|
-
|
|
400
|
-
const result = stringUtils.getAllText(div);
|
|
401
|
-
expect(result).toContain('Text');
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
it('should handle elements with both aria-label and text content', () => {
|
|
405
|
-
const div = document.createElement('div');
|
|
406
|
-
const button = document.createElement('button');
|
|
407
|
-
button.textContent = 'Visual Text';
|
|
408
|
-
button.setAttribute('aria-label', 'Accessible Label');
|
|
409
|
-
div.appendChild(button);
|
|
410
|
-
|
|
411
|
-
const result = stringUtils.getAllText(div);
|
|
412
|
-
expect(result).toContain('Accessible Label');
|
|
413
|
-
expect(result).toContain('Visual Text');
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
it('should handle img without alt attribute', () => {
|
|
417
|
-
const div = document.createElement('div');
|
|
418
|
-
const img = document.createElement('img');
|
|
419
|
-
// No alt attribute
|
|
420
|
-
div.appendChild(img);
|
|
421
|
-
|
|
422
|
-
const result = stringUtils.getAllText(div);
|
|
423
|
-
expect(typeof result).toBe('string');
|
|
424
|
-
});
|
|
425
|
-
});
|
|
426
|
-
|
|
427
261
|
describe('isEmptyOrWhitespace', () => {
|
|
428
262
|
it('should return true for null/undefined/empty', () => {
|
|
429
263
|
expect(stringUtils.isEmptyOrWhitespace(null)).toBe(true);
|
|
@@ -493,6 +327,45 @@ describe('stringUtils', () => {
|
|
|
493
327
|
it('should trim whitespace', () => {
|
|
494
328
|
expect(stringUtils.isGenericTitle(' iframe ')).toBe(true);
|
|
495
329
|
});
|
|
330
|
+
|
|
331
|
+
it('should detect browser/authoring tool default titles', () => {
|
|
332
|
+
expect(stringUtils.isGenericTitle('untitled document')).toBe(true);
|
|
333
|
+
expect(stringUtils.isGenericTitle('new page')).toBe(true);
|
|
334
|
+
expect(stringUtils.isGenericTitle('blank')).toBe(true);
|
|
335
|
+
expect(stringUtils.isGenericTitle('no title')).toBe(true);
|
|
336
|
+
expect(stringUtils.isGenericTitle('default')).toBe(true);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should detect CMS and structural generic titles', () => {
|
|
340
|
+
expect(stringUtils.isGenericTitle('placeholder')).toBe(true);
|
|
341
|
+
expect(stringUtils.isGenericTitle('sample page')).toBe(true);
|
|
342
|
+
expect(stringUtils.isGenericTitle('banner')).toBe(true);
|
|
343
|
+
expect(stringUtils.isGenericTitle('navigation')).toBe(true);
|
|
344
|
+
expect(stringUtils.isGenericTitle('advertisement')).toBe(true);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should match numbered variants with spaces', () => {
|
|
348
|
+
expect(stringUtils.isGenericTitle('untitled 1')).toBe(true);
|
|
349
|
+
expect(stringUtils.isGenericTitle('frame 2')).toBe(true);
|
|
350
|
+
expect(stringUtils.isGenericTitle('page 3')).toBe(true);
|
|
351
|
+
expect(stringUtils.isGenericTitle('section 42')).toBe(true);
|
|
352
|
+
expect(stringUtils.isGenericTitle('document 5')).toBe(true);
|
|
353
|
+
expect(stringUtils.isGenericTitle('tab 1')).toBe(true);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('should match expanded numbered pattern base words', () => {
|
|
357
|
+
expect(stringUtils.isGenericTitle('slide1')).toBe(true);
|
|
358
|
+
expect(stringUtils.isGenericTitle('sheet2')).toBe(true);
|
|
359
|
+
expect(stringUtils.isGenericTitle('panel 3')).toBe(true);
|
|
360
|
+
expect(stringUtils.isGenericTitle('window4')).toBe(true);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('should still return false for descriptive titles', () => {
|
|
364
|
+
expect(stringUtils.isGenericTitle('Contact Us Form')).toBe(false);
|
|
365
|
+
expect(stringUtils.isGenericTitle('Product Catalog')).toBe(false);
|
|
366
|
+
expect(stringUtils.isGenericTitle('John Smith Profile')).toBe(false);
|
|
367
|
+
expect(stringUtils.isGenericTitle('Q4 Sales Report')).toBe(false);
|
|
368
|
+
});
|
|
496
369
|
});
|
|
497
370
|
|
|
498
371
|
describe('isGenericLinkText', () => {
|
|
@@ -563,6 +436,66 @@ describe('stringUtils', () => {
|
|
|
563
436
|
});
|
|
564
437
|
});
|
|
565
438
|
|
|
439
|
+
describe('containsVisibleText', () => {
|
|
440
|
+
it('should return true when accessible name exactly matches visible text', () => {
|
|
441
|
+
expect(stringUtils.containsVisibleText('Home', 'Home')).toBe(true);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('should return true when accessible name is a superset of visible text', () => {
|
|
445
|
+
expect(
|
|
446
|
+
stringUtils.containsVisibleText(
|
|
447
|
+
'Report a Concern Opens in new window',
|
|
448
|
+
'Report a Concern'
|
|
449
|
+
)
|
|
450
|
+
).toBe(true);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('should be case-insensitive', () => {
|
|
454
|
+
expect(stringUtils.containsVisibleText('REPORT A CONCERN', 'Report a Concern')).toBe(
|
|
455
|
+
true
|
|
456
|
+
);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('should return false when visible text is a partial word match', () => {
|
|
460
|
+
expect(stringUtils.containsVisibleText('Homepage', 'Home')).toBe(false);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('should return false when accessible name does not contain visible text', () => {
|
|
464
|
+
expect(
|
|
465
|
+
stringUtils.containsVisibleText('Click here for more information', 'Learn more')
|
|
466
|
+
).toBe(false);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('should return false for null/undefined inputs', () => {
|
|
470
|
+
expect(stringUtils.containsVisibleText(null, 'text')).toBe(false);
|
|
471
|
+
expect(stringUtils.containsVisibleText('text', null)).toBe(false);
|
|
472
|
+
expect(stringUtils.containsVisibleText(undefined, undefined)).toBe(false);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('should return false for empty visible text', () => {
|
|
476
|
+
expect(stringUtils.containsVisibleText('some name', '')).toBe(false);
|
|
477
|
+
expect(stringUtils.containsVisibleText('some name', ' ')).toBe(false);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('should normalize whitespace before matching', () => {
|
|
481
|
+
expect(stringUtils.containsVisibleText('Report a Concern', 'Report a Concern')).toBe(
|
|
482
|
+
true
|
|
483
|
+
);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('should match visible text at the end of accessible name', () => {
|
|
487
|
+
expect(stringUtils.containsVisibleText('Click here to Search', 'Search')).toBe(true);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('should not match across word boundaries at end', () => {
|
|
491
|
+
expect(stringUtils.containsVisibleText('Searching', 'Search')).toBe(false);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('should handle visible text that is the entire accessible name', () => {
|
|
495
|
+
expect(stringUtils.containsVisibleText('Buy now', 'Buy now')).toBe(true);
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
|
|
566
499
|
describe('hasNewWindowWarning', () => {
|
|
567
500
|
it('should return true for text containing "new window"', () => {
|
|
568
501
|
expect(stringUtils.hasNewWindowWarning('Opens in a new window')).toBe(true);
|
|
@@ -599,50 +532,4 @@ describe('stringUtils', () => {
|
|
|
599
532
|
expect(stringUtils.hasNewWindowWarning(undefined)).toBe(false);
|
|
600
533
|
});
|
|
601
534
|
});
|
|
602
|
-
|
|
603
|
-
describe('textIncludingImgAlt', () => {
|
|
604
|
-
it('should return text content from text nodes', () => {
|
|
605
|
-
const el = document.createElement('div');
|
|
606
|
-
el.textContent = 'Hello World';
|
|
607
|
-
expect(stringUtils.textIncludingImgAlt(el).trim()).toBe('Hello World');
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
it('should include img alt text', () => {
|
|
611
|
-
const el = document.createElement('div');
|
|
612
|
-
el.innerHTML = 'Text <img alt="photo"> more text';
|
|
613
|
-
const result = stringUtils.textIncludingImgAlt(el);
|
|
614
|
-
expect(result).toContain('Text');
|
|
615
|
-
expect(result).toContain('photo');
|
|
616
|
-
expect(result).toContain('more text');
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
it('should handle img without alt', () => {
|
|
620
|
-
const el = document.createElement('div');
|
|
621
|
-
el.innerHTML = 'Text <img src="img.png"> more';
|
|
622
|
-
const result = stringUtils.textIncludingImgAlt(el);
|
|
623
|
-
expect(result).toContain('Text');
|
|
624
|
-
expect(result).toContain('more');
|
|
625
|
-
});
|
|
626
|
-
|
|
627
|
-
it('should not include aria-label text', () => {
|
|
628
|
-
const el = document.createElement('div');
|
|
629
|
-
el.innerHTML = '<button aria-label="Close">X</button>';
|
|
630
|
-
const result = stringUtils.textIncludingImgAlt(el);
|
|
631
|
-
expect(result).toContain('X');
|
|
632
|
-
expect(result).not.toContain('Close');
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
it('should handle empty element', () => {
|
|
636
|
-
const el = document.createElement('div');
|
|
637
|
-
expect(stringUtils.textIncludingImgAlt(el)).toBe('');
|
|
638
|
-
});
|
|
639
|
-
|
|
640
|
-
it('should concatenate text from nested elements', () => {
|
|
641
|
-
const el = document.createElement('div');
|
|
642
|
-
el.innerHTML = '<span>First</span> <span>Second</span>';
|
|
643
|
-
const result = stringUtils.textIncludingImgAlt(el);
|
|
644
|
-
expect(result).toContain('First');
|
|
645
|
-
expect(result).toContain('Second');
|
|
646
|
-
});
|
|
647
|
-
});
|
|
648
535
|
});
|
package/todo.md
CHANGED
|
@@ -6,6 +6,6 @@ _No pending tasks_
|
|
|
6
6
|
|
|
7
7
|
## Completed
|
|
8
8
|
|
|
9
|
-
- ✅ Test coverage improvements (
|
|
9
|
+
- ✅ Test coverage improvements (87.69% line coverage, 82.11% branch coverage)
|
|
10
10
|
- ✅ Playwright integration for CSS pseudo-element tests
|
|
11
|
-
- ✅
|
|
11
|
+
- ✅ 963 total tests passing (943 JSDOM + 20 Playwright)
|
package/src/isVisible.js
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Checks if the selected item is visible to assistive technologies.
|
|
3
|
-
* @param {Element} element - The element to check.
|
|
4
|
-
* @param {boolean} strict - Option to be more strict about visibility of aria-hidden="true" in addition to CSS display: none.
|
|
5
|
-
* @returns {boolean}
|
|
6
|
-
*/
|
|
7
|
-
function isVisible(element, strict = false) {
|
|
8
|
-
// Add null check at the beginning
|
|
9
|
-
if (!element || !(element instanceof Element)) {
|
|
10
|
-
return false;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// Check if element is still connected to the DOM
|
|
14
|
-
if (!element.isConnected) {
|
|
15
|
-
return false;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const id = element.id;
|
|
19
|
-
let visible = true;
|
|
20
|
-
|
|
21
|
-
// These elements are inherently not visible
|
|
22
|
-
const nonVisibleSelectors = [
|
|
23
|
-
'base',
|
|
24
|
-
'head',
|
|
25
|
-
'meta',
|
|
26
|
-
'title',
|
|
27
|
-
'link',
|
|
28
|
-
'style',
|
|
29
|
-
'script',
|
|
30
|
-
'br',
|
|
31
|
-
'nobr',
|
|
32
|
-
'col',
|
|
33
|
-
'embed',
|
|
34
|
-
'input[type="hidden"]',
|
|
35
|
-
'keygen',
|
|
36
|
-
'source',
|
|
37
|
-
'track',
|
|
38
|
-
'wbr',
|
|
39
|
-
'datalist',
|
|
40
|
-
'area',
|
|
41
|
-
'param',
|
|
42
|
-
'noframes',
|
|
43
|
-
'ruby > rp',
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
if (nonVisibleSelectors.some(selector => element.matches(selector))) {
|
|
47
|
-
return true;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const optionalAriaHidden = (el, strictCheck) =>
|
|
51
|
-
strictCheck && el.getAttribute('aria-hidden') === 'true';
|
|
52
|
-
|
|
53
|
-
const isElemHiddenByCSS = el => {
|
|
54
|
-
const style = window.getComputedStyle(el);
|
|
55
|
-
return style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0';
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const isHidden = () => {
|
|
59
|
-
if (isElemHiddenByCSS(element)) {
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
let parent = element.parentElement;
|
|
64
|
-
while (parent) {
|
|
65
|
-
if (isElemHiddenByCSS(parent)) {
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
parent = parent.parentElement;
|
|
69
|
-
}
|
|
70
|
-
return optionalAriaHidden(element, strict);
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
if (isHidden()) {
|
|
74
|
-
visible = false;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Check if element is referenced by aria-labelledby or aria-describedby
|
|
78
|
-
document
|
|
79
|
-
.querySelectorAll(`*[aria-labelledby~="${id}"], *[aria-describedby~="${id}"]`)
|
|
80
|
-
.forEach(referencingElement => {
|
|
81
|
-
if (window.getComputedStyle(referencingElement).display !== 'none') {
|
|
82
|
-
visible = true;
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
// Check if any parent has aria-hidden="true" when strict mode is on
|
|
87
|
-
if (visible && strict) {
|
|
88
|
-
let parent = element.parentElement;
|
|
89
|
-
while (parent) {
|
|
90
|
-
if (optionalAriaHidden(parent, strict)) {
|
|
91
|
-
visible = false;
|
|
92
|
-
break;
|
|
93
|
-
}
|
|
94
|
-
parent = parent.parentElement;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return visible;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
module.exports = {
|
|
102
|
-
isVisible,
|
|
103
|
-
};
|