@surea11y/core 1.0.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 +49 -0
- package/LICENSE +21 -0
- package/README.md +145 -0
- package/bin/core.js +244 -0
- package/docs/BINDING_AUTHORS_GUIDE.md +41 -0
- package/docs/CLI.md +49 -0
- package/docs/ENGINE_OPTIONS.md +155 -0
- package/docs/I18N.md +47 -0
- package/docs/INTEGRATION.md +156 -0
- package/docs/LIMITATIONS.md +31 -0
- package/docs/OUTPUT_SCHEMA.md +237 -0
- package/docs/POLICY.md +71 -0
- package/docs/RULE_AUTHORING.md +375 -0
- package/docs/RULE_CATALOG.md +180 -0
- package/docs/RULE_TAXONOMY.md +145 -0
- package/docs/TROUBLESHOOTING.md +48 -0
- package/docs/WCAG_CONFORMANCE.md +49 -0
- package/package.json +60 -0
- package/src/catalogs/composites.wcag.js +490 -0
- package/src/checks/automatic/area-alt-present.js +225 -0
- package/src/checks/automatic/aria-allowed-attr.js +206 -0
- package/src/checks/automatic/aria-allowed-role.js +102 -0
- package/src/checks/automatic/aria-braille-equivalent.js +139 -0
- package/src/checks/automatic/aria-conditional-attr.js +110 -0
- package/src/checks/automatic/aria-deprecated-role.js +106 -0
- package/src/checks/automatic/aria-hidden-body.js +87 -0
- package/src/checks/automatic/aria-hidden-focus.js +480 -0
- package/src/checks/automatic/aria-prohibited-attr.js +156 -0
- package/src/checks/automatic/aria-prohibited-children.js +265 -0
- package/src/checks/automatic/aria-required-attr.js +154 -0
- package/src/checks/automatic/aria-required-children.js +274 -0
- package/src/checks/automatic/aria-required-parent.js +222 -0
- package/src/checks/automatic/aria-role-name-present.js +201 -0
- package/src/checks/automatic/aria-roles-valid.js +110 -0
- package/src/checks/automatic/aria-valid-attr-value.js +123 -0
- package/src/checks/automatic/aria-valid-attr.js +109 -0
- package/src/checks/automatic/autocomplete-valid.js +134 -0
- package/src/checks/automatic/avoid-inline-spacing.js +107 -0
- package/src/checks/automatic/binary-control-name-present.js +294 -0
- package/src/checks/automatic/button-name-present.js +146 -0
- package/src/checks/automatic/bypass-blocks-present.js +162 -0
- package/src/checks/automatic/canvas-text-alternative-present.js +140 -0
- package/src/checks/automatic/combobox-name-present.js +267 -0
- package/src/checks/automatic/contrast-computable.js +378 -0
- package/src/checks/automatic/contrast-enhanced.js +517 -0
- package/src/checks/automatic/contrast-minimum.js +512 -0
- package/src/checks/automatic/css-orientation-lock.js +206 -0
- package/src/checks/automatic/definition-list-children-valid.js +148 -0
- package/src/checks/automatic/deprecated-elements-not-used.js +91 -0
- package/src/checks/automatic/dialog-name-present.js +209 -0
- package/src/checks/automatic/dlitem-parent-valid.js +100 -0
- package/src/checks/automatic/duplicate-id-aria.js +126 -0
- package/src/checks/automatic/embed-text-alternative-present.js +190 -0
- package/src/checks/automatic/form-control-programmatic-label-present.js +409 -0
- package/src/checks/automatic/form-control-single-label.js +117 -0
- package/src/checks/automatic/html-xml-lang-mismatch.js +91 -0
- package/src/checks/automatic/iframe-focusable-content.js +141 -0
- package/src/checks/automatic/iframe-name-present.js +102 -0
- package/src/checks/automatic/iframe-title-unique.js +107 -0
- package/src/checks/automatic/img-alt-present.js +223 -0
- package/src/checks/automatic/input-image-alt-present.js +155 -0
- package/src/checks/automatic/label-in-name.js +326 -0
- package/src/checks/automatic/language-page-present.js +159 -0
- package/src/checks/automatic/link-in-text-block.js +218 -0
- package/src/checks/automatic/link-name-present.js +114 -0
- package/src/checks/automatic/list-children-valid.js +152 -0
- package/src/checks/automatic/listbox-name-present.js +236 -0
- package/src/checks/automatic/listitem-parent-valid.js +118 -0
- package/src/checks/automatic/menuitem-name-present.js +201 -0
- package/src/checks/automatic/meta-refresh-no-exceptions.js +105 -0
- package/src/checks/automatic/meta-refresh-timing-absent.js +107 -0
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +118 -0
- package/src/checks/automatic/meter-name-present.js +160 -0
- package/src/checks/automatic/nested-interactive-controls-absent.js +135 -0
- package/src/checks/automatic/object-text-alternative-present.js +193 -0
- package/src/checks/automatic/option-name-present.js +157 -0
- package/src/checks/automatic/page-title-present.js +86 -0
- package/src/checks/automatic/progressbar-name-present.js +165 -0
- package/src/checks/automatic/role-img-alt-present.js +206 -0
- package/src/checks/automatic/searchbox-name-present.js +236 -0
- package/src/checks/automatic/server-side-image-map-absent.js +88 -0
- package/src/checks/automatic/slider-name-present.js +276 -0
- package/src/checks/automatic/spinbutton-name-present.js +236 -0
- package/src/checks/automatic/summary-name-present.js +153 -0
- package/src/checks/automatic/svg-image-text-alternative-present.js +220 -0
- package/src/checks/automatic/svg-text-alternative-present.js +298 -0
- package/src/checks/automatic/tab-name-present.js +200 -0
- package/src/checks/automatic/table-headers-attr-valid.js +122 -0
- package/src/checks/automatic/table-th-has-data-cells.js +117 -0
- package/src/checks/automatic/target-size-minimum.js +605 -0
- package/src/checks/automatic/td-has-header.js +151 -0
- package/src/checks/automatic/textbox-name-present.js +236 -0
- package/src/checks/automatic/tooltip-name-present.js +158 -0
- package/src/checks/automatic/treeitem-name-present.js +157 -0
- package/src/checks/automatic/valid-lang.js +100 -0
- package/src/checks/automatic/video-poster-text-alternative-present.js +193 -0
- package/src/checks/manual/accesskeys-manual.js +93 -0
- package/src/checks/manual/area-alt-decorative-manual.js +247 -0
- package/src/checks/manual/area-alt-quality-manual.js +204 -0
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +141 -0
- package/src/checks/manual/aria-text-manual.js +109 -0
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +170 -0
- package/src/checks/manual/css-hidden-focus.js +259 -0
- package/src/checks/manual/embed-text-alternative-quality-manual.js +204 -0
- package/src/checks/manual/empty-heading-manual.js +182 -0
- package/src/checks/manual/empty-table-header-manual.js +163 -0
- package/src/checks/manual/focus-order-semantics-manual.js +117 -0
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +291 -0
- package/src/checks/manual/heading-order-manual.js +130 -0
- package/src/checks/manual/identical-links-same-purpose-manual.js +142 -0
- package/src/checks/manual/image-redundant-alt-manual.js +118 -0
- package/src/checks/manual/img-alt-decorative-manual.js +148 -0
- package/src/checks/manual/img-alt-quality-manual.js +182 -0
- package/src/checks/manual/input-image-alt-decorative-manual.js +144 -0
- package/src/checks/manual/input-image-alt-quality-manual.js +144 -0
- package/src/checks/manual/label-title-only-manual.js +115 -0
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +180 -0
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +169 -0
- package/src/checks/manual/landmark-main-is-top-level-manual.js +167 -0
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +177 -0
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +169 -0
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +132 -0
- package/src/checks/manual/landmark-one-main-manual.js +151 -0
- package/src/checks/manual/landmark-unique-manual.js +252 -0
- package/src/checks/manual/link-name-quality-manual.js +143 -0
- package/src/checks/manual/media-transcript-present-manual.js +373 -0
- package/src/checks/manual/meta-viewport-large-manual.js +119 -0
- package/src/checks/manual/mouse-only-event-handlers-manual.js +134 -0
- package/src/checks/manual/no-autoplay-audio-manual.js +116 -0
- package/src/checks/manual/object-text-alternative-quality-manual.js +194 -0
- package/src/checks/manual/p-as-heading-manual.js +163 -0
- package/src/checks/manual/page-has-heading-one-manual.js +110 -0
- package/src/checks/manual/page-title-patterns-manual.js +262 -0
- package/src/checks/manual/presentation-role-conflict-manual.js +159 -0
- package/src/checks/manual/region-manual.js +183 -0
- package/src/checks/manual/scope-attr-valid-manual.js +93 -0
- package/src/checks/manual/scrollable-region-focusable-manual.js +168 -0
- package/src/checks/manual/skip-link-manual.js +150 -0
- package/src/checks/manual/svg-text-alternative-quality-manual.js +209 -0
- package/src/checks/manual/tabindex-manual.js +94 -0
- package/src/checks/manual/table-duplicate-name-manual.js +99 -0
- package/src/checks/manual/table-fake-caption-manual.js +122 -0
- package/src/checks/manual/video-caption-manual.js +118 -0
- package/src/checks/manual-review.js +95 -0
- package/src/checks/rules-and-tags.full.csv +19 -0
- package/src/checks/rules-and-tags.full.json +259 -0
- package/src/core/aria-helpers.js +906 -0
- package/src/core/contrast-helpers.js +1147 -0
- package/src/core/dom-helpers.js +4085 -0
- package/src/core/dom-runner.js +627 -0
- package/src/core/frame-messaging.js +210 -0
- package/src/core/frame-scan.js +178 -0
- package/src/core/rollup-composites.js +135 -0
- package/src/core/rule-meta.js +140 -0
- package/src/core.js +79055 -0
- package/src/coverage/wcag-facets.js +1079 -0
- package/src/coverage/wcag-version-map.js +84 -0
- package/src/i18n/en.js +919 -0
- package/src/i18n/fr.js +527 -0
- package/src/index.js +4 -0
- package/src/policy/contracts.js +18 -0
- package/src/policy/resolvePolicy.js +55 -0
- package/src/policy/schemas/engine-options.schema.json +103 -0
- package/src/policy/schemas/policy-contract.schema.json +40 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @check img-alt-decorative
|
|
5
|
+
* @atomic true
|
|
6
|
+
* @summary Manual review: text alternative appropriateness (WCAG 1.1.1)
|
|
7
|
+
* @standard WCAG 2.2
|
|
8
|
+
* @sc 1.1.1
|
|
9
|
+
* @type manual
|
|
10
|
+
* @expectation
|
|
11
|
+
* Human review is required to confirm that the provided text alternative is accurate and appropriate.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const id = "img-alt-decorative";
|
|
15
|
+
|
|
16
|
+
const meta = {
|
|
17
|
+
title: "<img> with alt=\"\" must be decorative (manual review)",
|
|
18
|
+
description: "Flags <img> elements with empty alt for human review that they are purely decorative.",
|
|
19
|
+
i18n: {
|
|
20
|
+
titleKey: "img_altDecorative_title",
|
|
21
|
+
descriptionKey: "img_altDecorative_description"
|
|
22
|
+
},
|
|
23
|
+
helpUrl: null,
|
|
24
|
+
tags: ["wcag2a", "wcag111", "nontext", "images", "manual", "atomic"],
|
|
25
|
+
wcagSc: ['1.1.1'],
|
|
26
|
+
normativeMappings: [
|
|
27
|
+
{ standard: 'WCAG', version: '2.2', requirement: '1.1.1', title: 'Non-text Content', conformanceLevel: 'A' }
|
|
28
|
+
],
|
|
29
|
+
defaultSeverity: 'minor',
|
|
30
|
+
category: 'perceivable',
|
|
31
|
+
type: 'manual',
|
|
32
|
+
defaultConfidence: 'medium',
|
|
33
|
+
coverage: {
|
|
34
|
+
facetsBySc: {
|
|
35
|
+
'1.1.1': ['text-alternative-quality']
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function runInPage(ctx) {
|
|
41
|
+
|
|
42
|
+
const { document, root, helpers, rule } = ctx;
|
|
43
|
+
const safeRoot = root || document;
|
|
44
|
+
|
|
45
|
+
const queryAllSmart = helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
46
|
+
const queryAll = helpers && typeof helpers.queryAll === 'function'
|
|
47
|
+
? helpers.queryAll
|
|
48
|
+
: (sel) => {
|
|
49
|
+
try { return safeRoot && safeRoot.querySelectorAll ? Array.from(safeRoot.querySelectorAll(sel)) : []; }
|
|
50
|
+
catch { return []; }
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function'
|
|
54
|
+
? helpers.isAccTreeEligible
|
|
55
|
+
: null;
|
|
56
|
+
|
|
57
|
+
const getFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function'
|
|
58
|
+
? helpers.getFocusableInfo
|
|
59
|
+
: null;
|
|
60
|
+
|
|
61
|
+
function isRolePresentationExcluded(el) {
|
|
62
|
+
const role = (() => {
|
|
63
|
+
try { return String(el.getAttribute('role') || '').trim().toLowerCase(); }
|
|
64
|
+
catch { return ''; }
|
|
65
|
+
})();
|
|
66
|
+
if (role !== 'presentation' && role !== 'none') return false;
|
|
67
|
+
|
|
68
|
+
// Exclude only when NOT focusable (mirrors img-alt-present policy)
|
|
69
|
+
let focusable = false;
|
|
70
|
+
if (getFocusableInfo) {
|
|
71
|
+
const fi = (() => { try { return getFocusableInfo(el, ctx); } catch { return null; } })();
|
|
72
|
+
focusable = !!(fi && fi.focusable);
|
|
73
|
+
} else {
|
|
74
|
+
const tabindex = el.getAttribute('tabindex');
|
|
75
|
+
focusable = tabindex != null && String(tabindex).trim() !== '' && !Number.isNaN(Number(String(tabindex).trim()));
|
|
76
|
+
}
|
|
77
|
+
return !focusable;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const els = (() => {
|
|
81
|
+
// Only likely candidates:
|
|
82
|
+
// - alt="" (exact)
|
|
83
|
+
// - alt that starts/ends with space (to catch whitespace-only like " ")
|
|
84
|
+
const sel = 'img[alt=""], img[alt^=" "], img[alt$=" "]';
|
|
85
|
+
try { return Array.from((queryAllSmart ? queryAllSmart(sel) : queryAll(sel)) || []); }
|
|
86
|
+
catch { return queryAll(sel); }
|
|
87
|
+
})();
|
|
88
|
+
const uniqueEls = [];
|
|
89
|
+
const seen = new Set();
|
|
90
|
+
for (const el of els) {
|
|
91
|
+
if (!seen.has(el)) { seen.add(el); uniqueEls.push(el); }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!els.length) {
|
|
95
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const occurrences = [];
|
|
99
|
+
let applicableCount = 0;
|
|
100
|
+
|
|
101
|
+
for (const el of uniqueEls) {
|
|
102
|
+
if (!el || !el.getAttribute) continue;
|
|
103
|
+
|
|
104
|
+
if (isAccTreeEligible) {
|
|
105
|
+
const elig = (() => {
|
|
106
|
+
try { return isAccTreeEligible(el, ctx); } catch { return { eligible: true, reasons: [] }; }
|
|
107
|
+
})();
|
|
108
|
+
if (elig && elig.eligible === false) continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (isRolePresentationExcluded(el)) continue;
|
|
112
|
+
|
|
113
|
+
// Rule-specific applicability (only elements that already have a text alternative mechanism)
|
|
114
|
+
const rawAlt = el.getAttribute('alt');
|
|
115
|
+
if (rawAlt == null) continue;
|
|
116
|
+
if (String(rawAlt).trim() !== '') continue;
|
|
117
|
+
|
|
118
|
+
applicableCount += 1;
|
|
119
|
+
|
|
120
|
+
const baseOccurrence = {
|
|
121
|
+
summary: 'Review whether <img> is decorative (alt="").',
|
|
122
|
+
hint: 'Confirm the image is purely decorative. If it conveys information or function, provide meaningful alt text.',
|
|
123
|
+
i18n: {
|
|
124
|
+
summaryKey: 'img_altDecorative_summary_cantTell',
|
|
125
|
+
hintKey: 'img_altDecorative_hint_cantTell',
|
|
126
|
+
params: { element: 'img' }
|
|
127
|
+
},
|
|
128
|
+
data: {
|
|
129
|
+
visibilityFilter: { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
130
|
+
details: null
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
135
|
+
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
136
|
+
} else {
|
|
137
|
+
occurrences.push({ selector: '', html: '', ...baseOccurrence });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (applicableCount === 0) {
|
|
142
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @check img-alt-quality
|
|
5
|
+
* @atomic true
|
|
6
|
+
* @summary Manual review: text alternative appropriateness (WCAG 1.1.1)
|
|
7
|
+
* @standard WCAG 2.2
|
|
8
|
+
* @sc 1.1.1
|
|
9
|
+
* @type manual
|
|
10
|
+
* @expectation
|
|
11
|
+
* Human review is required to confirm that the provided text alternative is accurate and appropriate.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const id = "img-alt-quality";
|
|
15
|
+
|
|
16
|
+
const meta = {
|
|
17
|
+
title: "<img> alt text must be appropriate (manual review)",
|
|
18
|
+
description: "Flags <img> elements with non-empty alt text for human review of appropriateness.",
|
|
19
|
+
i18n: {
|
|
20
|
+
titleKey: "img_altQuality_title",
|
|
21
|
+
descriptionKey: "img_altQuality_description"
|
|
22
|
+
},
|
|
23
|
+
helpUrl: null,
|
|
24
|
+
tags: ["wcag2a", "wcag111", "nontext", "images", "manual", "atomic"],
|
|
25
|
+
wcagSc: ['1.1.1'],
|
|
26
|
+
normativeMappings: [
|
|
27
|
+
{ standard: 'WCAG', version: '2.2', requirement: '1.1.1', title: 'Non-text Content', conformanceLevel: 'A' }
|
|
28
|
+
],
|
|
29
|
+
defaultSeverity: 'minor',
|
|
30
|
+
category: 'perceivable',
|
|
31
|
+
type: 'manual',
|
|
32
|
+
defaultConfidence: 'medium',
|
|
33
|
+
coverage: {
|
|
34
|
+
facetsBySc: {
|
|
35
|
+
'1.1.1': ['text-alternative-quality']
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function runInPage(ctx) {
|
|
41
|
+
const { document, root, helpers, rule } = ctx;
|
|
42
|
+
const safeRoot = root || document;
|
|
43
|
+
|
|
44
|
+
// Cap occurrences to keep manual “quality” checks fast on large pages.
|
|
45
|
+
// Deterministic: we keep DOM order, just stop collecting after N.
|
|
46
|
+
const MAX_OCCURRENCES = 50;
|
|
47
|
+
|
|
48
|
+
const queryAllSmart = helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
49
|
+
const queryAll = helpers && typeof helpers.queryAll === 'function'
|
|
50
|
+
? helpers.queryAll
|
|
51
|
+
: (sel) => {
|
|
52
|
+
try { return safeRoot && safeRoot.querySelectorAll ? Array.from(safeRoot.querySelectorAll(sel)) : []; }
|
|
53
|
+
catch { return []; }
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
|
|
57
|
+
? helpers.getEligibilityInfo
|
|
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
|
+
}
|
|
86
|
+
|
|
87
|
+
const selector = 'img[alt]:not([alt=""])';
|
|
88
|
+
const els = (() => {
|
|
89
|
+
try { return Array.from((queryAllSmart ? queryAllSmart(selector) : queryAll(selector)) || []); }
|
|
90
|
+
catch { return queryAll(selector); }
|
|
91
|
+
})();
|
|
92
|
+
|
|
93
|
+
if (!els.length) {
|
|
94
|
+
return {
|
|
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
|
+
}
|
|
102
|
+
|
|
103
|
+
const occurrences = [];
|
|
104
|
+
let applicableCount = 0; // total applicable elements
|
|
105
|
+
let collectedCount = 0; // how many occurrences we actually reported
|
|
106
|
+
|
|
107
|
+
for (const el of els) {
|
|
108
|
+
if (!el || !el.getAttribute) continue;
|
|
109
|
+
|
|
110
|
+
if (isAccTreeEligible) {
|
|
111
|
+
const elig = (() => {
|
|
112
|
+
try { return isAccTreeEligible(el, ctx); } catch { return { eligible: true, reasons: [] }; }
|
|
113
|
+
})();
|
|
114
|
+
if (elig && elig.eligible === false) continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (isRolePresentationExcluded(el)) continue;
|
|
118
|
+
|
|
119
|
+
// Rule-specific applicability: non-empty alt
|
|
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
|
+
};
|
|
144
|
+
|
|
145
|
+
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
146
|
+
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
147
|
+
} else {
|
|
148
|
+
occurrences.push({ selector: '', html: '', ...baseOccurrence });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
collectedCount += 1;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (applicableCount === 0) {
|
|
155
|
+
return {
|
|
156
|
+
ruleId: rule.ruleId,
|
|
157
|
+
outcome: 'notApplicable',
|
|
158
|
+
severity: 'minor',
|
|
159
|
+
occurrences: [],
|
|
160
|
+
data: { details: { applicableCount: 0, reportedCount: 0, maxOccurrences: MAX_OCCURRENCES, truncated: false } }
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const truncated = applicableCount > collectedCount;
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
ruleId: rule.ruleId,
|
|
168
|
+
outcome: 'cantTell',
|
|
169
|
+
severity: 'minor',
|
|
170
|
+
occurrences,
|
|
171
|
+
data: {
|
|
172
|
+
details: {
|
|
173
|
+
applicableCount,
|
|
174
|
+
reportedCount: collectedCount,
|
|
175
|
+
maxOccurrences: MAX_OCCURRENCES,
|
|
176
|
+
truncated
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @check input-image-alt-decorative
|
|
5
|
+
* @atomic true
|
|
6
|
+
* @summary Manual review: text alternative appropriateness (WCAG 1.1.1)
|
|
7
|
+
* @standard WCAG 2.2
|
|
8
|
+
* @sc 1.1.1
|
|
9
|
+
* @type manual
|
|
10
|
+
* @expectation
|
|
11
|
+
* Human review is required to confirm that the provided text alternative is accurate and appropriate.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const id = "input-image-alt-decorative";
|
|
15
|
+
|
|
16
|
+
const meta = {
|
|
17
|
+
title: "<input type=\"image\"> with alt=\"\" must be appropriate (manual review)",
|
|
18
|
+
description: "Flags <input type=\"image\"> elements with empty alt for human review (usually not appropriate for functional controls).",
|
|
19
|
+
i18n: {
|
|
20
|
+
titleKey: "inputImage_altDecorative_title",
|
|
21
|
+
descriptionKey: "inputImage_altDecorative_description"
|
|
22
|
+
},
|
|
23
|
+
helpUrl: null,
|
|
24
|
+
tags: ["wcag2a", "wcag111", "forms", "images", "manual", "atomic"],
|
|
25
|
+
wcagSc: ['1.1.1'],
|
|
26
|
+
normativeMappings: [
|
|
27
|
+
{ standard: 'WCAG', version: '2.2', requirement: '1.1.1', title: 'Non-text Content', conformanceLevel: 'A' }
|
|
28
|
+
],
|
|
29
|
+
defaultSeverity: 'minor',
|
|
30
|
+
category: 'perceivable',
|
|
31
|
+
type: 'manual',
|
|
32
|
+
defaultConfidence: 'medium',
|
|
33
|
+
coverage: {
|
|
34
|
+
facetsBySc: {
|
|
35
|
+
'1.1.1': ['text-alternative-quality']
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function runInPage(ctx) {
|
|
41
|
+
|
|
42
|
+
const { document, root, helpers, rule } = ctx;
|
|
43
|
+
const safeRoot = root || document;
|
|
44
|
+
|
|
45
|
+
const queryAllSmart = helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
46
|
+
const queryAll = helpers && typeof helpers.queryAll === 'function'
|
|
47
|
+
? helpers.queryAll
|
|
48
|
+
: (sel) => {
|
|
49
|
+
try { return safeRoot && safeRoot.querySelectorAll ? Array.from(safeRoot.querySelectorAll(sel)) : []; }
|
|
50
|
+
catch { return []; }
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
|
|
54
|
+
? helpers.getEligibilityInfo
|
|
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
|
+
}
|
|
93
|
+
|
|
94
|
+
const occurrences = [];
|
|
95
|
+
let applicableCount = 0;
|
|
96
|
+
|
|
97
|
+
for (const el of els) {
|
|
98
|
+
if (!el || !el.getAttribute) continue;
|
|
99
|
+
|
|
100
|
+
if (isAccTreeEligible) {
|
|
101
|
+
const elig = (() => {
|
|
102
|
+
try { return isAccTreeEligible(el, ctx); } catch { return { eligible: true, reasons: [] }; }
|
|
103
|
+
})();
|
|
104
|
+
if (elig && elig.eligible === false) continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (isRolePresentationExcluded(el)) continue;
|
|
108
|
+
|
|
109
|
+
// Rule-specific applicability (only elements that already have a text alternative mechanism)
|
|
110
|
+
if (!((el.getAttribute('alt') != null && String(el.getAttribute('alt')).trim() === ''))) continue;
|
|
111
|
+
|
|
112
|
+
applicableCount += 1;
|
|
113
|
+
|
|
114
|
+
const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
|
|
115
|
+
|
|
116
|
+
const baseOccurrence = {
|
|
117
|
+
summary: 'Review <input type="image"> with alt="".',
|
|
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
|
+
};
|
|
129
|
+
|
|
130
|
+
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
131
|
+
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
132
|
+
} else {
|
|
133
|
+
occurrences.push({ selector: '', html: '', ...baseOccurrence });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (applicableCount === 0) {
|
|
138
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @check input-image-alt-quality
|
|
5
|
+
* @atomic true
|
|
6
|
+
* @summary Manual review: text alternative appropriateness (WCAG 1.1.1)
|
|
7
|
+
* @standard WCAG 2.2
|
|
8
|
+
* @sc 1.1.1
|
|
9
|
+
* @type manual
|
|
10
|
+
* @expectation
|
|
11
|
+
* Human review is required to confirm that the provided text alternative is accurate and appropriate.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const id = "input-image-alt-quality";
|
|
15
|
+
|
|
16
|
+
const meta = {
|
|
17
|
+
title: "<input type=\"image\"> alt text must be appropriate (manual review)",
|
|
18
|
+
description: "Flags <input type=\"image\"> elements with non-empty alt text for human review of appropriateness.",
|
|
19
|
+
i18n: {
|
|
20
|
+
titleKey: "inputImage_altQuality_title",
|
|
21
|
+
descriptionKey: "inputImage_altQuality_description"
|
|
22
|
+
},
|
|
23
|
+
helpUrl: null,
|
|
24
|
+
tags: ["wcag2a", "wcag111", "forms", "images", "manual", "atomic"],
|
|
25
|
+
wcagSc: ['1.1.1'],
|
|
26
|
+
normativeMappings: [
|
|
27
|
+
{ standard: 'WCAG', version: '2.2', requirement: '1.1.1', title: 'Non-text Content', conformanceLevel: 'A' }
|
|
28
|
+
],
|
|
29
|
+
defaultSeverity: 'minor',
|
|
30
|
+
category: 'perceivable',
|
|
31
|
+
type: 'manual',
|
|
32
|
+
defaultConfidence: 'medium',
|
|
33
|
+
coverage: {
|
|
34
|
+
facetsBySc: {
|
|
35
|
+
'1.1.1': ['text-alternative-quality']
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function runInPage(ctx) {
|
|
41
|
+
|
|
42
|
+
const { document, root, helpers, rule } = ctx;
|
|
43
|
+
const safeRoot = root || document;
|
|
44
|
+
|
|
45
|
+
const queryAllSmart = helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
46
|
+
const queryAll = helpers && typeof helpers.queryAll === 'function'
|
|
47
|
+
? helpers.queryAll
|
|
48
|
+
: (sel) => {
|
|
49
|
+
try { return safeRoot && safeRoot.querySelectorAll ? Array.from(safeRoot.querySelectorAll(sel)) : []; }
|
|
50
|
+
catch { return []; }
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
|
|
54
|
+
? helpers.getEligibilityInfo
|
|
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
|
+
}
|
|
93
|
+
|
|
94
|
+
const occurrences = [];
|
|
95
|
+
let applicableCount = 0;
|
|
96
|
+
|
|
97
|
+
for (const el of els) {
|
|
98
|
+
if (!el || !el.getAttribute) continue;
|
|
99
|
+
|
|
100
|
+
if (isAccTreeEligible) {
|
|
101
|
+
const elig = (() => {
|
|
102
|
+
try { return isAccTreeEligible(el, ctx); } catch { return { eligible: true, reasons: [] }; }
|
|
103
|
+
})();
|
|
104
|
+
if (elig && elig.eligible === false) continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (isRolePresentationExcluded(el)) continue;
|
|
108
|
+
|
|
109
|
+
// Rule-specific applicability (only elements that already have a text alternative mechanism)
|
|
110
|
+
if (!((el.getAttribute('alt') != null && String(el.getAttribute('alt')).trim() !== ''))) continue;
|
|
111
|
+
|
|
112
|
+
applicableCount += 1;
|
|
113
|
+
|
|
114
|
+
const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
|
|
115
|
+
|
|
116
|
+
const baseOccurrence = {
|
|
117
|
+
summary: 'Review alt text on <input type="image"> for accuracy and appropriateness.',
|
|
118
|
+
hint: 'Ensure the alt text describes the control’s action (e.g., “Search”, “Submit order”) in context.',
|
|
119
|
+
i18n: {
|
|
120
|
+
summaryKey: 'inputImage_altQuality_summary_cantTell',
|
|
121
|
+
hintKey: 'inputImage_altQuality_hint_cantTell',
|
|
122
|
+
params: { element: 'input[type=image]' }
|
|
123
|
+
},
|
|
124
|
+
data: {
|
|
125
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
126
|
+
details: { alt: String(el.getAttribute('alt') || '') } // optional but useful
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
131
|
+
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
132
|
+
} else {
|
|
133
|
+
occurrences.push({ selector: '', html: '', ...baseOccurrence });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (applicableCount === 0) {
|
|
138
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = { id, meta, runInPage };
|