@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.
Files changed (39) hide show
  1. package/.claude/settings.local.json +1 -6
  2. package/BROWSER_TESTING.md +42 -22
  3. package/CHANGELOG.md +40 -0
  4. package/CLAUDE.md +10 -9
  5. package/package.json +1 -1
  6. package/src/constants.js +438 -1
  7. package/src/domUtils.js +17 -38
  8. package/src/formUtils.js +7 -24
  9. package/src/getAccessibleName.js +20 -56
  10. package/src/getCSSGeneratedContent.js +2 -0
  11. package/src/getFocusableElements.js +12 -21
  12. package/src/getGeneratedContent.js +18 -11
  13. package/src/getImageText.js +22 -7
  14. package/src/hasValidAriaRole.js +11 -19
  15. package/src/index.js +4 -4
  16. package/src/interactiveRoles.js +2 -19
  17. package/src/isA11yVisible.js +95 -0
  18. package/src/isAriaAttributesValid.js +5 -64
  19. package/src/isFocusable.js +30 -10
  20. package/src/isHidden.js +44 -8
  21. package/src/listEventListeners.js +115 -10
  22. package/src/stringUtils.js +19 -98
  23. package/src/tableUtils.js +4 -36
  24. package/src/testContrast.js +54 -0
  25. package/test/domUtils.test.js +156 -0
  26. package/test/formUtils.test.js +0 -47
  27. package/test/getAccessibleName.test.js +39 -0
  28. package/test/getGeneratedContent.test.js +305 -241
  29. package/test/getImageText.test.js +158 -99
  30. package/test/index.test.js +54 -17
  31. package/test/{isVisible.test.js → isA11yVisible.test.js} +39 -33
  32. package/test/isFocusable.test.js +265 -272
  33. package/test/isHidden.test.js +257 -153
  34. package/test/listEventListeners.test.js +163 -44
  35. package/test/playwright/css-pseudo-elements.spec.js +3 -13
  36. package/test/stringUtils.test.js +55 -228
  37. package/test/testContrast.test.js +104 -2
  38. package/todo.md +2 -2
  39. package/src/isVisible.js +0 -103
@@ -710,6 +710,72 @@ describe('domUtils', () => {
710
710
  expect(domUtils.hasInteractiveHandler(el)).toBe(false);
711
711
  });
712
712
 
713
+ it('should return true for element with ondblclick', () => {
714
+ const el = document.createElement('div');
715
+ el.setAttribute('ondblclick', 'handle()');
716
+ expect(domUtils.hasInteractiveHandler(el)).toBe(true);
717
+ });
718
+
719
+ it('should return true for element with oncontextmenu', () => {
720
+ const el = document.createElement('div');
721
+ el.setAttribute('oncontextmenu', 'handle()');
722
+ expect(domUtils.hasInteractiveHandler(el)).toBe(true);
723
+ });
724
+
725
+ it('should return true for element with onkeypress', () => {
726
+ const el = document.createElement('div');
727
+ el.setAttribute('onkeypress', 'handle()');
728
+ expect(domUtils.hasInteractiveHandler(el)).toBe(true);
729
+ });
730
+
731
+ it('should return true for element with onfocus', () => {
732
+ const el = document.createElement('div');
733
+ el.setAttribute('onfocus', 'handle()');
734
+ expect(domUtils.hasInteractiveHandler(el)).toBe(true);
735
+ });
736
+
737
+ it('should return true for element with onblur', () => {
738
+ const el = document.createElement('div');
739
+ el.setAttribute('onblur', 'handle()');
740
+ expect(domUtils.hasInteractiveHandler(el)).toBe(true);
741
+ });
742
+
743
+ it('should return true for element with oninput', () => {
744
+ const el = document.createElement('div');
745
+ el.setAttribute('oninput', 'handle()');
746
+ expect(domUtils.hasInteractiveHandler(el)).toBe(true);
747
+ });
748
+
749
+ it('should return true for element with onchange', () => {
750
+ const el = document.createElement('div');
751
+ el.setAttribute('onchange', 'handle()');
752
+ expect(domUtils.hasInteractiveHandler(el)).toBe(true);
753
+ });
754
+
755
+ it('should return true for element with onsubmit', () => {
756
+ const el = document.createElement('div');
757
+ el.setAttribute('onsubmit', 'handle()');
758
+ expect(domUtils.hasInteractiveHandler(el)).toBe(true);
759
+ });
760
+
761
+ it('should return true for element with ontouchend', () => {
762
+ const el = document.createElement('div');
763
+ el.setAttribute('ontouchend', 'handle()');
764
+ expect(domUtils.hasInteractiveHandler(el)).toBe(true);
765
+ });
766
+
767
+ it('should return true for element with onpointerdown', () => {
768
+ const el = document.createElement('div');
769
+ el.setAttribute('onpointerdown', 'handle()');
770
+ expect(domUtils.hasInteractiveHandler(el)).toBe(true);
771
+ });
772
+
773
+ it('should return true for element with onpointerup', () => {
774
+ const el = document.createElement('div');
775
+ el.setAttribute('onpointerup', 'handle()');
776
+ expect(domUtils.hasInteractiveHandler(el)).toBe(true);
777
+ });
778
+
713
779
  it('should return false for element with non-interactive event handlers', () => {
714
780
  const el = document.createElement('div');
715
781
  el.setAttribute('onload', 'handle()');
@@ -849,6 +915,96 @@ describe('domUtils', () => {
849
915
  const result = domUtils.getSemanticContainer(document.getElementById('child'));
850
916
  expect(result.tagName).toBe('SECTION');
851
917
  });
918
+
919
+ it('should return main ancestor', () => {
920
+ document.body.innerHTML = '<main><p id="child">Content</p></main>';
921
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
922
+ expect(result.tagName).toBe('MAIN');
923
+ });
924
+
925
+ it('should return nav ancestor', () => {
926
+ document.body.innerHTML = '<nav><p id="child">Content</p></nav>';
927
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
928
+ expect(result.tagName).toBe('NAV');
929
+ });
930
+
931
+ it('should return aside ancestor', () => {
932
+ document.body.innerHTML = '<aside><p id="child">Content</p></aside>';
933
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
934
+ expect(result.tagName).toBe('ASIDE');
935
+ });
936
+
937
+ it('should return header ancestor', () => {
938
+ document.body.innerHTML = '<header><p id="child">Content</p></header>';
939
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
940
+ expect(result.tagName).toBe('HEADER');
941
+ });
942
+
943
+ it('should return footer ancestor', () => {
944
+ document.body.innerHTML = '<footer><p id="child">Content</p></footer>';
945
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
946
+ expect(result.tagName).toBe('FOOTER');
947
+ });
948
+
949
+ it('should return form ancestor', () => {
950
+ document.body.innerHTML = '<form><p id="child">Content</p></form>';
951
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
952
+ expect(result.tagName).toBe('FORM');
953
+ });
954
+
955
+ it('should return fieldset ancestor', () => {
956
+ document.body.innerHTML = '<fieldset><p id="child">Content</p></fieldset>';
957
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
958
+ expect(result.tagName).toBe('FIELDSET');
959
+ });
960
+
961
+ it('should return details ancestor', () => {
962
+ document.body.innerHTML = '<details><p id="child">Content</p></details>';
963
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
964
+ expect(result.tagName).toBe('DETAILS');
965
+ });
966
+
967
+ it('should return dialog ancestor', () => {
968
+ document.body.innerHTML = '<dialog><p id="child">Content</p></dialog>';
969
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
970
+ expect(result.tagName).toBe('DIALOG');
971
+ });
972
+
973
+ it('should return element with role="group"', () => {
974
+ document.body.innerHTML = '<div role="group"><p id="child">Content</p></div>';
975
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
976
+ expect(result.getAttribute('role')).toBe('group');
977
+ });
978
+
979
+ it('should return element with role="tabpanel"', () => {
980
+ document.body.innerHTML = '<div role="tabpanel"><p id="child">Content</p></div>';
981
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
982
+ expect(result.getAttribute('role')).toBe('tabpanel');
983
+ });
984
+
985
+ it('should return element with role="dialog"', () => {
986
+ document.body.innerHTML = '<div role="dialog"><p id="child">Content</p></div>';
987
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
988
+ expect(result.getAttribute('role')).toBe('dialog');
989
+ });
990
+
991
+ it('should return element with role="navigation"', () => {
992
+ document.body.innerHTML = '<div role="navigation"><p id="child">Content</p></div>';
993
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
994
+ expect(result.getAttribute('role')).toBe('navigation');
995
+ });
996
+
997
+ it('should return element with role="complementary"', () => {
998
+ document.body.innerHTML = '<div role="complementary"><p id="child">Content</p></div>';
999
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
1000
+ expect(result.getAttribute('role')).toBe('complementary');
1001
+ });
1002
+
1003
+ it('should return null for non-structural role like presentation', () => {
1004
+ document.body.innerHTML = '<div role="presentation"><p id="child">Content</p></div>';
1005
+ const result = domUtils.getSemanticContainer(document.getElementById('child'));
1006
+ expect(result).toBeNull();
1007
+ });
852
1008
  });
853
1009
 
854
1010
  describe('getHeadingLevel', () => {
@@ -273,53 +273,6 @@ describe('formUtils', () => {
273
273
  });
274
274
  });
275
275
 
276
- describe('hasAssociatedLabel', () => {
277
- it('should return true when label[for] matches element id', () => {
278
- document.body.innerHTML = `
279
- <label for="name">Name</label>
280
- <input type="text" id="name">
281
- `;
282
- const input = document.getElementById('name');
283
- expect(formUtils.hasAssociatedLabel(input)).toBe(true);
284
- });
285
-
286
- it('should return false when label[for] matches but label is empty', () => {
287
- document.body.innerHTML = `
288
- <label for="name"> </label>
289
- <input type="text" id="name">
290
- `;
291
- const input = document.getElementById('name');
292
- expect(formUtils.hasAssociatedLabel(input)).toBe(false);
293
- });
294
-
295
- it('should return true when element is wrapped in label with text', () => {
296
- document.body.innerHTML = `
297
- <label>
298
- Username
299
- <input type="text" id="user">
300
- </label>
301
- `;
302
- const input = document.getElementById('user');
303
- expect(formUtils.hasAssociatedLabel(input)).toBe(true);
304
- });
305
-
306
- it('should return false when element has no label association', () => {
307
- document.body.innerHTML = `
308
- <input type="text" id="orphan">
309
- `;
310
- const input = document.getElementById('orphan');
311
- expect(formUtils.hasAssociatedLabel(input)).toBe(false);
312
- });
313
-
314
- it('should return false when element has no id and no wrapping label', () => {
315
- document.body.innerHTML = `
316
- <div><input type="text" id="nolabel"></div>
317
- `;
318
- const input = document.getElementById('nolabel');
319
- expect(formUtils.hasAssociatedLabel(input)).toBe(false);
320
- });
321
- });
322
-
323
276
  describe('getTextContentExcludingControls', () => {
324
277
  it('should return text excluding input elements', () => {
325
278
  document.body.innerHTML = `
@@ -428,4 +428,43 @@ describe('getAccessibleName', () => {
428
428
  const meter = document.querySelector('[role="meter"]');
429
429
  expect(getAccessibleName(meter)).toBe('Signal strength');
430
430
  });
431
+
432
+ describe('landmark elements should not get name from contents', () => {
433
+ const landmarkElements = [
434
+ { tag: 'nav', role: 'navigation' },
435
+ { tag: 'main', role: 'main' },
436
+ { tag: 'aside', role: 'complementary' },
437
+ { tag: 'header', role: 'banner' },
438
+ { tag: 'footer', role: 'contentinfo' },
439
+ { tag: 'section', role: 'region' },
440
+ { tag: 'form', role: 'form' },
441
+ { tag: 'search', role: 'search' },
442
+ ];
443
+
444
+ landmarkElements.forEach(({ tag, role }) => {
445
+ it(`should return false for <${tag}> without naming attributes`, () => {
446
+ document.body.innerHTML = `<${tag}><p>Some inner text content</p></${tag}>`;
447
+ const el = document.querySelector(tag);
448
+ expect(getAccessibleName(el)).toBe(false);
449
+ });
450
+
451
+ it(`should return title for <${tag}> with title attribute`, () => {
452
+ document.body.innerHTML = `<${tag} title="My ${tag} landmark"><p>Some inner text</p></${tag}>`;
453
+ const el = document.querySelector(tag);
454
+ expect(getAccessibleName(el)).toBe(`My ${tag} landmark`);
455
+ });
456
+
457
+ it(`should return false for div with role="${role}" without naming attributes`, () => {
458
+ document.body.innerHTML = `<div role="${role}"><p>Some inner text content</p></div>`;
459
+ const el = document.querySelector(`[role="${role}"]`);
460
+ expect(getAccessibleName(el)).toBe(false);
461
+ });
462
+
463
+ it(`should return title for div with role="${role}" and title attribute`, () => {
464
+ document.body.innerHTML = `<div role="${role}" title="My ${role} landmark"><p>Some inner text</p></div>`;
465
+ const el = document.querySelector(`[role="${role}"]`);
466
+ expect(getAccessibleName(el)).toBe(`My ${role} landmark`);
467
+ });
468
+ });
469
+ });
431
470
  });