@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
package/test/domUtils.test.js
CHANGED
|
@@ -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', () => {
|
package/test/formUtils.test.js
CHANGED
|
@@ -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
|
});
|