@afixt/test-utils 1.2.3 → 1.3.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.
|
@@ -18,12 +18,7 @@
|
|
|
18
18
|
"Bash(node:*)",
|
|
19
19
|
"Bash(npx vitest run:*)",
|
|
20
20
|
"Bash(1)",
|
|
21
|
-
"Bash(npm run test:playwright:css:*)"
|
|
22
|
-
"Bash(gh run list:*)",
|
|
23
|
-
"Bash(gh pr create:*)",
|
|
24
|
-
"Bash(git checkout:*)",
|
|
25
|
-
"Bash(git pull:*)",
|
|
26
|
-
"Bash(git merge:*)"
|
|
21
|
+
"Bash(npm run test:playwright:css:*)"
|
|
27
22
|
]
|
|
28
23
|
},
|
|
29
24
|
"enableAllProjectMcpServers": false
|
package/package.json
CHANGED
package/src/getAccessibleName.js
CHANGED
|
@@ -456,23 +456,45 @@ function getAccessibleName(element) {
|
|
|
456
456
|
}
|
|
457
457
|
|
|
458
458
|
// STEP 15: Landmark elements that require explicit accessible names
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
// via aria-labelledby, aria-label, or title attribute.
|
|
459
|
+
// Landmark roles do NOT support "Name From: contents" per WAI-ARIA spec.
|
|
460
|
+
// They only support "Name From: author" (aria-labelledby, aria-label, title).
|
|
462
461
|
// Since steps 1 & 2 already checked aria-labelledby and aria-label,
|
|
463
462
|
// we only need to check title here, then return false.
|
|
464
|
-
const
|
|
465
|
-
|
|
463
|
+
const landmarkRoles = [
|
|
464
|
+
'banner',
|
|
465
|
+
'complementary',
|
|
466
|
+
'contentinfo',
|
|
467
|
+
'form',
|
|
468
|
+
'main',
|
|
469
|
+
'navigation',
|
|
470
|
+
'region',
|
|
471
|
+
'search',
|
|
472
|
+
];
|
|
466
473
|
|
|
467
|
-
|
|
468
|
-
|
|
474
|
+
const landmarkElementMap = {
|
|
475
|
+
nav: 'navigation',
|
|
476
|
+
main: 'main',
|
|
477
|
+
aside: 'complementary',
|
|
478
|
+
header: 'banner',
|
|
479
|
+
footer: 'contentinfo',
|
|
480
|
+
section: 'region',
|
|
481
|
+
form: 'form',
|
|
482
|
+
search: 'search',
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const tagName = element.tagName.toLowerCase();
|
|
486
|
+
const role = element.getAttribute('role');
|
|
487
|
+
const isLandmark = landmarkRoles.includes(role) || tagName in landmarkElementMap;
|
|
488
|
+
|
|
489
|
+
if (isLandmark) {
|
|
490
|
+
// Title attribute is valid for landmark elements
|
|
469
491
|
if (element.hasAttribute('title')) {
|
|
470
492
|
const titleValue = element.getAttribute('title');
|
|
471
493
|
if (strlen(titleValue) > 0) {
|
|
472
494
|
return titleValue;
|
|
473
495
|
}
|
|
474
496
|
}
|
|
475
|
-
//
|
|
497
|
+
// Landmark elements do not get accessible name from text content
|
|
476
498
|
return false;
|
|
477
499
|
}
|
|
478
500
|
|
package/src/testContrast.js
CHANGED
|
@@ -1,3 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if an element or any of its ancestors has a background image set.
|
|
3
|
+
* Walks up the DOM tree until it finds a background image or an opaque background color.
|
|
4
|
+
* @param {Element} el - The element to check
|
|
5
|
+
* @returns {boolean} True if the element or a visible ancestor has a background image
|
|
6
|
+
*/
|
|
7
|
+
function hasBackgroundImage(el) {
|
|
8
|
+
if (!el || el.nodeType === 9 || !window.getComputedStyle) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let current = el;
|
|
13
|
+
while (current && current.nodeType !== 9) {
|
|
14
|
+
const styles = window.getComputedStyle(current);
|
|
15
|
+
if (!styles) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const bgImage = styles.getPropertyValue('background-image');
|
|
20
|
+
if (bgImage && bgImage !== 'none') {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// If this element has an opaque background color, stop traversal
|
|
25
|
+
// since no ancestor background image would be visible through it
|
|
26
|
+
const bgColor = styles.getPropertyValue('background-color');
|
|
27
|
+
const parsed = parseRGB(bgColor);
|
|
28
|
+
if (parsed) {
|
|
29
|
+
const isTransparent =
|
|
30
|
+
bgColor === 'rgba(0, 0, 0, 0)' ||
|
|
31
|
+
bgColor === 'transparent' ||
|
|
32
|
+
(parsed[4] !== undefined && parseFloat(parsed[4]) === 0);
|
|
33
|
+
const isSemiTransparent =
|
|
34
|
+
parsed[4] !== undefined && parseFloat(parsed[4]) > 0 && parseFloat(parsed[4]) < 1;
|
|
35
|
+
|
|
36
|
+
if (!isTransparent && !isSemiTransparent) {
|
|
37
|
+
// Opaque background color, no ancestor background image visible
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
current = current.parentElement;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
1
48
|
/**
|
|
2
49
|
* Get the computed background color for an element.
|
|
3
50
|
* @param {Element} el - the element to be tested
|
|
@@ -240,6 +287,12 @@ function testContrast(el, options = { level: 'AA' }) {
|
|
|
240
287
|
return false;
|
|
241
288
|
}
|
|
242
289
|
|
|
290
|
+
// Skip elements with a background image on the element itself or a visible ancestor.
|
|
291
|
+
// Contrast cannot be reliably tested against background images.
|
|
292
|
+
if (hasBackgroundImage(el)) {
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
|
|
243
296
|
const styles = window.getComputedStyle(el);
|
|
244
297
|
const selfFG = styles.getPropertyValue('color');
|
|
245
298
|
const selfBG = getComputedBackgroundColor(el);
|
|
@@ -346,6 +399,7 @@ function testContrast(el, options = { level: 'AA' }) {
|
|
|
346
399
|
|
|
347
400
|
module.exports = {
|
|
348
401
|
testContrast,
|
|
402
|
+
hasBackgroundImage,
|
|
349
403
|
getComputedBackgroundColor,
|
|
350
404
|
luminance,
|
|
351
405
|
parseRGB,
|
|
@@ -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
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
2
|
import {
|
|
3
3
|
testContrast,
|
|
4
|
+
hasBackgroundImage,
|
|
4
5
|
getComputedBackgroundColor,
|
|
5
6
|
luminance,
|
|
6
7
|
parseRGB,
|
|
@@ -94,17 +95,49 @@ describe('testContrast', () => {
|
|
|
94
95
|
expect(testContrast(div)).toBe(true);
|
|
95
96
|
});
|
|
96
97
|
|
|
97
|
-
it('should
|
|
98
|
+
it('should skip elements with a background image', () => {
|
|
98
99
|
const div = document.createElement('div');
|
|
99
100
|
div.textContent = 'Text with background image';
|
|
100
101
|
div.style.color = 'rgb(0, 0, 0)';
|
|
101
102
|
div.style.backgroundImage = 'url(image.jpg)';
|
|
102
103
|
document.body.appendChild(div);
|
|
103
104
|
|
|
104
|
-
// Should
|
|
105
|
+
// Should skip because contrast can't be reliably tested against background images
|
|
105
106
|
expect(testContrast(div)).toBe(true);
|
|
106
107
|
});
|
|
107
108
|
|
|
109
|
+
it('should skip elements whose ancestor has a background image', () => {
|
|
110
|
+
const parent = document.createElement('div');
|
|
111
|
+
parent.style.backgroundImage = 'url(bg.jpg)';
|
|
112
|
+
|
|
113
|
+
const child = document.createElement('div');
|
|
114
|
+
child.textContent = 'Text over parent background image';
|
|
115
|
+
child.style.color = 'rgb(0, 0, 0)';
|
|
116
|
+
child.style.backgroundColor = 'rgba(0, 0, 0, 0)';
|
|
117
|
+
|
|
118
|
+
parent.appendChild(child);
|
|
119
|
+
document.body.appendChild(parent);
|
|
120
|
+
|
|
121
|
+
expect(testContrast(child)).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should not skip elements with opaque background over ancestor background image', () => {
|
|
125
|
+
const parent = document.createElement('div');
|
|
126
|
+
parent.style.backgroundImage = 'url(bg.jpg)';
|
|
127
|
+
|
|
128
|
+
const child = document.createElement('div');
|
|
129
|
+
child.textContent = 'Text with opaque background';
|
|
130
|
+
child.style.color = 'rgb(0, 0, 0)';
|
|
131
|
+
child.style.backgroundColor = 'rgb(255, 255, 255)';
|
|
132
|
+
|
|
133
|
+
parent.appendChild(child);
|
|
134
|
+
document.body.appendChild(parent);
|
|
135
|
+
|
|
136
|
+
// The child has an opaque background, so the parent's background image
|
|
137
|
+
// is not visible through it. Contrast should be tested normally.
|
|
138
|
+
expect(testContrast(child)).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
108
141
|
it('should handle invisible elements', () => {
|
|
109
142
|
const div = document.createElement('div');
|
|
110
143
|
div.textContent = 'Hidden text';
|
|
@@ -499,6 +532,75 @@ describe('getComputedBackgroundColor', () => {
|
|
|
499
532
|
});
|
|
500
533
|
});
|
|
501
534
|
|
|
535
|
+
describe('hasBackgroundImage', () => {
|
|
536
|
+
beforeEach(() => {
|
|
537
|
+
document.body.innerHTML = '';
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it('should return false for null element', () => {
|
|
541
|
+
expect(hasBackgroundImage(null)).toBe(false);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('should return false for document node', () => {
|
|
545
|
+
expect(hasBackgroundImage(document)).toBe(false);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('should return true for element with background-image', () => {
|
|
549
|
+
const div = document.createElement('div');
|
|
550
|
+
div.style.backgroundImage = 'url(test.jpg)';
|
|
551
|
+
document.body.appendChild(div);
|
|
552
|
+
|
|
553
|
+
expect(hasBackgroundImage(div)).toBe(true);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it('should return false for element without background-image', () => {
|
|
557
|
+
const div = document.createElement('div');
|
|
558
|
+
div.style.backgroundColor = 'rgb(255, 255, 255)';
|
|
559
|
+
document.body.appendChild(div);
|
|
560
|
+
|
|
561
|
+
expect(hasBackgroundImage(div)).toBe(false);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('should return true when ancestor has a background image and element is transparent', () => {
|
|
565
|
+
const parent = document.createElement('div');
|
|
566
|
+
parent.style.backgroundImage = 'url(bg.jpg)';
|
|
567
|
+
|
|
568
|
+
const child = document.createElement('div');
|
|
569
|
+
child.style.backgroundColor = 'rgba(0, 0, 0, 0)';
|
|
570
|
+
|
|
571
|
+
parent.appendChild(child);
|
|
572
|
+
document.body.appendChild(parent);
|
|
573
|
+
|
|
574
|
+
expect(hasBackgroundImage(child)).toBe(true);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('should return false when ancestor has background image but element has opaque background', () => {
|
|
578
|
+
const parent = document.createElement('div');
|
|
579
|
+
parent.style.backgroundImage = 'url(bg.jpg)';
|
|
580
|
+
|
|
581
|
+
const child = document.createElement('div');
|
|
582
|
+
child.style.backgroundColor = 'rgb(255, 255, 255)';
|
|
583
|
+
|
|
584
|
+
parent.appendChild(child);
|
|
585
|
+
document.body.appendChild(parent);
|
|
586
|
+
|
|
587
|
+
expect(hasBackgroundImage(child)).toBe(false);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it('should return false if getComputedStyle is not available', () => {
|
|
591
|
+
const div = document.createElement('div');
|
|
592
|
+
div.style.backgroundImage = 'url(test.jpg)';
|
|
593
|
+
document.body.appendChild(div);
|
|
594
|
+
|
|
595
|
+
const originalGetComputedStyle = window.getComputedStyle;
|
|
596
|
+
window.getComputedStyle = null;
|
|
597
|
+
|
|
598
|
+
expect(hasBackgroundImage(div)).toBe(false);
|
|
599
|
+
|
|
600
|
+
window.getComputedStyle = originalGetComputedStyle;
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
|
|
502
604
|
describe('luminance', () => {
|
|
503
605
|
it('should calculate luminance for pure black (0, 0, 0)', () => {
|
|
504
606
|
const result = luminance(0, 0, 0);
|