@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
|
/**
|
|
@@ -18,7 +20,7 @@
|
|
|
18
20
|
* a frame can have a non-empty title while still failing uniqueness.
|
|
19
21
|
* - Compares the title ATTRIBUTE specifically, not the full computed
|
|
20
22
|
* accessible name (aria-label could legitimately differ in wording even
|
|
21
|
-
* when title happens to collide)
|
|
23
|
+
* when title happens to collide).
|
|
22
24
|
* - Not rule-gated on isAccTreeEligible: duplicate titles are a static
|
|
23
25
|
* markup property. Engine-level hidden-subtree filtering still applies
|
|
24
26
|
* unless engineOptions.includeHiddenElements is true.
|
|
@@ -28,7 +30,8 @@ const id = 'iframe-title-unique';
|
|
|
28
30
|
|
|
29
31
|
const meta = {
|
|
30
32
|
title: 'Frame titles must be unique',
|
|
31
|
-
description:
|
|
33
|
+
description:
|
|
34
|
+
'Checks that no two <iframe>/<frame> elements in scope share the same title attribute value.',
|
|
32
35
|
i18n: {
|
|
33
36
|
titleKey: 'iframeTitleUnique_title',
|
|
34
37
|
descriptionKey: 'iframeTitleUnique_description'
|
|
@@ -37,7 +40,13 @@ const meta = {
|
|
|
37
40
|
tags: ['wcag2a', 'wcag412', 'structure', 'atomic', 'automatic', 'name', 'iframe'],
|
|
38
41
|
wcagSc: ['4.1.2'],
|
|
39
42
|
normativeMappings: [
|
|
40
|
-
{
|
|
43
|
+
{
|
|
44
|
+
standard: 'WCAG',
|
|
45
|
+
version: '2.2',
|
|
46
|
+
requirement: '4.1.2',
|
|
47
|
+
title: 'Name, Role, Value',
|
|
48
|
+
conformanceLevel: 'A'
|
|
49
|
+
}
|
|
41
50
|
],
|
|
42
51
|
defaultSeverity: 'moderate',
|
|
43
52
|
category: 'robust',
|
|
@@ -47,9 +56,11 @@ const meta = {
|
|
|
47
56
|
};
|
|
48
57
|
|
|
49
58
|
function runInPage(ctx) {
|
|
50
|
-
const {
|
|
59
|
+
const { helpers, rule } = ctx;
|
|
51
60
|
|
|
52
|
-
const nodes = helpers.queryAllSmart
|
|
61
|
+
const nodes = helpers.queryAllSmart
|
|
62
|
+
? helpers.queryAllSmart('iframe, frame')
|
|
63
|
+
: helpers.queryAll('iframe, frame');
|
|
53
64
|
|
|
54
65
|
const groups = new Map(); // trimmed title -> elements[]
|
|
55
66
|
let applicableCount = 0;
|
|
@@ -79,7 +90,9 @@ function runInPage(ctx) {
|
|
|
79
90
|
for (const el of els) {
|
|
80
91
|
const tag = el.tagName.toLowerCase();
|
|
81
92
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
82
|
-
const html = helpers.getOuterHtmlSnippet
|
|
93
|
+
const html = helpers.getOuterHtmlSnippet
|
|
94
|
+
? helpers.getOuterHtmlSnippet(el)
|
|
95
|
+
: el.outerHTML || '';
|
|
83
96
|
|
|
84
97
|
occurrences.push({
|
|
85
98
|
selector: stableSelector,
|
|
@@ -92,16 +105,26 @@ function runInPage(ctx) {
|
|
|
92
105
|
params: { element: tag, title }
|
|
93
106
|
},
|
|
94
107
|
data: {
|
|
95
|
-
details: {
|
|
108
|
+
details: {
|
|
109
|
+
reasonCode: 'IFRAME_TITLE_DUPLICATE',
|
|
110
|
+
element: tag,
|
|
111
|
+
title,
|
|
112
|
+
duplicateCount: els.length
|
|
113
|
+
}
|
|
96
114
|
}
|
|
97
115
|
});
|
|
98
116
|
}
|
|
99
117
|
}
|
|
100
118
|
|
|
101
119
|
if (occurrences.length) {
|
|
102
|
-
return {
|
|
120
|
+
return {
|
|
121
|
+
ruleId: rule.ruleId,
|
|
122
|
+
outcome: 'fail',
|
|
123
|
+
severity: rule.defaultSeverity || 'moderate',
|
|
124
|
+
occurrences
|
|
125
|
+
};
|
|
103
126
|
}
|
|
104
127
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
105
128
|
}
|
|
106
129
|
|
|
107
|
-
module.exports = { id, meta, runInPage };
|
|
130
|
+
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
|
/**
|
|
@@ -20,7 +22,8 @@ const id = 'img-alt-present';
|
|
|
20
22
|
|
|
21
23
|
const meta = {
|
|
22
24
|
title: '<img> must have an alt attribute',
|
|
23
|
-
description:
|
|
25
|
+
description:
|
|
26
|
+
'Checks that <img> elements provide an alt attribute to support a text alternative mechanism.',
|
|
24
27
|
i18n: {
|
|
25
28
|
titleKey: 'img_altPresent_title',
|
|
26
29
|
descriptionKey: 'img_altPresent_description'
|
|
@@ -29,7 +32,13 @@ const meta = {
|
|
|
29
32
|
tags: ['wcag2a', 'wcag111', 'nontext', 'images', 'atomic', 'automatic'],
|
|
30
33
|
wcagSc: ['1.1.1'],
|
|
31
34
|
normativeMappings: [
|
|
32
|
-
{
|
|
35
|
+
{
|
|
36
|
+
standard: 'WCAG',
|
|
37
|
+
version: '2.2',
|
|
38
|
+
requirement: '1.1.1',
|
|
39
|
+
title: 'Non-text Content',
|
|
40
|
+
conformanceLevel: 'A'
|
|
41
|
+
}
|
|
33
42
|
],
|
|
34
43
|
defaultSeverity: 'serious',
|
|
35
44
|
category: 'perceivable',
|
|
@@ -46,12 +55,17 @@ function runInPage(ctx) {
|
|
|
46
55
|
const { document, root, helpers, rule } = ctx;
|
|
47
56
|
const safeRoot = root || document;
|
|
48
57
|
|
|
49
|
-
const queryAllSmart =
|
|
50
|
-
|
|
58
|
+
const queryAllSmart =
|
|
59
|
+
helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
60
|
+
const getEligibilityInfo =
|
|
61
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
51
62
|
|
|
52
|
-
const isAccTreeEligible =
|
|
53
|
-
|
|
54
|
-
const
|
|
63
|
+
const isAccTreeEligible =
|
|
64
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
65
|
+
const getFocusableInfo =
|
|
66
|
+
helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
|
|
67
|
+
const getAriaNameInfo =
|
|
68
|
+
helpers && typeof helpers.getAriaNameInfo === 'function' ? helpers.getAriaNameInfo : null;
|
|
55
69
|
|
|
56
70
|
// Prefer tagName collection when available (cheap), otherwise fall back.
|
|
57
71
|
function getImgsCollection() {
|
|
@@ -73,7 +87,8 @@ function runInPage(ctx) {
|
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
try {
|
|
76
|
-
if (safeRoot && typeof safeRoot.querySelectorAll === 'function')
|
|
90
|
+
if (safeRoot && typeof safeRoot.querySelectorAll === 'function')
|
|
91
|
+
return safeRoot.querySelectorAll('img');
|
|
77
92
|
} catch {
|
|
78
93
|
// fall through
|
|
79
94
|
}
|
|
@@ -94,19 +109,6 @@ function runInPage(ctx) {
|
|
|
94
109
|
// Helper-safe trim
|
|
95
110
|
const trim = (v) => (v == null ? '' : String(v)).trim();
|
|
96
111
|
|
|
97
|
-
// Deterministic tabindex fallback (only used if helper missing)
|
|
98
|
-
function isFocusableFallback(el) {
|
|
99
|
-
try {
|
|
100
|
-
const tabindex = el.getAttribute('tabindex');
|
|
101
|
-
if (tabindex == null) return false;
|
|
102
|
-
const s = String(tabindex).trim();
|
|
103
|
-
if (!s) return false;
|
|
104
|
-
return !Number.isNaN(Number(s));
|
|
105
|
-
} catch {
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
112
|
for (let i = 0; i < imgLen; i++) {
|
|
111
113
|
const el = imgs[i];
|
|
112
114
|
if (!el || !el.getAttribute) continue;
|
|
@@ -125,7 +127,7 @@ function runInPage(ctx) {
|
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
// Role (presentation/none) exclusion only when NOT focusable.
|
|
128
|
-
let role
|
|
130
|
+
let role;
|
|
129
131
|
try {
|
|
130
132
|
role = trim(el.getAttribute('role')).toLowerCase();
|
|
131
133
|
} catch {
|
|
@@ -133,17 +135,13 @@ function runInPage(ctx) {
|
|
|
133
135
|
}
|
|
134
136
|
|
|
135
137
|
if (role === 'presentation' || role === 'none') {
|
|
136
|
-
let focusable
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
focusable = false;
|
|
144
|
-
}
|
|
145
|
-
} else {
|
|
146
|
-
focusable = isFocusableFallback(el);
|
|
138
|
+
let focusable;
|
|
139
|
+
try {
|
|
140
|
+
const fi = getFocusableInfo(el, ctx);
|
|
141
|
+
// Preserve existing behavior: prefer fi.focusable; fall back to fi.tabbable if present.
|
|
142
|
+
focusable = !!(fi && (fi.focusable || fi.tabbable));
|
|
143
|
+
} catch {
|
|
144
|
+
focusable = false;
|
|
147
145
|
}
|
|
148
146
|
|
|
149
147
|
if (!focusable) continue;
|
|
@@ -153,7 +151,7 @@ function runInPage(ctx) {
|
|
|
153
151
|
applicableCount += 1;
|
|
154
152
|
|
|
155
153
|
// alt attribute presence check (empty allowed)
|
|
156
|
-
let hasAlt
|
|
154
|
+
let hasAlt;
|
|
157
155
|
try {
|
|
158
156
|
hasAlt = el.getAttribute('alt') !== null;
|
|
159
157
|
} catch {
|
|
@@ -167,7 +165,7 @@ function runInPage(ctx) {
|
|
|
167
165
|
// image with a non-empty ARIA name is not missing a text alternative
|
|
168
166
|
// just because it lacks an alt attribute.
|
|
169
167
|
if (getAriaNameInfo) {
|
|
170
|
-
let ariaName
|
|
168
|
+
let ariaName;
|
|
171
169
|
try {
|
|
172
170
|
ariaName = getAriaNameInfo(el, ctx);
|
|
173
171
|
} catch {
|
|
@@ -176,19 +174,23 @@ function runInPage(ctx) {
|
|
|
176
174
|
if (ariaName && ariaName.present) continue;
|
|
177
175
|
}
|
|
178
176
|
|
|
179
|
-
// A non-empty title attribute is HTML-AAM's
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
// title as one of its "any" satisfying conditions alongside has-alt/
|
|
185
|
-
// aria-label/aria-labelledby. Found via a real page (AliExpress's
|
|
186
|
-
// logo, 2026-07-23): <img src="..." title="...">, no alt attribute
|
|
187
|
-
// at all -- a real false positive, not a missing text alternative.
|
|
177
|
+
// A non-empty title attribute is HTML-AAM's next fallback naming source
|
|
178
|
+
// for <img> once alt is entirely absent (not merely alt="", which
|
|
179
|
+
// explicitly marks decorative and stays excluded from this branch since
|
|
180
|
+
// hasAlt already short-circuited above). An `<img src="..." title="...">`
|
|
181
|
+
// with no alt attribute at all is not missing a text alternative.
|
|
188
182
|
const title = trim(el.getAttribute('title'));
|
|
189
183
|
if (title) continue;
|
|
190
184
|
|
|
191
|
-
const eligInfo = getEligibilityInfo
|
|
185
|
+
const eligInfo = getEligibilityInfo
|
|
186
|
+
? (() => {
|
|
187
|
+
try {
|
|
188
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
})()
|
|
193
|
+
: null;
|
|
192
194
|
|
|
193
195
|
const baseOccurrence = {
|
|
194
196
|
summary: 'Missing alt attribute on <img>.',
|
|
@@ -201,12 +203,7 @@ function runInPage(ctx) {
|
|
|
201
203
|
data: { visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] } }
|
|
202
204
|
};
|
|
203
205
|
|
|
204
|
-
|
|
205
|
-
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
206
|
-
} else {
|
|
207
|
-
// Never compute selector/snippet in the rule.
|
|
208
|
-
occurrences.push({ __node: el, selector: '', html: '', ...baseOccurrence });
|
|
209
|
-
}
|
|
206
|
+
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
210
207
|
}
|
|
211
208
|
|
|
212
209
|
if (applicableCount === 0) {
|
|
@@ -217,7 +214,12 @@ function runInPage(ctx) {
|
|
|
217
214
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
218
215
|
}
|
|
219
216
|
|
|
220
|
-
return {
|
|
217
|
+
return {
|
|
218
|
+
ruleId: rule.ruleId,
|
|
219
|
+
outcome: 'fail',
|
|
220
|
+
severity: rule.defaultSeverity || 'minor',
|
|
221
|
+
occurrences
|
|
222
|
+
};
|
|
221
223
|
}
|
|
222
224
|
|
|
223
225
|
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
|
/**
|
|
@@ -18,138 +20,165 @@
|
|
|
18
20
|
const id = 'input-image-alt-present';
|
|
19
21
|
|
|
20
22
|
const meta = {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
23
|
+
title: '<input type="image"> must have an alt attribute',
|
|
24
|
+
description:
|
|
25
|
+
'Checks that <input type="image"> elements provide an alt attribute to support a text alternative mechanism.',
|
|
26
|
+
i18n: {
|
|
27
|
+
titleKey: 'inputImage_altPresent_title',
|
|
28
|
+
descriptionKey: 'inputImage_altPresent_description'
|
|
29
|
+
},
|
|
30
|
+
helpUrl: null,
|
|
31
|
+
tags: ['wcag2a', 'wcag111', 'nontext', 'images', 'atomic', 'automatic'],
|
|
32
|
+
wcagSc: ['1.1.1'],
|
|
33
|
+
normativeMappings: [
|
|
34
|
+
{
|
|
35
|
+
standard: 'WCAG',
|
|
36
|
+
version: '2.2',
|
|
37
|
+
requirement: '1.1.1',
|
|
38
|
+
title: 'Non-text Content',
|
|
39
|
+
conformanceLevel: 'A'
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
defaultSeverity: 'serious',
|
|
43
|
+
category: 'perceivable',
|
|
44
|
+
type: 'automatic',
|
|
45
|
+
defaultConfidence: 'high',
|
|
46
|
+
coverage: {
|
|
47
|
+
facetsBySc: {
|
|
48
|
+
'1.1.1': ['input-image-alt-attr-present']
|
|
42
49
|
}
|
|
50
|
+
}
|
|
43
51
|
};
|
|
44
52
|
|
|
45
53
|
function runInPage(ctx) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
const { document, root, helpers, rule } = ctx;
|
|
55
|
+
const safeRoot = root || document;
|
|
56
|
+
|
|
57
|
+
const queryAllSmart =
|
|
58
|
+
helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
59
|
+
const queryAll =
|
|
60
|
+
helpers && typeof helpers.queryAll === 'function'
|
|
61
|
+
? helpers.queryAll
|
|
62
|
+
: (sel) => {
|
|
63
|
+
try {
|
|
64
|
+
return safeRoot && safeRoot.querySelectorAll
|
|
65
|
+
? Array.from(safeRoot.querySelectorAll(sel))
|
|
66
|
+
: [];
|
|
67
|
+
} catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
55
70
|
};
|
|
56
71
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
: null;
|
|
72
|
+
const getEligibilityInfo =
|
|
73
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
60
74
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
: null;
|
|
75
|
+
const isAccTreeEligible =
|
|
76
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
64
77
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
: null;
|
|
78
|
+
const getAriaNameInfo =
|
|
79
|
+
helpers && typeof helpers.getAriaNameInfo === 'function' ? helpers.getAriaNameInfo : null;
|
|
68
80
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
const inputs = (() => {
|
|
82
|
+
try {
|
|
83
|
+
return Array.from(
|
|
84
|
+
(queryAllSmart ? queryAllSmart('input[type="image"]') : queryAll('input[type="image"]')) ||
|
|
85
|
+
[]
|
|
86
|
+
);
|
|
87
|
+
} catch {
|
|
88
|
+
return queryAll('input[type="image"]');
|
|
76
89
|
}
|
|
90
|
+
})();
|
|
77
91
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
for (const el of inputs) {
|
|
82
|
-
if (!el || !el.getAttribute) continue;
|
|
92
|
+
if (!inputs.length) {
|
|
93
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
94
|
+
}
|
|
83
95
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const elig = (() => {
|
|
87
|
-
try { return isAccTreeEligible(el, ctx); } catch { return { eligible: true, reasons: [] }; }
|
|
88
|
-
})();
|
|
89
|
-
if (elig && elig.eligible === false) continue;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
applicableCount += 1;
|
|
93
|
-
|
|
94
|
-
const hasAlt = el.getAttribute('alt') !== null;
|
|
95
|
-
if (hasAlt) continue;
|
|
96
|
-
|
|
97
|
-
// aria-label / aria-labelledby is also a valid, standards-recognized
|
|
98
|
-
// text-alternative mechanism for <input type="image"> (HTML-AAM
|
|
99
|
-
// accessible name computation includes ARIA naming before falling
|
|
100
|
-
// back to alt).
|
|
101
|
-
if (getAriaNameInfo) {
|
|
102
|
-
let ariaName = null;
|
|
103
|
-
try {
|
|
104
|
-
ariaName = getAriaNameInfo(el, ctx);
|
|
105
|
-
} catch {
|
|
106
|
-
ariaName = null;
|
|
107
|
-
}
|
|
108
|
-
if (ariaName && ariaName.present) continue;
|
|
109
|
-
}
|
|
96
|
+
const occurrences = [];
|
|
97
|
+
let applicableCount = 0;
|
|
110
98
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// reference engine's equivalent input-image-alt rule (non-empty-title, same "any" list
|
|
114
|
-
// as non-empty-alt/aria-label/aria-labelledby). See img-alt-present's
|
|
115
|
-
// sibling fix (2026-07-23, AliExpress's title-only logo <img>) for
|
|
116
|
-
// the real page this was found via -- same gap, same fix, different
|
|
117
|
-
// element.
|
|
118
|
-
const titleRaw = (() => { try { return el.getAttribute('title'); } catch { return null; } })();
|
|
119
|
-
if (titleRaw !== null && String(titleRaw).trim()) continue;
|
|
120
|
-
|
|
121
|
-
const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
|
|
122
|
-
|
|
123
|
-
const baseOccurrence = {
|
|
124
|
-
summary: 'Missing alt attribute on <input type="image">.',
|
|
125
|
-
hint: 'Add an alt attribute (use alt="" only when a separate accessible name is provided).',
|
|
126
|
-
i18n: {
|
|
127
|
-
summaryKey: 'inputImage_altPresent_summary_fail',
|
|
128
|
-
hintKey: 'inputImage_altPresent_hint_fail',
|
|
129
|
-
params: { element: 'input[type=image]' }
|
|
130
|
-
},
|
|
131
|
-
data: {
|
|
132
|
-
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
133
|
-
}
|
|
134
|
-
};
|
|
99
|
+
for (const el of inputs) {
|
|
100
|
+
if (!el || !el.getAttribute) continue;
|
|
135
101
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
102
|
+
// Applicability: eligible in the acc tree (with helper exceptions).
|
|
103
|
+
if (isAccTreeEligible) {
|
|
104
|
+
const elig = (() => {
|
|
105
|
+
try {
|
|
106
|
+
return isAccTreeEligible(el, ctx);
|
|
107
|
+
} catch {
|
|
108
|
+
return { eligible: true, reasons: [] };
|
|
141
109
|
}
|
|
110
|
+
})();
|
|
111
|
+
if (elig && elig.eligible === false) continue;
|
|
142
112
|
}
|
|
143
113
|
|
|
144
|
-
|
|
145
|
-
|
|
114
|
+
applicableCount += 1;
|
|
115
|
+
|
|
116
|
+
const hasAlt = el.getAttribute('alt') !== null;
|
|
117
|
+
if (hasAlt) continue;
|
|
118
|
+
|
|
119
|
+
// aria-label / aria-labelledby is also a valid, standards-recognized
|
|
120
|
+
// text-alternative mechanism for <input type="image"> (HTML-AAM
|
|
121
|
+
// accessible name computation includes ARIA naming before falling
|
|
122
|
+
// back to alt).
|
|
123
|
+
if (getAriaNameInfo) {
|
|
124
|
+
let ariaName;
|
|
125
|
+
try {
|
|
126
|
+
ariaName = getAriaNameInfo(el, ctx);
|
|
127
|
+
} catch {
|
|
128
|
+
ariaName = null;
|
|
129
|
+
}
|
|
130
|
+
if (ariaName && ariaName.present) continue;
|
|
146
131
|
}
|
|
147
132
|
|
|
148
|
-
|
|
149
|
-
|
|
133
|
+
// A non-empty title attribute is HTML-AAM's own next fallback naming
|
|
134
|
+
// source once alt is entirely absent. Same gap img-alt-present handles
|
|
135
|
+
// for <img title="..."> with no alt.
|
|
136
|
+
const titleRaw = (() => {
|
|
137
|
+
try {
|
|
138
|
+
return el.getAttribute('title');
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
})();
|
|
143
|
+
if (titleRaw !== null && String(titleRaw).trim()) continue;
|
|
144
|
+
|
|
145
|
+
const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
|
|
146
|
+
|
|
147
|
+
const baseOccurrence = {
|
|
148
|
+
summary: 'Missing alt attribute on <input type="image">.',
|
|
149
|
+
hint: 'Add an alt attribute (use alt="" only when a separate accessible name is provided).',
|
|
150
|
+
i18n: {
|
|
151
|
+
summaryKey: 'inputImage_altPresent_summary_fail',
|
|
152
|
+
hintKey: 'inputImage_altPresent_hint_fail',
|
|
153
|
+
params: { element: 'input[type=image]' }
|
|
154
|
+
},
|
|
155
|
+
data: {
|
|
156
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
161
|
+
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
162
|
+
} else {
|
|
163
|
+
// Never compute selector/snippet in the rule.
|
|
164
|
+
occurrences.push({ selector: '', html: '', ...baseOccurrence });
|
|
150
165
|
}
|
|
151
|
-
|
|
152
|
-
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (applicableCount === 0) {
|
|
169
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!occurrences.length) {
|
|
173
|
+
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
ruleId: rule.ruleId,
|
|
178
|
+
outcome: 'fail',
|
|
179
|
+
severity: rule.defaultSeverity || 'minor',
|
|
180
|
+
occurrences
|
|
181
|
+
};
|
|
153
182
|
}
|
|
154
183
|
|
|
155
184
|
module.exports = { id, meta, runInPage };
|