@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.
@@ -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('getAllText', () => {
156
- it('should extract text from simple text nodes', () => {
157
- const div = document.createElement('div');
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 handle elements without aria-label', () => {
279
- const div = document.createElement('div');
280
- const button = document.createElement('button');
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
- const button = document.createElement('button');
314
- button.setAttribute('aria-label', 'Close');
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 (76.4% line coverage, 87.53% branch coverage)
9
+ - ✅ Test coverage improvements (87.69% line coverage, 82.11% branch coverage)
10
10
  - ✅ Playwright integration for CSS pseudo-element tests
11
- - ✅ 666 total tests passing (656 JSDOM + 10 Playwright)
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
- };