@surea11y/core 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -7
- package/LICENSE +373 -21
- package/README.md +175 -35
- package/bin/surea11y-core.js +20 -0
- package/docs/API_STABILITY.md +27 -1
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/ENGINE_OPTIONS.md +2 -0
- package/docs/I18N.md +12 -9
- package/docs/INTEGRATION.md +19 -1
- package/docs/LIMITATIONS.md +1 -1
- package/docs/OUTPUT_SCHEMA.md +1 -1
- package/docs/REPORT.md +1 -1
- package/docs/RULE_CATALOG.md +1 -1
- package/docs/SARIF.md +59 -0
- package/package.json +63 -18
- package/src/baseline.js +0 -0
- package/src/checks/automatic/area-alt-present.js +63 -31
- package/src/checks/automatic/aria-allowed-attr.js +204 -80
- package/src/checks/automatic/aria-allowed-role.js +23 -7
- package/src/checks/automatic/aria-braille-equivalent.js +34 -10
- package/src/checks/automatic/aria-conditional-attr.js +32 -14
- package/src/checks/automatic/aria-deprecated-role.js +26 -11
- package/src/checks/automatic/aria-hidden-body.js +48 -23
- package/src/checks/automatic/aria-hidden-focus.js +420 -66
- package/src/checks/automatic/aria-prohibited-attr.js +327 -60
- package/src/checks/automatic/aria-prohibited-children.js +111 -103
- package/src/checks/automatic/aria-required-attr.js +29 -15
- package/src/checks/automatic/aria-required-children.js +44 -24
- package/src/checks/automatic/aria-required-parent.js +64 -35
- package/src/checks/automatic/aria-role-name-present.js +49 -21
- package/src/checks/automatic/aria-roles-valid.js +24 -12
- package/src/checks/automatic/aria-valid-attr-value.js +46 -22
- package/src/checks/automatic/aria-valid-attr.js +19 -5
- package/src/checks/automatic/autocomplete-valid.js +76 -16
- package/src/checks/automatic/avoid-inline-spacing.js +23 -8
- package/src/checks/automatic/binary-control-name-present.js +62 -50
- package/src/checks/automatic/button-name-present.js +54 -24
- package/src/checks/automatic/bypass-blocks-present.js +51 -32
- package/src/checks/automatic/canvas-text-alternative-present.js +59 -26
- package/src/checks/automatic/combobox-name-present.js +40 -45
- package/src/checks/automatic/contrast-computable.js +363 -341
- package/src/checks/automatic/contrast-enhanced.js +489 -466
- package/src/checks/automatic/contrast-minimum.js +488 -465
- package/src/checks/automatic/css-orientation-lock.js +51 -35
- package/src/checks/automatic/definition-list-children-valid.js +46 -25
- package/src/checks/automatic/deprecated-elements-not-used.js +25 -9
- package/src/checks/automatic/dialog-name-present.js +47 -85
- package/src/checks/automatic/dlitem-parent-valid.js +25 -8
- package/src/checks/automatic/duplicate-id-aria.js +28 -9
- package/src/checks/automatic/embed-text-alternative-present.js +88 -35
- package/src/checks/automatic/form-control-programmatic-label-present.js +81 -196
- package/src/checks/automatic/form-control-single-label.js +50 -14
- package/src/checks/automatic/html-xml-lang-mismatch.js +36 -18
- package/src/checks/automatic/iframe-focusable-content.js +265 -22
- package/src/checks/automatic/iframe-name-present.js +33 -9
- package/src/checks/automatic/iframe-title-unique.js +32 -9
- package/src/checks/automatic/img-alt-present.js +54 -52
- package/src/checks/automatic/input-image-alt-present.js +141 -112
- package/src/checks/automatic/label-in-name.js +65 -41
- package/src/checks/automatic/language-page-present.js +111 -109
- package/src/checks/automatic/link-in-text-block.js +61 -19
- package/src/checks/automatic/link-name-present.js +47 -14
- package/src/checks/automatic/list-children-valid.js +40 -33
- package/src/checks/automatic/listbox-name-present.js +41 -19
- package/src/checks/automatic/listitem-parent-valid.js +48 -13
- package/src/checks/automatic/menuitem-name-present.js +41 -61
- package/src/checks/automatic/meta-refresh-no-exceptions.js +32 -11
- package/src/checks/automatic/meta-refresh-timing-absent.js +22 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +26 -7
- package/src/checks/automatic/meter-name-present.js +40 -36
- package/src/checks/automatic/nested-interactive-controls-absent.js +58 -15
- package/src/checks/automatic/object-text-alternative-present.js +93 -39
- package/src/checks/automatic/option-name-present.js +40 -21
- package/src/checks/automatic/page-title-present.js +19 -6
- package/src/checks/automatic/progressbar-name-present.js +49 -44
- package/src/checks/automatic/role-img-alt-present.js +211 -159
- package/src/checks/automatic/searchbox-name-present.js +41 -19
- package/src/checks/automatic/server-side-image-map-absent.js +27 -11
- package/src/checks/automatic/slider-name-present.js +42 -47
- package/src/checks/automatic/spinbutton-name-present.js +41 -19
- package/src/checks/automatic/summary-name-present.js +39 -17
- package/src/checks/automatic/svg-image-text-alternative-present.js +116 -47
- package/src/checks/automatic/svg-text-alternative-present.js +262 -230
- package/src/checks/automatic/tab-name-present.js +39 -60
- package/src/checks/automatic/table-headers-attr-valid.js +27 -10
- package/src/checks/automatic/table-th-has-data-cells.js +24 -8
- package/src/checks/automatic/target-size-minimum.js +123 -48
- package/src/checks/automatic/td-has-header.js +53 -12
- package/src/checks/automatic/textbox-name-present.js +41 -19
- package/src/checks/automatic/tooltip-name-present.js +39 -18
- package/src/checks/automatic/treeitem-name-present.js +40 -21
- package/src/checks/automatic/valid-lang.js +22 -6
- package/src/checks/automatic/video-poster-text-alternative-present.js +81 -36
- package/src/checks/manual/accesskeys-manual.js +17 -6
- package/src/checks/manual/area-alt-decorative-manual.js +194 -193
- package/src/checks/manual/area-alt-quality-manual.js +184 -141
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +48 -34
- package/src/checks/manual/aria-text-manual.js +20 -11
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +151 -114
- package/src/checks/manual/css-hidden-focus.js +375 -169
- package/src/checks/manual/embed-text-alternative-quality-manual.js +178 -162
- package/src/checks/manual/empty-heading-manual.js +41 -24
- package/src/checks/manual/empty-table-header-manual.js +69 -31
- package/src/checks/manual/focus-order-semantics-manual.js +60 -13
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +209 -246
- package/src/checks/manual/heading-order-manual.js +50 -8
- package/src/checks/manual/identical-links-same-purpose-manual.js +36 -12
- package/src/checks/manual/image-redundant-alt-manual.js +38 -8
- package/src/checks/manual/img-alt-decorative-manual.js +133 -96
- package/src/checks/manual/img-alt-quality-manual.js +178 -127
- package/src/checks/manual/input-image-alt-decorative-manual.js +127 -92
- package/src/checks/manual/input-image-alt-quality-manual.js +127 -92
- package/src/checks/manual/label-title-only-manual.js +44 -28
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -38
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +85 -32
- package/src/checks/manual/landmark-main-is-top-level-manual.js +69 -27
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +45 -33
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +43 -31
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +27 -21
- package/src/checks/manual/landmark-one-main-manual.js +38 -43
- package/src/checks/manual/landmark-unique-manual.js +78 -67
- package/src/checks/manual/link-name-quality-manual.js +45 -12
- package/src/checks/manual/media-transcript-present-manual.js +37 -22
- package/src/checks/manual/meta-viewport-large-manual.js +19 -6
- package/src/checks/manual/mouse-only-event-handlers-manual.js +40 -11
- package/src/checks/manual/no-autoplay-audio-manual.js +22 -6
- package/src/checks/manual/object-text-alternative-quality-manual.js +177 -154
- package/src/checks/manual/p-as-heading-manual.js +24 -7
- package/src/checks/manual/page-has-heading-one-manual.js +42 -32
- package/src/checks/manual/page-title-patterns-manual.js +80 -50
- package/src/checks/manual/presentation-role-conflict-manual.js +101 -47
- package/src/checks/manual/region-manual.js +244 -60
- package/src/checks/manual/scope-attr-valid-manual.js +13 -4
- package/src/checks/manual/scrollable-region-focusable-manual.js +39 -11
- package/src/checks/manual/skip-link-manual.js +42 -18
- package/src/checks/manual/svg-text-alternative-quality-manual.js +208 -165
- package/src/checks/manual/tabindex-manual.js +13 -4
- package/src/checks/manual/table-duplicate-name-manual.js +22 -11
- package/src/checks/manual/table-fake-caption-manual.js +48 -10
- package/src/checks/manual/video-caption-manual.js +17 -4
- package/src/checks/manual-review.js +58 -12
- package/src/core.js +41705 -29650
- package/src/index.js +2 -0
- package/src/report.js +109 -47
- package/src/sarif.js +190 -0
- package/surea11y.browser.js +37774 -0
- package/bin/core.js +0 -348
- package/docs/CLI.md +0 -75
- package/src/catalogs/composites.wcag.js +0 -490
- package/src/checks/rules-and-tags.full.csv +0 -19
- package/src/checks/rules-and-tags.full.json +0 -259
- package/src/core/aria-helpers.js +0 -970
- package/src/core/contrast-helpers.js +0 -1147
- package/src/core/dom-helpers.js +0 -4235
- package/src/core/dom-runner.js +0 -671
- package/src/core/frame-messaging.js +0 -210
- package/src/core/frame-scan.js +0 -178
- package/src/core/rollup-composites.js +0 -135
- package/src/core/rule-meta.js +0 -159
- package/src/coverage/wcag-facets.js +0 -1079
- package/src/coverage/wcag-version-map.js +0 -84
- package/src/i18n/en.js +0 -923
- package/src/i18n/fr.js +0 -844
- package/src/policy/contracts.js +0 -18
- package/src/policy/resolvePolicy.js +0 -55
- package/src/policy/schemas/engine-options.schema.json +0 -103
- package/src/policy/schemas/policy-contract.schema.json +0 -40
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -20,25 +22,17 @@
|
|
|
20
22
|
* display orientation unless that orientation is essential.
|
|
21
23
|
* @implementation-notes
|
|
22
24
|
* - The rotation DEGREE is what makes this the exploit signature, not
|
|
23
|
-
* merely the presence of a `rotate()` function
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* version of this rule flagged the bare presence of "rotate(" in an
|
|
29
|
-
* orientation-media-query rule regardless of angle — a real, confirmed
|
|
30
|
-
* false positive found via the live-DOM cross-engine corpus 2026-07-23
|
|
31
|
-
* on a real page (Times of India) whose `@media (orientation:portrait)`
|
|
32
|
-
* block only rotated a small decorative arrow icon 45 degrees, nothing
|
|
33
|
-
* resembling a page-orientation lock.
|
|
25
|
+
* merely the presence of a `rotate()` function: a small decorative icon
|
|
26
|
+
* rotated 45 degrees inside an orientation media query is not a page
|
|
27
|
+
* lock. Compute the actual rotation angle and only flag ~90/~270 degrees,
|
|
28
|
+
* excluding ~0/~180 (a no-op or a flip, neither of which changes
|
|
29
|
+
* portrait<->landscape).
|
|
34
30
|
* - Only `rotate`/`rotateZ` (transform functions) and the standalone CSS
|
|
35
|
-
* `rotate` property are parsed for degrees
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* value" work already deferred elsewhere in this engine, e.g.
|
|
41
|
-
* `table-th-has-data-cells`'s narrower positional-header algorithm).
|
|
31
|
+
* `rotate` property are parsed for degrees, from the two sources
|
|
32
|
+
* `style.transform`/`style.rotate`; `matrix()`/`matrix3d()`/`rotate3d()`
|
|
33
|
+
* are not decomposed into an equivalent angle (deliberately deferred as
|
|
34
|
+
* the same class of higher-complexity/lower-value work deferred elsewhere,
|
|
35
|
+
* e.g. `table-th-has-data-cells`'s narrower positional-header algorithm).
|
|
42
36
|
* - Cross-origin stylesheets throw on `.cssRules` access (browser
|
|
43
37
|
* security model) and are skipped — same class of limitation as any
|
|
44
38
|
* check that can only see same-origin/inspectable content (compare
|
|
@@ -64,7 +58,13 @@ const meta = {
|
|
|
64
58
|
tags: ['wcag21aa', 'wcag134', 'structure', 'atomic', 'automatic'],
|
|
65
59
|
wcagSc: ['1.3.4'],
|
|
66
60
|
normativeMappings: [
|
|
67
|
-
{
|
|
61
|
+
{
|
|
62
|
+
standard: 'WCAG',
|
|
63
|
+
version: '2.2',
|
|
64
|
+
requirement: '1.3.4',
|
|
65
|
+
title: 'Orientation',
|
|
66
|
+
conformanceLevel: 'AA'
|
|
67
|
+
}
|
|
68
68
|
],
|
|
69
69
|
defaultSeverity: 'serious',
|
|
70
70
|
category: 'perceivable',
|
|
@@ -87,12 +87,14 @@ function runInPage(ctx) {
|
|
|
87
87
|
const CSS_MEDIA_RULE = 4;
|
|
88
88
|
const CSS_STYLE_RULE = 1;
|
|
89
89
|
|
|
90
|
-
function trim(v) {
|
|
90
|
+
function trim(v) {
|
|
91
|
+
return (v == null ? '' : String(v)).trim();
|
|
92
|
+
}
|
|
91
93
|
|
|
92
94
|
// Converts a rotate()/rotateZ() angle argument (with unit) to degrees.
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
95
|
+
// Only deg/grad/rad/turn are recognized; a unitless or unrecognized value
|
|
96
|
+
// contributes 0 (ignored, not treated as a lock -- an unparseable angle
|
|
97
|
+
// isn't a confirmed exploit).
|
|
96
98
|
function angleToDegrees(raw) {
|
|
97
99
|
const m = /(-?[\d.]+)\s*(deg|grad|rad|turn)/i.exec(raw);
|
|
98
100
|
if (!m) return 0;
|
|
@@ -119,16 +121,20 @@ function runInPage(ctx) {
|
|
|
119
121
|
return total;
|
|
120
122
|
}
|
|
121
123
|
|
|
122
|
-
// Whole-page-orientation-lock detection
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
124
|
+
// Whole-page-orientation-lock detection (see @implementation-notes): a
|
|
125
|
+
// rotation near 0 or 180 degrees (mod 180) is a no-op or a flip, neither
|
|
126
|
+
// of which changes portrait<->landscape, so it's NOT a lock; only a
|
|
127
|
+
// rotation near 90 or 270 degrees (mod 90, once the 0/180 case is
|
|
128
|
+
// excluded) is.
|
|
127
129
|
function isLockingRotation(styleDecl) {
|
|
128
130
|
if (!styleDecl) return false;
|
|
129
|
-
const transformVal =
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
const transformVal =
|
|
132
|
+
trim(styleDecl.transform) ||
|
|
133
|
+
trim(styleDecl.getPropertyValue ? styleDecl.getPropertyValue('-webkit-transform') : '');
|
|
134
|
+
// The standalone CSS `rotate` property (distinct from `transform: rotate()`).
|
|
135
|
+
const rotatePropVal = trim(
|
|
136
|
+
styleDecl.getPropertyValue ? styleDecl.getPropertyValue('rotate') : ''
|
|
137
|
+
);
|
|
132
138
|
|
|
133
139
|
let degrees = rotateDegreesFromTransform(transformVal);
|
|
134
140
|
if (!degrees && rotatePropVal) degrees = angleToDegrees(rotatePropVal);
|
|
@@ -189,9 +195,10 @@ function runInPage(ctx) {
|
|
|
189
195
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
190
196
|
}
|
|
191
197
|
|
|
192
|
-
const target = document.documentElement ||
|
|
198
|
+
const target = document.documentElement || document.body || null;
|
|
193
199
|
const stableSelector = helpers.buildSelector && target ? helpers.buildSelector(target) : 'html';
|
|
194
|
-
const html =
|
|
200
|
+
const html =
|
|
201
|
+
helpers.getOuterHtmlSnippet && target ? helpers.getOuterHtmlSnippet(target) : '<html>';
|
|
195
202
|
|
|
196
203
|
const occurrences = findings.map((f) => ({
|
|
197
204
|
selector: stableSelector,
|
|
@@ -204,11 +211,20 @@ function runInPage(ctx) {
|
|
|
204
211
|
params: { mediaText: f.mediaText, selectorText: f.selectorText }
|
|
205
212
|
},
|
|
206
213
|
data: {
|
|
207
|
-
details: {
|
|
214
|
+
details: {
|
|
215
|
+
reasonCode: 'ORIENTATION_MEDIA_ROTATE_TRANSFORM',
|
|
216
|
+
mediaText: f.mediaText,
|
|
217
|
+
selectorText: f.selectorText
|
|
218
|
+
}
|
|
208
219
|
}
|
|
209
220
|
}));
|
|
210
221
|
|
|
211
|
-
return {
|
|
222
|
+
return {
|
|
223
|
+
ruleId: rule.ruleId,
|
|
224
|
+
outcome: 'fail',
|
|
225
|
+
severity: rule.defaultSeverity || 'serious',
|
|
226
|
+
occurrences
|
|
227
|
+
};
|
|
212
228
|
}
|
|
213
229
|
|
|
214
230
|
module.exports = { id, meta, runInPage, applicability };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -18,12 +20,11 @@
|
|
|
18
20
|
* violation (see implementation-notes). Any other direct or wrapped child
|
|
19
21
|
* breaks the description-list semantics assistive technologies rely on.
|
|
20
22
|
* @implementation-notes
|
|
21
|
-
* - Only one level of <div> wrapping is flattened
|
|
22
|
-
* reference engine's definition-list algorithm — a <div> nested inside
|
|
23
|
+
* - Only one level of <div> wrapping is flattened — a <div> nested inside
|
|
23
24
|
* another wrapping <div> is not flattened further and its contents are
|
|
24
25
|
* reported invalid.
|
|
25
|
-
* -
|
|
26
|
-
*
|
|
26
|
+
* - The dt/dd pairing is only required "when not empty". A flattened set
|
|
27
|
+
* with NEITHER dt nor dd
|
|
27
28
|
* — whether from an empty wrapping <div>, only <script>/<template>/
|
|
28
29
|
* <style> content, or a genuinely childless <dl> — is not flagged; only
|
|
29
30
|
* an unbalanced dt/dd pairing is a real structural problem.
|
|
@@ -35,7 +36,8 @@ const id = 'definition-list-children-valid';
|
|
|
35
36
|
|
|
36
37
|
const meta = {
|
|
37
38
|
title: 'Description lists must be structured correctly',
|
|
38
|
-
description:
|
|
39
|
+
description:
|
|
40
|
+
'Checks that <dl> elements only directly contain <dt>/<dd> groups (optionally wrapped in one <div>), <script>, <template>, or <style>.',
|
|
39
41
|
i18n: {
|
|
40
42
|
titleKey: 'definitionListChildrenValid_title',
|
|
41
43
|
descriptionKey: 'definitionListChildrenValid_description'
|
|
@@ -44,7 +46,13 @@ const meta = {
|
|
|
44
46
|
tags: ['wcag2a', 'wcag131', 'structure', 'atomic', 'automatic', 'list'],
|
|
45
47
|
wcagSc: ['1.3.1'],
|
|
46
48
|
normativeMappings: [
|
|
47
|
-
{
|
|
49
|
+
{
|
|
50
|
+
standard: 'WCAG',
|
|
51
|
+
version: '2.2',
|
|
52
|
+
requirement: '1.3.1',
|
|
53
|
+
title: 'Info and Relationships',
|
|
54
|
+
conformanceLevel: 'A'
|
|
55
|
+
}
|
|
48
56
|
],
|
|
49
57
|
defaultSeverity: 'serious',
|
|
50
58
|
category: 'perceivable',
|
|
@@ -54,7 +62,7 @@ const meta = {
|
|
|
54
62
|
};
|
|
55
63
|
|
|
56
64
|
function runInPage(ctx) {
|
|
57
|
-
const {
|
|
65
|
+
const { helpers, rule } = ctx;
|
|
58
66
|
|
|
59
67
|
// Declared inside runInPage — see scripts/build-core.js header
|
|
60
68
|
// ("runInPage MUST be self-contained").
|
|
@@ -89,31 +97,39 @@ function runInPage(ctx) {
|
|
|
89
97
|
const invalidTags = [];
|
|
90
98
|
for (const node of flattened) {
|
|
91
99
|
const tag = node.tagName.toLowerCase();
|
|
92
|
-
if (tag === 'dt') {
|
|
93
|
-
|
|
100
|
+
if (tag === 'dt') {
|
|
101
|
+
hasDt = true;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (tag === 'dd') {
|
|
105
|
+
hasDd = true;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
94
108
|
if (!PASSTHROUGH_TAGS.has(tag)) invalidTags.push(tag);
|
|
95
109
|
}
|
|
110
|
+
const dedupedInvalidTags = [...new Set(invalidTags)];
|
|
96
111
|
|
|
97
|
-
//
|
|
98
|
-
// the dt/dd pairing is only required "when not empty" — a <dl> with
|
|
112
|
+
// The dt/dd pairing is only required "when not empty" — a <dl> with
|
|
99
113
|
// NEITHER dt nor dd (whether genuinely childless after flattening, only
|
|
100
114
|
// passthrough script/template/style content, or an empty wrapping div)
|
|
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 };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -14,8 +16,8 @@
|
|
|
14
16
|
* @expectation
|
|
15
17
|
* Neither element is present. Since their movement can never be paused,
|
|
16
18
|
* stopped, or hidden by the user, presence is itself the violation — this
|
|
17
|
-
* rule has no partial-pass case
|
|
18
|
-
*
|
|
19
|
+
* rule has no partial-pass case (it reports only when the element is
|
|
20
|
+
* found).
|
|
19
21
|
* @implementation-notes
|
|
20
22
|
* - Not rule-gated on isAccTreeEligible: presence in markup is itself the
|
|
21
23
|
* violation, independent of visibility (moving/blinking content inside a
|
|
@@ -29,7 +31,8 @@ const id = 'deprecated-elements-not-used';
|
|
|
29
31
|
|
|
30
32
|
const meta = {
|
|
31
33
|
title: 'Obsolete non-stoppable elements (<blink>, <marquee>) must not be used',
|
|
32
|
-
description:
|
|
34
|
+
description:
|
|
35
|
+
'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
36
|
i18n: {
|
|
34
37
|
titleKey: 'deprecatedElements_title',
|
|
35
38
|
descriptionKey: 'deprecatedElements_description'
|
|
@@ -38,7 +41,13 @@ const meta = {
|
|
|
38
41
|
tags: ['wcag2a', 'wcag222', 'structure', 'atomic', 'automatic'],
|
|
39
42
|
wcagSc: ['2.2.2'],
|
|
40
43
|
normativeMappings: [
|
|
41
|
-
{
|
|
44
|
+
{
|
|
45
|
+
standard: 'WCAG',
|
|
46
|
+
version: '2.2',
|
|
47
|
+
requirement: '2.2.2',
|
|
48
|
+
title: 'Pause, Stop, Hide',
|
|
49
|
+
conformanceLevel: 'A'
|
|
50
|
+
}
|
|
42
51
|
],
|
|
43
52
|
defaultSeverity: 'serious',
|
|
44
53
|
category: 'operable',
|
|
@@ -48,9 +57,11 @@ const meta = {
|
|
|
48
57
|
};
|
|
49
58
|
|
|
50
59
|
function runInPage(ctx) {
|
|
51
|
-
const {
|
|
60
|
+
const { helpers, rule } = ctx;
|
|
52
61
|
|
|
53
|
-
const nodes = helpers.queryAllSmart
|
|
62
|
+
const nodes = helpers.queryAllSmart
|
|
63
|
+
? helpers.queryAllSmart('blink, marquee')
|
|
64
|
+
: helpers.queryAll('blink, marquee');
|
|
54
65
|
|
|
55
66
|
const occurrences = [];
|
|
56
67
|
let applicableCount = 0;
|
|
@@ -62,7 +73,7 @@ function runInPage(ctx) {
|
|
|
62
73
|
|
|
63
74
|
const tag = el.tagName.toLowerCase();
|
|
64
75
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
65
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
76
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
66
77
|
|
|
67
78
|
occurrences.push({
|
|
68
79
|
selector: stableSelector,
|
|
@@ -84,9 +95,14 @@ function runInPage(ctx) {
|
|
|
84
95
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
85
96
|
}
|
|
86
97
|
if (occurrences.length) {
|
|
87
|
-
return {
|
|
98
|
+
return {
|
|
99
|
+
ruleId: rule.ruleId,
|
|
100
|
+
outcome: 'fail',
|
|
101
|
+
severity: rule.defaultSeverity || 'serious',
|
|
102
|
+
occurrences
|
|
103
|
+
};
|
|
88
104
|
}
|
|
89
105
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
90
106
|
}
|
|
91
107
|
|
|
92
|
-
module.exports = { id, meta, runInPage };
|
|
108
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -1,19 +1,28 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
const id = 'dialog-name-present';
|
|
4
6
|
|
|
5
7
|
const meta = {
|
|
6
8
|
title: 'Dialogs have an accessible name',
|
|
7
|
-
description:
|
|
9
|
+
description:
|
|
10
|
+
'Checks that elements with role="dialog" or role="alertdialog" expose a non-empty accessible name.',
|
|
8
11
|
i18n: {
|
|
9
12
|
titleKey: 'dialogNamePresent_title',
|
|
10
13
|
descriptionKey: 'dialogNamePresent_description'
|
|
11
14
|
},
|
|
12
15
|
helpUrl: null,
|
|
13
|
-
tags: ['wcag2a','wcag412','structure','atomic','automatic','name','dialog'],
|
|
16
|
+
tags: ['wcag2a', 'wcag412', 'structure', 'atomic', 'automatic', 'name', 'dialog'],
|
|
14
17
|
wcagSc: ['4.1.2'],
|
|
15
18
|
normativeMappings: [
|
|
16
|
-
{
|
|
19
|
+
{
|
|
20
|
+
standard: 'WCAG',
|
|
21
|
+
version: '2.2',
|
|
22
|
+
requirement: '4.1.2',
|
|
23
|
+
title: 'Name, Role, Value',
|
|
24
|
+
conformanceLevel: 'A'
|
|
25
|
+
}
|
|
17
26
|
],
|
|
18
27
|
defaultSeverity: 'serious',
|
|
19
28
|
category: 'robust',
|
|
@@ -24,69 +33,22 @@ const meta = {
|
|
|
24
33
|
|
|
25
34
|
function runInPage(ctx) {
|
|
26
35
|
const { document, helpers, rule } = ctx;
|
|
27
|
-
const getEligibilityInfo =
|
|
28
|
-
|
|
29
|
-
: null;
|
|
30
|
-
|
|
36
|
+
const getEligibilityInfo =
|
|
37
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
31
38
|
|
|
32
39
|
function normalizeWs(s) {
|
|
33
|
-
return String(s || '')
|
|
40
|
+
return String(s || '')
|
|
41
|
+
.replace(/\s+/g, ' ')
|
|
42
|
+
.trim();
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
function getAttr(el, name) {
|
|
37
46
|
try {
|
|
38
47
|
if (!el || !el.getAttribute) return '';
|
|
39
48
|
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 : '';
|
|
49
|
+
} catch {
|
|
50
|
+
return '';
|
|
87
51
|
}
|
|
88
|
-
const t = (container && container.textContent) ? String(container.textContent) : '';
|
|
89
|
-
return t.replace(/\s+/g, ' ').trim();
|
|
90
52
|
}
|
|
91
53
|
|
|
92
54
|
function resolveAriaLabelledbyText(document, el, maxRefs) {
|
|
@@ -95,16 +57,14 @@ function runInPage(ctx) {
|
|
|
95
57
|
// Resolve via the shared getTextFromIdRefs helper — computes each
|
|
96
58
|
// referenced element's own ACCESSIBLE NAME (aria-label, then
|
|
97
59
|
// aria-labelledby, then a value-like name, then content, then title),
|
|
98
|
-
// not just its content text.
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
// title attribute
|
|
102
|
-
// HTML-AAM, so "name from content" is always
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
// pattern was hand-copied into 15 other *-name-present rules; all
|
|
107
|
-
// fixed the same way.
|
|
60
|
+
// not just its content text. This matters when aria-labelledby points
|
|
61
|
+
// at an <iframe title="..."> (e.g. BBC News' cookie-consent dialog,
|
|
62
|
+
// pointing at an `<iframe title="SP Consent Message">`), whose only
|
|
63
|
+
// name source is its title attribute — an iframe's content is
|
|
64
|
+
// opaque/cross-origin per HTML-AAM, so "name from content" is always
|
|
65
|
+
// empty. Computing only name-from-content of the referenced node would
|
|
66
|
+
// silently miss the title fallback and report no accessible name at
|
|
67
|
+
// all, the same pattern every other *-name-present rule guards against.
|
|
108
68
|
if (helpers.getTextFromIdRefs) {
|
|
109
69
|
try {
|
|
110
70
|
const r = helpers.getTextFromIdRefs(raw, ctx, { maxRefs: maxRefs || 8 });
|
|
@@ -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
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -23,7 +25,8 @@ const id = 'dlitem-parent-valid';
|
|
|
23
25
|
|
|
24
26
|
const meta = {
|
|
25
27
|
title: 'Description-list items must be inside a description list',
|
|
26
|
-
description:
|
|
28
|
+
description:
|
|
29
|
+
'Checks that <dt>/<dd> elements are contained by a <dl>, directly or via one wrapping <div>.',
|
|
27
30
|
i18n: {
|
|
28
31
|
titleKey: 'dlitemParentValid_title',
|
|
29
32
|
descriptionKey: 'dlitemParentValid_description'
|
|
@@ -32,7 +35,13 @@ const meta = {
|
|
|
32
35
|
tags: ['wcag2a', 'wcag131', 'structure', 'atomic', 'automatic', 'list'],
|
|
33
36
|
wcagSc: ['1.3.1'],
|
|
34
37
|
normativeMappings: [
|
|
35
|
-
{
|
|
38
|
+
{
|
|
39
|
+
standard: 'WCAG',
|
|
40
|
+
version: '2.2',
|
|
41
|
+
requirement: '1.3.1',
|
|
42
|
+
title: 'Info and Relationships',
|
|
43
|
+
conformanceLevel: 'A'
|
|
44
|
+
}
|
|
36
45
|
],
|
|
37
46
|
defaultSeverity: 'serious',
|
|
38
47
|
category: 'perceivable',
|
|
@@ -42,9 +51,11 @@ const meta = {
|
|
|
42
51
|
};
|
|
43
52
|
|
|
44
53
|
function runInPage(ctx) {
|
|
45
|
-
const {
|
|
54
|
+
const { helpers, rule } = ctx;
|
|
46
55
|
|
|
47
|
-
const nodes = helpers.queryAllSmart
|
|
56
|
+
const nodes = helpers.queryAllSmart
|
|
57
|
+
? helpers.queryAllSmart('dt, dd')
|
|
58
|
+
: helpers.queryAll('dt, dd');
|
|
48
59
|
|
|
49
60
|
const occurrences = [];
|
|
50
61
|
let applicableCount = 0;
|
|
@@ -61,7 +72,8 @@ function runInPage(ctx) {
|
|
|
61
72
|
|
|
62
73
|
if (!valid && parentTag === 'div') {
|
|
63
74
|
const grandparent = parent.parentElement;
|
|
64
|
-
const grandparentTag =
|
|
75
|
+
const grandparentTag =
|
|
76
|
+
grandparent && grandparent.tagName ? grandparent.tagName.toLowerCase() : '';
|
|
65
77
|
valid = grandparentTag === 'dl';
|
|
66
78
|
}
|
|
67
79
|
|
|
@@ -69,7 +81,7 @@ function runInPage(ctx) {
|
|
|
69
81
|
|
|
70
82
|
const tag = el.tagName.toLowerCase();
|
|
71
83
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
72
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
84
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
73
85
|
|
|
74
86
|
occurrences.push({
|
|
75
87
|
selector: stableSelector,
|
|
@@ -91,9 +103,14 @@ function runInPage(ctx) {
|
|
|
91
103
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
92
104
|
}
|
|
93
105
|
if (occurrences.length) {
|
|
94
|
-
return {
|
|
106
|
+
return {
|
|
107
|
+
ruleId: rule.ruleId,
|
|
108
|
+
outcome: 'fail',
|
|
109
|
+
severity: rule.defaultSeverity || 'serious',
|
|
110
|
+
occurrences
|
|
111
|
+
};
|
|
95
112
|
}
|
|
96
113
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
97
114
|
}
|
|
98
115
|
|
|
99
|
-
module.exports = { id, meta, runInPage };
|
|
116
|
+
module.exports = { id, meta, runInPage };
|