@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,201 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generic name-presence rule for selected ARIA composite/widget roles that should expose an accessible name.
|
|
5
|
+
*
|
|
6
|
+
* Notes:
|
|
7
|
+
* - This rule intentionally focuses on author-provided naming mechanisms:
|
|
8
|
+
* aria-label, aria-labelledby, and title.
|
|
9
|
+
* - It does NOT treat descendant text content as a valid name source for these roles
|
|
10
|
+
* (to avoid false passes from labelled children inside composite widgets).
|
|
11
|
+
* - Eligibility is based on helpers.isAccTreeEligible(node, ctx) per engine checks.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const id = 'aria-role-name-present';
|
|
15
|
+
|
|
16
|
+
const meta = {
|
|
17
|
+
title: 'ARIA widget/container roles have an accessible name',
|
|
18
|
+
description: 'Checks that selected ARIA widget/container roles expose a non-empty accessible name.',
|
|
19
|
+
i18n: {
|
|
20
|
+
titleKey: 'ariaRoleNamePresent_title',
|
|
21
|
+
descriptionKey: 'ariaRoleNamePresent_description'
|
|
22
|
+
},
|
|
23
|
+
helpUrl: null,
|
|
24
|
+
tags: ['wcag2a', 'wcag412', 'navigation', 'atomic', 'automatic', 'name'],
|
|
25
|
+
wcagSc: ['4.1.2'],
|
|
26
|
+
normativeMappings: [
|
|
27
|
+
{ standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
|
|
28
|
+
],
|
|
29
|
+
defaultSeverity: 'serious',
|
|
30
|
+
category: 'robust',
|
|
31
|
+
type: 'automatic',
|
|
32
|
+
defaultConfidence: 'high',
|
|
33
|
+
coverage: {
|
|
34
|
+
facetsBySc: {
|
|
35
|
+
'4.1.2': ['aria-role-name-present']
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function runInPage(ctx) {
|
|
41
|
+
const { document, root, helpers, rule } = ctx;
|
|
42
|
+
const safeRoot = root || document;
|
|
43
|
+
|
|
44
|
+
const queryAllSmart = helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
45
|
+
const queryAll = helpers && typeof helpers.queryAll === 'function'
|
|
46
|
+
? helpers.queryAll
|
|
47
|
+
: (sel, rt) => {
|
|
48
|
+
try {
|
|
49
|
+
const scope = rt || safeRoot;
|
|
50
|
+
return scope && scope.querySelectorAll ? Array.from(scope.querySelectorAll(sel)) : [];
|
|
51
|
+
} catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const isAccTreeEligible =
|
|
57
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
58
|
+
|
|
59
|
+
const getEligibilityInfo =
|
|
60
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
61
|
+
|
|
62
|
+
const getAriaLabelledByInfo =
|
|
63
|
+
helpers && typeof helpers.getAriaLabelledByInfo === 'function' ? helpers.getAriaLabelledByInfo : null;
|
|
64
|
+
|
|
65
|
+
const normalizeWs = (s) => String(s || '').replace(/\s+/g, ' ').trim();
|
|
66
|
+
|
|
67
|
+
const getAttr = (el, name) => {
|
|
68
|
+
try {
|
|
69
|
+
if (!el || !el.getAttribute) return '';
|
|
70
|
+
return normalizeWs(el.getAttribute(name));
|
|
71
|
+
} catch {
|
|
72
|
+
return '';
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
// aria-labelledby resolution: delegate to the shared helper (used by
|
|
78
|
+
// button-name-present et al.), which correctly includes hidden/aria-hidden
|
|
79
|
+
// referenced nodes per the Accessible Name and Description Computation
|
|
80
|
+
// spec (a hidden node directly referenced by aria-labelledby still
|
|
81
|
+
// supplies its text — this is a standard visually-hidden-label pattern).
|
|
82
|
+
const resolveLabelledby = (el, maxRefs) => {
|
|
83
|
+
if (!getAriaLabelledByInfo) return '';
|
|
84
|
+
try {
|
|
85
|
+
const info = getAriaLabelledByInfo(el, ctx, { maxRefs: Math.max(1, maxRefs || 8) });
|
|
86
|
+
return info && info.present ? normalizeWs(info.value) : '';
|
|
87
|
+
} catch {
|
|
88
|
+
return '';
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const isEligible = (el) => {
|
|
93
|
+
if (!isAccTreeEligible) return true;
|
|
94
|
+
try {
|
|
95
|
+
const r = isAccTreeEligible(el, ctx);
|
|
96
|
+
if (typeof r === 'boolean') return r;
|
|
97
|
+
return !!(r && r.eligible);
|
|
98
|
+
} catch {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Frozen allowlist of roles to check in this generic rule.
|
|
104
|
+
// (Keep this small, explicit, and standards-based for determinism.)
|
|
105
|
+
const roleSet = new Set([
|
|
106
|
+
'scrollbar',
|
|
107
|
+
'toolbar',
|
|
108
|
+
'tablist',
|
|
109
|
+
'radiogroup',
|
|
110
|
+
'tree',
|
|
111
|
+
'grid',
|
|
112
|
+
'menu',
|
|
113
|
+
'menubar',
|
|
114
|
+
'meter',
|
|
115
|
+
'progressbar'
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
const selector =
|
|
119
|
+
'[role="scrollbar"],[role="toolbar"],[role="tablist"],[role="radiogroup"],[role="tree"],[role="grid"],[role="menu"],[role="menubar"],[role="meter"],[role="progressbar"]';
|
|
120
|
+
|
|
121
|
+
const nodes = (() => {
|
|
122
|
+
try {
|
|
123
|
+
return (queryAllSmart ? queryAllSmart(selector, safeRoot) : queryAll(selector, safeRoot)) || [];
|
|
124
|
+
} catch {
|
|
125
|
+
return queryAll(selector, safeRoot);
|
|
126
|
+
}
|
|
127
|
+
})();
|
|
128
|
+
|
|
129
|
+
if (!nodes.length) {
|
|
130
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const occurrences = [];
|
|
134
|
+
let applicableCount = 0;
|
|
135
|
+
|
|
136
|
+
for (const el of nodes) {
|
|
137
|
+
if (!el || !el.getAttribute) continue;
|
|
138
|
+
|
|
139
|
+
// Role normalization + allowlist check (defensive).
|
|
140
|
+
const role = (() => {
|
|
141
|
+
try {
|
|
142
|
+
return normalizeWs(el.getAttribute('role')).toLowerCase();
|
|
143
|
+
} catch {
|
|
144
|
+
return '';
|
|
145
|
+
}
|
|
146
|
+
})();
|
|
147
|
+
if (!roleSet.has(role)) continue;
|
|
148
|
+
|
|
149
|
+
if (!isEligible(el)) continue;
|
|
150
|
+
|
|
151
|
+
applicableCount += 1;
|
|
152
|
+
|
|
153
|
+
const ariaLabel = getAttr(el, 'aria-label');
|
|
154
|
+
const labelled = ariaLabel ? '' : resolveLabelledby(el, 8);
|
|
155
|
+
|
|
156
|
+
const title = ariaLabel || labelled ? '' : getAttr(el, 'title');
|
|
157
|
+
|
|
158
|
+
const ok = !!(ariaLabel || labelled || title);
|
|
159
|
+
if (ok) continue;
|
|
160
|
+
|
|
161
|
+
const eligInfo = getEligibilityInfo ? (() => {
|
|
162
|
+
try { return getEligibilityInfo(el, ctx, { targetSet: 'acc' }); } catch { return null; }
|
|
163
|
+
})() : null;
|
|
164
|
+
|
|
165
|
+
const baseOccurrence = {
|
|
166
|
+
summary: 'This element has no accessible name.',
|
|
167
|
+
hint: 'Provide aria-label or aria-labelledby (preferred), or a non-empty title attribute.',
|
|
168
|
+
i18n: {
|
|
169
|
+
summaryKey: 'ariaRoleNamePresent_summary_fail',
|
|
170
|
+
hintKey: 'ariaRoleNamePresent_hint_fail',
|
|
171
|
+
params: { role }
|
|
172
|
+
},
|
|
173
|
+
data: {
|
|
174
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
175
|
+
details: {
|
|
176
|
+
reasonCode: 'name_missing',
|
|
177
|
+
role,
|
|
178
|
+
nameSourcesChecked: ['aria-label', 'aria-labelledby', 'title']
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
184
|
+
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
185
|
+
} else {
|
|
186
|
+
occurrences.push({ selector: '', html: '', ...baseOccurrence });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (applicableCount === 0) {
|
|
191
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!occurrences.length) {
|
|
195
|
+
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @check aria-roles-valid
|
|
5
|
+
* @atomic true
|
|
6
|
+
* @summary An explicit role attribute must resolve to a real, non-abstract ARIA role
|
|
7
|
+
* @standard WCAG 2.2
|
|
8
|
+
* @sc 4.1.2
|
|
9
|
+
* @applicability
|
|
10
|
+
* Applies to any element with a non-empty role="" attribute in the composed DOM.
|
|
11
|
+
* @expectation
|
|
12
|
+
* The role attribute's first token (the role actually used by assistive technology;
|
|
13
|
+
* later space-separated tokens are author-supplied fallbacks and are not evaluated
|
|
14
|
+
* here) must be a real WAI-ARIA role name, and must not be an abstract role
|
|
15
|
+
* (abstract roles exist only for the specification's own role taxonomy and must
|
|
16
|
+
* never be used directly in markup).
|
|
17
|
+
* @implementation-notes
|
|
18
|
+
* - Unlike this engine's accessible-name rules, ARIA validity is a static markup
|
|
19
|
+
* property independent of current visibility/eligibility, so this rule does not
|
|
20
|
+
* gate on isAccTreeEligible: an invalid role is a defect whether or not the
|
|
21
|
+
* element happens to be hidden right now.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const id = 'aria-roles-valid';
|
|
25
|
+
|
|
26
|
+
const meta = {
|
|
27
|
+
title: 'role attribute must be a valid, non-abstract ARIA role',
|
|
28
|
+
description: 'Checks that an explicit role="" attribute resolves to a real, non-abstract WAI-ARIA role.',
|
|
29
|
+
i18n: {
|
|
30
|
+
titleKey: 'ariaRolesValid_title',
|
|
31
|
+
descriptionKey: 'ariaRolesValid_description'
|
|
32
|
+
},
|
|
33
|
+
helpUrl: null,
|
|
34
|
+
tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
|
|
35
|
+
wcagSc: ['4.1.2'],
|
|
36
|
+
normativeMappings: [
|
|
37
|
+
{ standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
|
|
38
|
+
],
|
|
39
|
+
defaultSeverity: 'serious',
|
|
40
|
+
category: 'robust',
|
|
41
|
+
type: 'automatic',
|
|
42
|
+
defaultConfidence: 'high',
|
|
43
|
+
coverage: { facetsBySc: { '4.1.2': ['aria-role-valid'] } }
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function runInPage(ctx) {
|
|
47
|
+
const { document, root, helpers, rule } = ctx;
|
|
48
|
+
const safeRoot = root || document;
|
|
49
|
+
|
|
50
|
+
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
51
|
+
if (!ariaHelpers) {
|
|
52
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
|
|
56
|
+
|
|
57
|
+
const occurrences = [];
|
|
58
|
+
let applicableCount = 0;
|
|
59
|
+
|
|
60
|
+
for (const el of nodes) {
|
|
61
|
+
if (!el || !el.getAttribute) continue;
|
|
62
|
+
|
|
63
|
+
const role = ariaHelpers.getExplicitRole(el);
|
|
64
|
+
if (!role) continue; // role="" or whitespace-only: not this rule's concern
|
|
65
|
+
|
|
66
|
+
applicableCount += 1;
|
|
67
|
+
|
|
68
|
+
const isAbstract = ariaHelpers.isAbstractRole(role);
|
|
69
|
+
const isKnown = ariaHelpers.isKnownRole(role);
|
|
70
|
+
|
|
71
|
+
if (isKnown && !isAbstract) continue;
|
|
72
|
+
|
|
73
|
+
const reasonCode = !isKnown ? 'ARIA_ROLE_INVALID' : 'ARIA_ROLE_ABSTRACT';
|
|
74
|
+
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
75
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
|
|
76
|
+
|
|
77
|
+
occurrences.push({
|
|
78
|
+
selector: stableSelector,
|
|
79
|
+
html,
|
|
80
|
+
summary: !isKnown
|
|
81
|
+
? 'The role attribute value is not a recognized ARIA role.'
|
|
82
|
+
: 'The role attribute value is an abstract ARIA role, which must not be used directly.',
|
|
83
|
+
hint: !isKnown
|
|
84
|
+
? 'Use a valid ARIA role token, or remove the role attribute if none applies.'
|
|
85
|
+
: 'Replace this abstract role with a concrete role appropriate for the widget/structure.',
|
|
86
|
+
i18n: {
|
|
87
|
+
summaryKey: !isKnown
|
|
88
|
+
? 'ariaRolesValid_summary_invalid'
|
|
89
|
+
: 'ariaRolesValid_summary_abstract',
|
|
90
|
+
hintKey: !isKnown
|
|
91
|
+
? 'ariaRolesValid_hint_invalid'
|
|
92
|
+
: 'ariaRolesValid_hint_abstract',
|
|
93
|
+
params: { role }
|
|
94
|
+
},
|
|
95
|
+
data: {
|
|
96
|
+
details: { reasonCode, role }
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (applicableCount === 0) {
|
|
102
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
103
|
+
}
|
|
104
|
+
if (occurrences.length) {
|
|
105
|
+
return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
|
|
106
|
+
}
|
|
107
|
+
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @check aria-valid-attr-value
|
|
5
|
+
* @atomic true
|
|
6
|
+
* @summary Every recognized aria-* attribute must have a value matching its declared type
|
|
7
|
+
* @standard WCAG 2.2
|
|
8
|
+
* @sc 4.1.2
|
|
9
|
+
* @applicability
|
|
10
|
+
* Applies to any element carrying at least one recognized aria-* attribute
|
|
11
|
+
* (unrecognized attribute names are aria-valid-attr's concern,
|
|
12
|
+
* not evaluated here).
|
|
13
|
+
* @expectation
|
|
14
|
+
* Each attribute's value conforms to its WAI-ARIA-declared value type:
|
|
15
|
+
* boolean ("true"/"false"), tristate ("true"/"false"/"mixed"), a token
|
|
16
|
+
* from a fixed enumerated set, an integer, a real number, or a non-empty
|
|
17
|
+
* ID reference (list) that resolves to an existing element in the
|
|
18
|
+
* document.
|
|
19
|
+
* @implementation-notes
|
|
20
|
+
* - Not gated on isAccTreeEligible: this is a static markup property.
|
|
21
|
+
* - ID-reference resolution (added 2026-07-20, see aria-helpers.js's
|
|
22
|
+
* idExists) only flags idref-list attributes (aria-labelledby,
|
|
23
|
+
* aria-describedby, aria-controls, aria-owns, etc.) when NONE of the
|
|
24
|
+
* space-separated ids resolve — a partially-dangling list (some ids
|
|
25
|
+
* exist, some don't) is left unflagged. Verified 2026-07-21 directly
|
|
26
|
+
* against a widely-used reference engine's own `validateAttrValue` source:
|
|
27
|
+
* this is not a conservative guess, it's an exact match for that engine's
|
|
28
|
+
* own behavior (`idrefs(vNode, attr).some(node => !!node)` — that engine
|
|
29
|
+
* itself only invalidates when every token fails to resolve). Single-idref
|
|
30
|
+
* attributes (aria-activedescendant, aria-errormessage) are flagged
|
|
31
|
+
* whenever their one id doesn't resolve, also matching that engine's `idref`
|
|
32
|
+
* case exactly.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const id = 'aria-valid-attr-value';
|
|
36
|
+
|
|
37
|
+
const meta = {
|
|
38
|
+
title: 'aria-* attribute values must match their declared type',
|
|
39
|
+
description: 'Checks that every recognized aria-* attribute has a value conforming to its WAI-ARIA-declared value type (boolean, tristate, token, integer, number, or ID reference).',
|
|
40
|
+
i18n: {
|
|
41
|
+
titleKey: 'ariaValidAttrValue_title',
|
|
42
|
+
descriptionKey: 'ariaValidAttrValue_description'
|
|
43
|
+
},
|
|
44
|
+
helpUrl: null,
|
|
45
|
+
tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
|
|
46
|
+
wcagSc: ['4.1.2'],
|
|
47
|
+
normativeMappings: [
|
|
48
|
+
{ standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
|
|
49
|
+
],
|
|
50
|
+
defaultSeverity: 'serious',
|
|
51
|
+
category: 'robust',
|
|
52
|
+
type: 'automatic',
|
|
53
|
+
defaultConfidence: 'high',
|
|
54
|
+
coverage: { facetsBySc: { '4.1.2': ['aria-attr-value-valid'] } }
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function runInPage(ctx) {
|
|
58
|
+
const { document, root, helpers, rule } = ctx;
|
|
59
|
+
const safeRoot = root || document;
|
|
60
|
+
|
|
61
|
+
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
62
|
+
if (!ariaHelpers) {
|
|
63
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('*', safeRoot) : helpers.queryAll('*', safeRoot);
|
|
67
|
+
|
|
68
|
+
const occurrences = [];
|
|
69
|
+
let applicableCount = 0;
|
|
70
|
+
|
|
71
|
+
for (const el of nodes) {
|
|
72
|
+
if (!el || !el.attributes || !el.getAttribute) continue;
|
|
73
|
+
|
|
74
|
+
let invalid = null;
|
|
75
|
+
const attrs = el.attributes;
|
|
76
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
77
|
+
const name = String(attrs[i].name || '').toLowerCase();
|
|
78
|
+
if (name.slice(0, 5) !== 'aria-') continue;
|
|
79
|
+
if (!ariaHelpers.isValidAriaAttrName(name)) continue; // aria-valid-attr's concern
|
|
80
|
+
|
|
81
|
+
applicableCount += 1;
|
|
82
|
+
|
|
83
|
+
const rawValue = el.getAttribute(name);
|
|
84
|
+
const result = ariaHelpers.validateAttrValue(name, rawValue);
|
|
85
|
+
if (!result.valid) {
|
|
86
|
+
if (!invalid) invalid = [];
|
|
87
|
+
invalid.push({ name, value: rawValue == null ? '' : String(rawValue), reason: result.reason });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!invalid || !invalid.length) continue;
|
|
92
|
+
|
|
93
|
+
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
94
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
|
|
95
|
+
|
|
96
|
+
for (const item of invalid) {
|
|
97
|
+
occurrences.push({
|
|
98
|
+
selector: stableSelector,
|
|
99
|
+
html,
|
|
100
|
+
summary: 'This element has an ARIA attribute with an invalid value.',
|
|
101
|
+
hint: 'Use a value that matches the attribute’s expected type (see the WAI-ARIA specification for this attribute).',
|
|
102
|
+
i18n: {
|
|
103
|
+
summaryKey: 'ariaValidAttrValue_summary_fail',
|
|
104
|
+
hintKey: 'ariaValidAttrValue_hint_fail',
|
|
105
|
+
params: { attr: item.name, value: item.value }
|
|
106
|
+
},
|
|
107
|
+
data: {
|
|
108
|
+
details: { reasonCode: 'ARIA_ATTR_VALUE_INVALID', attr: item.name, value: item.value, valueReason: item.reason }
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (applicableCount === 0) {
|
|
115
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
116
|
+
}
|
|
117
|
+
if (occurrences.length) {
|
|
118
|
+
return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
|
|
119
|
+
}
|
|
120
|
+
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @check aria-valid-attr
|
|
5
|
+
* @atomic true
|
|
6
|
+
* @summary Every aria-* attribute present must be a real, defined ARIA attribute
|
|
7
|
+
* @standard WCAG 2.2
|
|
8
|
+
* @sc 4.1.2
|
|
9
|
+
* @applicability
|
|
10
|
+
* Applies to any element in the composed DOM that carries at least one
|
|
11
|
+
* attribute whose name starts with "aria-".
|
|
12
|
+
* @expectation
|
|
13
|
+
* Each aria-* attribute name is a real attribute defined by the WAI-ARIA
|
|
14
|
+
* specification (catches typos / made-up attribute names, which are
|
|
15
|
+
* silently ignored by assistive technology and therefore a real,
|
|
16
|
+
* deterministic defect).
|
|
17
|
+
* @implementation-notes
|
|
18
|
+
* - Distinct from aria-valid-attr-value (which validates the VALUE
|
|
19
|
+
* of a recognized attribute) — this rule only validates the attribute
|
|
20
|
+
* NAME.
|
|
21
|
+
* - Not gated on isAccTreeEligible: this is a static markup property.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const id = 'aria-valid-attr';
|
|
25
|
+
|
|
26
|
+
const meta = {
|
|
27
|
+
title: 'aria-* attributes must be real, defined ARIA attributes',
|
|
28
|
+
description: 'Checks that every aria-* attribute name present in the DOM is a real attribute defined by the WAI-ARIA specification.',
|
|
29
|
+
i18n: {
|
|
30
|
+
titleKey: 'ariaValidAttr_title',
|
|
31
|
+
descriptionKey: 'ariaValidAttr_description'
|
|
32
|
+
},
|
|
33
|
+
helpUrl: null,
|
|
34
|
+
tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
|
|
35
|
+
wcagSc: ['4.1.2'],
|
|
36
|
+
normativeMappings: [
|
|
37
|
+
{ standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
|
|
38
|
+
],
|
|
39
|
+
defaultSeverity: 'serious',
|
|
40
|
+
category: 'robust',
|
|
41
|
+
type: 'automatic',
|
|
42
|
+
defaultConfidence: 'high',
|
|
43
|
+
coverage: { facetsBySc: { '4.1.2': ['aria-attr-name-valid'] } }
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function runInPage(ctx) {
|
|
47
|
+
const { document, root, helpers, rule } = ctx;
|
|
48
|
+
const safeRoot = root || document;
|
|
49
|
+
|
|
50
|
+
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
51
|
+
if (!ariaHelpers) {
|
|
52
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('*', safeRoot) : helpers.queryAll('*', safeRoot);
|
|
56
|
+
|
|
57
|
+
const occurrences = [];
|
|
58
|
+
let applicableCount = 0;
|
|
59
|
+
|
|
60
|
+
for (const el of nodes) {
|
|
61
|
+
if (!el || !el.attributes) continue;
|
|
62
|
+
|
|
63
|
+
let invalidNames = null;
|
|
64
|
+
const attrs = el.attributes;
|
|
65
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
66
|
+
const name = String(attrs[i].name || '').toLowerCase();
|
|
67
|
+
if (name.slice(0, 5) !== 'aria-') continue;
|
|
68
|
+
|
|
69
|
+
applicableCount += 1;
|
|
70
|
+
|
|
71
|
+
if (!ariaHelpers.isValidAriaAttrName(name)) {
|
|
72
|
+
if (!invalidNames) invalidNames = [];
|
|
73
|
+
invalidNames.push(name);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!invalidNames || !invalidNames.length) continue;
|
|
78
|
+
|
|
79
|
+
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
80
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
|
|
81
|
+
|
|
82
|
+
for (const name of invalidNames) {
|
|
83
|
+
occurrences.push({
|
|
84
|
+
selector: stableSelector,
|
|
85
|
+
html,
|
|
86
|
+
summary: 'This element has an attribute that is not a recognized ARIA attribute.',
|
|
87
|
+
hint: 'Correct the attribute name (check for typos), or remove it if not needed.',
|
|
88
|
+
i18n: {
|
|
89
|
+
summaryKey: 'ariaValidAttr_summary_fail',
|
|
90
|
+
hintKey: 'ariaValidAttr_hint_fail',
|
|
91
|
+
params: { attr: name }
|
|
92
|
+
},
|
|
93
|
+
data: {
|
|
94
|
+
details: { reasonCode: 'ARIA_ATTR_INVALID', attr: name }
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (applicableCount === 0) {
|
|
101
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
102
|
+
}
|
|
103
|
+
if (occurrences.length) {
|
|
104
|
+
return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
|
|
105
|
+
}
|
|
106
|
+
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @check autocomplete-valid
|
|
5
|
+
* @atomic true
|
|
6
|
+
* @summary A non-empty autocomplete attribute must follow the WHATWG autofill grammar
|
|
7
|
+
* @standard WCAG 2.2
|
|
8
|
+
* @sc 1.3.5
|
|
9
|
+
* @applicability
|
|
10
|
+
* Applies to form controls (input, select, textarea) with a non-empty
|
|
11
|
+
* autocomplete attribute.
|
|
12
|
+
* @expectation
|
|
13
|
+
* The value is "on"/"off" alone, or a well-formed autofill detail
|
|
14
|
+
* token list: an optional "section-*" token, then an optional
|
|
15
|
+
* "shipping"/"billing" token, then an optional contact-modality token
|
|
16
|
+
* (home/work/mobile/fax/pager/impp), then exactly one recognized
|
|
17
|
+
* field-name token (name, email, street-address, cc-number, tel, ...),
|
|
18
|
+
* optionally followed by "webauthn". A malformed value means the field
|
|
19
|
+
* is not reliably identified for assistive technology that relies on
|
|
20
|
+
* autocomplete to describe the expected input purpose.
|
|
21
|
+
* @implementation-notes
|
|
22
|
+
* - Implements the structural shape of the WHATWG autofill grammar
|
|
23
|
+
* (section/mode/contact-modality prefixes + one field-name token, in
|
|
24
|
+
* order) with the full fixed field-name vocabulary from the HTML
|
|
25
|
+
* Standard, rather than validating every field-specific constraint
|
|
26
|
+
* (e.g. which contact-modality tokens are legal for which field
|
|
27
|
+
* names) — matches this engine's established "deliberately scoped"
|
|
28
|
+
* precedent (see aria-helpers.js) for keeping high-confidence fail
|
|
29
|
+
* without reimplementing the entire spec.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const id = 'autocomplete-valid';
|
|
33
|
+
|
|
34
|
+
const meta = {
|
|
35
|
+
title: 'autocomplete attribute must be a valid autofill value',
|
|
36
|
+
description: 'Checks that a non-empty autocomplete attribute is "on"/"off" or a well-formed autofill detail token list.',
|
|
37
|
+
i18n: {
|
|
38
|
+
titleKey: 'autocompleteValid_title',
|
|
39
|
+
descriptionKey: 'autocompleteValid_description'
|
|
40
|
+
},
|
|
41
|
+
helpUrl: null,
|
|
42
|
+
tags: ['wcag21aa', 'wcag135', 'forms', 'atomic', 'automatic'],
|
|
43
|
+
wcagSc: ['1.3.5'],
|
|
44
|
+
normativeMappings: [
|
|
45
|
+
{ standard: 'WCAG', version: '2.2', requirement: '1.3.5', title: 'Identify Input Purpose', conformanceLevel: 'AA' }
|
|
46
|
+
],
|
|
47
|
+
defaultSeverity: 'moderate',
|
|
48
|
+
category: 'perceivable',
|
|
49
|
+
type: 'automatic',
|
|
50
|
+
defaultConfidence: 'high',
|
|
51
|
+
coverage: { facetsBySc: { '1.3.5': ['autocomplete-valid'] } }
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function runInPage(ctx) {
|
|
55
|
+
const { document, root, helpers, rule } = ctx;
|
|
56
|
+
const safeRoot = root || document;
|
|
57
|
+
|
|
58
|
+
// Declared inside runInPage — see scripts/build-core.js header
|
|
59
|
+
// ("runInPage MUST be self-contained").
|
|
60
|
+
const FIELD_NAMES = new Set([
|
|
61
|
+
'name', 'honorific-prefix', 'given-name', 'additional-name', 'family-name', 'honorific-suffix',
|
|
62
|
+
'nickname', 'username', 'new-password', 'current-password', 'one-time-code', 'organization-title',
|
|
63
|
+
'organization', 'street-address', 'address-line1', 'address-line2', 'address-line3',
|
|
64
|
+
'address-level4', 'address-level3', 'address-level2', 'address-level1', 'country', 'country-name',
|
|
65
|
+
'postal-code', 'cc-name', 'cc-given-name', 'cc-additional-name', 'cc-family-name', 'cc-number',
|
|
66
|
+
'cc-exp', 'cc-exp-month', 'cc-exp-year', 'cc-csc', 'cc-type', 'transaction-currency',
|
|
67
|
+
'transaction-amount', 'language', 'bday', 'bday-day', 'bday-month', 'bday-year', 'sex',
|
|
68
|
+
'tel', 'tel-country-code', 'tel-national', 'tel-area-code', 'tel-local', 'tel-extension',
|
|
69
|
+
'email', 'impp', 'url', 'photo'
|
|
70
|
+
]);
|
|
71
|
+
const CONTACT_MODALITY = new Set(['home', 'work', 'mobile', 'fax', 'pager', 'impp']);
|
|
72
|
+
|
|
73
|
+
function isValidAutocomplete(raw) {
|
|
74
|
+
const tokens = raw.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
75
|
+
if (!tokens.length) return false;
|
|
76
|
+
if (tokens.length === 1 && (tokens[0] === 'on' || tokens[0] === 'off')) return true;
|
|
77
|
+
|
|
78
|
+
let i = 0;
|
|
79
|
+
if (tokens[i] && tokens[i].startsWith('section-') && tokens[i].length > 'section-'.length) i += 1;
|
|
80
|
+
if (tokens[i] === 'shipping' || tokens[i] === 'billing') i += 1;
|
|
81
|
+
if (CONTACT_MODALITY.has(tokens[i])) i += 1;
|
|
82
|
+
|
|
83
|
+
let end = tokens.length;
|
|
84
|
+
if (tokens[end - 1] === 'webauthn') end -= 1;
|
|
85
|
+
|
|
86
|
+
const remaining = tokens.slice(i, end);
|
|
87
|
+
if (remaining.length !== 1) return false;
|
|
88
|
+
return FIELD_NAMES.has(remaining[0]);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('input, select, textarea', safeRoot) : helpers.queryAll('input, select, textarea', safeRoot);
|
|
92
|
+
|
|
93
|
+
const occurrences = [];
|
|
94
|
+
let applicableCount = 0;
|
|
95
|
+
|
|
96
|
+
for (const el of nodes) {
|
|
97
|
+
if (!el || !el.getAttribute) continue;
|
|
98
|
+
const raw = String(el.getAttribute('autocomplete') || '').trim();
|
|
99
|
+
if (!raw) continue;
|
|
100
|
+
|
|
101
|
+
applicableCount += 1;
|
|
102
|
+
|
|
103
|
+
if (isValidAutocomplete(raw)) continue;
|
|
104
|
+
|
|
105
|
+
const tag = el.tagName.toLowerCase();
|
|
106
|
+
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
107
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
|
|
108
|
+
|
|
109
|
+
occurrences.push({
|
|
110
|
+
selector: stableSelector,
|
|
111
|
+
html,
|
|
112
|
+
summary: 'This autocomplete attribute value is not a valid autofill value.',
|
|
113
|
+
hint: 'Use "on"/"off", or a valid autofill token list (e.g. "shipping street-address", "cc-number").',
|
|
114
|
+
i18n: {
|
|
115
|
+
summaryKey: 'autocompleteValid_summary_fail',
|
|
116
|
+
hintKey: 'autocompleteValid_hint_fail',
|
|
117
|
+
params: { element: tag, value: raw }
|
|
118
|
+
},
|
|
119
|
+
data: {
|
|
120
|
+
details: { reasonCode: 'AUTOCOMPLETE_VALUE_INVALID', element: tag, value: raw }
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (applicableCount === 0) {
|
|
126
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
127
|
+
}
|
|
128
|
+
if (occurrences.length) {
|
|
129
|
+
return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'moderate', occurrences };
|
|
130
|
+
}
|
|
131
|
+
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = { id, meta, runInPage };
|