@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
|
@@ -34,126 +34,134 @@
|
|
|
34
34
|
const id = 'html-lang-attr-present';
|
|
35
35
|
|
|
36
36
|
const meta = {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
],
|
|
55
|
-
defaultSeverity: 'serious',
|
|
56
|
-
category: 'understandable',
|
|
57
|
-
type: 'automatic',
|
|
58
|
-
defaultConfidence: 'high',
|
|
59
|
-
coverage: {
|
|
60
|
-
facetsBySc: {
|
|
61
|
-
'3.1.1': ['html-lang-attr-present']
|
|
62
|
-
}
|
|
37
|
+
title: 'Page language is declared',
|
|
38
|
+
description: 'Checks that the default language of the page is programmatically declared.',
|
|
39
|
+
i18n: {
|
|
40
|
+
titleKey: 'html_lang_attr_title',
|
|
41
|
+
descriptionKey: 'html_lang_attr_description'
|
|
42
|
+
},
|
|
43
|
+
helpUrl: null,
|
|
44
|
+
tags: ['wcag2a', 'wcag311', 'structure', 'language', 'automatic', 'atomic'],
|
|
45
|
+
wcagSc: ['3.1.1'],
|
|
46
|
+
normativeMappings: [
|
|
47
|
+
{
|
|
48
|
+
standard: 'WCAG',
|
|
49
|
+
version: '2.2',
|
|
50
|
+
requirement: '3.1.1',
|
|
51
|
+
title: 'Language of Page',
|
|
52
|
+
conformanceLevel: 'A'
|
|
63
53
|
}
|
|
54
|
+
],
|
|
55
|
+
defaultSeverity: 'serious',
|
|
56
|
+
category: 'understandable',
|
|
57
|
+
type: 'automatic',
|
|
58
|
+
defaultConfidence: 'high',
|
|
59
|
+
coverage: {
|
|
60
|
+
facetsBySc: {
|
|
61
|
+
'3.1.1': ['html-lang-attr-present']
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
+
// This check is inherently whole-document (does the PAGE have this
|
|
67
|
+
// property?), not evaluable per-subtree -- notApplicable when contextSelector
|
|
68
|
+
// scoped this run narrower than the whole document, or when
|
|
69
|
+
// engineOptions.fragment:true was set (see helpers.isWholeDocumentScope).
|
|
70
|
+
function applicability(ctx) {
|
|
71
|
+
return ctx.helpers.isWholeDocumentScope ? ctx.helpers.isWholeDocumentScope() : true;
|
|
72
|
+
}
|
|
73
|
+
|
|
66
74
|
function runInPage(ctx) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const tag = (html && html.tagName) ? String(html.tagName).toLowerCase() : '';
|
|
71
|
-
if (!html || tag !== 'html') {
|
|
72
|
-
return {
|
|
73
|
-
ruleId: rule.ruleId,
|
|
74
|
-
outcome: 'notApplicable',
|
|
75
|
-
severity: 'minor',
|
|
76
|
-
occurrences: []
|
|
77
|
-
};
|
|
78
|
-
}
|
|
75
|
+
const { document, rule, helpers } = ctx;
|
|
76
|
+
const html = document && document.documentElement;
|
|
79
77
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
params: params && typeof params === 'object' ? params : {}
|
|
90
|
-
},
|
|
91
|
-
data: {
|
|
92
|
-
visibilityFilter,
|
|
93
|
-
details: details && typeof details === 'object' ? details : {}
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
98
|
-
return [helpers.reportOccurrence(html, baseOccurrence)];
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Never compute selector/snippet in the rule.
|
|
102
|
-
return [{ selector: '', html: '', ...baseOccurrence }];
|
|
103
|
-
}
|
|
78
|
+
const tag = html && html.tagName ? String(html.tagName).toLowerCase() : '';
|
|
79
|
+
if (!html || tag !== 'html') {
|
|
80
|
+
return {
|
|
81
|
+
ruleId: rule.ruleId,
|
|
82
|
+
outcome: 'notApplicable',
|
|
83
|
+
severity: 'minor',
|
|
84
|
+
occurrences: []
|
|
85
|
+
};
|
|
86
|
+
}
|
|
104
87
|
|
|
105
|
-
|
|
106
|
-
const lang = (rawLang || '').trim();
|
|
107
|
-
|
|
108
|
-
if (rawLang === null) {
|
|
109
|
-
return {
|
|
110
|
-
ruleId: rule.ruleId,
|
|
111
|
-
outcome: 'fail',
|
|
112
|
-
severity: rule.defaultSeverity,
|
|
113
|
-
occurrences: pushFail(
|
|
114
|
-
'html_lang_attr_missing_absent',
|
|
115
|
-
'html_lang_attr_hint_missing_absent',
|
|
116
|
-
{},
|
|
117
|
-
{ reasonCode: 'lang-missing', location: 'html' }
|
|
118
|
-
)
|
|
119
|
-
};
|
|
120
|
-
}
|
|
88
|
+
const visibilityFilter = { targetSet: 'acc', accEligible: true, reasons: [] };
|
|
121
89
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
90
|
+
function pushFail(summaryKey, hintKey, params, details) {
|
|
91
|
+
const baseOccurrence = {
|
|
92
|
+
summary: '',
|
|
93
|
+
hint: '',
|
|
94
|
+
i18n: {
|
|
95
|
+
summaryKey,
|
|
96
|
+
hintKey,
|
|
97
|
+
params: params && typeof params === 'object' ? params : {}
|
|
98
|
+
},
|
|
99
|
+
data: {
|
|
100
|
+
visibilityFilter,
|
|
101
|
+
details: details && typeof details === 'object' ? details : {}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
135
104
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
ruleId: rule.ruleId,
|
|
140
|
-
outcome: 'fail',
|
|
141
|
-
severity: rule.defaultSeverity,
|
|
142
|
-
occurrences: pushFail(
|
|
143
|
-
'html_lang_attr_invalid',
|
|
144
|
-
'html_lang_attr_hint_invalid',
|
|
145
|
-
{ lang },
|
|
146
|
-
{ reasonCode: 'lang-invalid-bcp47', lang }
|
|
147
|
-
)
|
|
148
|
-
};
|
|
105
|
+
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
106
|
+
return [helpers.reportOccurrence(html, baseOccurrence)];
|
|
149
107
|
}
|
|
150
108
|
|
|
109
|
+
// Never compute selector/snippet in the rule.
|
|
110
|
+
return [{ selector: '', html: '', ...baseOccurrence }];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const rawLang = html.getAttribute('lang'); // null if missing
|
|
114
|
+
const lang = (rawLang || '').trim();
|
|
115
|
+
|
|
116
|
+
if (rawLang === null) {
|
|
117
|
+
return {
|
|
118
|
+
ruleId: rule.ruleId,
|
|
119
|
+
outcome: 'fail',
|
|
120
|
+
severity: rule.defaultSeverity,
|
|
121
|
+
occurrences: pushFail(
|
|
122
|
+
'html_lang_attr_missing_absent',
|
|
123
|
+
'html_lang_attr_hint_missing_absent',
|
|
124
|
+
{},
|
|
125
|
+
{ reasonCode: 'lang-missing', location: 'html' }
|
|
126
|
+
)
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (lang.length === 0) {
|
|
151
131
|
return {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
132
|
+
ruleId: rule.ruleId,
|
|
133
|
+
outcome: 'fail',
|
|
134
|
+
severity: rule.defaultSeverity,
|
|
135
|
+
occurrences: pushFail(
|
|
136
|
+
'html_lang_attr_missing_empty',
|
|
137
|
+
'html_lang_attr_hint_missing_empty',
|
|
138
|
+
{},
|
|
139
|
+
{ reasonCode: 'lang-empty', location: 'html' }
|
|
140
|
+
)
|
|
156
141
|
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Minimal BCP47 primary subtag check
|
|
145
|
+
if (!/^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/.test(lang)) {
|
|
146
|
+
return {
|
|
147
|
+
ruleId: rule.ruleId,
|
|
148
|
+
outcome: 'fail',
|
|
149
|
+
severity: rule.defaultSeverity,
|
|
150
|
+
occurrences: pushFail(
|
|
151
|
+
'html_lang_attr_invalid',
|
|
152
|
+
'html_lang_attr_hint_invalid',
|
|
153
|
+
{ lang },
|
|
154
|
+
{ reasonCode: 'lang-invalid-bcp47', lang }
|
|
155
|
+
)
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
ruleId: rule.ruleId,
|
|
161
|
+
outcome: 'pass',
|
|
162
|
+
severity: 'minor',
|
|
163
|
+
occurrences: []
|
|
164
|
+
};
|
|
157
165
|
}
|
|
158
166
|
|
|
159
|
-
module.exports = { id, meta, runInPage };
|
|
167
|
+
module.exports = { id, meta, runInPage, applicability };
|
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
const id = 'link-in-text-block';
|
|
48
48
|
|
|
49
49
|
const meta = {
|
|
50
|
-
title:
|
|
50
|
+
title:
|
|
51
|
+
'Links in text blocks must be distinguishable from surrounding text without relying on color alone',
|
|
51
52
|
description:
|
|
52
53
|
'Checks that a link inside a run of text is visually distinguishable from the surrounding text by underline, a font-weight/style difference, or a sufficient (>=3:1) color-contrast difference — not by color alone.',
|
|
53
54
|
i18n: {
|
|
@@ -55,10 +56,26 @@ const meta = {
|
|
|
55
56
|
descriptionKey: 'linkInTextBlock_description'
|
|
56
57
|
},
|
|
57
58
|
helpUrl: null,
|
|
58
|
-
tags: [
|
|
59
|
+
tags: [
|
|
60
|
+
'wcag2a',
|
|
61
|
+
'wcag141',
|
|
62
|
+
'navigation',
|
|
63
|
+
'color',
|
|
64
|
+
'links',
|
|
65
|
+
'contrast',
|
|
66
|
+
'atomic',
|
|
67
|
+
'automatic',
|
|
68
|
+
'dom'
|
|
69
|
+
],
|
|
59
70
|
wcagSc: ['1.4.1'],
|
|
60
71
|
normativeMappings: [
|
|
61
|
-
{
|
|
72
|
+
{
|
|
73
|
+
standard: 'WCAG',
|
|
74
|
+
version: '2.2',
|
|
75
|
+
requirement: '1.4.1',
|
|
76
|
+
title: 'Use of Color',
|
|
77
|
+
conformanceLevel: 'A'
|
|
78
|
+
}
|
|
62
79
|
],
|
|
63
80
|
defaultSeverity: 'serious',
|
|
64
81
|
category: 'perceivable',
|
|
@@ -68,7 +85,7 @@ const meta = {
|
|
|
68
85
|
};
|
|
69
86
|
|
|
70
87
|
function runInPage(ctx) {
|
|
71
|
-
const {
|
|
88
|
+
const { helpers, rule, engineOptions } = ctx;
|
|
72
89
|
|
|
73
90
|
function safeComputedStyle(el) {
|
|
74
91
|
try {
|
|
@@ -77,14 +94,16 @@ function runInPage(ctx) {
|
|
|
77
94
|
const cs = helpers.computedStyle(el);
|
|
78
95
|
if (cs) return cs;
|
|
79
96
|
}
|
|
80
|
-
const view =
|
|
97
|
+
const view =
|
|
98
|
+
el.ownerDocument && el.ownerDocument.defaultView ? el.ownerDocument.defaultView : null;
|
|
81
99
|
if (view && typeof view.getComputedStyle === 'function') return view.getComputedStyle(el);
|
|
82
100
|
} catch {}
|
|
83
101
|
return null;
|
|
84
102
|
}
|
|
85
103
|
|
|
86
104
|
function decorationTokens(cs) {
|
|
87
|
-
const raw =
|
|
105
|
+
const raw =
|
|
106
|
+
`${(cs && cs.textDecorationLine) || ''} ${(cs && cs.textDecoration) || ''}`.toLowerCase();
|
|
88
107
|
return raw.split(/\s+/).filter(Boolean);
|
|
89
108
|
}
|
|
90
109
|
|
|
@@ -102,9 +121,10 @@ function runInPage(ctx) {
|
|
|
102
121
|
return false;
|
|
103
122
|
}
|
|
104
123
|
|
|
105
|
-
const contrastOpts =
|
|
106
|
-
|
|
107
|
-
|
|
124
|
+
const contrastOpts =
|
|
125
|
+
engineOptions && typeof engineOptions.contrast === 'object' && engineOptions.contrast
|
|
126
|
+
? engineOptions.contrast
|
|
127
|
+
: {};
|
|
108
128
|
const mode = contrastOpts.mode === 'auditorAssist' ? 'auditorAssist' : 'strictConformance';
|
|
109
129
|
const rootCanvasFallback =
|
|
110
130
|
typeof contrastOpts.rootCanvasFallback === 'string' && contrastOpts.rootCanvasFallback.trim()
|
|
@@ -114,7 +134,9 @@ function runInPage(ctx) {
|
|
|
114
134
|
const c = helpers && helpers.contrast ? helpers.contrast : null;
|
|
115
135
|
|
|
116
136
|
const selector = 'a[href]';
|
|
117
|
-
const nodes = helpers.queryAllSmart
|
|
137
|
+
const nodes = helpers.queryAllSmart
|
|
138
|
+
? helpers.queryAllSmart(selector)
|
|
139
|
+
: helpers.queryAll(selector);
|
|
118
140
|
|
|
119
141
|
const occurrences = [];
|
|
120
142
|
let applicableCount = 0;
|
|
@@ -123,7 +145,8 @@ function runInPage(ctx) {
|
|
|
123
145
|
if (!el || !el.getAttribute) continue;
|
|
124
146
|
|
|
125
147
|
const eligResult = helpers.isAccTreeEligible ? helpers.isAccTreeEligible(el, ctx) : true;
|
|
126
|
-
const eligible =
|
|
148
|
+
const eligible =
|
|
149
|
+
typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
|
|
127
150
|
if (!eligible) continue;
|
|
128
151
|
|
|
129
152
|
const parent = el.parentElement;
|
|
@@ -156,13 +179,22 @@ function runInPage(ctx) {
|
|
|
156
179
|
if (blocker && blocker.ok === false) {
|
|
157
180
|
// Not confidently computable — skip (benefit of the doubt).
|
|
158
181
|
} else {
|
|
159
|
-
const bg = c.computeEffectiveBackground(el, {
|
|
182
|
+
const bg = c.computeEffectiveBackground(el, {
|
|
183
|
+
contrast: { mode, rootCanvasFallback },
|
|
184
|
+
collectStack: false
|
|
185
|
+
});
|
|
160
186
|
const fgLink = c.computeEffectiveForeground(el);
|
|
161
187
|
const fgParent = c.computeEffectiveForeground(parent);
|
|
162
188
|
|
|
163
189
|
if (bg && bg.ok && bg.rgba && fgLink && fgLink.rgba && fgParent && fgParent.rgba) {
|
|
164
|
-
const fgLinkOpaque =
|
|
165
|
-
|
|
190
|
+
const fgLinkOpaque =
|
|
191
|
+
fgLink.rgba.a < 1
|
|
192
|
+
? c.compositeRgba(fgLink.rgba, bg.rgba)
|
|
193
|
+
: { r: fgLink.rgba.r, g: fgLink.rgba.g, b: fgLink.rgba.b, a: 1 };
|
|
194
|
+
const fgParentOpaque =
|
|
195
|
+
fgParent.rgba.a < 1
|
|
196
|
+
? c.compositeRgba(fgParent.rgba, bg.rgba)
|
|
197
|
+
: { r: fgParent.rgba.r, g: fgParent.rgba.g, b: fgParent.rgba.b, a: 1 };
|
|
166
198
|
|
|
167
199
|
ratio = c.contrastRatio(fgLinkOpaque, fgParentOpaque);
|
|
168
200
|
fgLinkHex = c.rgbToHex ? c.rgbToHex(fgLinkOpaque) : '';
|
|
@@ -178,16 +210,19 @@ function runInPage(ctx) {
|
|
|
178
210
|
|
|
179
211
|
if (!flagged) continue;
|
|
180
212
|
|
|
181
|
-
const eligInfo = helpers.getEligibilityInfo
|
|
213
|
+
const eligInfo = helpers.getEligibilityInfo
|
|
214
|
+
? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' })
|
|
215
|
+
: null;
|
|
182
216
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
183
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
217
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
184
218
|
const tag = (el.tagName || '').toLowerCase();
|
|
185
219
|
const ratioStr = c.round2 ? c.round2(ratio) : String(ratio);
|
|
186
220
|
|
|
187
221
|
occurrences.push({
|
|
188
222
|
selector: stableSelector,
|
|
189
223
|
html,
|
|
190
|
-
summary:
|
|
224
|
+
summary:
|
|
225
|
+
'This link in a block of text relies on color alone to be distinguished from the surrounding text.',
|
|
191
226
|
hint: 'Add an underline, a font-weight/style difference, or increase the color contrast between the link and surrounding text to at least 3:1.',
|
|
192
227
|
i18n: {
|
|
193
228
|
summaryKey: 'linkInTextBlock_summary_fail',
|
|
@@ -209,9 +244,14 @@ function runInPage(ctx) {
|
|
|
209
244
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
210
245
|
}
|
|
211
246
|
if (occurrences.length) {
|
|
212
|
-
return {
|
|
247
|
+
return {
|
|
248
|
+
ruleId: rule.ruleId,
|
|
249
|
+
outcome: 'fail',
|
|
250
|
+
severity: rule.defaultSeverity || 'serious',
|
|
251
|
+
occurrences
|
|
252
|
+
};
|
|
213
253
|
}
|
|
214
254
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
215
255
|
}
|
|
216
256
|
|
|
217
|
-
module.exports = { id, meta, runInPage };
|
|
257
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -13,7 +13,13 @@ const meta = {
|
|
|
13
13
|
tags: ['wcag2a', 'wcag412', 'navigation', 'atomic', 'automatic', 'links', 'name'],
|
|
14
14
|
wcagSc: ['4.1.2'],
|
|
15
15
|
normativeMappings: [
|
|
16
|
-
{
|
|
16
|
+
{
|
|
17
|
+
standard: 'WCAG',
|
|
18
|
+
version: '2.2',
|
|
19
|
+
requirement: '4.1.2',
|
|
20
|
+
title: 'Name, Role, Value',
|
|
21
|
+
conformanceLevel: 'A'
|
|
22
|
+
}
|
|
17
23
|
],
|
|
18
24
|
defaultSeverity: 'serious',
|
|
19
25
|
category: 'robust',
|
|
@@ -24,7 +30,7 @@ const meta = {
|
|
|
24
30
|
};
|
|
25
31
|
|
|
26
32
|
function runInPage(ctx) {
|
|
27
|
-
const {
|
|
33
|
+
const { helpers, rule } = ctx;
|
|
28
34
|
|
|
29
35
|
const occurrences = [];
|
|
30
36
|
let applicableCount = 0;
|
|
@@ -40,48 +46,68 @@ function runInPage(ctx) {
|
|
|
40
46
|
const info = helpers.getContentNameInfo(container, ctx);
|
|
41
47
|
return info && info.present ? info.value : '';
|
|
42
48
|
}
|
|
43
|
-
const t =
|
|
49
|
+
const t = container && container.textContent ? String(container.textContent) : '';
|
|
44
50
|
return t.replace(/\s+/g, ' ').trim();
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
const selector = 'a[href], area[href], [role="link"]';
|
|
48
|
-
const nodes = helpers.queryAllSmart
|
|
54
|
+
const nodes = helpers.queryAllSmart
|
|
55
|
+
? helpers.queryAllSmart(selector)
|
|
56
|
+
: helpers.queryAll(selector);
|
|
49
57
|
|
|
50
58
|
for (const el of nodes) {
|
|
51
59
|
// isAccTreeEligible returns { eligible, reasons }, not a boolean.
|
|
52
60
|
const eligResult = helpers.isAccTreeEligible ? helpers.isAccTreeEligible(el, ctx) : true;
|
|
53
|
-
const eligible =
|
|
61
|
+
const eligible =
|
|
62
|
+
typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
|
|
54
63
|
if (!eligible) continue;
|
|
55
64
|
|
|
56
65
|
applicableCount += 1;
|
|
57
66
|
|
|
58
67
|
const nameInfo = helpers.getAccessibleNameInfo ? helpers.getAccessibleNameInfo(el, ctx) : null;
|
|
59
|
-
const programmaticName =
|
|
68
|
+
const programmaticName = nameInfo && typeof nameInfo.value === 'string' ? nameInfo.value : '';
|
|
60
69
|
|
|
61
70
|
// A native <a href>/<area href> (or [role="link"]) whose role has been
|
|
62
71
|
// overridden to one of these roles is no longer semantically a link —
|
|
63
72
|
// per the WAI-ARIA Accessible Name and Description Computation spec
|
|
64
73
|
// these roles are name-from-author-only, and their rendered content
|
|
65
|
-
// represents a VALUE, not a NAME (
|
|
66
|
-
// verified against its source; see button-name-present.js's identical
|
|
74
|
+
// represents a VALUE, not a NAME (see button-name-present.js's identical
|
|
67
75
|
// fix for a real-world example — <button role="combobox">List</button> —
|
|
68
76
|
// of the same class of bug on a different host tag).
|
|
69
77
|
const role = el.getAttribute ? el.getAttribute('role') : null;
|
|
70
|
-
const roleNorm = String(role || '')
|
|
71
|
-
|
|
78
|
+
const roleNorm = String(role || '')
|
|
79
|
+
.replace(/\s+/g, ' ')
|
|
80
|
+
.trim()
|
|
81
|
+
.toLowerCase();
|
|
82
|
+
const VALUE_ROLES = [
|
|
83
|
+
'textbox',
|
|
84
|
+
'progressbar',
|
|
85
|
+
'scrollbar',
|
|
86
|
+
'slider',
|
|
87
|
+
'spinbutton',
|
|
88
|
+
'combobox',
|
|
89
|
+
'listbox'
|
|
90
|
+
];
|
|
72
91
|
const isContentNameCandidate = !VALUE_ROLES.includes(roleNorm);
|
|
73
92
|
|
|
74
|
-
const contentName =
|
|
93
|
+
const contentName =
|
|
94
|
+
programmaticName.trim().length === 0 && isContentNameCandidate
|
|
95
|
+
? getConservativeSubtreeText(el)
|
|
96
|
+
: '';
|
|
75
97
|
const finalName = (programmaticName.trim().length ? programmaticName : contentName).trim();
|
|
76
98
|
|
|
77
99
|
if (finalName.length === 0) {
|
|
78
100
|
// Only compute the richer eligibility-info payload (used solely for
|
|
79
101
|
// the occurrence's visibilityFilter) once we know an occurrence is
|
|
80
102
|
// actually being built, rather than for every applicable element.
|
|
81
|
-
const eligInfo = helpers.getEligibilityInfo
|
|
103
|
+
const eligInfo = helpers.getEligibilityInfo
|
|
104
|
+
? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' })
|
|
105
|
+
: null;
|
|
82
106
|
|
|
83
107
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
84
|
-
const html = helpers.getOuterHtmlSnippet
|
|
108
|
+
const html = helpers.getOuterHtmlSnippet
|
|
109
|
+
? helpers.getOuterHtmlSnippet(el)
|
|
110
|
+
: el.outerHTML || '';
|
|
85
111
|
const tag = (el.tagName || '').toLowerCase();
|
|
86
112
|
|
|
87
113
|
occurrences.push({
|
|
@@ -118,7 +144,12 @@ function runInPage(ctx) {
|
|
|
118
144
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
119
145
|
}
|
|
120
146
|
if (occurrences.length) {
|
|
121
|
-
return {
|
|
147
|
+
return {
|
|
148
|
+
ruleId: rule.ruleId,
|
|
149
|
+
outcome: 'fail',
|
|
150
|
+
severity: rule.defaultSeverity || 'minor',
|
|
151
|
+
occurrences
|
|
152
|
+
};
|
|
122
153
|
}
|
|
123
154
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
124
155
|
}
|
|
@@ -52,7 +52,8 @@ const id = 'list-children-valid';
|
|
|
52
52
|
|
|
53
53
|
const meta = {
|
|
54
54
|
title: 'Lists must only directly contain list items',
|
|
55
|
-
description:
|
|
55
|
+
description:
|
|
56
|
+
'Checks that <ul>/<ol> elements only have <li>, <script>, or <template> as direct children.',
|
|
56
57
|
i18n: {
|
|
57
58
|
titleKey: 'listChildrenValid_title',
|
|
58
59
|
descriptionKey: 'listChildrenValid_description'
|
|
@@ -61,7 +62,13 @@ const meta = {
|
|
|
61
62
|
tags: ['wcag2a', 'wcag131', 'structure', 'atomic', 'automatic', 'list'],
|
|
62
63
|
wcagSc: ['1.3.1'],
|
|
63
64
|
normativeMappings: [
|
|
64
|
-
{
|
|
65
|
+
{
|
|
66
|
+
standard: 'WCAG',
|
|
67
|
+
version: '2.2',
|
|
68
|
+
requirement: '1.3.1',
|
|
69
|
+
title: 'Info and Relationships',
|
|
70
|
+
conformanceLevel: 'A'
|
|
71
|
+
}
|
|
65
72
|
],
|
|
66
73
|
defaultSeverity: 'serious',
|
|
67
74
|
category: 'perceivable',
|
|
@@ -71,15 +78,18 @@ const meta = {
|
|
|
71
78
|
};
|
|
72
79
|
|
|
73
80
|
function runInPage(ctx) {
|
|
74
|
-
const {
|
|
81
|
+
const { helpers, rule } = ctx;
|
|
75
82
|
|
|
76
83
|
// Declared inside runInPage — see scripts/build-core.js header
|
|
77
84
|
// ("runInPage MUST be self-contained").
|
|
78
85
|
const ALLOWED_CHILD_TAGS = new Set(['li', 'script', 'template']);
|
|
79
86
|
|
|
80
|
-
const nodes = helpers.queryAllSmart
|
|
87
|
+
const nodes = helpers.queryAllSmart
|
|
88
|
+
? helpers.queryAllSmart('ul, ol')
|
|
89
|
+
: helpers.queryAll('ul, ol');
|
|
81
90
|
|
|
82
|
-
const isAccTreeEligible =
|
|
91
|
+
const isAccTreeEligible =
|
|
92
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
83
93
|
|
|
84
94
|
function isExposedToAt(el) {
|
|
85
95
|
if (!isAccTreeEligible) return true;
|
|
@@ -119,9 +129,11 @@ function runInPage(ctx) {
|
|
|
119
129
|
|
|
120
130
|
if (!invalidTags.length) continue;
|
|
121
131
|
|
|
132
|
+
const dedupedInvalidTags = [...new Set(invalidTags)];
|
|
133
|
+
|
|
122
134
|
const tag = el.tagName.toLowerCase();
|
|
123
135
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
124
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
136
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
125
137
|
|
|
126
138
|
occurrences.push({
|
|
127
139
|
selector: stableSelector,
|
|
@@ -131,7 +143,7 @@ function runInPage(ctx) {
|
|
|
131
143
|
i18n: {
|
|
132
144
|
summaryKey: 'listChildrenValid_summary_fail',
|
|
133
145
|
hintKey: 'listChildrenValid_hint_fail',
|
|
134
|
-
params: { element: tag, invalidChildren:
|
|
146
|
+
params: { element: tag, invalidChildren: dedupedInvalidTags.join(', ') }
|
|
135
147
|
},
|
|
136
148
|
data: {
|
|
137
149
|
details: { reasonCode: 'LIST_INVALID_CHILD', element: tag, invalidChildren: invalidTags }
|
|
@@ -143,9 +155,14 @@ function runInPage(ctx) {
|
|
|
143
155
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
144
156
|
}
|
|
145
157
|
if (occurrences.length) {
|
|
146
|
-
return {
|
|
158
|
+
return {
|
|
159
|
+
ruleId: rule.ruleId,
|
|
160
|
+
outcome: 'fail',
|
|
161
|
+
severity: rule.defaultSeverity || 'serious',
|
|
162
|
+
occurrences
|
|
163
|
+
};
|
|
147
164
|
}
|
|
148
165
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
149
166
|
}
|
|
150
167
|
|
|
151
|
-
module.exports = { id, meta, runInPage };
|
|
168
|
+
module.exports = { id, meta, runInPage };
|