@surea11y/core 1.2.0 → 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 +37 -7
- package/LICENSE +373 -21
- package/README.md +67 -1
- package/bin/core.js +144 -19
- package/docs/API_STABILITY.md +1 -1
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/CLI.md +54 -1
- package/docs/ENGINE_OPTIONS.md +2 -0
- package/docs/INTEGRATION.md +18 -0
- package/docs/OUTPUT_SCHEMA.md +1 -1
- 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 +42 -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 +55 -16
- 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 +42 -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 +30 -8
- 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 +34 -18
- 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 +109 -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 +28 -7
- package/src/checks/automatic/meta-refresh-timing-absent.js +20 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +24 -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 +17 -6
- 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 -7
- package/src/checks/manual/empty-table-header-manual.js +17 -6
- 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 -7
- 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 +66 -16
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +53 -16
- package/src/checks/manual/landmark-main-is-top-level-manual.js +35 -10
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +29 -10
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +29 -10
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
- package/src/checks/manual/landmark-one-main-manual.js +26 -20
- package/src/checks/manual/landmark-unique-manual.js +41 -15
- 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 +16 -5
- 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 +31 -22
- package/src/checks/manual/page-title-patterns-manual.js +78 -50
- package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
- package/src/checks/manual/region-manual.js +241 -48
- package/src/checks/manual/scope-attr-valid-manual.js +10 -3
- 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 +10 -3
- 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 +1127 -886
- package/src/core/contrast-helpers.js +1217 -1062
- package/src/core/dom-helpers.js +4175 -3917
- package/src/core/dom-runner.js +720 -604
- 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 +58 -41
- package/src/core.js +36552 -29111
- package/src/i18n/en.js +1194 -889
- package/src/i18n/fr.js +1136 -795
- package/src/policy/contracts.js +13 -13
- package/src/policy/resolvePolicy.js +48 -44
- package/src/report.js +63 -43
- package/src/sarif.js +175 -0
- package/surea11y.browser.js +36042 -0
|
@@ -10,10 +10,16 @@ const meta = {
|
|
|
10
10
|
descriptionKey: 'listboxNamePresent_description'
|
|
11
11
|
},
|
|
12
12
|
helpUrl: null,
|
|
13
|
-
tags: [
|
|
13
|
+
tags: ['wcag2a', 'wcag412', 'forms', 'atomic', 'automatic', 'name', 'listbox'],
|
|
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,20 +30,22 @@ const meta = {
|
|
|
24
30
|
|
|
25
31
|
function runInPage(ctx) {
|
|
26
32
|
const { document, helpers, rule } = ctx;
|
|
27
|
-
const getEligibilityInfo =
|
|
28
|
-
|
|
29
|
-
: null;
|
|
30
|
-
|
|
33
|
+
const getEligibilityInfo =
|
|
34
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
31
35
|
|
|
32
36
|
function normalizeWs(s) {
|
|
33
|
-
return String(s || '')
|
|
37
|
+
return String(s || '')
|
|
38
|
+
.replace(/\s+/g, ' ')
|
|
39
|
+
.trim();
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
function getAttr(el, name) {
|
|
37
43
|
try {
|
|
38
44
|
if (!el || !el.getAttribute) return '';
|
|
39
45
|
return normalizeWs(el.getAttribute(name));
|
|
40
|
-
} catch {
|
|
46
|
+
} catch {
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
function getConservativeSubtreeText(document, container) {
|
|
@@ -52,7 +60,7 @@ function runInPage(ctx) {
|
|
|
52
60
|
const info = helpers.getContentNameInfo(container, ctx);
|
|
53
61
|
return info && info.present ? info.value : '';
|
|
54
62
|
}
|
|
55
|
-
const t =
|
|
63
|
+
const t = container && container.textContent ? String(container.textContent) : '';
|
|
56
64
|
return t.replace(/\s+/g, ' ').trim();
|
|
57
65
|
}
|
|
58
66
|
|
|
@@ -100,7 +108,8 @@ function runInPage(ctx) {
|
|
|
100
108
|
}
|
|
101
109
|
|
|
102
110
|
function isEligibleAcc(helpers, el, ctx) {
|
|
103
|
-
const fn =
|
|
111
|
+
const fn =
|
|
112
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
104
113
|
if (!fn) return true;
|
|
105
114
|
try {
|
|
106
115
|
const r = fn(el, ctx);
|
|
@@ -126,12 +135,13 @@ function runInPage(ctx) {
|
|
|
126
135
|
return map;
|
|
127
136
|
}
|
|
128
137
|
|
|
129
|
-
|
|
130
138
|
const occurrences = [];
|
|
131
139
|
let applicableCount = 0;
|
|
132
140
|
|
|
133
|
-
const selector =
|
|
134
|
-
const nodes = helpers.queryAllSmart
|
|
141
|
+
const selector = '[role="listbox"]';
|
|
142
|
+
const nodes = helpers.queryAllSmart
|
|
143
|
+
? helpers.queryAllSmart(selector)
|
|
144
|
+
: helpers.queryAll(selector);
|
|
135
145
|
|
|
136
146
|
// Precompute label[for] map for listbox elements that are labelable
|
|
137
147
|
// native form controls (e.g. <select multiple role="listbox">).
|
|
@@ -200,16 +210,22 @@ function runInPage(ctx) {
|
|
|
200
210
|
if (res.ok) continue;
|
|
201
211
|
|
|
202
212
|
const eligInfo = getEligibilityInfo
|
|
203
|
-
|
|
204
|
-
|
|
213
|
+
? (() => {
|
|
214
|
+
try {
|
|
215
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
216
|
+
} catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
})()
|
|
220
|
+
: null;
|
|
205
221
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
206
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
222
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
207
223
|
|
|
208
224
|
occurrences.push({
|
|
209
225
|
selector: stableSelector,
|
|
210
226
|
html,
|
|
211
227
|
summary: 'This element has no accessible name.',
|
|
212
|
-
hint:
|
|
228
|
+
hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this listbox's accessible name.",
|
|
213
229
|
i18n: {
|
|
214
230
|
summaryKey: 'listboxNamePresent_summary_fail',
|
|
215
231
|
hintKey: 'listboxNamePresent_hint_fail',
|
|
@@ -222,12 +238,16 @@ function runInPage(ctx) {
|
|
|
222
238
|
});
|
|
223
239
|
}
|
|
224
240
|
|
|
225
|
-
|
|
226
241
|
if (applicableCount === 0) {
|
|
227
242
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
228
243
|
}
|
|
229
244
|
if (occurrences.length) {
|
|
230
|
-
return {
|
|
245
|
+
return {
|
|
246
|
+
ruleId: rule.ruleId,
|
|
247
|
+
outcome: 'fail',
|
|
248
|
+
severity: rule.defaultSeverity || 'minor',
|
|
249
|
+
occurrences
|
|
250
|
+
};
|
|
231
251
|
}
|
|
232
252
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
233
253
|
}
|
|
@@ -37,7 +37,8 @@ const id = 'listitem-parent-valid';
|
|
|
37
37
|
|
|
38
38
|
const meta = {
|
|
39
39
|
title: 'List items must be inside a list container',
|
|
40
|
-
description:
|
|
40
|
+
description:
|
|
41
|
+
'Checks that <li> elements are contained by <ul>, <ol>, or an element with role="list".',
|
|
41
42
|
i18n: {
|
|
42
43
|
titleKey: 'listitemParentValid_title',
|
|
43
44
|
descriptionKey: 'listitemParentValid_description'
|
|
@@ -46,7 +47,13 @@ const meta = {
|
|
|
46
47
|
tags: ['wcag2a', 'wcag131', 'structure', 'atomic', 'automatic', 'list'],
|
|
47
48
|
wcagSc: ['1.3.1'],
|
|
48
49
|
normativeMappings: [
|
|
49
|
-
{
|
|
50
|
+
{
|
|
51
|
+
standard: 'WCAG',
|
|
52
|
+
version: '2.2',
|
|
53
|
+
requirement: '1.3.1',
|
|
54
|
+
title: 'Info and Relationships',
|
|
55
|
+
conformanceLevel: 'A'
|
|
56
|
+
}
|
|
50
57
|
],
|
|
51
58
|
defaultSeverity: 'serious',
|
|
52
59
|
category: 'perceivable',
|
|
@@ -56,7 +63,7 @@ const meta = {
|
|
|
56
63
|
};
|
|
57
64
|
|
|
58
65
|
function runInPage(ctx) {
|
|
59
|
-
const {
|
|
66
|
+
const { helpers, rule } = ctx;
|
|
60
67
|
|
|
61
68
|
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('li') : helpers.queryAll('li');
|
|
62
69
|
|
|
@@ -87,7 +94,7 @@ function runInPage(ctx) {
|
|
|
87
94
|
if (valid) continue;
|
|
88
95
|
|
|
89
96
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
90
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
97
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
91
98
|
|
|
92
99
|
occurrences.push({
|
|
93
100
|
selector: stableSelector,
|
|
@@ -109,9 +116,14 @@ function runInPage(ctx) {
|
|
|
109
116
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
110
117
|
}
|
|
111
118
|
if (occurrences.length) {
|
|
112
|
-
return {
|
|
119
|
+
return {
|
|
120
|
+
ruleId: rule.ruleId,
|
|
121
|
+
outcome: 'fail',
|
|
122
|
+
severity: rule.defaultSeverity || 'serious',
|
|
123
|
+
occurrences
|
|
124
|
+
};
|
|
113
125
|
}
|
|
114
126
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
115
127
|
}
|
|
116
128
|
|
|
117
|
-
module.exports = { id, meta, runInPage };
|
|
129
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -4,16 +4,23 @@ const id = 'menuitem-name-present';
|
|
|
4
4
|
|
|
5
5
|
const meta = {
|
|
6
6
|
title: 'Menu items have an accessible name',
|
|
7
|
-
description:
|
|
7
|
+
description:
|
|
8
|
+
'Checks that menu items (role="menuitem*", including checkbox/radio variants) expose a non-empty accessible name.',
|
|
8
9
|
i18n: {
|
|
9
10
|
titleKey: 'menuitemNamePresent_title',
|
|
10
11
|
descriptionKey: 'menuitemNamePresent_description'
|
|
11
12
|
},
|
|
12
13
|
helpUrl: null,
|
|
13
|
-
tags: ['wcag2a','wcag412','navigation','atomic','automatic','name','menuitem'],
|
|
14
|
+
tags: ['wcag2a', 'wcag412', 'navigation', 'atomic', 'automatic', 'name', 'menuitem'],
|
|
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,53 +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;
|
|
47
|
+
} catch {
|
|
48
|
+
return '';
|
|
49
|
+
}
|
|
74
50
|
}
|
|
75
51
|
|
|
76
52
|
function getConservativeSubtreeText(document, container) {
|
|
@@ -85,7 +61,7 @@ function runInPage(ctx) {
|
|
|
85
61
|
const info = helpers.getContentNameInfo(container, ctx);
|
|
86
62
|
return info && info.present ? info.value : '';
|
|
87
63
|
}
|
|
88
|
-
const t =
|
|
64
|
+
const t = container && container.textContent ? String(container.textContent) : '';
|
|
89
65
|
return t.replace(/\s+/g, ' ').trim();
|
|
90
66
|
}
|
|
91
67
|
|
|
@@ -106,17 +82,9 @@ function runInPage(ctx) {
|
|
|
106
82
|
return '';
|
|
107
83
|
}
|
|
108
84
|
|
|
109
|
-
function getInputValueName(el) {
|
|
110
|
-
try {
|
|
111
|
-
const type = getAttr(el, 'type').toLowerCase();
|
|
112
|
-
if (type !== 'button' && type !== 'submit' && type !== 'reset') return '';
|
|
113
|
-
const v = getAttr(el, 'value');
|
|
114
|
-
return v;
|
|
115
|
-
} catch { return ''; }
|
|
116
|
-
}
|
|
117
|
-
|
|
118
85
|
function isEligibleAcc(helpers, el, ctx) {
|
|
119
|
-
const fn =
|
|
86
|
+
const fn =
|
|
87
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
120
88
|
if (!fn) return true;
|
|
121
89
|
try {
|
|
122
90
|
const r = fn(el, ctx);
|
|
@@ -127,13 +95,13 @@ function runInPage(ctx) {
|
|
|
127
95
|
}
|
|
128
96
|
}
|
|
129
97
|
|
|
130
|
-
|
|
131
98
|
const occurrences = [];
|
|
132
99
|
let applicableCount = 0;
|
|
133
100
|
|
|
134
101
|
const selector = '[role="menuitem"],[role="menuitemcheckbox"],[role="menuitemradio"]';
|
|
135
|
-
const nodes = helpers.queryAllSmart
|
|
136
|
-
|
|
102
|
+
const nodes = helpers.queryAllSmart
|
|
103
|
+
? helpers.queryAllSmart(selector)
|
|
104
|
+
: helpers.queryAll(selector);
|
|
137
105
|
|
|
138
106
|
function evaluate(el) {
|
|
139
107
|
const ariaLabel = getAttr(el, 'aria-label');
|
|
@@ -165,10 +133,16 @@ function runInPage(ctx) {
|
|
|
165
133
|
if (res.ok) continue;
|
|
166
134
|
|
|
167
135
|
const eligInfo = getEligibilityInfo
|
|
168
|
-
|
|
169
|
-
|
|
136
|
+
? (() => {
|
|
137
|
+
try {
|
|
138
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
})()
|
|
143
|
+
: null;
|
|
170
144
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
171
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
145
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
172
146
|
|
|
173
147
|
occurrences.push({
|
|
174
148
|
selector: stableSelector,
|
|
@@ -187,12 +161,16 @@ function runInPage(ctx) {
|
|
|
187
161
|
});
|
|
188
162
|
}
|
|
189
163
|
|
|
190
|
-
|
|
191
164
|
if (applicableCount === 0) {
|
|
192
165
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
193
166
|
}
|
|
194
167
|
if (occurrences.length) {
|
|
195
|
-
return {
|
|
168
|
+
return {
|
|
169
|
+
ruleId: rule.ruleId,
|
|
170
|
+
outcome: 'fail',
|
|
171
|
+
severity: rule.defaultSeverity || 'minor',
|
|
172
|
+
occurrences
|
|
173
|
+
};
|
|
196
174
|
}
|
|
197
175
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
198
176
|
}
|
|
@@ -34,7 +34,8 @@ const id = 'meta-refresh-no-exceptions';
|
|
|
34
34
|
|
|
35
35
|
const meta = {
|
|
36
36
|
title: 'Page must not use a meta refresh at all (AAA)',
|
|
37
|
-
description:
|
|
37
|
+
description:
|
|
38
|
+
'Checks that <meta http-equiv="refresh"> is not present at all, regardless of delay — the stricter AAA-level counterpart of the A-level positive-delay-only check.',
|
|
38
39
|
i18n: {
|
|
39
40
|
titleKey: 'metaRefreshNoExceptions_title',
|
|
40
41
|
descriptionKey: 'metaRefreshNoExceptions_description'
|
|
@@ -43,8 +44,20 @@ const meta = {
|
|
|
43
44
|
tags: ['wcag2aaa', 'wcag224', 'wcag325', 'structure', 'atomic', 'automatic'],
|
|
44
45
|
wcagSc: ['2.2.4', '3.2.5'],
|
|
45
46
|
normativeMappings: [
|
|
46
|
-
{
|
|
47
|
-
|
|
47
|
+
{
|
|
48
|
+
standard: 'WCAG',
|
|
49
|
+
version: '2.2',
|
|
50
|
+
requirement: '2.2.4',
|
|
51
|
+
title: 'Interruptions',
|
|
52
|
+
conformanceLevel: 'AAA'
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
standard: 'WCAG',
|
|
56
|
+
version: '2.2',
|
|
57
|
+
requirement: '3.2.5',
|
|
58
|
+
title: 'Change on Request',
|
|
59
|
+
conformanceLevel: 'AAA'
|
|
60
|
+
}
|
|
48
61
|
],
|
|
49
62
|
defaultSeverity: 'moderate',
|
|
50
63
|
category: 'operable',
|
|
@@ -69,7 +82,9 @@ function applicability(ctx) {
|
|
|
69
82
|
function runInPage(ctx) {
|
|
70
83
|
const { document, helpers, rule } = ctx;
|
|
71
84
|
|
|
72
|
-
const nodes = document.querySelectorAll
|
|
85
|
+
const nodes = document.querySelectorAll
|
|
86
|
+
? document.querySelectorAll('meta[http-equiv="refresh" i]')
|
|
87
|
+
: [];
|
|
73
88
|
|
|
74
89
|
const occurrences = [];
|
|
75
90
|
let applicableCount = 0;
|
|
@@ -83,12 +98,13 @@ function runInPage(ctx) {
|
|
|
83
98
|
applicableCount += 1;
|
|
84
99
|
|
|
85
100
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
86
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
101
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
87
102
|
|
|
88
103
|
occurrences.push({
|
|
89
104
|
selector: stableSelector,
|
|
90
105
|
html,
|
|
91
|
-
summary:
|
|
106
|
+
summary:
|
|
107
|
+
'This page uses a meta refresh, which is an automatic context change not initiated by the user.',
|
|
92
108
|
hint: 'Remove the meta refresh; trigger the redirect/refresh only in response to a user action instead.',
|
|
93
109
|
i18n: {
|
|
94
110
|
summaryKey: 'metaRefreshNoExceptions_summary_fail',
|
|
@@ -105,7 +121,12 @@ function runInPage(ctx) {
|
|
|
105
121
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
106
122
|
}
|
|
107
123
|
if (occurrences.length) {
|
|
108
|
-
return {
|
|
124
|
+
return {
|
|
125
|
+
ruleId: rule.ruleId,
|
|
126
|
+
outcome: 'fail',
|
|
127
|
+
severity: rule.defaultSeverity || 'moderate',
|
|
128
|
+
occurrences
|
|
129
|
+
};
|
|
109
130
|
}
|
|
110
131
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
111
132
|
}
|
|
@@ -31,7 +31,8 @@ const id = 'meta-refresh-timing-absent';
|
|
|
31
31
|
|
|
32
32
|
const meta = {
|
|
33
33
|
title: 'Page must not use a timed meta refresh',
|
|
34
|
-
description:
|
|
34
|
+
description:
|
|
35
|
+
'Checks that <meta http-equiv="refresh"> does not impose a positive delay of 20 hours or less.',
|
|
35
36
|
i18n: {
|
|
36
37
|
titleKey: 'metaRefreshTimingAbsent_title',
|
|
37
38
|
descriptionKey: 'metaRefreshTimingAbsent_description'
|
|
@@ -40,7 +41,13 @@ const meta = {
|
|
|
40
41
|
tags: ['wcag2a', 'wcag221', 'structure', 'atomic', 'automatic'],
|
|
41
42
|
wcagSc: ['2.2.1'],
|
|
42
43
|
normativeMappings: [
|
|
43
|
-
{
|
|
44
|
+
{
|
|
45
|
+
standard: 'WCAG',
|
|
46
|
+
version: '2.2',
|
|
47
|
+
requirement: '2.2.1',
|
|
48
|
+
title: 'Timing Adjustable',
|
|
49
|
+
conformanceLevel: 'A'
|
|
50
|
+
}
|
|
44
51
|
],
|
|
45
52
|
defaultSeverity: 'serious',
|
|
46
53
|
category: 'operable',
|
|
@@ -63,7 +70,9 @@ function runInPage(ctx) {
|
|
|
63
70
|
// WCAG 2.2.1 Exception 3: time limits longer than 20 hours are exempt.
|
|
64
71
|
const EXEMPT_DELAY_SECONDS = 20 * 60 * 60;
|
|
65
72
|
|
|
66
|
-
const nodes = document.querySelectorAll
|
|
73
|
+
const nodes = document.querySelectorAll
|
|
74
|
+
? document.querySelectorAll('meta[http-equiv="refresh" i]')
|
|
75
|
+
: [];
|
|
67
76
|
|
|
68
77
|
const occurrences = [];
|
|
69
78
|
let applicableCount = 0;
|
|
@@ -85,7 +94,7 @@ function runInPage(ctx) {
|
|
|
85
94
|
if (delay > EXEMPT_DELAY_SECONDS) continue; // WCAG 2.2.1 Exception 3 (>20 hours)
|
|
86
95
|
|
|
87
96
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
88
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
97
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
89
98
|
|
|
90
99
|
occurrences.push({
|
|
91
100
|
selector: stableSelector,
|
|
@@ -107,9 +116,14 @@ function runInPage(ctx) {
|
|
|
107
116
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
108
117
|
}
|
|
109
118
|
if (occurrences.length) {
|
|
110
|
-
return {
|
|
119
|
+
return {
|
|
120
|
+
ruleId: rule.ruleId,
|
|
121
|
+
outcome: 'fail',
|
|
122
|
+
severity: rule.defaultSeverity || 'serious',
|
|
123
|
+
occurrences
|
|
124
|
+
};
|
|
111
125
|
}
|
|
112
126
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
113
127
|
}
|
|
114
128
|
|
|
115
|
-
module.exports = { id, meta, runInPage, applicability };
|
|
129
|
+
module.exports = { id, meta, runInPage, applicability };
|
|
@@ -24,7 +24,8 @@ const id = 'meta-viewport-zoom-enabled';
|
|
|
24
24
|
|
|
25
25
|
const meta = {
|
|
26
26
|
title: 'Viewport meta tag must not disable zoom',
|
|
27
|
-
description:
|
|
27
|
+
description:
|
|
28
|
+
'Checks that <meta name="viewport"> does not set user-scalable=no or maximum-scale below 2 (200%).',
|
|
28
29
|
i18n: {
|
|
29
30
|
titleKey: 'metaViewportZoomEnabled_title',
|
|
30
31
|
descriptionKey: 'metaViewportZoomEnabled_description'
|
|
@@ -33,7 +34,13 @@ const meta = {
|
|
|
33
34
|
tags: ['wcag2aa', 'wcag144', 'structure', 'atomic', 'automatic'],
|
|
34
35
|
wcagSc: ['1.4.4'],
|
|
35
36
|
normativeMappings: [
|
|
36
|
-
{
|
|
37
|
+
{
|
|
38
|
+
standard: 'WCAG',
|
|
39
|
+
version: '2.2',
|
|
40
|
+
requirement: '1.4.4',
|
|
41
|
+
title: 'Resize Text',
|
|
42
|
+
conformanceLevel: 'AA'
|
|
43
|
+
}
|
|
37
44
|
],
|
|
38
45
|
defaultSeverity: 'serious',
|
|
39
46
|
category: 'perceivable',
|
|
@@ -59,13 +66,18 @@ function runInPage(ctx) {
|
|
|
59
66
|
const eq = pair.indexOf('=');
|
|
60
67
|
if (eq === -1) continue;
|
|
61
68
|
const key = pair.slice(0, eq).trim().toLowerCase();
|
|
62
|
-
const value = pair
|
|
69
|
+
const value = pair
|
|
70
|
+
.slice(eq + 1)
|
|
71
|
+
.trim()
|
|
72
|
+
.toLowerCase();
|
|
63
73
|
if (key) out[key] = value;
|
|
64
74
|
}
|
|
65
75
|
return out;
|
|
66
76
|
}
|
|
67
77
|
|
|
68
|
-
const nodes = document.querySelectorAll
|
|
78
|
+
const nodes = document.querySelectorAll
|
|
79
|
+
? document.querySelectorAll('meta[name="viewport" i]')
|
|
80
|
+
: [];
|
|
69
81
|
|
|
70
82
|
const occurrences = [];
|
|
71
83
|
let applicableCount = 0;
|
|
@@ -96,7 +108,7 @@ function runInPage(ctx) {
|
|
|
96
108
|
if (!reasons.length) continue;
|
|
97
109
|
|
|
98
110
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
99
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
111
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
100
112
|
|
|
101
113
|
occurrences.push({
|
|
102
114
|
selector: stableSelector,
|
|
@@ -118,9 +130,14 @@ function runInPage(ctx) {
|
|
|
118
130
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
119
131
|
}
|
|
120
132
|
if (occurrences.length) {
|
|
121
|
-
return {
|
|
133
|
+
return {
|
|
134
|
+
ruleId: rule.ruleId,
|
|
135
|
+
outcome: 'fail',
|
|
136
|
+
severity: rule.defaultSeverity || 'serious',
|
|
137
|
+
occurrences
|
|
138
|
+
};
|
|
122
139
|
}
|
|
123
140
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
124
141
|
}
|
|
125
142
|
|
|
126
|
-
module.exports = { id, meta, runInPage, applicability };
|
|
143
|
+
module.exports = { id, meta, runInPage, applicability };
|