@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
|
/**
|
|
@@ -11,20 +13,26 @@
|
|
|
11
13
|
* Human review is required to confirm that the provided text alternative is accurate and appropriate.
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
|
-
const id =
|
|
16
|
+
const id = 'img-alt-quality';
|
|
15
17
|
|
|
16
18
|
const meta = {
|
|
17
|
-
title:
|
|
18
|
-
description:
|
|
19
|
+
title: '<img> alt text must be appropriate (manual review)',
|
|
20
|
+
description: 'Flags <img> elements with non-empty alt text for human review of appropriateness.',
|
|
19
21
|
i18n: {
|
|
20
|
-
titleKey:
|
|
21
|
-
descriptionKey:
|
|
22
|
+
titleKey: 'img_altQuality_title',
|
|
23
|
+
descriptionKey: 'img_altQuality_description'
|
|
22
24
|
},
|
|
23
25
|
helpUrl: null,
|
|
24
|
-
tags: [
|
|
26
|
+
tags: ['wcag2a', 'wcag111', 'nontext', 'images', 'manual', 'atomic'],
|
|
25
27
|
wcagSc: ['1.1.1'],
|
|
26
28
|
normativeMappings: [
|
|
27
|
-
{
|
|
29
|
+
{
|
|
30
|
+
standard: 'WCAG',
|
|
31
|
+
version: '2.2',
|
|
32
|
+
requirement: '1.1.1',
|
|
33
|
+
title: 'Non-text Content',
|
|
34
|
+
conformanceLevel: 'A'
|
|
35
|
+
}
|
|
28
36
|
],
|
|
29
37
|
defaultSeverity: 'minor',
|
|
30
38
|
category: 'perceivable',
|
|
@@ -38,145 +46,188 @@ const meta = {
|
|
|
38
46
|
};
|
|
39
47
|
|
|
40
48
|
function runInPage(ctx) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
const { document, root, helpers, rule } = ctx;
|
|
50
|
+
const safeRoot = root || document;
|
|
51
|
+
|
|
52
|
+
// Cap occurrences to keep manual “quality” checks fast on large pages.
|
|
53
|
+
// Deterministic: we keep DOM order, just stop collecting after N.
|
|
54
|
+
const MAX_OCCURRENCES = 50;
|
|
55
|
+
|
|
56
|
+
const queryAllSmart =
|
|
57
|
+
helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
58
|
+
const queryAll =
|
|
59
|
+
helpers && typeof helpers.queryAll === 'function'
|
|
60
|
+
? helpers.queryAll
|
|
61
|
+
: (sel) => {
|
|
62
|
+
try {
|
|
63
|
+
return safeRoot && safeRoot.querySelectorAll
|
|
64
|
+
? Array.from(safeRoot.querySelectorAll(sel))
|
|
65
|
+
: [];
|
|
66
|
+
} catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
54
69
|
};
|
|
55
70
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
: null;
|
|
59
|
-
|
|
60
|
-
const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
61
|
-
? helpers.isAccTreeEligible
|
|
62
|
-
: null;
|
|
63
|
-
|
|
64
|
-
const getFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function'
|
|
65
|
-
? helpers.getFocusableInfo
|
|
66
|
-
: null;
|
|
67
|
-
|
|
68
|
-
function isRolePresentationExcluded(el) {
|
|
69
|
-
const role = (() => {
|
|
70
|
-
try { return String(el.getAttribute('role') || '').trim().toLowerCase(); }
|
|
71
|
-
catch { return ''; }
|
|
72
|
-
})();
|
|
73
|
-
if (role !== 'presentation' && role !== 'none') return false;
|
|
74
|
-
|
|
75
|
-
// Exclude only when NOT focusable (mirrors img-alt-present policy)
|
|
76
|
-
let focusable = false;
|
|
77
|
-
if (getFocusableInfo) {
|
|
78
|
-
const fi = (() => { try { return getFocusableInfo(el, ctx); } catch { return null; } })();
|
|
79
|
-
focusable = !!(fi && fi.focusable);
|
|
80
|
-
} else {
|
|
81
|
-
const tabindex = el.getAttribute('tabindex');
|
|
82
|
-
focusable = tabindex != null && String(tabindex).trim() !== '' && !Number.isNaN(Number(String(tabindex).trim()));
|
|
83
|
-
}
|
|
84
|
-
return !focusable;
|
|
85
|
-
}
|
|
71
|
+
const getEligibilityInfo =
|
|
72
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
86
73
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
try { return Array.from((queryAllSmart ? queryAllSmart(selector) : queryAll(selector)) || []); }
|
|
90
|
-
catch { return queryAll(selector); }
|
|
91
|
-
})();
|
|
74
|
+
const isAccTreeEligible =
|
|
75
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
92
76
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
ruleId: rule.ruleId,
|
|
96
|
-
outcome: 'notApplicable',
|
|
97
|
-
severity: 'minor',
|
|
98
|
-
occurrences: [],
|
|
99
|
-
data: { details: { applicableCount: 0, reportedCount: 0, maxOccurrences: MAX_OCCURRENCES, truncated: false } }
|
|
100
|
-
};
|
|
101
|
-
}
|
|
77
|
+
const getFocusableInfo =
|
|
78
|
+
helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
|
|
102
79
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
80
|
+
function isRolePresentationExcluded(el) {
|
|
81
|
+
const role = (() => {
|
|
82
|
+
try {
|
|
83
|
+
return String(el.getAttribute('role') || '')
|
|
84
|
+
.trim()
|
|
85
|
+
.toLowerCase();
|
|
86
|
+
} catch {
|
|
87
|
+
return '';
|
|
88
|
+
}
|
|
89
|
+
})();
|
|
90
|
+
if (role !== 'presentation' && role !== 'none') return false;
|
|
91
|
+
|
|
92
|
+
// Exclude only when NOT focusable (mirrors img-alt-present policy)
|
|
93
|
+
let focusable;
|
|
94
|
+
if (getFocusableInfo) {
|
|
95
|
+
const fi = (() => {
|
|
96
|
+
try {
|
|
97
|
+
return getFocusableInfo(el, ctx);
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
})();
|
|
102
|
+
focusable = !!(fi && fi.focusable);
|
|
103
|
+
} else {
|
|
104
|
+
const tabindex = el.getAttribute('tabindex');
|
|
105
|
+
focusable =
|
|
106
|
+
tabindex != null &&
|
|
107
|
+
String(tabindex).trim() !== '' &&
|
|
108
|
+
!Number.isNaN(Number(String(tabindex).trim()));
|
|
109
|
+
}
|
|
110
|
+
return !focusable;
|
|
111
|
+
}
|
|
106
112
|
|
|
107
|
-
|
|
108
|
-
|
|
113
|
+
const selector = 'img[alt]:not([alt=""])';
|
|
114
|
+
const els = (() => {
|
|
115
|
+
try {
|
|
116
|
+
return Array.from((queryAllSmart ? queryAllSmart(selector) : queryAll(selector)) || []);
|
|
117
|
+
} catch {
|
|
118
|
+
return queryAll(selector);
|
|
119
|
+
}
|
|
120
|
+
})();
|
|
109
121
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
122
|
+
if (!els.length) {
|
|
123
|
+
return {
|
|
124
|
+
ruleId: rule.ruleId,
|
|
125
|
+
outcome: 'notApplicable',
|
|
126
|
+
severity: 'minor',
|
|
127
|
+
occurrences: [],
|
|
128
|
+
data: {
|
|
129
|
+
details: {
|
|
130
|
+
applicableCount: 0,
|
|
131
|
+
reportedCount: 0,
|
|
132
|
+
maxOccurrences: MAX_OCCURRENCES,
|
|
133
|
+
truncated: false
|
|
115
134
|
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
116
138
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const alt = (() => {
|
|
121
|
-
try { return String(el.getAttribute('alt') || '').trim(); } catch { return ''; }
|
|
122
|
-
})();
|
|
123
|
-
if (!alt) continue;
|
|
124
|
-
|
|
125
|
-
applicableCount += 1;
|
|
126
|
-
|
|
127
|
-
// IMPORTANT: stop doing expensive occurrence building after we hit the cap
|
|
128
|
-
if (collectedCount >= MAX_OCCURRENCES) continue;
|
|
129
|
-
|
|
130
|
-
const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
|
|
131
|
-
const baseOccurrence = {
|
|
132
|
-
summary: 'Review alt text on <img> for accuracy and appropriateness.',
|
|
133
|
-
hint: 'Ensure the alt text conveys the image’s purpose/information in context (not redundant, not filename-like).',
|
|
134
|
-
i18n: {
|
|
135
|
-
summaryKey: 'img_altQuality_summary_cantTell',
|
|
136
|
-
hintKey: 'img_altQuality_hint_cantTell',
|
|
137
|
-
params: { element: 'img' }
|
|
138
|
-
},
|
|
139
|
-
data: {
|
|
140
|
-
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
141
|
-
details: null
|
|
142
|
-
}
|
|
143
|
-
};
|
|
139
|
+
const occurrences = [];
|
|
140
|
+
let applicableCount = 0; // total applicable elements
|
|
141
|
+
let collectedCount = 0; // how many occurrences we actually reported
|
|
144
142
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
} else {
|
|
148
|
-
occurrences.push({ selector: '', html: '', ...baseOccurrence });
|
|
149
|
-
}
|
|
143
|
+
for (const el of els) {
|
|
144
|
+
if (!el || !el.getAttribute) continue;
|
|
150
145
|
|
|
151
|
-
|
|
146
|
+
if (isAccTreeEligible) {
|
|
147
|
+
const elig = (() => {
|
|
148
|
+
try {
|
|
149
|
+
return isAccTreeEligible(el, ctx);
|
|
150
|
+
} catch {
|
|
151
|
+
return { eligible: true, reasons: [] };
|
|
152
|
+
}
|
|
153
|
+
})();
|
|
154
|
+
if (elig && elig.eligible === false) continue;
|
|
152
155
|
}
|
|
153
156
|
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
157
|
+
if (isRolePresentationExcluded(el)) continue;
|
|
158
|
+
|
|
159
|
+
// Rule-specific applicability: non-empty alt
|
|
160
|
+
const alt = (() => {
|
|
161
|
+
try {
|
|
162
|
+
return String(el.getAttribute('alt') || '').trim();
|
|
163
|
+
} catch {
|
|
164
|
+
return '';
|
|
165
|
+
}
|
|
166
|
+
})();
|
|
167
|
+
if (!alt) continue;
|
|
168
|
+
|
|
169
|
+
applicableCount += 1;
|
|
170
|
+
|
|
171
|
+
// IMPORTANT: stop doing expensive occurrence building after we hit the cap
|
|
172
|
+
if (collectedCount >= MAX_OCCURRENCES) continue;
|
|
173
|
+
|
|
174
|
+
const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
|
|
175
|
+
const baseOccurrence = {
|
|
176
|
+
summary: 'Review alt text on <img> for accuracy and appropriateness.',
|
|
177
|
+
hint: 'Ensure the alt text conveys the image’s purpose/information in context (not redundant, not filename-like).',
|
|
178
|
+
i18n: {
|
|
179
|
+
summaryKey: 'img_altQuality_summary_cantTell',
|
|
180
|
+
hintKey: 'img_altQuality_hint_cantTell',
|
|
181
|
+
params: { element: 'img' }
|
|
182
|
+
},
|
|
183
|
+
data: {
|
|
184
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
185
|
+
details: null
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
190
|
+
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
191
|
+
} else {
|
|
192
|
+
occurrences.push({ selector: '', html: '', ...baseOccurrence });
|
|
162
193
|
}
|
|
163
194
|
|
|
164
|
-
|
|
195
|
+
collectedCount += 1;
|
|
196
|
+
}
|
|
165
197
|
|
|
198
|
+
if (applicableCount === 0) {
|
|
166
199
|
return {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
200
|
+
ruleId: rule.ruleId,
|
|
201
|
+
outcome: 'notApplicable',
|
|
202
|
+
severity: 'minor',
|
|
203
|
+
occurrences: [],
|
|
204
|
+
data: {
|
|
205
|
+
details: {
|
|
206
|
+
applicableCount: 0,
|
|
207
|
+
reportedCount: 0,
|
|
208
|
+
maxOccurrences: MAX_OCCURRENCES,
|
|
209
|
+
truncated: false
|
|
178
210
|
}
|
|
211
|
+
}
|
|
179
212
|
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const truncated = applicableCount > collectedCount;
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
ruleId: rule.ruleId,
|
|
219
|
+
outcome: 'cantTell',
|
|
220
|
+
severity: 'minor',
|
|
221
|
+
occurrences,
|
|
222
|
+
data: {
|
|
223
|
+
details: {
|
|
224
|
+
applicableCount,
|
|
225
|
+
reportedCount: collectedCount,
|
|
226
|
+
maxOccurrences: MAX_OCCURRENCES,
|
|
227
|
+
truncated
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
};
|
|
180
231
|
}
|
|
181
232
|
|
|
182
233
|
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
|
/**
|
|
@@ -11,20 +13,27 @@
|
|
|
11
13
|
* Human review is required to confirm that the provided text alternative is accurate and appropriate.
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
|
-
const id =
|
|
16
|
+
const id = 'input-image-alt-decorative';
|
|
15
17
|
|
|
16
18
|
const meta = {
|
|
17
|
-
title:
|
|
18
|
-
description:
|
|
19
|
+
title: '<input type="image"> with alt="" must be appropriate (manual review)',
|
|
20
|
+
description:
|
|
21
|
+
'Flags <input type="image"> elements with empty alt for human review (usually not appropriate for functional controls).',
|
|
19
22
|
i18n: {
|
|
20
|
-
titleKey:
|
|
21
|
-
descriptionKey:
|
|
23
|
+
titleKey: 'inputImage_altDecorative_title',
|
|
24
|
+
descriptionKey: 'inputImage_altDecorative_description'
|
|
22
25
|
},
|
|
23
26
|
helpUrl: null,
|
|
24
|
-
tags: [
|
|
27
|
+
tags: ['wcag2a', 'wcag111', 'forms', 'images', 'manual', 'atomic'],
|
|
25
28
|
wcagSc: ['1.1.1'],
|
|
26
29
|
normativeMappings: [
|
|
27
|
-
{
|
|
30
|
+
{
|
|
31
|
+
standard: 'WCAG',
|
|
32
|
+
version: '2.2',
|
|
33
|
+
requirement: '1.1.1',
|
|
34
|
+
title: 'Non-text Content',
|
|
35
|
+
conformanceLevel: 'A'
|
|
36
|
+
}
|
|
28
37
|
],
|
|
29
38
|
defaultSeverity: 'minor',
|
|
30
39
|
category: 'perceivable',
|
|
@@ -38,107 +47,133 @@ const meta = {
|
|
|
38
47
|
};
|
|
39
48
|
|
|
40
49
|
function runInPage(ctx) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
const { document, root, helpers, rule } = ctx;
|
|
51
|
+
const safeRoot = root || document;
|
|
52
|
+
|
|
53
|
+
const queryAllSmart =
|
|
54
|
+
helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
55
|
+
const queryAll =
|
|
56
|
+
helpers && typeof helpers.queryAll === 'function'
|
|
57
|
+
? helpers.queryAll
|
|
58
|
+
: (sel) => {
|
|
59
|
+
try {
|
|
60
|
+
return safeRoot && safeRoot.querySelectorAll
|
|
61
|
+
? Array.from(safeRoot.querySelectorAll(sel))
|
|
62
|
+
: [];
|
|
63
|
+
} catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
51
66
|
};
|
|
52
67
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
: null;
|
|
56
|
-
|
|
57
|
-
const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
58
|
-
? helpers.isAccTreeEligible
|
|
59
|
-
: null;
|
|
60
|
-
|
|
61
|
-
const getFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function'
|
|
62
|
-
? helpers.getFocusableInfo
|
|
63
|
-
: null;
|
|
64
|
-
|
|
65
|
-
function isRolePresentationExcluded(el) {
|
|
66
|
-
const role = (() => {
|
|
67
|
-
try { return String(el.getAttribute('role') || '').trim().toLowerCase(); }
|
|
68
|
-
catch { return ''; }
|
|
69
|
-
})();
|
|
70
|
-
if (role !== 'presentation' && role !== 'none') return false;
|
|
71
|
-
|
|
72
|
-
// Exclude only when NOT focusable (mirrors img-alt-present policy)
|
|
73
|
-
let focusable = false;
|
|
74
|
-
if (getFocusableInfo) {
|
|
75
|
-
const fi = (() => { try { return getFocusableInfo(el, ctx); } catch { return null; } })();
|
|
76
|
-
focusable = !!(fi && fi.focusable);
|
|
77
|
-
} else {
|
|
78
|
-
const tabindex = el.getAttribute('tabindex');
|
|
79
|
-
focusable = tabindex != null && String(tabindex).trim() !== '' && !Number.isNaN(Number(String(tabindex).trim()));
|
|
80
|
-
}
|
|
81
|
-
return !focusable;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const els = (() => {
|
|
86
|
-
try { return Array.from((queryAllSmart ? queryAllSmart('input[type="image"]') : queryAll('input[type="image"]')) || []); }
|
|
87
|
-
catch { return queryAll('input[type="image"]'); }
|
|
88
|
-
})();
|
|
89
|
-
|
|
90
|
-
if (!els.length) {
|
|
91
|
-
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
92
|
-
}
|
|
68
|
+
const getEligibilityInfo =
|
|
69
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
93
70
|
|
|
94
|
-
|
|
95
|
-
|
|
71
|
+
const isAccTreeEligible =
|
|
72
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
96
73
|
|
|
97
|
-
|
|
98
|
-
|
|
74
|
+
const getFocusableInfo =
|
|
75
|
+
helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
|
|
99
76
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
77
|
+
function isRolePresentationExcluded(el) {
|
|
78
|
+
const role = (() => {
|
|
79
|
+
try {
|
|
80
|
+
return String(el.getAttribute('role') || '')
|
|
81
|
+
.trim()
|
|
82
|
+
.toLowerCase();
|
|
83
|
+
} catch {
|
|
84
|
+
return '';
|
|
85
|
+
}
|
|
86
|
+
})();
|
|
87
|
+
if (role !== 'presentation' && role !== 'none') return false;
|
|
88
|
+
|
|
89
|
+
// Exclude only when NOT focusable (mirrors img-alt-present policy)
|
|
90
|
+
let focusable;
|
|
91
|
+
if (getFocusableInfo) {
|
|
92
|
+
const fi = (() => {
|
|
93
|
+
try {
|
|
94
|
+
return getFocusableInfo(el, ctx);
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
105
97
|
}
|
|
98
|
+
})();
|
|
99
|
+
focusable = !!(fi && fi.focusable);
|
|
100
|
+
} else {
|
|
101
|
+
const tabindex = el.getAttribute('tabindex');
|
|
102
|
+
focusable =
|
|
103
|
+
tabindex != null &&
|
|
104
|
+
String(tabindex).trim() !== '' &&
|
|
105
|
+
!Number.isNaN(Number(String(tabindex).trim()));
|
|
106
|
+
}
|
|
107
|
+
return !focusable;
|
|
108
|
+
}
|
|
106
109
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
const els = (() => {
|
|
111
|
+
try {
|
|
112
|
+
return Array.from(
|
|
113
|
+
(queryAllSmart ? queryAllSmart('input[type="image"]') : queryAll('input[type="image"]')) ||
|
|
114
|
+
[]
|
|
115
|
+
);
|
|
116
|
+
} catch {
|
|
117
|
+
return queryAll('input[type="image"]');
|
|
118
|
+
}
|
|
119
|
+
})();
|
|
111
120
|
|
|
112
|
-
|
|
121
|
+
if (!els.length) {
|
|
122
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
123
|
+
}
|
|
113
124
|
|
|
114
|
-
|
|
125
|
+
const occurrences = [];
|
|
126
|
+
let applicableCount = 0;
|
|
115
127
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
hint: 'This control is typically functional. Confirm it has an equivalent accessible name elsewhere, or provide meaningful alt text.',
|
|
119
|
-
i18n: {
|
|
120
|
-
summaryKey: 'inputImage_altDecorative_summary_cantTell',
|
|
121
|
-
hintKey: 'inputImage_altDecorative_hint_cantTell',
|
|
122
|
-
params: { element: 'input[type=image]' }
|
|
123
|
-
},
|
|
124
|
-
data: {
|
|
125
|
-
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
126
|
-
details: null
|
|
127
|
-
}
|
|
128
|
-
};
|
|
128
|
+
for (const el of els) {
|
|
129
|
+
if (!el || !el.getAttribute) continue;
|
|
129
130
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
if (isAccTreeEligible) {
|
|
132
|
+
const elig = (() => {
|
|
133
|
+
try {
|
|
134
|
+
return isAccTreeEligible(el, ctx);
|
|
135
|
+
} catch {
|
|
136
|
+
return { eligible: true, reasons: [] };
|
|
134
137
|
}
|
|
138
|
+
})();
|
|
139
|
+
if (elig && elig.eligible === false) continue;
|
|
135
140
|
}
|
|
136
141
|
|
|
137
|
-
if (
|
|
138
|
-
|
|
142
|
+
if (isRolePresentationExcluded(el)) continue;
|
|
143
|
+
|
|
144
|
+
// Rule-specific applicability (only elements that already have a text alternative mechanism)
|
|
145
|
+
if (!(el.getAttribute('alt') != null && String(el.getAttribute('alt')).trim() === '')) continue;
|
|
146
|
+
|
|
147
|
+
applicableCount += 1;
|
|
148
|
+
|
|
149
|
+
const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
|
|
150
|
+
|
|
151
|
+
const baseOccurrence = {
|
|
152
|
+
summary: 'Review <input type="image"> with alt="".',
|
|
153
|
+
hint: 'This control is typically functional. Confirm it has an equivalent accessible name elsewhere, or provide meaningful alt text.',
|
|
154
|
+
i18n: {
|
|
155
|
+
summaryKey: 'inputImage_altDecorative_summary_cantTell',
|
|
156
|
+
hintKey: 'inputImage_altDecorative_hint_cantTell',
|
|
157
|
+
params: { element: 'input[type=image]' }
|
|
158
|
+
},
|
|
159
|
+
data: {
|
|
160
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
161
|
+
details: null
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
166
|
+
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
167
|
+
} else {
|
|
168
|
+
occurrences.push({ selector: '', html: '', ...baseOccurrence });
|
|
139
169
|
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (applicableCount === 0) {
|
|
173
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
174
|
+
}
|
|
140
175
|
|
|
141
|
-
|
|
176
|
+
return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
|
|
142
177
|
}
|
|
143
178
|
|
|
144
179
|
module.exports = { id, meta, runInPage };
|