@surea11y/core 1.1.2 → 1.3.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 +48 -4
- package/LICENSE +373 -21
- package/README.md +70 -1
- package/bin/core.js +240 -11
- package/docs/API_STABILITY.md +61 -0
- package/docs/BASELINE.md +66 -0
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/CLI.md +80 -1
- package/docs/ENGINE_OPTIONS.md +4 -0
- package/docs/INTEGRATION.md +19 -1
- package/docs/OUTPUT_SCHEMA.md +2 -2
- package/docs/REPORT.md +33 -0
- package/docs/RULE_AUTHORING.md +31 -0
- package/docs/RULE_CATALOG.md +1 -1
- package/docs/SARIF.md +59 -0
- package/package.json +15 -4
- package/src/baseline.js +0 -0
- package/src/catalogs/composites.wcag.js +414 -450
- package/src/checks/automatic/area-alt-present.js +59 -25
- package/src/checks/automatic/aria-allowed-attr.js +193 -33
- package/src/checks/automatic/aria-allowed-role.js +21 -7
- package/src/checks/automatic/aria-braille-equivalent.js +32 -10
- package/src/checks/automatic/aria-conditional-attr.js +24 -7
- package/src/checks/automatic/aria-deprecated-role.js +22 -8
- package/src/checks/automatic/aria-hidden-body.js +50 -19
- package/src/checks/automatic/aria-hidden-focus.js +408 -53
- package/src/checks/automatic/aria-prohibited-attr.js +296 -22
- package/src/checks/automatic/aria-prohibited-children.js +125 -29
- package/src/checks/automatic/aria-required-attr.js +23 -8
- package/src/checks/automatic/aria-required-children.js +37 -14
- package/src/checks/automatic/aria-required-parent.js +48 -14
- package/src/checks/automatic/aria-role-name-present.js +47 -21
- package/src/checks/automatic/aria-roles-valid.js +22 -12
- package/src/checks/automatic/aria-valid-attr-value.js +28 -7
- package/src/checks/automatic/aria-valid-attr.js +17 -5
- package/src/checks/automatic/autocomplete-valid.js +74 -16
- package/src/checks/automatic/avoid-inline-spacing.js +20 -6
- package/src/checks/automatic/binary-control-name-present.js +60 -50
- package/src/checks/automatic/button-name-present.js +48 -18
- package/src/checks/automatic/bypass-blocks-present.js +50 -25
- package/src/checks/automatic/canvas-text-alternative-present.js +57 -26
- package/src/checks/automatic/combobox-name-present.js +38 -45
- package/src/checks/automatic/contrast-computable.js +361 -341
- package/src/checks/automatic/contrast-enhanced.js +487 -466
- package/src/checks/automatic/contrast-minimum.js +486 -465
- package/src/checks/automatic/css-orientation-lock.js +39 -9
- package/src/checks/automatic/definition-list-children-valid.js +40 -19
- package/src/checks/automatic/deprecated-elements-not-used.js +21 -7
- package/src/checks/automatic/dialog-name-present.js +37 -75
- package/src/checks/automatic/dlitem-parent-valid.js +23 -8
- package/src/checks/automatic/duplicate-id-aria.js +24 -6
- package/src/checks/automatic/embed-text-alternative-present.js +86 -35
- package/src/checks/automatic/form-control-programmatic-label-present.js +79 -196
- package/src/checks/automatic/form-control-single-label.js +47 -10
- package/src/checks/automatic/html-xml-lang-mismatch.js +43 -19
- package/src/checks/automatic/iframe-focusable-content.js +26 -11
- package/src/checks/automatic/iframe-name-present.js +31 -9
- package/src/checks/automatic/iframe-title-unique.js +29 -8
- package/src/checks/automatic/img-alt-present.js +47 -43
- package/src/checks/automatic/input-image-alt-present.js +143 -112
- package/src/checks/automatic/label-in-name.js +50 -22
- package/src/checks/automatic/language-page-present.js +117 -109
- package/src/checks/automatic/link-in-text-block.js +59 -19
- package/src/checks/automatic/link-name-present.js +45 -14
- package/src/checks/automatic/list-children-valid.js +26 -9
- package/src/checks/automatic/listbox-name-present.js +39 -19
- package/src/checks/automatic/listitem-parent-valid.js +18 -6
- package/src/checks/automatic/menuitem-name-present.js +39 -61
- package/src/checks/automatic/meta-refresh-no-exceptions.js +37 -8
- package/src/checks/automatic/meta-refresh-timing-absent.js +28 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +32 -7
- package/src/checks/automatic/meter-name-present.js +36 -33
- package/src/checks/automatic/nested-interactive-controls-absent.js +31 -10
- package/src/checks/automatic/object-text-alternative-present.js +91 -39
- package/src/checks/automatic/option-name-present.js +38 -21
- package/src/checks/automatic/page-title-present.js +26 -7
- package/src/checks/automatic/progressbar-name-present.js +41 -34
- package/src/checks/automatic/role-img-alt-present.js +209 -157
- package/src/checks/automatic/searchbox-name-present.js +39 -19
- package/src/checks/automatic/server-side-image-map-absent.js +23 -8
- package/src/checks/automatic/slider-name-present.js +40 -47
- package/src/checks/automatic/spinbutton-name-present.js +39 -19
- package/src/checks/automatic/summary-name-present.js +37 -17
- package/src/checks/automatic/svg-image-text-alternative-present.js +114 -47
- package/src/checks/automatic/svg-text-alternative-present.js +246 -226
- package/src/checks/automatic/tab-name-present.js +37 -60
- package/src/checks/automatic/table-headers-attr-valid.js +24 -8
- package/src/checks/automatic/table-th-has-data-cells.js +22 -8
- package/src/checks/automatic/target-size-minimum.js +118 -48
- package/src/checks/automatic/td-has-header.js +29 -11
- package/src/checks/automatic/textbox-name-present.js +39 -19
- package/src/checks/automatic/tooltip-name-present.js +37 -18
- package/src/checks/automatic/treeitem-name-present.js +38 -21
- package/src/checks/automatic/valid-lang.js +20 -6
- package/src/checks/automatic/video-poster-text-alternative-present.js +79 -36
- package/src/checks/manual/accesskeys-manual.js +14 -5
- package/src/checks/manual/area-alt-decorative-manual.js +192 -193
- package/src/checks/manual/area-alt-quality-manual.js +182 -141
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +34 -11
- package/src/checks/manual/aria-text-manual.js +14 -6
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +149 -114
- package/src/checks/manual/css-hidden-focus.js +196 -165
- package/src/checks/manual/embed-text-alternative-quality-manual.js +171 -160
- package/src/checks/manual/empty-heading-manual.js +24 -12
- package/src/checks/manual/empty-table-header-manual.js +21 -14
- package/src/checks/manual/focus-order-semantics-manual.js +45 -10
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +207 -246
- package/src/checks/manual/heading-order-manual.js +22 -12
- package/src/checks/manual/identical-links-same-purpose-manual.js +34 -12
- package/src/checks/manual/image-redundant-alt-manual.js +17 -7
- package/src/checks/manual/img-alt-decorative-manual.js +131 -96
- package/src/checks/manual/img-alt-quality-manual.js +176 -127
- package/src/checks/manual/input-image-alt-decorative-manual.js +125 -92
- package/src/checks/manual/input-image-alt-quality-manual.js +125 -92
- package/src/checks/manual/label-title-only-manual.js +14 -5
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -29
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +82 -29
- package/src/checks/manual/landmark-main-is-top-level-manual.js +64 -23
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +54 -23
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +54 -23
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
- package/src/checks/manual/landmark-one-main-manual.js +35 -21
- package/src/checks/manual/landmark-unique-manual.js +73 -45
- package/src/checks/manual/link-name-quality-manual.js +43 -12
- package/src/checks/manual/media-transcript-present-manual.js +35 -22
- package/src/checks/manual/meta-viewport-large-manual.js +25 -6
- package/src/checks/manual/mouse-only-event-handlers-manual.js +38 -11
- package/src/checks/manual/no-autoplay-audio-manual.js +20 -6
- package/src/checks/manual/object-text-alternative-quality-manual.js +175 -154
- package/src/checks/manual/p-as-heading-manual.js +22 -7
- package/src/checks/manual/page-has-heading-one-manual.js +40 -23
- package/src/checks/manual/page-title-patterns-manual.js +87 -51
- package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
- package/src/checks/manual/region-manual.js +275 -62
- package/src/checks/manual/scope-attr-valid-manual.js +11 -9
- package/src/checks/manual/scrollable-region-focusable-manual.js +37 -11
- package/src/checks/manual/skip-link-manual.js +35 -12
- package/src/checks/manual/svg-text-alternative-quality-manual.js +206 -165
- package/src/checks/manual/tabindex-manual.js +11 -9
- package/src/checks/manual/table-duplicate-name-manual.js +17 -7
- package/src/checks/manual/table-fake-caption-manual.js +24 -7
- package/src/checks/manual/video-caption-manual.js +15 -4
- package/src/checks/manual-review.js +56 -12
- package/src/core/aria-helpers.js +1128 -823
- package/src/core/contrast-helpers.js +1217 -1062
- package/src/core/dom-helpers.js +4176 -3886
- package/src/core/dom-runner.js +720 -596
- package/src/core/frame-messaging.js +189 -138
- package/src/core/frame-scan.js +94 -82
- package/src/core/rollup-composites.js +94 -102
- package/src/core/rule-meta.js +71 -35
- package/src/core.js +37939 -29121
- package/src/i18n/en.js +1194 -887
- package/src/i18n/fr.js +1136 -793
- package/src/policy/contracts.js +13 -13
- package/src/policy/resolvePolicy.js +48 -44
- package/src/report.js +502 -0
- package/src/sarif.js +175 -0
- package/surea11y.browser.js +36042 -0
|
@@ -64,7 +64,13 @@ const meta = {
|
|
|
64
64
|
tags: ['wcag21aa', 'wcag134', 'structure', 'atomic', 'automatic'],
|
|
65
65
|
wcagSc: ['1.3.4'],
|
|
66
66
|
normativeMappings: [
|
|
67
|
-
{
|
|
67
|
+
{
|
|
68
|
+
standard: 'WCAG',
|
|
69
|
+
version: '2.2',
|
|
70
|
+
requirement: '1.3.4',
|
|
71
|
+
title: 'Orientation',
|
|
72
|
+
conformanceLevel: 'AA'
|
|
73
|
+
}
|
|
68
74
|
],
|
|
69
75
|
defaultSeverity: 'serious',
|
|
70
76
|
category: 'perceivable',
|
|
@@ -73,13 +79,23 @@ const meta = {
|
|
|
73
79
|
coverage: { facetsBySc: { '1.3.4': ['css-orientation-lock'] } }
|
|
74
80
|
};
|
|
75
81
|
|
|
82
|
+
// This check is inherently whole-document (does the PAGE have this
|
|
83
|
+
// property?), not evaluable per-subtree -- notApplicable when contextSelector
|
|
84
|
+
// scoped this run narrower than the whole document, or when
|
|
85
|
+
// engineOptions.fragment:true was set (see helpers.isWholeDocumentScope).
|
|
86
|
+
function applicability(ctx) {
|
|
87
|
+
return ctx.helpers.isWholeDocumentScope ? ctx.helpers.isWholeDocumentScope() : true;
|
|
88
|
+
}
|
|
89
|
+
|
|
76
90
|
function runInPage(ctx) {
|
|
77
91
|
const { document, helpers, rule } = ctx;
|
|
78
92
|
|
|
79
93
|
const CSS_MEDIA_RULE = 4;
|
|
80
94
|
const CSS_STYLE_RULE = 1;
|
|
81
95
|
|
|
82
|
-
function trim(v) {
|
|
96
|
+
function trim(v) {
|
|
97
|
+
return (v == null ? '' : String(v)).trim();
|
|
98
|
+
}
|
|
83
99
|
|
|
84
100
|
// Converts a rotate()/rotateZ() angle argument (with unit) to degrees.
|
|
85
101
|
// Matches a widely-used reference engine's own getAngleInDegrees: only deg/grad/rad/turn are
|
|
@@ -118,9 +134,13 @@ function runInPage(ctx) {
|
|
|
118
134
|
// near 90 or 270 degrees (mod 90, once the 0/180 case is excluded) is.
|
|
119
135
|
function isLockingRotation(styleDecl) {
|
|
120
136
|
if (!styleDecl) return false;
|
|
121
|
-
const transformVal =
|
|
137
|
+
const transformVal =
|
|
138
|
+
trim(styleDecl.transform) ||
|
|
139
|
+
trim(styleDecl.getPropertyValue ? styleDecl.getPropertyValue('-webkit-transform') : '');
|
|
122
140
|
// The standalone CSS `rotate` property (distinct from `transform: rotate()`) -- that reference engine checks this too.
|
|
123
|
-
const rotatePropVal = trim(
|
|
141
|
+
const rotatePropVal = trim(
|
|
142
|
+
styleDecl.getPropertyValue ? styleDecl.getPropertyValue('rotate') : ''
|
|
143
|
+
);
|
|
124
144
|
|
|
125
145
|
let degrees = rotateDegreesFromTransform(transformVal);
|
|
126
146
|
if (!degrees && rotatePropVal) degrees = angleToDegrees(rotatePropVal);
|
|
@@ -181,9 +201,10 @@ function runInPage(ctx) {
|
|
|
181
201
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
182
202
|
}
|
|
183
203
|
|
|
184
|
-
const target = document.documentElement ||
|
|
204
|
+
const target = document.documentElement || document.body || null;
|
|
185
205
|
const stableSelector = helpers.buildSelector && target ? helpers.buildSelector(target) : 'html';
|
|
186
|
-
const html =
|
|
206
|
+
const html =
|
|
207
|
+
helpers.getOuterHtmlSnippet && target ? helpers.getOuterHtmlSnippet(target) : '<html>';
|
|
187
208
|
|
|
188
209
|
const occurrences = findings.map((f) => ({
|
|
189
210
|
selector: stableSelector,
|
|
@@ -196,11 +217,20 @@ function runInPage(ctx) {
|
|
|
196
217
|
params: { mediaText: f.mediaText, selectorText: f.selectorText }
|
|
197
218
|
},
|
|
198
219
|
data: {
|
|
199
|
-
details: {
|
|
220
|
+
details: {
|
|
221
|
+
reasonCode: 'ORIENTATION_MEDIA_ROTATE_TRANSFORM',
|
|
222
|
+
mediaText: f.mediaText,
|
|
223
|
+
selectorText: f.selectorText
|
|
224
|
+
}
|
|
200
225
|
}
|
|
201
226
|
}));
|
|
202
227
|
|
|
203
|
-
return {
|
|
228
|
+
return {
|
|
229
|
+
ruleId: rule.ruleId,
|
|
230
|
+
outcome: 'fail',
|
|
231
|
+
severity: rule.defaultSeverity || 'serious',
|
|
232
|
+
occurrences
|
|
233
|
+
};
|
|
204
234
|
}
|
|
205
235
|
|
|
206
|
-
module.exports = { id, meta, runInPage };
|
|
236
|
+
module.exports = { id, meta, runInPage, applicability };
|
|
@@ -35,7 +35,8 @@ const id = 'definition-list-children-valid';
|
|
|
35
35
|
|
|
36
36
|
const meta = {
|
|
37
37
|
title: 'Description lists must be structured correctly',
|
|
38
|
-
description:
|
|
38
|
+
description:
|
|
39
|
+
'Checks that <dl> elements only directly contain <dt>/<dd> groups (optionally wrapped in one <div>), <script>, <template>, or <style>.',
|
|
39
40
|
i18n: {
|
|
40
41
|
titleKey: 'definitionListChildrenValid_title',
|
|
41
42
|
descriptionKey: 'definitionListChildrenValid_description'
|
|
@@ -44,7 +45,13 @@ const meta = {
|
|
|
44
45
|
tags: ['wcag2a', 'wcag131', 'structure', 'atomic', 'automatic', 'list'],
|
|
45
46
|
wcagSc: ['1.3.1'],
|
|
46
47
|
normativeMappings: [
|
|
47
|
-
{
|
|
48
|
+
{
|
|
49
|
+
standard: 'WCAG',
|
|
50
|
+
version: '2.2',
|
|
51
|
+
requirement: '1.3.1',
|
|
52
|
+
title: 'Info and Relationships',
|
|
53
|
+
conformanceLevel: 'A'
|
|
54
|
+
}
|
|
48
55
|
],
|
|
49
56
|
defaultSeverity: 'serious',
|
|
50
57
|
category: 'perceivable',
|
|
@@ -54,7 +61,7 @@ const meta = {
|
|
|
54
61
|
};
|
|
55
62
|
|
|
56
63
|
function runInPage(ctx) {
|
|
57
|
-
const {
|
|
64
|
+
const { helpers, rule } = ctx;
|
|
58
65
|
|
|
59
66
|
// Declared inside runInPage — see scripts/build-core.js header
|
|
60
67
|
// ("runInPage MUST be self-contained").
|
|
@@ -89,10 +96,17 @@ function runInPage(ctx) {
|
|
|
89
96
|
const invalidTags = [];
|
|
90
97
|
for (const node of flattened) {
|
|
91
98
|
const tag = node.tagName.toLowerCase();
|
|
92
|
-
if (tag === 'dt') {
|
|
93
|
-
|
|
99
|
+
if (tag === 'dt') {
|
|
100
|
+
hasDt = true;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (tag === 'dd') {
|
|
104
|
+
hasDd = true;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
94
107
|
if (!PASSTHROUGH_TAGS.has(tag)) invalidTags.push(tag);
|
|
95
108
|
}
|
|
109
|
+
const dedupedInvalidTags = [...new Set(invalidTags)];
|
|
96
110
|
|
|
97
111
|
// Verified 2026-07-20 against a widely-used reference engine's definition-list check:
|
|
98
112
|
// the dt/dd pairing is only required "when not empty" — a <dl> with
|
|
@@ -101,19 +115,21 @@ function runInPage(ctx) {
|
|
|
101
115
|
// is vacuously fine, not a violation. Only an UNBALANCED pairing (dt
|
|
102
116
|
// present without any dd, or vice versa) is a real structural problem.
|
|
103
117
|
const reasonCode = invalidTags.length
|
|
104
|
-
|
|
105
|
-
|
|
118
|
+
? 'DL_INVALID_CHILD'
|
|
119
|
+
: (hasDt || hasDd) && !(hasDt && hasDd)
|
|
120
|
+
? 'DL_NO_DT_DD'
|
|
121
|
+
: null;
|
|
106
122
|
if (!reasonCode) continue;
|
|
107
123
|
|
|
108
124
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
109
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
125
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
110
126
|
|
|
111
127
|
const summary = invalidTags.length
|
|
112
|
-
|
|
113
|
-
|
|
128
|
+
? 'This description list contains a direct or wrapped child that is not part of a dt/dd group.'
|
|
129
|
+
: 'This description list has no <dt>/<dd> term-definition group.';
|
|
114
130
|
const hint = invalidTags.length
|
|
115
|
-
|
|
116
|
-
|
|
131
|
+
? 'Only use <dt>/<dd> (optionally wrapped in one <div>), <script>, <template>, or <style> inside <dl>.'
|
|
132
|
+
: 'Add at least one <dt>/<dd> pair inside this <dl>.';
|
|
117
133
|
|
|
118
134
|
occurrences.push({
|
|
119
135
|
selector: stableSelector,
|
|
@@ -122,12 +138,12 @@ function runInPage(ctx) {
|
|
|
122
138
|
hint,
|
|
123
139
|
i18n: {
|
|
124
140
|
summaryKey: invalidTags.length
|
|
125
|
-
|
|
126
|
-
|
|
141
|
+
? 'definitionListChildrenValid_summary_fail_invalidChild'
|
|
142
|
+
: 'definitionListChildrenValid_summary_fail_noDtDd',
|
|
127
143
|
hintKey: invalidTags.length
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
params: { invalidChildren:
|
|
144
|
+
? 'definitionListChildrenValid_hint_fail_invalidChild'
|
|
145
|
+
: 'definitionListChildrenValid_hint_fail_noDtDd',
|
|
146
|
+
params: { invalidChildren: dedupedInvalidTags.join(', ') }
|
|
131
147
|
},
|
|
132
148
|
data: {
|
|
133
149
|
details: { reasonCode, invalidChildren: invalidTags }
|
|
@@ -139,9 +155,14 @@ function runInPage(ctx) {
|
|
|
139
155
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
140
156
|
}
|
|
141
157
|
if (occurrences.length) {
|
|
142
|
-
return {
|
|
158
|
+
return {
|
|
159
|
+
ruleId: rule.ruleId,
|
|
160
|
+
outcome: 'fail',
|
|
161
|
+
severity: rule.defaultSeverity || 'serious',
|
|
162
|
+
occurrences
|
|
163
|
+
};
|
|
143
164
|
}
|
|
144
165
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
145
166
|
}
|
|
146
167
|
|
|
147
|
-
module.exports = { id, meta, runInPage };
|
|
168
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -29,7 +29,8 @@ const id = 'deprecated-elements-not-used';
|
|
|
29
29
|
|
|
30
30
|
const meta = {
|
|
31
31
|
title: 'Obsolete non-stoppable elements (<blink>, <marquee>) must not be used',
|
|
32
|
-
description:
|
|
32
|
+
description:
|
|
33
|
+
'Checks that deprecated, non-standard HTML elements whose blinking/scrolling content cannot be paused, stopped, or hidden by the user (<blink>, <marquee>) are not present.',
|
|
33
34
|
i18n: {
|
|
34
35
|
titleKey: 'deprecatedElements_title',
|
|
35
36
|
descriptionKey: 'deprecatedElements_description'
|
|
@@ -38,7 +39,13 @@ const meta = {
|
|
|
38
39
|
tags: ['wcag2a', 'wcag222', 'structure', 'atomic', 'automatic'],
|
|
39
40
|
wcagSc: ['2.2.2'],
|
|
40
41
|
normativeMappings: [
|
|
41
|
-
{
|
|
42
|
+
{
|
|
43
|
+
standard: 'WCAG',
|
|
44
|
+
version: '2.2',
|
|
45
|
+
requirement: '2.2.2',
|
|
46
|
+
title: 'Pause, Stop, Hide',
|
|
47
|
+
conformanceLevel: 'A'
|
|
48
|
+
}
|
|
42
49
|
],
|
|
43
50
|
defaultSeverity: 'serious',
|
|
44
51
|
category: 'operable',
|
|
@@ -48,9 +55,11 @@ const meta = {
|
|
|
48
55
|
};
|
|
49
56
|
|
|
50
57
|
function runInPage(ctx) {
|
|
51
|
-
const {
|
|
58
|
+
const { helpers, rule } = ctx;
|
|
52
59
|
|
|
53
|
-
const nodes = helpers.queryAllSmart
|
|
60
|
+
const nodes = helpers.queryAllSmart
|
|
61
|
+
? helpers.queryAllSmart('blink, marquee')
|
|
62
|
+
: helpers.queryAll('blink, marquee');
|
|
54
63
|
|
|
55
64
|
const occurrences = [];
|
|
56
65
|
let applicableCount = 0;
|
|
@@ -62,7 +71,7 @@ function runInPage(ctx) {
|
|
|
62
71
|
|
|
63
72
|
const tag = el.tagName.toLowerCase();
|
|
64
73
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
65
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
74
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
66
75
|
|
|
67
76
|
occurrences.push({
|
|
68
77
|
selector: stableSelector,
|
|
@@ -84,9 +93,14 @@ function runInPage(ctx) {
|
|
|
84
93
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
85
94
|
}
|
|
86
95
|
if (occurrences.length) {
|
|
87
|
-
return {
|
|
96
|
+
return {
|
|
97
|
+
ruleId: rule.ruleId,
|
|
98
|
+
outcome: 'fail',
|
|
99
|
+
severity: rule.defaultSeverity || 'serious',
|
|
100
|
+
occurrences
|
|
101
|
+
};
|
|
88
102
|
}
|
|
89
103
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
90
104
|
}
|
|
91
105
|
|
|
92
|
-
module.exports = { id, meta, runInPage };
|
|
106
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -4,16 +4,23 @@ const id = 'dialog-name-present';
|
|
|
4
4
|
|
|
5
5
|
const meta = {
|
|
6
6
|
title: 'Dialogs have an accessible name',
|
|
7
|
-
description:
|
|
7
|
+
description:
|
|
8
|
+
'Checks that elements with role="dialog" or role="alertdialog" expose a non-empty accessible name.',
|
|
8
9
|
i18n: {
|
|
9
10
|
titleKey: 'dialogNamePresent_title',
|
|
10
11
|
descriptionKey: 'dialogNamePresent_description'
|
|
11
12
|
},
|
|
12
13
|
helpUrl: null,
|
|
13
|
-
tags: ['wcag2a','wcag412','structure','atomic','automatic','name','dialog'],
|
|
14
|
+
tags: ['wcag2a', 'wcag412', 'structure', 'atomic', 'automatic', 'name', 'dialog'],
|
|
14
15
|
wcagSc: ['4.1.2'],
|
|
15
16
|
normativeMappings: [
|
|
16
|
-
{
|
|
17
|
+
{
|
|
18
|
+
standard: 'WCAG',
|
|
19
|
+
version: '2.2',
|
|
20
|
+
requirement: '4.1.2',
|
|
21
|
+
title: 'Name, Role, Value',
|
|
22
|
+
conformanceLevel: 'A'
|
|
23
|
+
}
|
|
17
24
|
],
|
|
18
25
|
defaultSeverity: 'serious',
|
|
19
26
|
category: 'robust',
|
|
@@ -24,69 +31,22 @@ const meta = {
|
|
|
24
31
|
|
|
25
32
|
function runInPage(ctx) {
|
|
26
33
|
const { document, helpers, rule } = ctx;
|
|
27
|
-
const getEligibilityInfo =
|
|
28
|
-
|
|
29
|
-
: null;
|
|
30
|
-
|
|
34
|
+
const getEligibilityInfo =
|
|
35
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
31
36
|
|
|
32
37
|
function normalizeWs(s) {
|
|
33
|
-
return String(s || '')
|
|
38
|
+
return String(s || '')
|
|
39
|
+
.replace(/\s+/g, ' ')
|
|
40
|
+
.trim();
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
function getAttr(el, name) {
|
|
37
44
|
try {
|
|
38
45
|
if (!el || !el.getAttribute) return '';
|
|
39
46
|
return normalizeWs(el.getAttribute(name));
|
|
40
|
-
} catch {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
function hasAttr(el, name) {
|
|
44
|
-
try { return !!(el && el.hasAttribute && el.hasAttribute(name)); } catch { return false; }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function isExplicitProgrammatic(el) {
|
|
48
|
-
// Programmatic name mechanisms we treat as authoritative (presence-only):
|
|
49
|
-
// - aria-label (non-empty)
|
|
50
|
-
// - aria-labelledby (non-empty)
|
|
51
|
-
// - title (non-empty) [weak but allowed for presence]
|
|
52
|
-
const al = getAttr(el, 'aria-label');
|
|
53
|
-
if (al) return true;
|
|
54
|
-
const alb = getAttr(el, 'aria-labelledby');
|
|
55
|
-
if (alb) return true;
|
|
56
|
-
const t = getAttr(el, 'title');
|
|
57
|
-
if (t) return true;
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function buildLabelForMap(doc) {
|
|
62
|
-
const map = new Map(); // id -> label element (first)
|
|
63
|
-
try {
|
|
64
|
-
const labels = doc && doc.getElementsByTagName ? doc.getElementsByTagName('label') : [];
|
|
65
|
-
for (let i = 0; i < labels.length; i += 1) {
|
|
66
|
-
const lab = labels[i];
|
|
67
|
-
if (!lab || !lab.getAttribute) continue;
|
|
68
|
-
const f = normalizeWs(lab.getAttribute('for'));
|
|
69
|
-
if (!f) continue;
|
|
70
|
-
if (!map.has(f)) map.set(f, lab);
|
|
71
|
-
}
|
|
72
|
-
} catch {}
|
|
73
|
-
return map;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function getConservativeSubtreeText(document, container) {
|
|
77
|
-
// "Name from content" — recurses into descendants and uses each one's
|
|
78
|
-
// own accessible name (img alt, aria-label/aria-labelledby, title) when
|
|
79
|
-
// it has one, not just literal text nodes. See getContentNameInfo's
|
|
80
|
-
// header comment in src/core/dom-helpers.js for the full rationale
|
|
81
|
-
// (this replaced a text-node-only TreeWalker that missed the common
|
|
82
|
-
// "<a><img alt='...'></a>" logo-link / "<button><img alt='...'></button>"
|
|
83
|
-
// icon-button pattern).
|
|
84
|
-
if (helpers.getContentNameInfo) {
|
|
85
|
-
const info = helpers.getContentNameInfo(container, ctx);
|
|
86
|
-
return info && info.present ? info.value : '';
|
|
47
|
+
} catch {
|
|
48
|
+
return '';
|
|
87
49
|
}
|
|
88
|
-
const t = (container && container.textContent) ? String(container.textContent) : '';
|
|
89
|
-
return t.replace(/\s+/g, ' ').trim();
|
|
90
50
|
}
|
|
91
51
|
|
|
92
52
|
function resolveAriaLabelledbyText(document, el, maxRefs) {
|
|
@@ -114,17 +74,9 @@ function runInPage(ctx) {
|
|
|
114
74
|
return '';
|
|
115
75
|
}
|
|
116
76
|
|
|
117
|
-
function getInputValueName(el) {
|
|
118
|
-
try {
|
|
119
|
-
const type = getAttr(el, 'type').toLowerCase();
|
|
120
|
-
if (type !== 'button' && type !== 'submit' && type !== 'reset') return '';
|
|
121
|
-
const v = getAttr(el, 'value');
|
|
122
|
-
return v;
|
|
123
|
-
} catch { return ''; }
|
|
124
|
-
}
|
|
125
|
-
|
|
126
77
|
function isEligibleAcc(helpers, el, ctx) {
|
|
127
|
-
const fn =
|
|
78
|
+
const fn =
|
|
79
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
128
80
|
if (!fn) return true;
|
|
129
81
|
try {
|
|
130
82
|
const r = fn(el, ctx);
|
|
@@ -135,13 +87,13 @@ function runInPage(ctx) {
|
|
|
135
87
|
}
|
|
136
88
|
}
|
|
137
89
|
|
|
138
|
-
|
|
139
90
|
const occurrences = [];
|
|
140
91
|
let applicableCount = 0;
|
|
141
92
|
|
|
142
93
|
const selector = '[role="dialog"],[role="alertdialog"]';
|
|
143
|
-
const nodes = helpers.queryAllSmart
|
|
144
|
-
|
|
94
|
+
const nodes = helpers.queryAllSmart
|
|
95
|
+
? helpers.queryAllSmart(selector)
|
|
96
|
+
: helpers.queryAll(selector);
|
|
145
97
|
|
|
146
98
|
function evaluate(el) {
|
|
147
99
|
const ariaLabel = getAttr(el, 'aria-label');
|
|
@@ -173,10 +125,16 @@ function runInPage(ctx) {
|
|
|
173
125
|
if (res.ok) continue;
|
|
174
126
|
|
|
175
127
|
const eligInfo = getEligibilityInfo
|
|
176
|
-
|
|
177
|
-
|
|
128
|
+
? (() => {
|
|
129
|
+
try {
|
|
130
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
})()
|
|
135
|
+
: null;
|
|
178
136
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
179
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
137
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
180
138
|
|
|
181
139
|
occurrences.push({
|
|
182
140
|
selector: stableSelector,
|
|
@@ -195,12 +153,16 @@ function runInPage(ctx) {
|
|
|
195
153
|
});
|
|
196
154
|
}
|
|
197
155
|
|
|
198
|
-
|
|
199
156
|
if (applicableCount === 0) {
|
|
200
157
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
201
158
|
}
|
|
202
159
|
if (occurrences.length) {
|
|
203
|
-
return {
|
|
160
|
+
return {
|
|
161
|
+
ruleId: rule.ruleId,
|
|
162
|
+
outcome: 'fail',
|
|
163
|
+
severity: rule.defaultSeverity || 'minor',
|
|
164
|
+
occurrences
|
|
165
|
+
};
|
|
204
166
|
}
|
|
205
167
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
206
168
|
}
|
|
@@ -23,7 +23,8 @@ const id = 'dlitem-parent-valid';
|
|
|
23
23
|
|
|
24
24
|
const meta = {
|
|
25
25
|
title: 'Description-list items must be inside a description list',
|
|
26
|
-
description:
|
|
26
|
+
description:
|
|
27
|
+
'Checks that <dt>/<dd> elements are contained by a <dl>, directly or via one wrapping <div>.',
|
|
27
28
|
i18n: {
|
|
28
29
|
titleKey: 'dlitemParentValid_title',
|
|
29
30
|
descriptionKey: 'dlitemParentValid_description'
|
|
@@ -32,7 +33,13 @@ const meta = {
|
|
|
32
33
|
tags: ['wcag2a', 'wcag131', 'structure', 'atomic', 'automatic', 'list'],
|
|
33
34
|
wcagSc: ['1.3.1'],
|
|
34
35
|
normativeMappings: [
|
|
35
|
-
{
|
|
36
|
+
{
|
|
37
|
+
standard: 'WCAG',
|
|
38
|
+
version: '2.2',
|
|
39
|
+
requirement: '1.3.1',
|
|
40
|
+
title: 'Info and Relationships',
|
|
41
|
+
conformanceLevel: 'A'
|
|
42
|
+
}
|
|
36
43
|
],
|
|
37
44
|
defaultSeverity: 'serious',
|
|
38
45
|
category: 'perceivable',
|
|
@@ -42,9 +49,11 @@ const meta = {
|
|
|
42
49
|
};
|
|
43
50
|
|
|
44
51
|
function runInPage(ctx) {
|
|
45
|
-
const {
|
|
52
|
+
const { helpers, rule } = ctx;
|
|
46
53
|
|
|
47
|
-
const nodes = helpers.queryAllSmart
|
|
54
|
+
const nodes = helpers.queryAllSmart
|
|
55
|
+
? helpers.queryAllSmart('dt, dd')
|
|
56
|
+
: helpers.queryAll('dt, dd');
|
|
48
57
|
|
|
49
58
|
const occurrences = [];
|
|
50
59
|
let applicableCount = 0;
|
|
@@ -61,7 +70,8 @@ function runInPage(ctx) {
|
|
|
61
70
|
|
|
62
71
|
if (!valid && parentTag === 'div') {
|
|
63
72
|
const grandparent = parent.parentElement;
|
|
64
|
-
const grandparentTag =
|
|
73
|
+
const grandparentTag =
|
|
74
|
+
grandparent && grandparent.tagName ? grandparent.tagName.toLowerCase() : '';
|
|
65
75
|
valid = grandparentTag === 'dl';
|
|
66
76
|
}
|
|
67
77
|
|
|
@@ -69,7 +79,7 @@ function runInPage(ctx) {
|
|
|
69
79
|
|
|
70
80
|
const tag = el.tagName.toLowerCase();
|
|
71
81
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
72
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
82
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
73
83
|
|
|
74
84
|
occurrences.push({
|
|
75
85
|
selector: stableSelector,
|
|
@@ -91,9 +101,14 @@ function runInPage(ctx) {
|
|
|
91
101
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
92
102
|
}
|
|
93
103
|
if (occurrences.length) {
|
|
94
|
-
return {
|
|
104
|
+
return {
|
|
105
|
+
ruleId: rule.ruleId,
|
|
106
|
+
outcome: 'fail',
|
|
107
|
+
severity: rule.defaultSeverity || 'serious',
|
|
108
|
+
occurrences
|
|
109
|
+
};
|
|
95
110
|
}
|
|
96
111
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
97
112
|
}
|
|
98
113
|
|
|
99
|
-
module.exports = { id, meta, runInPage };
|
|
114
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -29,7 +29,8 @@ const id = 'duplicate-id-aria';
|
|
|
29
29
|
|
|
30
30
|
const meta = {
|
|
31
31
|
title: 'IDs referenced by ARIA must be unique',
|
|
32
|
-
description:
|
|
32
|
+
description:
|
|
33
|
+
'Checks that any id value referenced by an ARIA ID-reference attribute (aria-labelledby, aria-describedby, aria-owns, aria-controls, aria-activedescendant, aria-flowto, aria-errormessage, aria-details) is unique in the document.',
|
|
33
34
|
i18n: {
|
|
34
35
|
titleKey: 'duplicateIdAria_title',
|
|
35
36
|
descriptionKey: 'duplicateIdAria_description'
|
|
@@ -38,7 +39,13 @@ const meta = {
|
|
|
38
39
|
tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
|
|
39
40
|
wcagSc: ['4.1.2'],
|
|
40
41
|
normativeMappings: [
|
|
41
|
-
{
|
|
42
|
+
{
|
|
43
|
+
standard: 'WCAG',
|
|
44
|
+
version: '2.2',
|
|
45
|
+
requirement: '4.1.2',
|
|
46
|
+
title: 'Name, Role, Value',
|
|
47
|
+
conformanceLevel: 'A'
|
|
48
|
+
}
|
|
42
49
|
],
|
|
43
50
|
defaultSeverity: 'serious',
|
|
44
51
|
category: 'robust',
|
|
@@ -95,7 +102,9 @@ function runInPage(ctx) {
|
|
|
95
102
|
|
|
96
103
|
for (const el of els) {
|
|
97
104
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
98
|
-
const html = helpers.getOuterHtmlSnippet
|
|
105
|
+
const html = helpers.getOuterHtmlSnippet
|
|
106
|
+
? helpers.getOuterHtmlSnippet(el)
|
|
107
|
+
: el.outerHTML || '';
|
|
99
108
|
|
|
100
109
|
occurrences.push({
|
|
101
110
|
selector: stableSelector,
|
|
@@ -108,7 +117,11 @@ function runInPage(ctx) {
|
|
|
108
117
|
params: { id: refId, duplicateCount: String(els.length) }
|
|
109
118
|
},
|
|
110
119
|
data: {
|
|
111
|
-
details: {
|
|
120
|
+
details: {
|
|
121
|
+
reasonCode: 'DUPLICATE_ID_ARIA_REFERENCED',
|
|
122
|
+
id: refId,
|
|
123
|
+
duplicateCount: els.length
|
|
124
|
+
}
|
|
112
125
|
}
|
|
113
126
|
});
|
|
114
127
|
}
|
|
@@ -118,9 +131,14 @@ function runInPage(ctx) {
|
|
|
118
131
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
119
132
|
}
|
|
120
133
|
if (occurrences.length) {
|
|
121
|
-
return {
|
|
134
|
+
return {
|
|
135
|
+
ruleId: rule.ruleId,
|
|
136
|
+
outcome: 'fail',
|
|
137
|
+
severity: rule.defaultSeverity || 'serious',
|
|
138
|
+
occurrences
|
|
139
|
+
};
|
|
122
140
|
}
|
|
123
141
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
124
142
|
}
|
|
125
143
|
|
|
126
|
-
module.exports = { id, meta, runInPage };
|
|
144
|
+
module.exports = { id, meta, runInPage };
|