Skip to content

Commit 771473b

Browse files
Merge branch 'main' of github.com:browserstack/a11y-engine-axe-core into AXE-999-validate-job
2 parents fcdd790 + 733d1e8 commit 771473b

20 files changed

+355
-211
lines changed

.eslintrc.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ module.exports = {
1010
},
1111
globals: {
1212
axe: true,
13-
Promise: true
13+
Promise: true,
14+
a11yEngine: true
1415
},
1516
rules: {
1617
'no-bitwise': 2,

.github/workflows/test.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@ jobs:
1919
cache: 'npm'
2020
- run: npm ci
2121
- run: npm run build
22-
# v4 download seems to have some flakiness with the download of artifacts so pinning to v3 for now
23-
# @see https://github.com/actions/download-artifact/issues/249
24-
- uses: actions/upload-artifact@v3
22+
- uses: actions/upload-artifact@v4
2523
with:
2624
name: axe-core
2725
path: axe.js

lib/checks/color/color-contrast-enhanced.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
},
2121
"pseudoSizeThreshold": 0.25,
2222
"shadowOutlineEmMax": 0.1,
23-
"textStrokeEmMin": 0.03
23+
"textStrokeEmMin": 0.03,
24+
"ruleId": "color-contrast-enhanced"
2425
},
2526
"metadata": {
2627
"impact": "serious",

lib/checks/color/color-contrast-evaluate.js

Lines changed: 142 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -17,156 +17,162 @@ import {
1717
import { memoize } from '../../core/utils';
1818

1919
export default function colorContrastEvaluate(node, options, virtualNode) {
20-
const {
21-
ignoreUnicode,
22-
ignoreLength,
23-
ignorePseudo,
24-
boldValue,
25-
boldTextPt,
26-
largeTextPt,
27-
contrastRatio,
28-
shadowOutlineEmMax,
29-
pseudoSizeThreshold
30-
} = options;
31-
32-
if (!isVisibleOnScreen(node)) {
33-
this.data({ messageKey: 'hidden' });
34-
return true;
35-
}
20+
try {
21+
const {
22+
ignoreUnicode,
23+
ignoreLength,
24+
ignorePseudo,
25+
boldValue,
26+
boldTextPt,
27+
largeTextPt,
28+
contrastRatio,
29+
shadowOutlineEmMax,
30+
pseudoSizeThreshold
31+
} = options;
32+
33+
if (!isVisibleOnScreen(node)) {
34+
this.data({ messageKey: 'hidden' });
35+
return true;
36+
}
3637

37-
const visibleText = visibleVirtual(virtualNode, false, true);
38-
if (ignoreUnicode && textIsEmojis(visibleText)) {
39-
this.data({ messageKey: 'nonBmp' });
40-
return undefined;
41-
}
38+
const visibleText = visibleVirtual(virtualNode, false, true);
39+
if (ignoreUnicode && textIsEmojis(visibleText)) {
40+
this.data({ messageKey: 'nonBmp' });
41+
return undefined;
42+
}
4243

43-
const nodeStyle = window.getComputedStyle(node);
44-
const fontSize = parseFloat(nodeStyle.getPropertyValue('font-size'));
45-
const fontWeight = nodeStyle.getPropertyValue('font-weight');
46-
const bold = parseFloat(fontWeight) >= boldValue || fontWeight === 'bold';
47-
48-
const ptSize = Math.ceil(fontSize * 72) / 96;
49-
const isSmallFont =
50-
(bold && ptSize < boldTextPt) || (!bold && ptSize < largeTextPt);
51-
52-
const { expected, minThreshold, maxThreshold } = isSmallFont
53-
? contrastRatio.normal
54-
: contrastRatio.large;
55-
56-
// if element or a parent has pseudo content then we need to mark
57-
// as needs review
58-
const pseudoElm = findPseudoElement(virtualNode, {
59-
ignorePseudo,
60-
pseudoSizeThreshold
61-
});
62-
if (pseudoElm) {
63-
this.data({
64-
fontSize: `${((fontSize * 72) / 96).toFixed(1)}pt (${fontSize}px)`,
65-
fontWeight: bold ? 'bold' : 'normal',
66-
messageKey: 'pseudoContent',
67-
expectedContrastRatio: expected + ':1'
44+
const nodeStyle = window.getComputedStyle(node);
45+
const fontSize = parseFloat(nodeStyle.getPropertyValue('font-size'));
46+
const fontWeight = nodeStyle.getPropertyValue('font-weight');
47+
const bold = parseFloat(fontWeight) >= boldValue || fontWeight === 'bold';
48+
49+
const ptSize = Math.ceil(fontSize * 72) / 96;
50+
const isSmallFont =
51+
(bold && ptSize < boldTextPt) || (!bold && ptSize < largeTextPt);
52+
53+
const { expected, minThreshold, maxThreshold } = isSmallFont
54+
? contrastRatio.normal
55+
: contrastRatio.large;
56+
57+
// if element or a parent has pseudo content then we need to mark
58+
// as needs review
59+
const pseudoElm = findPseudoElement(virtualNode, {
60+
ignorePseudo,
61+
pseudoSizeThreshold
6862
});
63+
if (pseudoElm) {
64+
this.data({
65+
fontSize: `${((fontSize * 72) / 96).toFixed(1)}pt (${fontSize}px)`,
66+
fontWeight: bold ? 'bold' : 'normal',
67+
messageKey: 'pseudoContent',
68+
expectedContrastRatio: expected + ':1'
69+
});
70+
71+
this.relatedNodes(pseudoElm.actualNode);
72+
return undefined;
73+
}
6974

70-
this.relatedNodes(pseudoElm.actualNode);
71-
return undefined;
72-
}
75+
// Thin shadows only. Thicker shadows are included in the background instead
76+
const shadowColors = getTextShadowColors(node, {
77+
minRatio: 0.001,
78+
maxRatio: shadowOutlineEmMax
79+
});
80+
if (shadowColors === null) {
81+
this.data({ messageKey: 'complexTextShadows' });
82+
return undefined;
83+
}
7384

74-
// Thin shadows only. Thicker shadows are included in the background instead
75-
const shadowColors = getTextShadowColors(node, {
76-
minRatio: 0.001,
77-
maxRatio: shadowOutlineEmMax
78-
});
79-
if (shadowColors === null) {
80-
this.data({ messageKey: 'complexTextShadows' });
81-
return undefined;
82-
}
85+
const bgNodes = [];
86+
const bgColor = getBackgroundColor(node, bgNodes, shadowOutlineEmMax);
87+
const fgColor = getForegroundColor(node, false, bgColor, options);
88+
89+
let contrast = null;
90+
let contrastContributor = null;
91+
let shadowColor = null;
92+
if (shadowColors.length === 0) {
93+
contrast = getContrast(bgColor, fgColor);
94+
} else if (fgColor && bgColor) {
95+
shadowColor = [...shadowColors, bgColor].reduce(flattenShadowColors);
96+
// Compare shadow, bgColor, textColor. Check passes if any is sufficient
97+
const fgBgContrast = getContrast(bgColor, fgColor);
98+
const bgShContrast = getContrast(bgColor, shadowColor);
99+
const fgShContrast = getContrast(shadowColor, fgColor);
100+
contrast = Math.max(fgBgContrast, bgShContrast, fgShContrast);
101+
if (contrast !== fgBgContrast) {
102+
contrastContributor =
103+
bgShContrast > fgShContrast ? 'shadowOnBgColor' : 'fgOnShadowColor';
104+
}
105+
}
83106

84-
const bgNodes = [];
85-
const bgColor = getBackgroundColor(node, bgNodes, shadowOutlineEmMax);
86-
const fgColor = getForegroundColor(node, false, bgColor, options);
87-
88-
let contrast = null;
89-
let contrastContributor = null;
90-
let shadowColor = null;
91-
if (shadowColors.length === 0) {
92-
contrast = getContrast(bgColor, fgColor);
93-
} else if (fgColor && bgColor) {
94-
shadowColor = [...shadowColors, bgColor].reduce(flattenShadowColors);
95-
// Compare shadow, bgColor, textColor. Check passes if any is sufficient
96-
const fgBgContrast = getContrast(bgColor, fgColor);
97-
const bgShContrast = getContrast(bgColor, shadowColor);
98-
const fgShContrast = getContrast(shadowColor, fgColor);
99-
contrast = Math.max(fgBgContrast, bgShContrast, fgShContrast);
100-
if (contrast !== fgBgContrast) {
101-
contrastContributor =
102-
bgShContrast > fgShContrast ? 'shadowOnBgColor' : 'fgOnShadowColor';
107+
const isValid = contrast > expected;
108+
109+
// ratio is outside range
110+
if (
111+
(typeof minThreshold === 'number' &&
112+
(typeof contrast !== 'number' || contrast < minThreshold)) ||
113+
(typeof maxThreshold === 'number' &&
114+
(typeof contrast !== 'number' || contrast > maxThreshold))
115+
) {
116+
this.data({ contrastRatio: contrast });
117+
return true;
103118
}
104-
}
105119

106-
const isValid = contrast > expected;
107-
108-
// ratio is outside range
109-
if (
110-
(typeof minThreshold === 'number' &&
111-
(typeof contrast !== 'number' || contrast < minThreshold)) ||
112-
(typeof maxThreshold === 'number' &&
113-
(typeof contrast !== 'number' || contrast > maxThreshold))
114-
) {
115-
this.data({ contrastRatio: contrast });
116-
return true;
117-
}
120+
// truncate ratio to three digits while rounding down
121+
// 4.499 = 4.49, 4.019 = 4.01
122+
const truncatedResult = Math.floor(contrast * 100) / 100;
118123

119-
// truncate ratio to three digits while rounding down
120-
// 4.499 = 4.49, 4.019 = 4.01
121-
const truncatedResult = Math.floor(contrast * 100) / 100;
124+
// if fgColor or bgColor are missing, get more information.
125+
let missing;
126+
if (bgColor === null) {
127+
missing = incompleteData.get('bgColor');
128+
} else if (!isValid) {
129+
missing = contrastContributor;
130+
}
122131

123-
// if fgColor or bgColor are missing, get more information.
124-
let missing;
125-
if (bgColor === null) {
126-
missing = incompleteData.get('bgColor');
127-
} else if (!isValid) {
128-
missing = contrastContributor;
129-
}
132+
const equalRatio = truncatedResult === 1;
133+
const shortTextContent = visibleText.length === 1;
134+
if (equalRatio) {
135+
missing = incompleteData.set('bgColor', 'equalRatio');
136+
} else if (!isValid && shortTextContent && !ignoreLength) {
137+
// Check that the text content is a single character long
138+
missing = 'shortTextContent';
139+
}
130140

131-
const equalRatio = truncatedResult === 1;
132-
const shortTextContent = visibleText.length === 1;
133-
if (equalRatio) {
134-
missing = incompleteData.set('bgColor', 'equalRatio');
135-
} else if (!isValid && shortTextContent && !ignoreLength) {
136-
// Check that the text content is a single character long
137-
missing = 'shortTextContent';
138-
}
141+
// need both independently in case both are missing
142+
this.data({
143+
fgColor: fgColor ? fgColor.toHexString() : undefined,
144+
bgColor: bgColor ? bgColor.toHexString() : undefined,
145+
contrastRatio: truncatedResult,
146+
fontSize: `${((fontSize * 72) / 96).toFixed(1)}pt (${fontSize}px)`,
147+
fontWeight: bold ? 'bold' : 'normal',
148+
messageKey: missing,
149+
expectedContrastRatio: expected + ':1',
150+
shadowColor: shadowColor ? shadowColor.toHexString() : undefined
151+
});
139152

140-
// need both independently in case both are missing
141-
this.data({
142-
fgColor: fgColor ? fgColor.toHexString() : undefined,
143-
bgColor: bgColor ? bgColor.toHexString() : undefined,
144-
contrastRatio: truncatedResult,
145-
fontSize: `${((fontSize * 72) / 96).toFixed(1)}pt (${fontSize}px)`,
146-
fontWeight: bold ? 'bold' : 'normal',
147-
messageKey: missing,
148-
expectedContrastRatio: expected + ':1',
149-
shadowColor: shadowColor ? shadowColor.toHexString() : undefined
150-
});
151-
152-
// We don't know, so we'll put it into Can't Tell
153-
if (
154-
fgColor === null ||
155-
bgColor === null ||
156-
equalRatio ||
157-
(shortTextContent && !ignoreLength && !isValid)
158-
) {
159-
missing = null;
160-
incompleteData.clear();
161-
this.relatedNodes(bgNodes);
162-
return undefined;
163-
}
153+
// We don't know, so we'll put it into Can't Tell
154+
if (
155+
fgColor === null ||
156+
bgColor === null ||
157+
equalRatio ||
158+
(shortTextContent && !ignoreLength && !isValid)
159+
) {
160+
missing = null;
161+
incompleteData.clear();
162+
this.relatedNodes(bgNodes);
163+
return undefined;
164+
}
164165

165-
if (!isValid) {
166-
this.relatedNodes(bgNodes);
167-
}
166+
if (!isValid) {
167+
this.relatedNodes(bgNodes);
168+
}
168169

169-
return isValid;
170+
return isValid;
171+
} catch (err) {
172+
this.data({ messageKey: 'Check error' });
173+
a11yEngine.errorHandler.addCheckError(options?.ruleId, err);
174+
return undefined;
175+
}
170176
}
171177

172178
function findPseudoElement(

lib/checks/color/color-contrast.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
},
1919
"pseudoSizeThreshold": 0.25,
2020
"shadowOutlineEmMax": 0.2,
21-
"textStrokeEmMin": 0.03
21+
"textStrokeEmMin": 0.03,
22+
"ruleId": "color-contrast"
2223
},
2324
"metadata": {
2425
"impact": "serious",

lib/checks/color/link-in-text-block-evaluate.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
getBackgroundColor,
55
incompleteData
66
} from '../../commons/color';
7-
import a11yEngineCommons from '../../commons/a11y-engine-index';
87

98
function getContrast(color1, color2) {
109
var c1lum = color1.getRelativeLuminance();
@@ -53,7 +52,7 @@ function linkInTextBlockEvaluate(node, options) {
5352
) {
5453
return true;
5554
}
56-
return a11yEngineCommons.distinguishableLinkEvaluate(node, parentBlock);
55+
return a11yEngine.commons.distinguishableLinkEvaluate(node, parentBlock);
5756
}
5857

5958
// Capture colors
@@ -136,6 +135,14 @@ function getColorContrast(node, parentBlock) {
136135
nodeColor = getForegroundColor(node);
137136
parentColor = getForegroundColor(parentBlock);
138137

138+
if (!nodeColor) {
139+
nodeColor = new axe.commons.color.Color(0, 0, 0, 0);
140+
}
141+
142+
if (!parentColor) {
143+
parentColor = new axe.commons.color.Color(0, 0, 0, 0);
144+
}
145+
139146
if (!nodeColor || !parentColor) {
140147
return undefined;
141148
}

lib/checks/forms/autocomplete-a11y-evaluate.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { isValidAutocomplete } from '../../commons/text';
2-
import ErrorHandler from '../../core/errors/error-handler';
32

43
function checkIsElementValidAutocomplete(node, options, virtualNode) {
54
const autocomplete = virtualNode.attr('autocomplete')?.toLowerCase().trim();
@@ -67,7 +66,10 @@ function autocompleteA11yEvaluate(node, options, virtualNode) {
6766
return checkIsElementValidAutocomplete(node, options, virtualNode);
6867
}
6968
} catch (err) {
70-
ErrorHandler.addCheckError('autocomplete-attribute-valid-check', err);
69+
a11yEngine.errorHandler.addCheckError(
70+
'autocomplete-attribute-valid-check',
71+
err
72+
);
7173
return undefined;
7274
}
7375
}

lib/checks/label/label-content-name-mismatch.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"impact": "serious",
1010
"messages": {
1111
"pass": "Element contains visible text as part of it's accessible name",
12-
"fail": "Text inside the element is not included in the accessible name"
12+
"fail": "Ensure that the visible label of the element and the accessible name (provided by `aria-label`, etc.) match exactly. Ensure that the visible label of the element is part of the accessible name of the element. It is advisable that the visible label comes in the beginning of the accessible name",
13+
"incomplete": "Check if the visible text is part of the accessible name (provided by `aria-label`, etc.) for the element. Check the accessibility tree to get the accessible name of the element."
1314
}
1415
}
1516
}

0 commit comments

Comments
 (0)