@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
|
@@ -78,7 +78,7 @@ function runInPage(ctx) {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
function getExplicitRoleToken(el) {
|
|
81
|
-
const raw = (el.getAttribute && el.getAttribute('role') || '').trim();
|
|
81
|
+
const raw = ((el.getAttribute && el.getAttribute('role')) || '').trim();
|
|
82
82
|
if (!raw) return '';
|
|
83
83
|
return raw.split(/\s+/)[0].toLowerCase();
|
|
84
84
|
}
|
|
@@ -91,7 +91,8 @@ function runInPage(ctx) {
|
|
|
91
91
|
return tag === 'main';
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
const isAccTreeEligible =
|
|
94
|
+
const isAccTreeEligible =
|
|
95
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
95
96
|
|
|
96
97
|
function isExposedToAt(el) {
|
|
97
98
|
if (!isAccTreeEligible) return true;
|
|
@@ -108,11 +109,12 @@ function runInPage(ctx) {
|
|
|
108
109
|
// landmark-unique-manual.js's header comment for the real page (Airtable, 2026-07-23)
|
|
109
110
|
// that surfaced this gap: a third-party shadow-DOM-hosted widget's own landmark is
|
|
110
111
|
// invisible to a light-DOM-only query.
|
|
111
|
-
let nodes
|
|
112
|
+
let nodes;
|
|
112
113
|
try {
|
|
113
|
-
nodes =
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
nodes =
|
|
115
|
+
helpers && typeof helpers.queryAllSmart === 'function'
|
|
116
|
+
? helpers.queryAllSmart('main, [role]')
|
|
117
|
+
: document.querySelectorAll('main, [role]');
|
|
116
118
|
} catch {
|
|
117
119
|
nodes = [];
|
|
118
120
|
}
|
|
@@ -133,26 +135,30 @@ function runInPage(ctx) {
|
|
|
133
135
|
}
|
|
134
136
|
|
|
135
137
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(body) : 'body';
|
|
136
|
-
const html = helpers.getOuterHtmlSnippet
|
|
138
|
+
const html = helpers.getOuterHtmlSnippet
|
|
139
|
+
? helpers.getOuterHtmlSnippet(body)
|
|
140
|
+
: (body.outerHTML || '').slice(0, 200);
|
|
137
141
|
|
|
138
142
|
return {
|
|
139
143
|
ruleId: rule.ruleId,
|
|
140
144
|
outcome: 'cantTell',
|
|
141
145
|
severity: rule.defaultSeverity || 'minor',
|
|
142
|
-
occurrences: [
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
146
|
+
occurrences: [
|
|
147
|
+
{
|
|
148
|
+
selector: stableSelector,
|
|
149
|
+
html,
|
|
150
|
+
summary: 'This page has no main landmark.',
|
|
151
|
+
hint: 'Add a main landmark (<main> or role="main") around the page\'s primary content.',
|
|
152
|
+
i18n: {
|
|
153
|
+
summaryKey: 'landmarkOneMain_summary_cantTell_missing',
|
|
154
|
+
hintKey: 'landmarkOneMain_hint_cantTell_missing',
|
|
155
|
+
params: {}
|
|
156
|
+
},
|
|
157
|
+
data: {
|
|
158
|
+
details: { reasonCode: 'LANDMARK_MAIN_MISSING' }
|
|
159
|
+
}
|
|
154
160
|
}
|
|
155
|
-
|
|
161
|
+
]
|
|
156
162
|
};
|
|
157
163
|
}
|
|
158
164
|
|
|
@@ -31,7 +31,8 @@ const id = 'landmark-unique';
|
|
|
31
31
|
|
|
32
32
|
const meta = {
|
|
33
33
|
title: 'Landmarks with the same role must have unique names',
|
|
34
|
-
description:
|
|
34
|
+
description:
|
|
35
|
+
'Checks that when two or more landmarks share the same role, each has a distinct accessible name.',
|
|
35
36
|
i18n: {
|
|
36
37
|
titleKey: 'landmarkUnique_title',
|
|
37
38
|
descriptionKey: 'landmarkUnique_description'
|
|
@@ -51,7 +52,9 @@ function runInPage(ctx) {
|
|
|
51
52
|
const { document, helpers, rule } = ctx;
|
|
52
53
|
|
|
53
54
|
function normalizeWs(s) {
|
|
54
|
-
return String(s || '')
|
|
55
|
+
return String(s || '')
|
|
56
|
+
.replace(/\s+/g, ' ')
|
|
57
|
+
.trim();
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
// Delegates to the shared helpers.getLandmarkNameInfo (aria-label -> aria-labelledby, via the
|
|
@@ -124,7 +127,16 @@ function runInPage(ctx) {
|
|
|
124
127
|
return '';
|
|
125
128
|
}
|
|
126
129
|
|
|
127
|
-
const LANDMARK_ROLES = new Set([
|
|
130
|
+
const LANDMARK_ROLES = new Set([
|
|
131
|
+
'banner',
|
|
132
|
+
'contentinfo',
|
|
133
|
+
'main',
|
|
134
|
+
'navigation',
|
|
135
|
+
'complementary',
|
|
136
|
+
'region',
|
|
137
|
+
'form',
|
|
138
|
+
'search'
|
|
139
|
+
]);
|
|
128
140
|
|
|
129
141
|
function getLandmarkRole(el) {
|
|
130
142
|
if (!el || !el.getAttribute) return '';
|
|
@@ -152,7 +164,8 @@ function runInPage(ctx) {
|
|
|
152
164
|
return getImplicitLandmarkRole(el);
|
|
153
165
|
}
|
|
154
166
|
|
|
155
|
-
const isAccTreeEligible =
|
|
167
|
+
const isAccTreeEligible =
|
|
168
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
156
169
|
|
|
157
170
|
function isExposedToAt(el) {
|
|
158
171
|
if (!isAccTreeEligible) return true;
|
|
@@ -172,11 +185,12 @@ function runInPage(ctx) {
|
|
|
172
185
|
// own landmark-unique (a real browser DOM, shadow roots included by design) correctly sees as colliding
|
|
173
186
|
// with the page's own unnamed header <nav>/page <footer> -- a real, confirmed surea11y
|
|
174
187
|
// false-negative miss, invisible to plain querySelectorAll's light-DOM-only reach.
|
|
175
|
-
let nodes
|
|
188
|
+
let nodes;
|
|
176
189
|
try {
|
|
177
|
-
nodes =
|
|
178
|
-
|
|
179
|
-
|
|
190
|
+
nodes =
|
|
191
|
+
helpers && typeof helpers.queryAllSmart === 'function'
|
|
192
|
+
? helpers.queryAllSmart('header, footer, main, nav, aside, section, form, [role]')
|
|
193
|
+
: document.querySelectorAll('header, footer, main, nav, aside, section, form, [role]');
|
|
180
194
|
} catch {
|
|
181
195
|
nodes = [];
|
|
182
196
|
}
|
|
@@ -221,24 +235,31 @@ function runInPage(ctx) {
|
|
|
221
235
|
|
|
222
236
|
for (const { el } of group) {
|
|
223
237
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
224
|
-
const html = helpers.getOuterHtmlSnippet
|
|
238
|
+
const html = helpers.getOuterHtmlSnippet
|
|
239
|
+
? helpers.getOuterHtmlSnippet(el)
|
|
240
|
+
: el.outerHTML || '';
|
|
225
241
|
|
|
226
242
|
occurrences.push({
|
|
227
243
|
selector: stableSelector,
|
|
228
244
|
html,
|
|
229
245
|
summary: normalizedName
|
|
230
|
-
|
|
231
|
-
|
|
246
|
+
? `This ${role} landmark shares its accessible name with another ${role} landmark.`
|
|
247
|
+
: `This ${role} landmark has no accessible name, and more than one unnamed ${role} landmark exists on this page.`,
|
|
232
248
|
hint: `Give each ${role} landmark a distinct name via aria-label or aria-labelledby.`,
|
|
233
249
|
i18n: {
|
|
234
250
|
summaryKey: normalizedName
|
|
235
|
-
|
|
236
|
-
|
|
251
|
+
? 'landmarkUnique_summary_cantTell_duplicateName'
|
|
252
|
+
: 'landmarkUnique_summary_cantTell_bothUnnamed',
|
|
237
253
|
hintKey: 'landmarkUnique_hint_cantTell',
|
|
238
254
|
params: { role }
|
|
239
255
|
},
|
|
240
256
|
data: {
|
|
241
|
-
details: {
|
|
257
|
+
details: {
|
|
258
|
+
reasonCode: 'LANDMARK_NOT_UNIQUE',
|
|
259
|
+
role,
|
|
260
|
+
name: normalizedName,
|
|
261
|
+
groupSize: group.length
|
|
262
|
+
}
|
|
242
263
|
}
|
|
243
264
|
});
|
|
244
265
|
}
|
|
@@ -248,7 +269,12 @@ function runInPage(ctx) {
|
|
|
248
269
|
if (!occurrences.length) {
|
|
249
270
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
250
271
|
}
|
|
251
|
-
return {
|
|
272
|
+
return {
|
|
273
|
+
ruleId: rule.ruleId,
|
|
274
|
+
outcome: 'cantTell',
|
|
275
|
+
severity: rule.defaultSeverity || 'minor',
|
|
276
|
+
occurrences
|
|
277
|
+
};
|
|
252
278
|
}
|
|
253
279
|
|
|
254
280
|
module.exports = { id, meta, runInPage };
|
|
@@ -50,7 +50,13 @@ const meta = {
|
|
|
50
50
|
tags: ['wcag2a', 'wcag244', 'navigation', 'quality', 'atomic', 'manual'],
|
|
51
51
|
wcagSc: ['2.4.4'],
|
|
52
52
|
normativeMappings: [
|
|
53
|
-
{
|
|
53
|
+
{
|
|
54
|
+
standard: 'WCAG',
|
|
55
|
+
version: '2.2',
|
|
56
|
+
requirement: '2.4.4',
|
|
57
|
+
title: 'Link Purpose (In Context)',
|
|
58
|
+
conformanceLevel: 'A'
|
|
59
|
+
}
|
|
54
60
|
],
|
|
55
61
|
defaultSeverity: 'minor',
|
|
56
62
|
category: 'operable',
|
|
@@ -60,13 +66,28 @@ const meta = {
|
|
|
60
66
|
};
|
|
61
67
|
|
|
62
68
|
function runInPage(ctx) {
|
|
63
|
-
const {
|
|
69
|
+
const { helpers, rule } = ctx;
|
|
64
70
|
|
|
65
71
|
const GENERIC_LINK_TEXT = new Set([
|
|
66
|
-
'click here',
|
|
67
|
-
'
|
|
68
|
-
'
|
|
69
|
-
'
|
|
72
|
+
'click here',
|
|
73
|
+
'here',
|
|
74
|
+
'click',
|
|
75
|
+
'more',
|
|
76
|
+
'more info',
|
|
77
|
+
'more information',
|
|
78
|
+
'read more',
|
|
79
|
+
'learn more',
|
|
80
|
+
'continue reading',
|
|
81
|
+
'continue',
|
|
82
|
+
'details',
|
|
83
|
+
'more details',
|
|
84
|
+
'link',
|
|
85
|
+
'this link',
|
|
86
|
+
'go',
|
|
87
|
+
'download',
|
|
88
|
+
'view more',
|
|
89
|
+
'see more',
|
|
90
|
+
'info'
|
|
70
91
|
]);
|
|
71
92
|
|
|
72
93
|
function normalize(s) {
|
|
@@ -79,7 +100,9 @@ function runInPage(ctx) {
|
|
|
79
100
|
}
|
|
80
101
|
|
|
81
102
|
const selector = 'a[href]';
|
|
82
|
-
const nodes = helpers.queryAllSmart
|
|
103
|
+
const nodes = helpers.queryAllSmart
|
|
104
|
+
? helpers.queryAllSmart(selector)
|
|
105
|
+
: helpers.queryAll(selector);
|
|
83
106
|
|
|
84
107
|
const occurrences = [];
|
|
85
108
|
let applicableCount = 0;
|
|
@@ -88,11 +111,12 @@ function runInPage(ctx) {
|
|
|
88
111
|
if (!el || !el.getAttribute) continue;
|
|
89
112
|
|
|
90
113
|
const eligResult = helpers.isAccTreeEligible ? helpers.isAccTreeEligible(el, ctx) : true;
|
|
91
|
-
const eligible =
|
|
114
|
+
const eligible =
|
|
115
|
+
typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
|
|
92
116
|
if (!eligible) continue;
|
|
93
117
|
|
|
94
118
|
const nameInfo = helpers.getAccessibleNameInfo ? helpers.getAccessibleNameInfo(el, ctx) : null;
|
|
95
|
-
const programmaticName =
|
|
119
|
+
const programmaticName = nameInfo && typeof nameInfo.value === 'string' ? nameInfo.value : '';
|
|
96
120
|
|
|
97
121
|
let rawName = programmaticName;
|
|
98
122
|
if (!rawName.trim() && helpers.getContentNameInfo) {
|
|
@@ -108,8 +132,10 @@ function runInPage(ctx) {
|
|
|
108
132
|
if (!GENERIC_LINK_TEXT.has(normalized)) continue;
|
|
109
133
|
|
|
110
134
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
111
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
112
|
-
const eligInfo = helpers.getEligibilityInfo
|
|
135
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
136
|
+
const eligInfo = helpers.getEligibilityInfo
|
|
137
|
+
? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' })
|
|
138
|
+
: null;
|
|
113
139
|
|
|
114
140
|
occurrences.push({
|
|
115
141
|
selector: stableSelector,
|
|
@@ -133,7 +159,12 @@ function runInPage(ctx) {
|
|
|
133
159
|
}
|
|
134
160
|
|
|
135
161
|
if (occurrences.length) {
|
|
136
|
-
return {
|
|
162
|
+
return {
|
|
163
|
+
ruleId: rule.ruleId,
|
|
164
|
+
outcome: 'cantTell',
|
|
165
|
+
severity: rule.defaultSeverity || 'minor',
|
|
166
|
+
occurrences
|
|
167
|
+
};
|
|
137
168
|
}
|
|
138
169
|
|
|
139
170
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
@@ -99,20 +99,23 @@ function runInPage(ctx) {
|
|
|
99
99
|
const __eligCache = new WeakMap();
|
|
100
100
|
|
|
101
101
|
function getEligibility(node) {
|
|
102
|
-
if (!node || node.nodeType !== 1)
|
|
102
|
+
if (!node || node.nodeType !== 1)
|
|
103
|
+
return { eligible: true, reasons: [], targetSet: 'acc', accEligible: null };
|
|
103
104
|
const cached = __eligCache.get(node);
|
|
104
105
|
if (cached) return cached;
|
|
105
106
|
|
|
106
|
-
let info
|
|
107
|
+
let info;
|
|
107
108
|
try {
|
|
108
|
-
info =
|
|
109
|
+
info =
|
|
110
|
+
helpers && typeof helpers.getEligibilityInfo === 'function'
|
|
109
111
|
? helpers.getEligibilityInfo(node, ctx, { targetSet: 'acc' })
|
|
110
112
|
: null;
|
|
111
113
|
} catch {
|
|
112
114
|
info = null;
|
|
113
115
|
}
|
|
114
116
|
|
|
115
|
-
const norm =
|
|
117
|
+
const norm =
|
|
118
|
+
info && typeof info === 'object'
|
|
116
119
|
? info
|
|
117
120
|
: { eligible: true, reasons: [], targetSet: 'acc', accEligible: null };
|
|
118
121
|
|
|
@@ -125,10 +128,6 @@ function runInPage(ctx) {
|
|
|
125
128
|
return !!(info && info.eligible);
|
|
126
129
|
}
|
|
127
130
|
|
|
128
|
-
function safeSelector(node) {
|
|
129
|
-
return helpers.buildSelector ? helpers.buildSelector(node) : 'html';
|
|
130
|
-
}
|
|
131
|
-
|
|
132
131
|
// Evidence object includes strength, so the rule can treat "unverified external link" as insufficient proof.
|
|
133
132
|
function evidenceNone() {
|
|
134
133
|
return {
|
|
@@ -173,7 +172,9 @@ function runInPage(ctx) {
|
|
|
173
172
|
if (hasToken || len >= MIN_DESCRIBEDBY_CHARS_WITHOUT_TOKEN) {
|
|
174
173
|
evidence.strength = 'strong';
|
|
175
174
|
evidence.method = 'aria-describedby';
|
|
176
|
-
evidence.notes.push(
|
|
175
|
+
evidence.notes.push(
|
|
176
|
+
'media has aria-describedby with explicit or substantial transcript text'
|
|
177
|
+
);
|
|
177
178
|
return evidence;
|
|
178
179
|
}
|
|
179
180
|
}
|
|
@@ -197,7 +198,9 @@ function runInPage(ctx) {
|
|
|
197
198
|
evidence.strength = 'strong';
|
|
198
199
|
evidence.method = 'adjacent-heading';
|
|
199
200
|
evidence.transcriptNodeSelector = nodeRef(h);
|
|
200
|
-
evidence.notes.push(
|
|
201
|
+
evidence.notes.push(
|
|
202
|
+
'found transcript heading with substantial visible adjacent text'
|
|
203
|
+
);
|
|
201
204
|
return evidence;
|
|
202
205
|
}
|
|
203
206
|
}
|
|
@@ -215,7 +218,9 @@ function runInPage(ctx) {
|
|
|
215
218
|
for (const a of links) {
|
|
216
219
|
if (!isEligible(a)) continue;
|
|
217
220
|
|
|
218
|
-
const nameInfo = helpers.getAccessibleNameInfo
|
|
221
|
+
const nameInfo = helpers.getAccessibleNameInfo
|
|
222
|
+
? helpers.getAccessibleNameInfo(a, ctx)
|
|
223
|
+
: null;
|
|
219
224
|
const linkName = nameInfo && nameInfo.value ? nameInfo.value : getNodeText(a);
|
|
220
225
|
if (!containsTranscriptToken(linkName)) continue;
|
|
221
226
|
|
|
@@ -227,7 +232,9 @@ function runInPage(ctx) {
|
|
|
227
232
|
if (href.startsWith('#')) {
|
|
228
233
|
const targetId = href.slice(1);
|
|
229
234
|
const target = targetId
|
|
230
|
-
?
|
|
235
|
+
? safeRoot.getElementById
|
|
236
|
+
? safeRoot.getElementById(targetId)
|
|
237
|
+
: document.getElementById(targetId)
|
|
231
238
|
: null;
|
|
232
239
|
|
|
233
240
|
if (isElement(target) && isEligible(target)) {
|
|
@@ -246,7 +253,9 @@ function runInPage(ctx) {
|
|
|
246
253
|
if (hasTranscriptHeading && textLen(targetText) >= MIN_TRANSCRIPT_CHARS) {
|
|
247
254
|
evidence.strength = 'strong';
|
|
248
255
|
evidence.method = 'anchor-target';
|
|
249
|
-
evidence.notes.push(
|
|
256
|
+
evidence.notes.push(
|
|
257
|
+
'resolved transcript link to an on-page section with transcript heading and substantial text'
|
|
258
|
+
);
|
|
250
259
|
return evidence;
|
|
251
260
|
}
|
|
252
261
|
}
|
|
@@ -254,7 +263,9 @@ function runInPage(ctx) {
|
|
|
254
263
|
// If anchor cannot be verified, treat as weak (still better than nothing, but not proof).
|
|
255
264
|
evidence.strength = 'weak';
|
|
256
265
|
evidence.method = 'anchor-unverified';
|
|
257
|
-
evidence.notes.push(
|
|
266
|
+
evidence.notes.push(
|
|
267
|
+
'transcript link found but anchor target could not be verified as a transcript section'
|
|
268
|
+
);
|
|
258
269
|
return evidence;
|
|
259
270
|
}
|
|
260
271
|
|
|
@@ -300,9 +311,8 @@ function runInPage(ctx) {
|
|
|
300
311
|
if (evidence.strength === 'none') {
|
|
301
312
|
const baseOccurrence = {
|
|
302
313
|
summary:
|
|
303
|
-
|
|
304
|
-
hint:
|
|
305
|
-
'Provide a clearly identified transcript or other text alternative for audio-only/video-only prerecorded media (for example, a “Transcript” section or link).',
|
|
314
|
+
'A transcript or other text alternative for this time-based media is not strongly evidenced on the page.',
|
|
315
|
+
hint: 'Provide a clearly identified transcript or other text alternative for audio-only/video-only prerecorded media (for example, a “Transcript” section or link).',
|
|
306
316
|
i18n: {
|
|
307
317
|
summaryKey: 'mediaTranscriptPresent_summary_cantTell_missing',
|
|
308
318
|
hintKey: 'mediaTranscriptPresent_hint_cantTell_missing',
|
|
@@ -327,12 +337,10 @@ function runInPage(ctx) {
|
|
|
327
337
|
}
|
|
328
338
|
|
|
329
339
|
if (evidence.strength === 'weak') {
|
|
330
|
-
|
|
331
340
|
const baseOccurrence = {
|
|
332
341
|
summary:
|
|
333
|
-
|
|
334
|
-
hint:
|
|
335
|
-
'Ensure a clearly identified transcript or other text alternative is available and programmatically or visibly associated with the media on the page.',
|
|
342
|
+
'A transcript or other text alternative may be available for this time-based media, but it could not be verified from the page content.',
|
|
343
|
+
hint: 'Ensure a clearly identified transcript or other text alternative is available and programmatically or visibly associated with the media on the page.',
|
|
336
344
|
i18n: {
|
|
337
345
|
summaryKey: 'mediaTranscriptPresent_summary_cantTell_unverified',
|
|
338
346
|
hintKey: 'mediaTranscriptPresent_hint_cantTell_unverified',
|
|
@@ -361,7 +369,12 @@ function runInPage(ctx) {
|
|
|
361
369
|
}
|
|
362
370
|
|
|
363
371
|
if (occurrences.length) {
|
|
364
|
-
return {
|
|
372
|
+
return {
|
|
373
|
+
ruleId: rule.ruleId,
|
|
374
|
+
outcome: 'cantTell',
|
|
375
|
+
severity: rule.defaultSeverity || 'minor',
|
|
376
|
+
occurrences
|
|
377
|
+
};
|
|
365
378
|
}
|
|
366
379
|
|
|
367
380
|
// Manual rules may only emit cantTell/notApplicable (never pass/fail).
|
|
@@ -27,7 +27,8 @@ const id = 'meta-viewport-large';
|
|
|
27
27
|
|
|
28
28
|
const meta = {
|
|
29
29
|
title: 'Viewport meta tag should allow zooming up to 500%',
|
|
30
|
-
description:
|
|
30
|
+
description:
|
|
31
|
+
'Checks that <meta name="viewport"> does not set user-scalable=no or maximum-scale below 5 (500%).',
|
|
31
32
|
i18n: {
|
|
32
33
|
titleKey: 'metaViewportLarge_title',
|
|
33
34
|
descriptionKey: 'metaViewportLarge_description'
|
|
@@ -60,13 +61,18 @@ function runInPage(ctx) {
|
|
|
60
61
|
const eq = pair.indexOf('=');
|
|
61
62
|
if (eq === -1) continue;
|
|
62
63
|
const key = pair.slice(0, eq).trim().toLowerCase();
|
|
63
|
-
const value = pair
|
|
64
|
+
const value = pair
|
|
65
|
+
.slice(eq + 1)
|
|
66
|
+
.trim()
|
|
67
|
+
.toLowerCase();
|
|
64
68
|
if (key) out[key] = value;
|
|
65
69
|
}
|
|
66
70
|
return out;
|
|
67
71
|
}
|
|
68
72
|
|
|
69
|
-
const nodes = document.querySelectorAll
|
|
73
|
+
const nodes = document.querySelectorAll
|
|
74
|
+
? document.querySelectorAll('meta[name="viewport" i]')
|
|
75
|
+
: [];
|
|
70
76
|
|
|
71
77
|
const occurrences = [];
|
|
72
78
|
let applicableCount = 0;
|
|
@@ -97,7 +103,7 @@ function runInPage(ctx) {
|
|
|
97
103
|
if (!reasons.length) continue;
|
|
98
104
|
|
|
99
105
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
100
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
106
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
101
107
|
|
|
102
108
|
occurrences.push({
|
|
103
109
|
selector: stableSelector,
|
|
@@ -119,7 +125,12 @@ function runInPage(ctx) {
|
|
|
119
125
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
120
126
|
}
|
|
121
127
|
if (occurrences.length) {
|
|
122
|
-
return {
|
|
128
|
+
return {
|
|
129
|
+
ruleId: rule.ruleId,
|
|
130
|
+
outcome: 'cantTell',
|
|
131
|
+
severity: rule.defaultSeverity || 'minor',
|
|
132
|
+
occurrences
|
|
133
|
+
};
|
|
123
134
|
}
|
|
124
135
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
125
136
|
}
|
|
@@ -57,7 +57,13 @@ const meta = {
|
|
|
57
57
|
tags: ['wcag2a', 'wcag211', 'structure', 'atomic', 'manual'],
|
|
58
58
|
wcagSc: ['2.1.1'],
|
|
59
59
|
normativeMappings: [
|
|
60
|
-
{
|
|
60
|
+
{
|
|
61
|
+
standard: 'WCAG',
|
|
62
|
+
version: '2.2',
|
|
63
|
+
requirement: '2.1.1',
|
|
64
|
+
title: 'Keyboard',
|
|
65
|
+
conformanceLevel: 'A'
|
|
66
|
+
}
|
|
61
67
|
],
|
|
62
68
|
defaultSeverity: 'moderate',
|
|
63
69
|
category: 'operable',
|
|
@@ -67,18 +73,28 @@ const meta = {
|
|
|
67
73
|
};
|
|
68
74
|
|
|
69
75
|
function runInPage(ctx) {
|
|
70
|
-
const {
|
|
76
|
+
const { helpers, rule } = ctx;
|
|
71
77
|
|
|
72
78
|
const MOUSE_ONLY_ATTRS = [
|
|
73
|
-
'onmouseover',
|
|
74
|
-
'
|
|
79
|
+
'onmouseover',
|
|
80
|
+
'onmouseout',
|
|
81
|
+
'onmousedown',
|
|
82
|
+
'onmouseup',
|
|
83
|
+
'ondblclick',
|
|
84
|
+
'onmousemove',
|
|
85
|
+
'onmouseenter',
|
|
86
|
+
'onmouseleave'
|
|
75
87
|
];
|
|
76
88
|
const KEYBOARD_EQUIV_ATTRS = ['onkeydown', 'onkeyup', 'onkeypress', 'onfocus', 'onblur'];
|
|
77
89
|
|
|
78
|
-
function trim(v) {
|
|
90
|
+
function trim(v) {
|
|
91
|
+
return (v == null ? '' : String(v)).trim();
|
|
92
|
+
}
|
|
79
93
|
|
|
80
94
|
const selector = MOUSE_ONLY_ATTRS.map((a) => `[${a}]`).join(', ');
|
|
81
|
-
const nodes = helpers.queryAllSmart
|
|
95
|
+
const nodes = helpers.queryAllSmart
|
|
96
|
+
? helpers.queryAllSmart(selector)
|
|
97
|
+
: helpers.queryAll(selector);
|
|
82
98
|
|
|
83
99
|
const occurrences = [];
|
|
84
100
|
let applicableCount = 0;
|
|
@@ -90,7 +106,8 @@ function runInPage(ctx) {
|
|
|
90
106
|
if (!presentMouseAttrs.length) continue;
|
|
91
107
|
|
|
92
108
|
const eligResult = helpers.isAccTreeEligible ? helpers.isAccTreeEligible(el, ctx) : true;
|
|
93
|
-
const eligible =
|
|
109
|
+
const eligible =
|
|
110
|
+
typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
|
|
94
111
|
if (!eligible) continue;
|
|
95
112
|
|
|
96
113
|
applicableCount += 1;
|
|
@@ -99,8 +116,10 @@ function runInPage(ctx) {
|
|
|
99
116
|
if (hasKeyboardEquiv) continue;
|
|
100
117
|
|
|
101
118
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
102
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
103
|
-
const eligInfo = helpers.getEligibilityInfo
|
|
119
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
120
|
+
const eligInfo = helpers.getEligibilityInfo
|
|
121
|
+
? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' })
|
|
122
|
+
: null;
|
|
104
123
|
|
|
105
124
|
occurrences.push({
|
|
106
125
|
selector: stableSelector,
|
|
@@ -113,7 +132,10 @@ function runInPage(ctx) {
|
|
|
113
132
|
params: { attrs: presentMouseAttrs.join(', ') }
|
|
114
133
|
},
|
|
115
134
|
data: {
|
|
116
|
-
details: {
|
|
135
|
+
details: {
|
|
136
|
+
reasonCode: 'MOUSE_ONLY_HANDLER_NO_KEYBOARD_EQUIVALENT',
|
|
137
|
+
mouseAttrs: presentMouseAttrs
|
|
138
|
+
},
|
|
117
139
|
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
118
140
|
}
|
|
119
141
|
});
|
|
@@ -124,7 +146,12 @@ function runInPage(ctx) {
|
|
|
124
146
|
}
|
|
125
147
|
|
|
126
148
|
if (occurrences.length) {
|
|
127
|
-
return {
|
|
149
|
+
return {
|
|
150
|
+
ruleId: rule.ruleId,
|
|
151
|
+
outcome: 'cantTell',
|
|
152
|
+
severity: rule.defaultSeverity || 'moderate',
|
|
153
|
+
occurrences
|
|
154
|
+
};
|
|
128
155
|
}
|
|
129
156
|
|
|
130
157
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
@@ -48,7 +48,13 @@ const meta = {
|
|
|
48
48
|
tags: ['wcag2a', 'wcag142', 'media', 'atomic', 'manual'],
|
|
49
49
|
wcagSc: ['1.4.2'],
|
|
50
50
|
normativeMappings: [
|
|
51
|
-
{
|
|
51
|
+
{
|
|
52
|
+
standard: 'WCAG',
|
|
53
|
+
version: '2.2',
|
|
54
|
+
requirement: '1.4.2',
|
|
55
|
+
title: 'Audio Control',
|
|
56
|
+
conformanceLevel: 'A'
|
|
57
|
+
}
|
|
52
58
|
],
|
|
53
59
|
defaultSeverity: 'moderate',
|
|
54
60
|
category: 'perceivable',
|
|
@@ -58,9 +64,11 @@ const meta = {
|
|
|
58
64
|
};
|
|
59
65
|
|
|
60
66
|
function runInPage(ctx) {
|
|
61
|
-
const {
|
|
67
|
+
const { helpers, rule } = ctx;
|
|
62
68
|
|
|
63
|
-
const nodes = helpers.queryAllSmart
|
|
69
|
+
const nodes = helpers.queryAllSmart
|
|
70
|
+
? helpers.queryAllSmart('audio[autoplay], video[autoplay]')
|
|
71
|
+
: helpers.queryAll('audio[autoplay], video[autoplay]');
|
|
64
72
|
|
|
65
73
|
const occurrences = [];
|
|
66
74
|
let applicableCount = 0;
|
|
@@ -75,12 +83,13 @@ function runInPage(ctx) {
|
|
|
75
83
|
|
|
76
84
|
const mediaTag = (el.tagName || '').toLowerCase();
|
|
77
85
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
78
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
86
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
79
87
|
|
|
80
88
|
const baseOccurrence = {
|
|
81
89
|
selector: stableSelector,
|
|
82
90
|
html,
|
|
83
|
-
summary:
|
|
91
|
+
summary:
|
|
92
|
+
'This element autoplays audio without a native pause/stop or volume-control mechanism.',
|
|
84
93
|
hint: 'If this clip plays for more than 3 seconds, add a `controls` attribute (or an equivalent custom mechanism) so users can pause/stop it or control its volume independently of the system volume.',
|
|
85
94
|
i18n: {
|
|
86
95
|
summaryKey: 'noAutoplayAudio_summary_cantTell',
|
|
@@ -104,7 +113,12 @@ function runInPage(ctx) {
|
|
|
104
113
|
}
|
|
105
114
|
|
|
106
115
|
if (occurrences.length) {
|
|
107
|
-
return {
|
|
116
|
+
return {
|
|
117
|
+
ruleId: rule.ruleId,
|
|
118
|
+
outcome: 'cantTell',
|
|
119
|
+
severity: rule.defaultSeverity || 'moderate',
|
|
120
|
+
occurrences
|
|
121
|
+
};
|
|
108
122
|
}
|
|
109
123
|
|
|
110
124
|
// Manual rules may only emit cantTell/notApplicable (never pass/fail):
|