@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,19 +1,28 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
const id = 'menuitem-name-present';
|
|
4
6
|
|
|
5
7
|
const meta = {
|
|
6
8
|
title: 'Menu items have an accessible name',
|
|
7
|
-
description:
|
|
9
|
+
description:
|
|
10
|
+
'Checks that menu items (role="menuitem*", including checkbox/radio variants) expose a non-empty accessible name.',
|
|
8
11
|
i18n: {
|
|
9
12
|
titleKey: 'menuitemNamePresent_title',
|
|
10
13
|
descriptionKey: 'menuitemNamePresent_description'
|
|
11
14
|
},
|
|
12
15
|
helpUrl: null,
|
|
13
|
-
tags: ['wcag2a','wcag412','navigation','atomic','automatic','name','menuitem'],
|
|
16
|
+
tags: ['wcag2a', 'wcag412', 'navigation', 'atomic', 'automatic', 'name', 'menuitem'],
|
|
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,53 +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;
|
|
49
|
+
} catch {
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
74
52
|
}
|
|
75
53
|
|
|
76
54
|
function getConservativeSubtreeText(document, container) {
|
|
@@ -85,7 +63,7 @@ function runInPage(ctx) {
|
|
|
85
63
|
const info = helpers.getContentNameInfo(container, ctx);
|
|
86
64
|
return info && info.present ? info.value : '';
|
|
87
65
|
}
|
|
88
|
-
const t =
|
|
66
|
+
const t = container && container.textContent ? String(container.textContent) : '';
|
|
89
67
|
return t.replace(/\s+/g, ' ').trim();
|
|
90
68
|
}
|
|
91
69
|
|
|
@@ -106,17 +84,9 @@ function runInPage(ctx) {
|
|
|
106
84
|
return '';
|
|
107
85
|
}
|
|
108
86
|
|
|
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
87
|
function isEligibleAcc(helpers, el, ctx) {
|
|
119
|
-
const fn =
|
|
88
|
+
const fn =
|
|
89
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
120
90
|
if (!fn) return true;
|
|
121
91
|
try {
|
|
122
92
|
const r = fn(el, ctx);
|
|
@@ -127,13 +97,13 @@ function runInPage(ctx) {
|
|
|
127
97
|
}
|
|
128
98
|
}
|
|
129
99
|
|
|
130
|
-
|
|
131
100
|
const occurrences = [];
|
|
132
101
|
let applicableCount = 0;
|
|
133
102
|
|
|
134
103
|
const selector = '[role="menuitem"],[role="menuitemcheckbox"],[role="menuitemradio"]';
|
|
135
|
-
const nodes = helpers.queryAllSmart
|
|
136
|
-
|
|
104
|
+
const nodes = helpers.queryAllSmart
|
|
105
|
+
? helpers.queryAllSmart(selector)
|
|
106
|
+
: helpers.queryAll(selector);
|
|
137
107
|
|
|
138
108
|
function evaluate(el) {
|
|
139
109
|
const ariaLabel = getAttr(el, 'aria-label');
|
|
@@ -165,10 +135,16 @@ function runInPage(ctx) {
|
|
|
165
135
|
if (res.ok) continue;
|
|
166
136
|
|
|
167
137
|
const eligInfo = getEligibilityInfo
|
|
168
|
-
|
|
169
|
-
|
|
138
|
+
? (() => {
|
|
139
|
+
try {
|
|
140
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
})()
|
|
145
|
+
: null;
|
|
170
146
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
171
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
147
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
172
148
|
|
|
173
149
|
occurrences.push({
|
|
174
150
|
selector: stableSelector,
|
|
@@ -187,12 +163,16 @@ function runInPage(ctx) {
|
|
|
187
163
|
});
|
|
188
164
|
}
|
|
189
165
|
|
|
190
|
-
|
|
191
166
|
if (applicableCount === 0) {
|
|
192
167
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
193
168
|
}
|
|
194
169
|
if (occurrences.length) {
|
|
195
|
-
return {
|
|
170
|
+
return {
|
|
171
|
+
ruleId: rule.ruleId,
|
|
172
|
+
outcome: 'fail',
|
|
173
|
+
severity: rule.defaultSeverity || 'minor',
|
|
174
|
+
occurrences
|
|
175
|
+
};
|
|
196
176
|
}
|
|
197
177
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
198
178
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -24,17 +26,16 @@
|
|
|
24
26
|
* - A <meta> nested inside <noscript> is excluded: it only ever takes
|
|
25
27
|
* effect when scripting is disabled, which is never the case for any
|
|
26
28
|
* context capable of running accessibility tooling in the first place
|
|
27
|
-
* (
|
|
28
|
-
*
|
|
29
|
-
* URL=/?nojsmode=1"></noscript> JS-disabled fallback — a widely-used
|
|
30
|
-
* reference engine's own same-named check correctly doesn't flag it either).
|
|
29
|
+
* (e.g. a <noscript><meta http-equiv="refresh" content="0; URL=/?nojs=1">
|
|
30
|
+
* </noscript> JS-disabled fallback).
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
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
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -31,7 +33,8 @@ const id = 'meta-refresh-timing-absent';
|
|
|
31
33
|
|
|
32
34
|
const meta = {
|
|
33
35
|
title: 'Page must not use a timed meta refresh',
|
|
34
|
-
description:
|
|
36
|
+
description:
|
|
37
|
+
'Checks that <meta http-equiv="refresh"> does not impose a positive delay of 20 hours or less.',
|
|
35
38
|
i18n: {
|
|
36
39
|
titleKey: 'metaRefreshTimingAbsent_title',
|
|
37
40
|
descriptionKey: 'metaRefreshTimingAbsent_description'
|
|
@@ -40,7 +43,13 @@ const meta = {
|
|
|
40
43
|
tags: ['wcag2a', 'wcag221', 'structure', 'atomic', 'automatic'],
|
|
41
44
|
wcagSc: ['2.2.1'],
|
|
42
45
|
normativeMappings: [
|
|
43
|
-
{
|
|
46
|
+
{
|
|
47
|
+
standard: 'WCAG',
|
|
48
|
+
version: '2.2',
|
|
49
|
+
requirement: '2.2.1',
|
|
50
|
+
title: 'Timing Adjustable',
|
|
51
|
+
conformanceLevel: 'A'
|
|
52
|
+
}
|
|
44
53
|
],
|
|
45
54
|
defaultSeverity: 'serious',
|
|
46
55
|
category: 'operable',
|
|
@@ -63,7 +72,9 @@ function runInPage(ctx) {
|
|
|
63
72
|
// WCAG 2.2.1 Exception 3: time limits longer than 20 hours are exempt.
|
|
64
73
|
const EXEMPT_DELAY_SECONDS = 20 * 60 * 60;
|
|
65
74
|
|
|
66
|
-
const nodes = document.querySelectorAll
|
|
75
|
+
const nodes = document.querySelectorAll
|
|
76
|
+
? document.querySelectorAll('meta[http-equiv="refresh" i]')
|
|
77
|
+
: [];
|
|
67
78
|
|
|
68
79
|
const occurrences = [];
|
|
69
80
|
let applicableCount = 0;
|
|
@@ -85,7 +96,7 @@ function runInPage(ctx) {
|
|
|
85
96
|
if (delay > EXEMPT_DELAY_SECONDS) continue; // WCAG 2.2.1 Exception 3 (>20 hours)
|
|
86
97
|
|
|
87
98
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
88
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
99
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
89
100
|
|
|
90
101
|
occurrences.push({
|
|
91
102
|
selector: stableSelector,
|
|
@@ -107,9 +118,14 @@ function runInPage(ctx) {
|
|
|
107
118
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
108
119
|
}
|
|
109
120
|
if (occurrences.length) {
|
|
110
|
-
return {
|
|
121
|
+
return {
|
|
122
|
+
ruleId: rule.ruleId,
|
|
123
|
+
outcome: 'fail',
|
|
124
|
+
severity: rule.defaultSeverity || 'serious',
|
|
125
|
+
occurrences
|
|
126
|
+
};
|
|
111
127
|
}
|
|
112
128
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
113
129
|
}
|
|
114
130
|
|
|
115
|
-
module.exports = { id, meta, runInPage, applicability };
|
|
131
|
+
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
|
/**
|
|
@@ -24,7 +26,8 @@ const id = 'meta-viewport-zoom-enabled';
|
|
|
24
26
|
|
|
25
27
|
const meta = {
|
|
26
28
|
title: 'Viewport meta tag must not disable zoom',
|
|
27
|
-
description:
|
|
29
|
+
description:
|
|
30
|
+
'Checks that <meta name="viewport"> does not set user-scalable=no or maximum-scale below 2 (200%).',
|
|
28
31
|
i18n: {
|
|
29
32
|
titleKey: 'metaViewportZoomEnabled_title',
|
|
30
33
|
descriptionKey: 'metaViewportZoomEnabled_description'
|
|
@@ -33,7 +36,13 @@ const meta = {
|
|
|
33
36
|
tags: ['wcag2aa', 'wcag144', 'structure', 'atomic', 'automatic'],
|
|
34
37
|
wcagSc: ['1.4.4'],
|
|
35
38
|
normativeMappings: [
|
|
36
|
-
{
|
|
39
|
+
{
|
|
40
|
+
standard: 'WCAG',
|
|
41
|
+
version: '2.2',
|
|
42
|
+
requirement: '1.4.4',
|
|
43
|
+
title: 'Resize Text',
|
|
44
|
+
conformanceLevel: 'AA'
|
|
45
|
+
}
|
|
37
46
|
],
|
|
38
47
|
defaultSeverity: 'serious',
|
|
39
48
|
category: 'perceivable',
|
|
@@ -59,13 +68,18 @@ function runInPage(ctx) {
|
|
|
59
68
|
const eq = pair.indexOf('=');
|
|
60
69
|
if (eq === -1) continue;
|
|
61
70
|
const key = pair.slice(0, eq).trim().toLowerCase();
|
|
62
|
-
const value = pair
|
|
71
|
+
const value = pair
|
|
72
|
+
.slice(eq + 1)
|
|
73
|
+
.trim()
|
|
74
|
+
.toLowerCase();
|
|
63
75
|
if (key) out[key] = value;
|
|
64
76
|
}
|
|
65
77
|
return out;
|
|
66
78
|
}
|
|
67
79
|
|
|
68
|
-
const nodes = document.querySelectorAll
|
|
80
|
+
const nodes = document.querySelectorAll
|
|
81
|
+
? document.querySelectorAll('meta[name="viewport" i]')
|
|
82
|
+
: [];
|
|
69
83
|
|
|
70
84
|
const occurrences = [];
|
|
71
85
|
let applicableCount = 0;
|
|
@@ -96,7 +110,7 @@ function runInPage(ctx) {
|
|
|
96
110
|
if (!reasons.length) continue;
|
|
97
111
|
|
|
98
112
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
99
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
113
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
100
114
|
|
|
101
115
|
occurrences.push({
|
|
102
116
|
selector: stableSelector,
|
|
@@ -118,9 +132,14 @@ function runInPage(ctx) {
|
|
|
118
132
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
119
133
|
}
|
|
120
134
|
if (occurrences.length) {
|
|
121
|
-
return {
|
|
135
|
+
return {
|
|
136
|
+
ruleId: rule.ruleId,
|
|
137
|
+
outcome: 'fail',
|
|
138
|
+
severity: rule.defaultSeverity || 'serious',
|
|
139
|
+
occurrences
|
|
140
|
+
};
|
|
122
141
|
}
|
|
123
142
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
124
143
|
}
|
|
125
144
|
|
|
126
|
-
module.exports = { id, meta, runInPage, applicability };
|
|
145
|
+
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
|
const id = 'meter-name-present';
|
|
@@ -13,7 +15,13 @@ const meta = {
|
|
|
13
15
|
tags: ['wcag2a', 'wcag111', 'nontext', 'atomic', 'automatic', 'name'],
|
|
14
16
|
wcagSc: ['1.1.1'],
|
|
15
17
|
normativeMappings: [
|
|
16
|
-
{
|
|
18
|
+
{
|
|
19
|
+
standard: 'WCAG',
|
|
20
|
+
version: '2.2',
|
|
21
|
+
requirement: '1.1.1',
|
|
22
|
+
title: 'Non-text Content',
|
|
23
|
+
conformanceLevel: 'A'
|
|
24
|
+
}
|
|
17
25
|
],
|
|
18
26
|
defaultSeverity: 'serious',
|
|
19
27
|
category: 'perceivable',
|
|
@@ -24,36 +32,22 @@ const meta = {
|
|
|
24
32
|
|
|
25
33
|
function runInPage(ctx) {
|
|
26
34
|
const { document, helpers, rule } = ctx;
|
|
27
|
-
const getEligibilityInfo =
|
|
28
|
-
|
|
29
|
-
: null;
|
|
30
|
-
|
|
35
|
+
const getEligibilityInfo =
|
|
36
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
31
37
|
|
|
32
38
|
function normalizeWs(s) {
|
|
33
|
-
return String(s || '')
|
|
39
|
+
return String(s || '')
|
|
40
|
+
.replace(/\s+/g, ' ')
|
|
41
|
+
.trim();
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
function getAttr(el, name) {
|
|
37
45
|
try {
|
|
38
46
|
if (!el || !el.getAttribute) return '';
|
|
39
47
|
return normalizeWs(el.getAttribute(name));
|
|
40
|
-
} catch {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
function getConservativeSubtreeText(document, container) {
|
|
44
|
-
// "Name from content" — recurses into descendants and uses each one's
|
|
45
|
-
// own accessible name (img alt, aria-label/aria-labelledby, title) when
|
|
46
|
-
// it has one, not just literal text nodes. See getContentNameInfo's
|
|
47
|
-
// header comment in src/core/dom-helpers.js for the full rationale
|
|
48
|
-
// (this replaced a text-node-only TreeWalker that missed the common
|
|
49
|
-
// "<a><img alt='...'></a>" logo-link / "<button><img alt='...'></button>"
|
|
50
|
-
// icon-button pattern).
|
|
51
|
-
if (helpers.getContentNameInfo) {
|
|
52
|
-
const info = helpers.getContentNameInfo(container, ctx);
|
|
53
|
-
return info && info.present ? info.value : '';
|
|
48
|
+
} catch {
|
|
49
|
+
return '';
|
|
54
50
|
}
|
|
55
|
-
const t = (container && container.textContent) ? String(container.textContent) : '';
|
|
56
|
-
return t.replace(/\s+/g, ' ').trim();
|
|
57
51
|
}
|
|
58
52
|
|
|
59
53
|
function resolveAriaLabelledbyText(document, el, maxRefs) {
|
|
@@ -74,7 +68,8 @@ function runInPage(ctx) {
|
|
|
74
68
|
}
|
|
75
69
|
|
|
76
70
|
function isEligibleAcc(helpers, el, ctx) {
|
|
77
|
-
const fn =
|
|
71
|
+
const fn =
|
|
72
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
78
73
|
if (!fn) return true;
|
|
79
74
|
try {
|
|
80
75
|
const r = fn(el, ctx);
|
|
@@ -85,13 +80,13 @@ function runInPage(ctx) {
|
|
|
85
80
|
}
|
|
86
81
|
}
|
|
87
82
|
|
|
88
|
-
|
|
89
83
|
const occurrences = [];
|
|
90
84
|
let applicableCount = 0;
|
|
91
85
|
|
|
92
86
|
const selector = '[role="meter"]';
|
|
93
|
-
const nodes = helpers.queryAllSmart
|
|
94
|
-
|
|
87
|
+
const nodes = helpers.queryAllSmart
|
|
88
|
+
? helpers.queryAllSmart(selector)
|
|
89
|
+
: helpers.queryAll(selector);
|
|
95
90
|
|
|
96
91
|
function evaluate(el) {
|
|
97
92
|
const ariaLabel = getAttr(el, 'aria-label');
|
|
@@ -103,9 +98,8 @@ function runInPage(ctx) {
|
|
|
103
98
|
const title = getAttr(el, 'title');
|
|
104
99
|
if (title) return { ok: true, method: 'title' };
|
|
105
100
|
|
|
106
|
-
// role="meter" is name-from-author-only per WAI-ARIA
|
|
107
|
-
//
|
|
108
|
-
// 'aria-labelledby', title] — no content-based naming method at all).
|
|
101
|
+
// role="meter" is name-from-author-only per WAI-ARIA: aria-label,
|
|
102
|
+
// aria-labelledby, or title — no content-based naming method at all.
|
|
109
103
|
// It must NOT fall back to subtree content — visible text near/inside a
|
|
110
104
|
// custom meter widget is not reliably exposed as its accessible name.
|
|
111
105
|
return { ok: false, method: 'none' };
|
|
@@ -124,16 +118,22 @@ function runInPage(ctx) {
|
|
|
124
118
|
if (res.ok) continue;
|
|
125
119
|
|
|
126
120
|
const eligInfo = getEligibilityInfo
|
|
127
|
-
|
|
128
|
-
|
|
121
|
+
? (() => {
|
|
122
|
+
try {
|
|
123
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
})()
|
|
128
|
+
: null;
|
|
129
129
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
130
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
130
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
131
131
|
|
|
132
132
|
occurrences.push({
|
|
133
133
|
selector: stableSelector,
|
|
134
134
|
html,
|
|
135
135
|
summary: 'This meter has no accessible name.',
|
|
136
|
-
hint:
|
|
136
|
+
hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this meter's accessible name.",
|
|
137
137
|
i18n: {
|
|
138
138
|
summaryKey: 'meterNamePresent_summary_fail',
|
|
139
139
|
hintKey: 'meterNamePresent_hint_fail',
|
|
@@ -146,14 +146,18 @@ function runInPage(ctx) {
|
|
|
146
146
|
});
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
|
|
150
149
|
if (applicableCount === 0) {
|
|
151
150
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
152
151
|
}
|
|
153
152
|
if (occurrences.length) {
|
|
154
|
-
return {
|
|
153
|
+
return {
|
|
154
|
+
ruleId: rule.ruleId,
|
|
155
|
+
outcome: 'fail',
|
|
156
|
+
severity: rule.defaultSeverity || 'minor',
|
|
157
|
+
occurrences
|
|
158
|
+
};
|
|
155
159
|
}
|
|
156
160
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
157
161
|
}
|
|
158
162
|
|
|
159
|
-
module.exports = { id, meta, runInPage };
|
|
163
|
+
module.exports = { id, meta, runInPage };
|