@surea11y/core 1.2.0 → 1.4.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/CHANGELOG.md +81 -7
- package/LICENSE +373 -21
- package/README.md +175 -35
- package/bin/surea11y-core.js +20 -0
- package/docs/API_STABILITY.md +27 -1
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/ENGINE_OPTIONS.md +2 -0
- package/docs/I18N.md +12 -9
- package/docs/INTEGRATION.md +19 -1
- package/docs/LIMITATIONS.md +1 -1
- package/docs/OUTPUT_SCHEMA.md +1 -1
- package/docs/REPORT.md +1 -1
- package/docs/RULE_CATALOG.md +1 -1
- package/docs/SARIF.md +59 -0
- package/package.json +63 -18
- package/src/baseline.js +0 -0
- package/src/checks/automatic/area-alt-present.js +63 -31
- package/src/checks/automatic/aria-allowed-attr.js +204 -80
- package/src/checks/automatic/aria-allowed-role.js +23 -7
- package/src/checks/automatic/aria-braille-equivalent.js +34 -10
- package/src/checks/automatic/aria-conditional-attr.js +32 -14
- package/src/checks/automatic/aria-deprecated-role.js +26 -11
- package/src/checks/automatic/aria-hidden-body.js +48 -23
- package/src/checks/automatic/aria-hidden-focus.js +420 -66
- package/src/checks/automatic/aria-prohibited-attr.js +327 -60
- package/src/checks/automatic/aria-prohibited-children.js +111 -103
- package/src/checks/automatic/aria-required-attr.js +29 -15
- package/src/checks/automatic/aria-required-children.js +44 -24
- package/src/checks/automatic/aria-required-parent.js +64 -35
- package/src/checks/automatic/aria-role-name-present.js +49 -21
- package/src/checks/automatic/aria-roles-valid.js +24 -12
- package/src/checks/automatic/aria-valid-attr-value.js +46 -22
- package/src/checks/automatic/aria-valid-attr.js +19 -5
- package/src/checks/automatic/autocomplete-valid.js +76 -16
- package/src/checks/automatic/avoid-inline-spacing.js +23 -8
- package/src/checks/automatic/binary-control-name-present.js +62 -50
- package/src/checks/automatic/button-name-present.js +54 -24
- package/src/checks/automatic/bypass-blocks-present.js +51 -32
- package/src/checks/automatic/canvas-text-alternative-present.js +59 -26
- package/src/checks/automatic/combobox-name-present.js +40 -45
- package/src/checks/automatic/contrast-computable.js +363 -341
- package/src/checks/automatic/contrast-enhanced.js +489 -466
- package/src/checks/automatic/contrast-minimum.js +488 -465
- package/src/checks/automatic/css-orientation-lock.js +51 -35
- package/src/checks/automatic/definition-list-children-valid.js +46 -25
- package/src/checks/automatic/deprecated-elements-not-used.js +25 -9
- package/src/checks/automatic/dialog-name-present.js +47 -85
- package/src/checks/automatic/dlitem-parent-valid.js +25 -8
- package/src/checks/automatic/duplicate-id-aria.js +28 -9
- package/src/checks/automatic/embed-text-alternative-present.js +88 -35
- package/src/checks/automatic/form-control-programmatic-label-present.js +81 -196
- package/src/checks/automatic/form-control-single-label.js +50 -14
- package/src/checks/automatic/html-xml-lang-mismatch.js +36 -18
- package/src/checks/automatic/iframe-focusable-content.js +265 -22
- package/src/checks/automatic/iframe-name-present.js +33 -9
- package/src/checks/automatic/iframe-title-unique.js +32 -9
- package/src/checks/automatic/img-alt-present.js +54 -52
- package/src/checks/automatic/input-image-alt-present.js +141 -112
- package/src/checks/automatic/label-in-name.js +65 -41
- package/src/checks/automatic/language-page-present.js +111 -109
- package/src/checks/automatic/link-in-text-block.js +61 -19
- package/src/checks/automatic/link-name-present.js +47 -14
- package/src/checks/automatic/list-children-valid.js +40 -33
- package/src/checks/automatic/listbox-name-present.js +41 -19
- package/src/checks/automatic/listitem-parent-valid.js +48 -13
- package/src/checks/automatic/menuitem-name-present.js +41 -61
- package/src/checks/automatic/meta-refresh-no-exceptions.js +32 -11
- package/src/checks/automatic/meta-refresh-timing-absent.js +22 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +26 -7
- package/src/checks/automatic/meter-name-present.js +40 -36
- package/src/checks/automatic/nested-interactive-controls-absent.js +58 -15
- package/src/checks/automatic/object-text-alternative-present.js +93 -39
- package/src/checks/automatic/option-name-present.js +40 -21
- package/src/checks/automatic/page-title-present.js +19 -6
- package/src/checks/automatic/progressbar-name-present.js +49 -44
- package/src/checks/automatic/role-img-alt-present.js +211 -159
- package/src/checks/automatic/searchbox-name-present.js +41 -19
- package/src/checks/automatic/server-side-image-map-absent.js +27 -11
- package/src/checks/automatic/slider-name-present.js +42 -47
- package/src/checks/automatic/spinbutton-name-present.js +41 -19
- package/src/checks/automatic/summary-name-present.js +39 -17
- package/src/checks/automatic/svg-image-text-alternative-present.js +116 -47
- package/src/checks/automatic/svg-text-alternative-present.js +262 -230
- package/src/checks/automatic/tab-name-present.js +39 -60
- package/src/checks/automatic/table-headers-attr-valid.js +27 -10
- package/src/checks/automatic/table-th-has-data-cells.js +24 -8
- package/src/checks/automatic/target-size-minimum.js +123 -48
- package/src/checks/automatic/td-has-header.js +53 -12
- package/src/checks/automatic/textbox-name-present.js +41 -19
- package/src/checks/automatic/tooltip-name-present.js +39 -18
- package/src/checks/automatic/treeitem-name-present.js +40 -21
- package/src/checks/automatic/valid-lang.js +22 -6
- package/src/checks/automatic/video-poster-text-alternative-present.js +81 -36
- package/src/checks/manual/accesskeys-manual.js +17 -6
- package/src/checks/manual/area-alt-decorative-manual.js +194 -193
- package/src/checks/manual/area-alt-quality-manual.js +184 -141
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +48 -34
- package/src/checks/manual/aria-text-manual.js +20 -11
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +151 -114
- package/src/checks/manual/css-hidden-focus.js +375 -169
- package/src/checks/manual/embed-text-alternative-quality-manual.js +178 -162
- package/src/checks/manual/empty-heading-manual.js +41 -24
- package/src/checks/manual/empty-table-header-manual.js +69 -31
- package/src/checks/manual/focus-order-semantics-manual.js +60 -13
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +209 -246
- package/src/checks/manual/heading-order-manual.js +50 -8
- package/src/checks/manual/identical-links-same-purpose-manual.js +36 -12
- package/src/checks/manual/image-redundant-alt-manual.js +38 -8
- package/src/checks/manual/img-alt-decorative-manual.js +133 -96
- package/src/checks/manual/img-alt-quality-manual.js +178 -127
- package/src/checks/manual/input-image-alt-decorative-manual.js +127 -92
- package/src/checks/manual/input-image-alt-quality-manual.js +127 -92
- package/src/checks/manual/label-title-only-manual.js +44 -28
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -38
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +85 -32
- package/src/checks/manual/landmark-main-is-top-level-manual.js +69 -27
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +45 -33
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +43 -31
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +27 -21
- package/src/checks/manual/landmark-one-main-manual.js +38 -43
- package/src/checks/manual/landmark-unique-manual.js +78 -67
- package/src/checks/manual/link-name-quality-manual.js +45 -12
- package/src/checks/manual/media-transcript-present-manual.js +37 -22
- package/src/checks/manual/meta-viewport-large-manual.js +19 -6
- package/src/checks/manual/mouse-only-event-handlers-manual.js +40 -11
- package/src/checks/manual/no-autoplay-audio-manual.js +22 -6
- package/src/checks/manual/object-text-alternative-quality-manual.js +177 -154
- package/src/checks/manual/p-as-heading-manual.js +24 -7
- package/src/checks/manual/page-has-heading-one-manual.js +42 -32
- package/src/checks/manual/page-title-patterns-manual.js +80 -50
- package/src/checks/manual/presentation-role-conflict-manual.js +101 -47
- package/src/checks/manual/region-manual.js +244 -60
- package/src/checks/manual/scope-attr-valid-manual.js +13 -4
- package/src/checks/manual/scrollable-region-focusable-manual.js +39 -11
- package/src/checks/manual/skip-link-manual.js +42 -18
- package/src/checks/manual/svg-text-alternative-quality-manual.js +208 -165
- package/src/checks/manual/tabindex-manual.js +13 -4
- package/src/checks/manual/table-duplicate-name-manual.js +22 -11
- package/src/checks/manual/table-fake-caption-manual.js +48 -10
- package/src/checks/manual/video-caption-manual.js +17 -4
- package/src/checks/manual-review.js +58 -12
- package/src/core.js +41705 -29650
- package/src/index.js +2 -0
- package/src/report.js +109 -47
- package/src/sarif.js +190 -0
- package/surea11y.browser.js +37774 -0
- package/bin/core.js +0 -348
- package/docs/CLI.md +0 -75
- package/src/catalogs/composites.wcag.js +0 -490
- package/src/checks/rules-and-tags.full.csv +0 -19
- package/src/checks/rules-and-tags.full.json +0 -259
- package/src/core/aria-helpers.js +0 -970
- package/src/core/contrast-helpers.js +0 -1147
- package/src/core/dom-helpers.js +0 -4235
- package/src/core/dom-runner.js +0 -671
- package/src/core/frame-messaging.js +0 -210
- package/src/core/frame-scan.js +0 -178
- package/src/core/rollup-composites.js +0 -135
- package/src/core/rule-meta.js +0 -159
- package/src/coverage/wcag-facets.js +0 -1079
- package/src/coverage/wcag-version-map.js +0 -84
- package/src/i18n/en.js +0 -923
- package/src/i18n/fr.js +0 -844
- package/src/policy/contracts.js +0 -18
- package/src/policy/resolvePolicy.js +0 -55
- package/src/policy/schemas/engine-options.schema.json +0 -103
- package/src/policy/schemas/policy-contract.schema.json +0 -40
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
const id = 'progressbar-name-present';
|
|
@@ -13,7 +15,13 @@ const meta = {
|
|
|
13
15
|
tags: ['wcag2a', 'wcag111', 'nontext', 'atomic', 'automatic', 'name'],
|
|
14
16
|
wcagSc: ['1.1.1'],
|
|
15
17
|
normativeMappings: [
|
|
16
|
-
{
|
|
18
|
+
{
|
|
19
|
+
standard: 'WCAG',
|
|
20
|
+
version: '2.2',
|
|
21
|
+
requirement: '1.1.1',
|
|
22
|
+
title: 'Non-text Content',
|
|
23
|
+
conformanceLevel: 'A'
|
|
24
|
+
}
|
|
17
25
|
],
|
|
18
26
|
defaultSeverity: 'serious',
|
|
19
27
|
category: 'perceivable',
|
|
@@ -24,36 +32,22 @@ const meta = {
|
|
|
24
32
|
|
|
25
33
|
function runInPage(ctx) {
|
|
26
34
|
const { document, helpers, rule } = ctx;
|
|
27
|
-
const getEligibilityInfo =
|
|
28
|
-
|
|
29
|
-
: null;
|
|
30
|
-
|
|
35
|
+
const getEligibilityInfo =
|
|
36
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
31
37
|
|
|
32
38
|
function normalizeWs(s) {
|
|
33
|
-
return String(s || '')
|
|
39
|
+
return String(s || '')
|
|
40
|
+
.replace(/\s+/g, ' ')
|
|
41
|
+
.trim();
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
function getAttr(el, name) {
|
|
37
45
|
try {
|
|
38
46
|
if (!el || !el.getAttribute) return '';
|
|
39
47
|
return normalizeWs(el.getAttribute(name));
|
|
40
|
-
} catch {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
function getConservativeSubtreeText(document, container) {
|
|
44
|
-
// "Name from content" — recurses into descendants and uses each one's
|
|
45
|
-
// own accessible name (img alt, aria-label/aria-labelledby, title) when
|
|
46
|
-
// it has one, not just literal text nodes. See getContentNameInfo's
|
|
47
|
-
// header comment in src/core/dom-helpers.js for the full rationale
|
|
48
|
-
// (this replaced a text-node-only TreeWalker that missed the common
|
|
49
|
-
// "<a><img alt='...'></a>" logo-link / "<button><img alt='...'></button>"
|
|
50
|
-
// icon-button pattern).
|
|
51
|
-
if (helpers.getContentNameInfo) {
|
|
52
|
-
const info = helpers.getContentNameInfo(container, ctx);
|
|
53
|
-
return info && info.present ? info.value : '';
|
|
48
|
+
} catch {
|
|
49
|
+
return '';
|
|
54
50
|
}
|
|
55
|
-
const t = (container && container.textContent) ? String(container.textContent) : '';
|
|
56
|
-
return t.replace(/\s+/g, ' ').trim();
|
|
57
51
|
}
|
|
58
52
|
|
|
59
53
|
function resolveAriaLabelledbyText(document, el, maxRefs) {
|
|
@@ -74,7 +68,8 @@ function runInPage(ctx) {
|
|
|
74
68
|
}
|
|
75
69
|
|
|
76
70
|
function isEligibleAcc(helpers, el, ctx) {
|
|
77
|
-
const fn =
|
|
71
|
+
const fn =
|
|
72
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
78
73
|
if (!fn) return true;
|
|
79
74
|
try {
|
|
80
75
|
const r = fn(el, ctx);
|
|
@@ -85,13 +80,13 @@ function runInPage(ctx) {
|
|
|
85
80
|
}
|
|
86
81
|
}
|
|
87
82
|
|
|
88
|
-
|
|
89
83
|
const occurrences = [];
|
|
90
84
|
let applicableCount = 0;
|
|
91
85
|
|
|
92
86
|
const selector = '[role="progressbar"]';
|
|
93
|
-
const nodes = helpers.queryAllSmart
|
|
94
|
-
|
|
87
|
+
const nodes = helpers.queryAllSmart
|
|
88
|
+
? helpers.queryAllSmart(selector)
|
|
89
|
+
: helpers.queryAll(selector);
|
|
95
90
|
|
|
96
91
|
function evaluate(el) {
|
|
97
92
|
const ariaLabel = getAttr(el, 'aria-label');
|
|
@@ -103,16 +98,12 @@ function runInPage(ctx) {
|
|
|
103
98
|
const title = getAttr(el, 'title');
|
|
104
99
|
if (title) return { ok: true, method: 'title' };
|
|
105
100
|
|
|
106
|
-
// role="progressbar" is name-from-author-only per WAI-ARIA
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
// aria-label="Loading Box" (for a completely different purpose) —
|
|
113
|
-
// falling back to subtree content picked that up and wrongly treated
|
|
114
|
-
// it as the progressbar's own name, when that reference engine correctly
|
|
115
|
-
// still flags it as unnamed.
|
|
101
|
+
// role="progressbar" is name-from-author-only per WAI-ARIA: aria-label,
|
|
102
|
+
// aria-labelledby, or title — no content-based naming method at all. It
|
|
103
|
+
// must NOT fall back to subtree content: e.g. a <ul role="progressbar">
|
|
104
|
+
// loading skeleton with no name of its own, whose nested descendants
|
|
105
|
+
// carry their own aria-label for a different purpose — falling back to
|
|
106
|
+
// subtree content would wrongly treat that as the progressbar's name.
|
|
116
107
|
return { ok: false, method: 'none' };
|
|
117
108
|
}
|
|
118
109
|
|
|
@@ -129,36 +120,50 @@ function runInPage(ctx) {
|
|
|
129
120
|
if (res.ok) continue;
|
|
130
121
|
|
|
131
122
|
const eligInfo = getEligibilityInfo
|
|
132
|
-
|
|
133
|
-
|
|
123
|
+
? (() => {
|
|
124
|
+
try {
|
|
125
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
})()
|
|
130
|
+
: null;
|
|
134
131
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
135
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
132
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
136
133
|
|
|
137
134
|
occurrences.push({
|
|
138
135
|
selector: stableSelector,
|
|
139
136
|
html,
|
|
140
137
|
summary: 'This progress bar has no accessible name.',
|
|
141
|
-
hint:
|
|
138
|
+
hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this progress bar's accessible name.",
|
|
142
139
|
i18n: {
|
|
143
140
|
summaryKey: 'progressbarNamePresent_summary_fail',
|
|
144
141
|
hintKey: 'progressbarNamePresent_hint_fail',
|
|
145
142
|
params: {}
|
|
146
143
|
},
|
|
147
144
|
data: {
|
|
148
|
-
details: {
|
|
145
|
+
details: {
|
|
146
|
+
reasonCode: 'name_missing',
|
|
147
|
+
controlType: 'progressbar',
|
|
148
|
+
methodTried: res.method
|
|
149
|
+
},
|
|
149
150
|
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
150
151
|
}
|
|
151
152
|
});
|
|
152
153
|
}
|
|
153
154
|
|
|
154
|
-
|
|
155
155
|
if (applicableCount === 0) {
|
|
156
156
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
157
157
|
}
|
|
158
158
|
if (occurrences.length) {
|
|
159
|
-
return {
|
|
159
|
+
return {
|
|
160
|
+
ruleId: rule.ruleId,
|
|
161
|
+
outcome: 'fail',
|
|
162
|
+
severity: rule.defaultSeverity || 'minor',
|
|
163
|
+
occurrences
|
|
164
|
+
};
|
|
160
165
|
}
|
|
161
166
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
162
167
|
}
|
|
163
168
|
|
|
164
|
-
module.exports = { id, meta, runInPage };
|
|
169
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -14,193 +16,243 @@
|
|
|
14
16
|
* Each applicable element with role="img" has an accessible text alternative:
|
|
15
17
|
* - aria-label with a non-empty value; OR
|
|
16
18
|
* - aria-labelledby referencing at least one existing element that contributes non-empty text; OR
|
|
17
|
-
* - a non-empty title attribute (last-resort accessible-name source per HTML-AAM
|
|
18
|
-
* also accepted by a widely-used reference engine's equivalent role-img-alt rule).
|
|
19
|
+
* - a non-empty title attribute (last-resort accessible-name source per HTML-AAM).
|
|
19
20
|
*/
|
|
20
21
|
|
|
21
22
|
const id = 'role-img-text-alternative-present';
|
|
22
23
|
|
|
23
24
|
const meta = {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
25
|
+
title: '[role="img"] must have an accessible text alternative',
|
|
26
|
+
description:
|
|
27
|
+
'Checks that elements with role="img" provide an accessible text alternative via aria-label, aria-labelledby, or a title attribute.',
|
|
28
|
+
i18n: {
|
|
29
|
+
titleKey: 'roleImg_textAlternativePresent_title',
|
|
30
|
+
descriptionKey: 'roleImg_textAlternativePresent_description'
|
|
31
|
+
},
|
|
32
|
+
helpUrl: null,
|
|
33
|
+
tags: ['wcag2a', 'wcag111', 'nontext', 'images', 'aria', 'atomic', 'automatic'],
|
|
34
|
+
wcagSc: ['1.1.1'],
|
|
35
|
+
normativeMappings: [
|
|
36
|
+
{
|
|
37
|
+
standard: 'WCAG',
|
|
38
|
+
version: '2.2',
|
|
39
|
+
requirement: '1.1.1',
|
|
40
|
+
title: 'Non-text Content',
|
|
41
|
+
conformanceLevel: 'A'
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
defaultSeverity: 'serious',
|
|
45
|
+
category: 'perceivable',
|
|
46
|
+
type: 'automatic',
|
|
47
|
+
defaultConfidence: 'high',
|
|
48
|
+
coverage: {
|
|
49
|
+
facetsBySc: {
|
|
50
|
+
'1.1.1': ['role-img-text-alternative-present']
|
|
45
51
|
}
|
|
52
|
+
}
|
|
46
53
|
};
|
|
47
54
|
|
|
48
55
|
function runInPage(ctx) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
const { root, helpers, rule } = ctx;
|
|
57
|
+
const safeRoot = root || document;
|
|
58
|
+
|
|
59
|
+
const queryAllSmart =
|
|
60
|
+
helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
61
|
+
const queryAll =
|
|
62
|
+
helpers && typeof helpers.queryAll === 'function'
|
|
63
|
+
? helpers.queryAll
|
|
64
|
+
: (sel) => {
|
|
65
|
+
try {
|
|
66
|
+
return safeRoot && safeRoot.querySelectorAll
|
|
67
|
+
? Array.from(safeRoot.querySelectorAll(sel))
|
|
68
|
+
: [];
|
|
69
|
+
} catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
58
72
|
};
|
|
59
73
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
: null;
|
|
74
|
+
const getEligibilityInfo =
|
|
75
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
63
76
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
const trim = (v) => {
|
|
78
|
+
try {
|
|
79
|
+
return String(v == null ? '' : v)
|
|
80
|
+
.replace(/\s+/g, ' ')
|
|
81
|
+
.trim();
|
|
82
|
+
} catch {
|
|
83
|
+
return '';
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const imgElements = (() => {
|
|
88
|
+
// do not consider element "img" because it has its own rule
|
|
89
|
+
const sel = '[role="img" i]:not(img)';
|
|
90
|
+
try {
|
|
91
|
+
return Array.from((queryAllSmart ? queryAllSmart(sel) : queryAll(sel)) || []);
|
|
92
|
+
} catch {
|
|
93
|
+
return queryAll(sel);
|
|
78
94
|
}
|
|
95
|
+
})();
|
|
79
96
|
|
|
80
|
-
|
|
81
|
-
|
|
97
|
+
if (!imgElements.length) {
|
|
98
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
99
|
+
}
|
|
82
100
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
: null;
|
|
101
|
+
const occurrences = [];
|
|
102
|
+
let applicableCount = 0;
|
|
86
103
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
: null;
|
|
104
|
+
const isAccTreeEligible =
|
|
105
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
90
106
|
|
|
91
|
-
|
|
92
|
-
|
|
107
|
+
const getAccessibleNameInfo =
|
|
108
|
+
helpers && typeof helpers.getAccessibleNameInfo === 'function'
|
|
109
|
+
? helpers.getAccessibleNameInfo
|
|
110
|
+
: null;
|
|
93
111
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const elig = (() => {
|
|
97
|
-
try { return isAccTreeEligible(el, ctx); } catch { return { eligible: true, reasons: [] }; }
|
|
98
|
-
})();
|
|
99
|
-
if (elig && elig.eligible === false) continue;
|
|
100
|
-
}
|
|
112
|
+
for (const el of imgElements) {
|
|
113
|
+
if (!el || !el.getAttribute) continue;
|
|
101
114
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const ariaLabelledbyRaw = (() => { try { return el.getAttribute('aria-labelledby'); } catch { return null; } })();
|
|
111
|
-
const ariaLabelledby = trim(ariaLabelledbyRaw);
|
|
112
|
-
|
|
113
|
-
const hasAriaLabelAttr = ariaLabelRaw !== null;
|
|
114
|
-
const hasAriaLabelledbyAttr = ariaLabelledbyRaw !== null;
|
|
115
|
-
|
|
116
|
-
const hasValidAriaLabel = hasAriaLabelAttr && ariaLabel.length > 0;
|
|
117
|
-
const hasValidAriaLabelledbyAttr = hasAriaLabelledbyAttr && ariaLabelledby.length > 0;
|
|
118
|
-
|
|
119
|
-
// Last-resort naming mechanism per HTML-AAM (also accepted by
|
|
120
|
-
// a widely-used reference engine's role-img-alt): a non-empty title attribute.
|
|
121
|
-
const titleRaw = (() => { try { return el.getAttribute('title'); } catch { return null; } })();
|
|
122
|
-
const hasValidTitle = titleRaw !== null && trim(titleRaw).length > 0;
|
|
123
|
-
|
|
124
|
-
let nameInfo = null;
|
|
125
|
-
|
|
126
|
-
// Fast outcomes first (no helper needed)
|
|
127
|
-
let reasonCode = '';
|
|
128
|
-
let hasName = false;
|
|
129
|
-
|
|
130
|
-
if (!hasAriaLabelAttr && !hasAriaLabelledbyAttr) {
|
|
131
|
-
if (hasValidTitle) {
|
|
132
|
-
hasName = true;
|
|
133
|
-
} else {
|
|
134
|
-
reasonCode = 'missingTextAlternative';
|
|
135
|
-
}
|
|
136
|
-
} else if (hasAriaLabelAttr && !hasValidAriaLabel) {
|
|
137
|
-
if (hasValidTitle) {
|
|
138
|
-
hasName = true;
|
|
139
|
-
} else {
|
|
140
|
-
reasonCode = 'emptyAriaLabel';
|
|
141
|
-
}
|
|
142
|
-
} else if (hasAriaLabelledbyAttr && !hasValidAriaLabelledbyAttr) {
|
|
143
|
-
if (hasValidTitle) {
|
|
144
|
-
hasName = true;
|
|
145
|
-
} else {
|
|
146
|
-
reasonCode = 'emptyAriaLabelledby';
|
|
147
|
-
}
|
|
148
|
-
} else {
|
|
149
|
-
// Mechanism present + non-empty. Optionally validate resolution via helper.
|
|
150
|
-
if (getAccessibleNameInfo) {
|
|
151
|
-
nameInfo = (() => { try { return getAccessibleNameInfo(el, ctx); } catch { return null; } })();
|
|
152
|
-
const helperSaysHasName = !!(nameInfo && nameInfo.present && trim(nameInfo.value));
|
|
153
|
-
if (helperSaysHasName) {
|
|
154
|
-
hasName = true;
|
|
155
|
-
} else {
|
|
156
|
-
reasonCode = 'nameNotResolved';
|
|
157
|
-
}
|
|
158
|
-
} else {
|
|
159
|
-
// Without helper, accept non-empty aria-label/labelledby as sufficient.
|
|
160
|
-
hasName = true;
|
|
161
|
-
}
|
|
115
|
+
// Applicability: eligible in the acc tree (with helper exceptions).
|
|
116
|
+
if (isAccTreeEligible) {
|
|
117
|
+
const elig = (() => {
|
|
118
|
+
try {
|
|
119
|
+
return isAccTreeEligible(el, ctx);
|
|
120
|
+
} catch {
|
|
121
|
+
return { eligible: true, reasons: [] };
|
|
162
122
|
}
|
|
123
|
+
})();
|
|
124
|
+
if (elig && elig.eligible === false) continue;
|
|
125
|
+
}
|
|
163
126
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
127
|
+
applicableCount += 1;
|
|
128
|
+
|
|
129
|
+
// Expectation: aria-label OR aria-labelledby. We use helper name-info when available,
|
|
130
|
+
// but we also validate the source to keep this rule scoped/deterministic.
|
|
131
|
+
|
|
132
|
+
const ariaLabelRaw = (() => {
|
|
133
|
+
try {
|
|
134
|
+
return el.getAttribute('aria-label');
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
})();
|
|
139
|
+
const ariaLabel = trim(ariaLabelRaw);
|
|
140
|
+
|
|
141
|
+
const ariaLabelledbyRaw = (() => {
|
|
142
|
+
try {
|
|
143
|
+
return el.getAttribute('aria-labelledby');
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
})();
|
|
148
|
+
const ariaLabelledby = trim(ariaLabelledbyRaw);
|
|
149
|
+
|
|
150
|
+
const hasAriaLabelAttr = ariaLabelRaw !== null;
|
|
151
|
+
const hasAriaLabelledbyAttr = ariaLabelledbyRaw !== null;
|
|
187
152
|
|
|
188
|
-
|
|
189
|
-
|
|
153
|
+
const hasValidAriaLabel = hasAriaLabelAttr && ariaLabel.length > 0;
|
|
154
|
+
const hasValidAriaLabelledbyAttr = hasAriaLabelledbyAttr && ariaLabelledby.length > 0;
|
|
155
|
+
|
|
156
|
+
// Last-resort naming mechanism per HTML-AAM: a non-empty title attribute.
|
|
157
|
+
const titleRaw = (() => {
|
|
158
|
+
try {
|
|
159
|
+
return el.getAttribute('title');
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
})();
|
|
164
|
+
const hasValidTitle = titleRaw !== null && trim(titleRaw).length > 0;
|
|
165
|
+
|
|
166
|
+
let nameInfo = null;
|
|
167
|
+
|
|
168
|
+
// Fast outcomes first (no helper needed)
|
|
169
|
+
let reasonCode = '';
|
|
170
|
+
let hasName = false;
|
|
171
|
+
|
|
172
|
+
if (!hasAriaLabelAttr && !hasAriaLabelledbyAttr) {
|
|
173
|
+
if (hasValidTitle) {
|
|
174
|
+
hasName = true;
|
|
175
|
+
} else {
|
|
176
|
+
reasonCode = 'missingTextAlternative';
|
|
177
|
+
}
|
|
178
|
+
} else if (hasAriaLabelAttr && !hasValidAriaLabel) {
|
|
179
|
+
if (hasValidTitle) {
|
|
180
|
+
hasName = true;
|
|
181
|
+
} else {
|
|
182
|
+
reasonCode = 'emptyAriaLabel';
|
|
183
|
+
}
|
|
184
|
+
} else if (hasAriaLabelledbyAttr && !hasValidAriaLabelledbyAttr) {
|
|
185
|
+
if (hasValidTitle) {
|
|
186
|
+
hasName = true;
|
|
187
|
+
} else {
|
|
188
|
+
reasonCode = 'emptyAriaLabelledby';
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
// Mechanism present + non-empty. Optionally validate resolution via helper.
|
|
192
|
+
if (getAccessibleNameInfo) {
|
|
193
|
+
nameInfo = (() => {
|
|
194
|
+
try {
|
|
195
|
+
return getAccessibleNameInfo(el, ctx);
|
|
196
|
+
} catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
})();
|
|
200
|
+
const helperSaysHasName = !!(nameInfo && nameInfo.present && trim(nameInfo.value));
|
|
201
|
+
if (helperSaysHasName) {
|
|
202
|
+
hasName = true;
|
|
190
203
|
} else {
|
|
191
|
-
|
|
204
|
+
reasonCode = 'nameNotResolved';
|
|
192
205
|
}
|
|
206
|
+
} else {
|
|
207
|
+
// Without helper, accept non-empty aria-label/labelledby as sufficient.
|
|
208
|
+
hasName = true;
|
|
209
|
+
}
|
|
193
210
|
}
|
|
194
211
|
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
}
|
|
212
|
+
if (hasName) continue;
|
|
213
|
+
|
|
214
|
+
const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
|
|
215
|
+
|
|
216
|
+
const baseOccurrence = {
|
|
217
|
+
summary: 'Missing text alternative on element with role="img".',
|
|
218
|
+
hint: 'Provide aria-label or aria-labelledby (referencing non-empty text) to give this image a text alternative.',
|
|
219
|
+
i18n: {
|
|
220
|
+
summaryKey: 'roleImg_textAlternativePresent_summary_fail',
|
|
221
|
+
hintKey: 'roleImg_textAlternativePresent_hint_fail',
|
|
222
|
+
params: { role: 'img' }
|
|
223
|
+
},
|
|
224
|
+
data: {
|
|
225
|
+
details: {
|
|
226
|
+
reasonCode,
|
|
227
|
+
ariaLabel: ariaLabelRaw === null ? null : ariaLabel,
|
|
228
|
+
ariaLabelledby: ariaLabelledbyRaw === null ? null : ariaLabelledby,
|
|
229
|
+
accessibleNameInfo: nameInfo || null
|
|
230
|
+
},
|
|
231
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
232
|
+
}
|
|
233
|
+
};
|
|
198
234
|
|
|
199
|
-
if (
|
|
200
|
-
|
|
235
|
+
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
236
|
+
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
237
|
+
} else {
|
|
238
|
+
occurrences.push({ selector: '', html: '', ...baseOccurrence });
|
|
201
239
|
}
|
|
202
|
-
|
|
203
|
-
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (applicableCount === 0) {
|
|
243
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!occurrences.length) {
|
|
247
|
+
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
ruleId: rule.ruleId,
|
|
252
|
+
outcome: 'fail',
|
|
253
|
+
severity: rule.defaultSeverity || 'minor',
|
|
254
|
+
occurrences
|
|
255
|
+
};
|
|
204
256
|
}
|
|
205
257
|
|
|
206
258
|
module.exports = { id, meta, runInPage };
|