@surea11y/core 1.1.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/CHANGELOG.md +48 -4
  2. package/LICENSE +373 -21
  3. package/README.md +70 -1
  4. package/bin/core.js +240 -11
  5. package/docs/API_STABILITY.md +61 -0
  6. package/docs/BASELINE.md +66 -0
  7. package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
  8. package/docs/CI_INTEGRATIONS.md +103 -0
  9. package/docs/CLI.md +80 -1
  10. package/docs/ENGINE_OPTIONS.md +4 -0
  11. package/docs/INTEGRATION.md +19 -1
  12. package/docs/OUTPUT_SCHEMA.md +2 -2
  13. package/docs/REPORT.md +33 -0
  14. package/docs/RULE_AUTHORING.md +31 -0
  15. package/docs/RULE_CATALOG.md +1 -1
  16. package/docs/SARIF.md +59 -0
  17. package/package.json +15 -4
  18. package/src/baseline.js +0 -0
  19. package/src/catalogs/composites.wcag.js +414 -450
  20. package/src/checks/automatic/area-alt-present.js +59 -25
  21. package/src/checks/automatic/aria-allowed-attr.js +193 -33
  22. package/src/checks/automatic/aria-allowed-role.js +21 -7
  23. package/src/checks/automatic/aria-braille-equivalent.js +32 -10
  24. package/src/checks/automatic/aria-conditional-attr.js +24 -7
  25. package/src/checks/automatic/aria-deprecated-role.js +22 -8
  26. package/src/checks/automatic/aria-hidden-body.js +50 -19
  27. package/src/checks/automatic/aria-hidden-focus.js +408 -53
  28. package/src/checks/automatic/aria-prohibited-attr.js +296 -22
  29. package/src/checks/automatic/aria-prohibited-children.js +125 -29
  30. package/src/checks/automatic/aria-required-attr.js +23 -8
  31. package/src/checks/automatic/aria-required-children.js +37 -14
  32. package/src/checks/automatic/aria-required-parent.js +48 -14
  33. package/src/checks/automatic/aria-role-name-present.js +47 -21
  34. package/src/checks/automatic/aria-roles-valid.js +22 -12
  35. package/src/checks/automatic/aria-valid-attr-value.js +28 -7
  36. package/src/checks/automatic/aria-valid-attr.js +17 -5
  37. package/src/checks/automatic/autocomplete-valid.js +74 -16
  38. package/src/checks/automatic/avoid-inline-spacing.js +20 -6
  39. package/src/checks/automatic/binary-control-name-present.js +60 -50
  40. package/src/checks/automatic/button-name-present.js +48 -18
  41. package/src/checks/automatic/bypass-blocks-present.js +50 -25
  42. package/src/checks/automatic/canvas-text-alternative-present.js +57 -26
  43. package/src/checks/automatic/combobox-name-present.js +38 -45
  44. package/src/checks/automatic/contrast-computable.js +361 -341
  45. package/src/checks/automatic/contrast-enhanced.js +487 -466
  46. package/src/checks/automatic/contrast-minimum.js +486 -465
  47. package/src/checks/automatic/css-orientation-lock.js +39 -9
  48. package/src/checks/automatic/definition-list-children-valid.js +40 -19
  49. package/src/checks/automatic/deprecated-elements-not-used.js +21 -7
  50. package/src/checks/automatic/dialog-name-present.js +37 -75
  51. package/src/checks/automatic/dlitem-parent-valid.js +23 -8
  52. package/src/checks/automatic/duplicate-id-aria.js +24 -6
  53. package/src/checks/automatic/embed-text-alternative-present.js +86 -35
  54. package/src/checks/automatic/form-control-programmatic-label-present.js +79 -196
  55. package/src/checks/automatic/form-control-single-label.js +47 -10
  56. package/src/checks/automatic/html-xml-lang-mismatch.js +43 -19
  57. package/src/checks/automatic/iframe-focusable-content.js +26 -11
  58. package/src/checks/automatic/iframe-name-present.js +31 -9
  59. package/src/checks/automatic/iframe-title-unique.js +29 -8
  60. package/src/checks/automatic/img-alt-present.js +47 -43
  61. package/src/checks/automatic/input-image-alt-present.js +143 -112
  62. package/src/checks/automatic/label-in-name.js +50 -22
  63. package/src/checks/automatic/language-page-present.js +117 -109
  64. package/src/checks/automatic/link-in-text-block.js +59 -19
  65. package/src/checks/automatic/link-name-present.js +45 -14
  66. package/src/checks/automatic/list-children-valid.js +26 -9
  67. package/src/checks/automatic/listbox-name-present.js +39 -19
  68. package/src/checks/automatic/listitem-parent-valid.js +18 -6
  69. package/src/checks/automatic/menuitem-name-present.js +39 -61
  70. package/src/checks/automatic/meta-refresh-no-exceptions.js +37 -8
  71. package/src/checks/automatic/meta-refresh-timing-absent.js +28 -6
  72. package/src/checks/automatic/meta-viewport-zoom-enabled.js +32 -7
  73. package/src/checks/automatic/meter-name-present.js +36 -33
  74. package/src/checks/automatic/nested-interactive-controls-absent.js +31 -10
  75. package/src/checks/automatic/object-text-alternative-present.js +91 -39
  76. package/src/checks/automatic/option-name-present.js +38 -21
  77. package/src/checks/automatic/page-title-present.js +26 -7
  78. package/src/checks/automatic/progressbar-name-present.js +41 -34
  79. package/src/checks/automatic/role-img-alt-present.js +209 -157
  80. package/src/checks/automatic/searchbox-name-present.js +39 -19
  81. package/src/checks/automatic/server-side-image-map-absent.js +23 -8
  82. package/src/checks/automatic/slider-name-present.js +40 -47
  83. package/src/checks/automatic/spinbutton-name-present.js +39 -19
  84. package/src/checks/automatic/summary-name-present.js +37 -17
  85. package/src/checks/automatic/svg-image-text-alternative-present.js +114 -47
  86. package/src/checks/automatic/svg-text-alternative-present.js +246 -226
  87. package/src/checks/automatic/tab-name-present.js +37 -60
  88. package/src/checks/automatic/table-headers-attr-valid.js +24 -8
  89. package/src/checks/automatic/table-th-has-data-cells.js +22 -8
  90. package/src/checks/automatic/target-size-minimum.js +118 -48
  91. package/src/checks/automatic/td-has-header.js +29 -11
  92. package/src/checks/automatic/textbox-name-present.js +39 -19
  93. package/src/checks/automatic/tooltip-name-present.js +37 -18
  94. package/src/checks/automatic/treeitem-name-present.js +38 -21
  95. package/src/checks/automatic/valid-lang.js +20 -6
  96. package/src/checks/automatic/video-poster-text-alternative-present.js +79 -36
  97. package/src/checks/manual/accesskeys-manual.js +14 -5
  98. package/src/checks/manual/area-alt-decorative-manual.js +192 -193
  99. package/src/checks/manual/area-alt-quality-manual.js +182 -141
  100. package/src/checks/manual/aria-checked-state-mismatch-manual.js +34 -11
  101. package/src/checks/manual/aria-text-manual.js +14 -6
  102. package/src/checks/manual/canvas-text-alternative-quality-manual.js +149 -114
  103. package/src/checks/manual/css-hidden-focus.js +196 -165
  104. package/src/checks/manual/embed-text-alternative-quality-manual.js +171 -160
  105. package/src/checks/manual/empty-heading-manual.js +24 -12
  106. package/src/checks/manual/empty-table-header-manual.js +21 -14
  107. package/src/checks/manual/focus-order-semantics-manual.js +45 -10
  108. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +207 -246
  109. package/src/checks/manual/heading-order-manual.js +22 -12
  110. package/src/checks/manual/identical-links-same-purpose-manual.js +34 -12
  111. package/src/checks/manual/image-redundant-alt-manual.js +17 -7
  112. package/src/checks/manual/img-alt-decorative-manual.js +131 -96
  113. package/src/checks/manual/img-alt-quality-manual.js +176 -127
  114. package/src/checks/manual/input-image-alt-decorative-manual.js +125 -92
  115. package/src/checks/manual/input-image-alt-quality-manual.js +125 -92
  116. package/src/checks/manual/label-title-only-manual.js +14 -5
  117. package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -29
  118. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +82 -29
  119. package/src/checks/manual/landmark-main-is-top-level-manual.js +64 -23
  120. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +54 -23
  121. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +54 -23
  122. package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
  123. package/src/checks/manual/landmark-one-main-manual.js +35 -21
  124. package/src/checks/manual/landmark-unique-manual.js +73 -45
  125. package/src/checks/manual/link-name-quality-manual.js +43 -12
  126. package/src/checks/manual/media-transcript-present-manual.js +35 -22
  127. package/src/checks/manual/meta-viewport-large-manual.js +25 -6
  128. package/src/checks/manual/mouse-only-event-handlers-manual.js +38 -11
  129. package/src/checks/manual/no-autoplay-audio-manual.js +20 -6
  130. package/src/checks/manual/object-text-alternative-quality-manual.js +175 -154
  131. package/src/checks/manual/p-as-heading-manual.js +22 -7
  132. package/src/checks/manual/page-has-heading-one-manual.js +40 -23
  133. package/src/checks/manual/page-title-patterns-manual.js +87 -51
  134. package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
  135. package/src/checks/manual/region-manual.js +275 -62
  136. package/src/checks/manual/scope-attr-valid-manual.js +11 -9
  137. package/src/checks/manual/scrollable-region-focusable-manual.js +37 -11
  138. package/src/checks/manual/skip-link-manual.js +35 -12
  139. package/src/checks/manual/svg-text-alternative-quality-manual.js +206 -165
  140. package/src/checks/manual/tabindex-manual.js +11 -9
  141. package/src/checks/manual/table-duplicate-name-manual.js +17 -7
  142. package/src/checks/manual/table-fake-caption-manual.js +24 -7
  143. package/src/checks/manual/video-caption-manual.js +15 -4
  144. package/src/checks/manual-review.js +56 -12
  145. package/src/core/aria-helpers.js +1128 -823
  146. package/src/core/contrast-helpers.js +1217 -1062
  147. package/src/core/dom-helpers.js +4176 -3886
  148. package/src/core/dom-runner.js +720 -596
  149. package/src/core/frame-messaging.js +189 -138
  150. package/src/core/frame-scan.js +94 -82
  151. package/src/core/rollup-composites.js +94 -102
  152. package/src/core/rule-meta.js +71 -35
  153. package/src/core.js +37939 -29121
  154. package/src/i18n/en.js +1194 -887
  155. package/src/i18n/fr.js +1136 -793
  156. package/src/policy/contracts.js +13 -13
  157. package/src/policy/resolvePolicy.js +48 -44
  158. package/src/report.js +502 -0
  159. package/src/sarif.js +175 -0
  160. 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: ["wcag2a", "wcag412", "forms", "atomic", "automatic", "name", "listbox"],
13
+ tags: ['wcag2a', 'wcag412', 'forms', 'atomic', 'automatic', 'name', 'listbox'],
14
14
  wcagSc: ['4.1.2'],
15
15
  normativeMappings: [
16
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
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 = helpers && typeof helpers.getEligibilityInfo === 'function'
28
- ? helpers.getEligibilityInfo
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 || '').replace(/\s+/g, ' ').trim();
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 { return ''; }
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 = (container && container.textContent) ? String(container.textContent) : '';
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 = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
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 = "[role=\"listbox\"]";
134
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
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
- ? (() => { try { return getEligibilityInfo(el, ctx, { targetSet: 'acc' }); } catch { return null; } })()
204
- : null;
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) : (el.outerHTML || '');
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: 'Provide aria-label or aria-labelledby (preferred), or provide visible text that is not hidden from assistive technologies.',
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 { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
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: 'Checks that <li> elements are contained by <ul>, <ol>, or an element with role="list".',
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
- { standard: 'WCAG', version: '2.2', requirement: '1.3.1', title: 'Info and Relationships', conformanceLevel: 'A' }
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 { document, helpers, rule } = ctx;
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) : (el.outerHTML || '');
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 { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
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: 'Checks that menu items (role="menuitem*", including checkbox/radio variants) expose a non-empty accessible name.',
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
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
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 = helpers && typeof helpers.getEligibilityInfo === 'function'
28
- ? helpers.getEligibilityInfo
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 || '').replace(/\s+/g, ' ').trim();
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 { return ''; }
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 = (container && container.textContent) ? String(container.textContent) : '';
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 = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
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 ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
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
- ? (() => { try { return getEligibilityInfo(el, ctx, { targetSet: 'acc' }); } catch { return null; } })()
169
- : null;
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) : (el.outerHTML || '');
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 { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
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: '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.',
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
- { standard: 'WCAG', version: '2.2', requirement: '2.2.4', title: 'Interruptions', conformanceLevel: 'AAA' },
47
- { standard: 'WCAG', version: '2.2', requirement: '3.2.5', title: 'Change on Request', conformanceLevel: 'AAA' }
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',
@@ -58,10 +71,20 @@ const meta = {
58
71
  }
59
72
  };
60
73
 
74
+ // This check is inherently whole-document (does the PAGE have this
75
+ // property?), not evaluable per-subtree -- notApplicable when contextSelector
76
+ // scoped this run narrower than the whole document, or when
77
+ // engineOptions.fragment:true was set (see helpers.isWholeDocumentScope).
78
+ function applicability(ctx) {
79
+ return ctx.helpers.isWholeDocumentScope ? ctx.helpers.isWholeDocumentScope() : true;
80
+ }
81
+
61
82
  function runInPage(ctx) {
62
83
  const { document, helpers, rule } = ctx;
63
84
 
64
- const nodes = document.querySelectorAll ? document.querySelectorAll('meta[http-equiv="refresh" i]') : [];
85
+ const nodes = document.querySelectorAll
86
+ ? document.querySelectorAll('meta[http-equiv="refresh" i]')
87
+ : [];
65
88
 
66
89
  const occurrences = [];
67
90
  let applicableCount = 0;
@@ -75,12 +98,13 @@ function runInPage(ctx) {
75
98
  applicableCount += 1;
76
99
 
77
100
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
78
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
101
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
79
102
 
80
103
  occurrences.push({
81
104
  selector: stableSelector,
82
105
  html,
83
- summary: 'This page uses a meta refresh, which is an automatic context change not initiated by the user.',
106
+ summary:
107
+ 'This page uses a meta refresh, which is an automatic context change not initiated by the user.',
84
108
  hint: 'Remove the meta refresh; trigger the redirect/refresh only in response to a user action instead.',
85
109
  i18n: {
86
110
  summaryKey: 'metaRefreshNoExceptions_summary_fail',
@@ -97,9 +121,14 @@ function runInPage(ctx) {
97
121
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
98
122
  }
99
123
  if (occurrences.length) {
100
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'moderate', occurrences };
124
+ return {
125
+ ruleId: rule.ruleId,
126
+ outcome: 'fail',
127
+ severity: rule.defaultSeverity || 'moderate',
128
+ occurrences
129
+ };
101
130
  }
102
131
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
103
132
  }
104
133
 
105
- module.exports = { id, meta, runInPage };
134
+ module.exports = { id, meta, runInPage, applicability };
@@ -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: 'Checks that <meta http-equiv="refresh"> does not impose a positive delay of 20 hours or less.',
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
- { standard: 'WCAG', version: '2.2', requirement: '2.2.1', title: 'Timing Adjustable', conformanceLevel: 'A' }
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',
@@ -49,13 +56,23 @@ const meta = {
49
56
  coverage: { facetsBySc: { '2.2.1': ['meta-refresh-timing-absent'] } }
50
57
  };
51
58
 
59
+ // This check is inherently whole-document (does the PAGE have this
60
+ // property?), not evaluable per-subtree -- notApplicable when contextSelector
61
+ // scoped this run narrower than the whole document, or when
62
+ // engineOptions.fragment:true was set (see helpers.isWholeDocumentScope).
63
+ function applicability(ctx) {
64
+ return ctx.helpers.isWholeDocumentScope ? ctx.helpers.isWholeDocumentScope() : true;
65
+ }
66
+
52
67
  function runInPage(ctx) {
53
68
  const { document, helpers, rule } = ctx;
54
69
 
55
70
  // WCAG 2.2.1 Exception 3: time limits longer than 20 hours are exempt.
56
71
  const EXEMPT_DELAY_SECONDS = 20 * 60 * 60;
57
72
 
58
- const nodes = document.querySelectorAll ? document.querySelectorAll('meta[http-equiv="refresh" i]') : [];
73
+ const nodes = document.querySelectorAll
74
+ ? document.querySelectorAll('meta[http-equiv="refresh" i]')
75
+ : [];
59
76
 
60
77
  const occurrences = [];
61
78
  let applicableCount = 0;
@@ -77,7 +94,7 @@ function runInPage(ctx) {
77
94
  if (delay > EXEMPT_DELAY_SECONDS) continue; // WCAG 2.2.1 Exception 3 (>20 hours)
78
95
 
79
96
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
80
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
97
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
81
98
 
82
99
  occurrences.push({
83
100
  selector: stableSelector,
@@ -99,9 +116,14 @@ function runInPage(ctx) {
99
116
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
100
117
  }
101
118
  if (occurrences.length) {
102
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
119
+ return {
120
+ ruleId: rule.ruleId,
121
+ outcome: 'fail',
122
+ severity: rule.defaultSeverity || 'serious',
123
+ occurrences
124
+ };
103
125
  }
104
126
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
105
127
  }
106
128
 
107
- module.exports = { id, meta, runInPage };
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: 'Checks that <meta name="viewport"> does not set user-scalable=no or maximum-scale below 2 (200%).',
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
- { standard: 'WCAG', version: '2.2', requirement: '1.4.4', title: 'Resize Text', conformanceLevel: 'AA' }
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',
@@ -42,6 +49,14 @@ const meta = {
42
49
  coverage: { facetsBySc: { '1.4.4': ['meta-viewport-zoom-enabled'] } }
43
50
  };
44
51
 
52
+ // This check is inherently whole-document (does the PAGE have this
53
+ // property?), not evaluable per-subtree -- notApplicable when contextSelector
54
+ // scoped this run narrower than the whole document, or when
55
+ // engineOptions.fragment:true was set (see helpers.isWholeDocumentScope).
56
+ function applicability(ctx) {
57
+ return ctx.helpers.isWholeDocumentScope ? ctx.helpers.isWholeDocumentScope() : true;
58
+ }
59
+
45
60
  function runInPage(ctx) {
46
61
  const { document, helpers, rule } = ctx;
47
62
 
@@ -51,13 +66,18 @@ function runInPage(ctx) {
51
66
  const eq = pair.indexOf('=');
52
67
  if (eq === -1) continue;
53
68
  const key = pair.slice(0, eq).trim().toLowerCase();
54
- const value = pair.slice(eq + 1).trim().toLowerCase();
69
+ const value = pair
70
+ .slice(eq + 1)
71
+ .trim()
72
+ .toLowerCase();
55
73
  if (key) out[key] = value;
56
74
  }
57
75
  return out;
58
76
  }
59
77
 
60
- const nodes = document.querySelectorAll ? document.querySelectorAll('meta[name="viewport" i]') : [];
78
+ const nodes = document.querySelectorAll
79
+ ? document.querySelectorAll('meta[name="viewport" i]')
80
+ : [];
61
81
 
62
82
  const occurrences = [];
63
83
  let applicableCount = 0;
@@ -88,7 +108,7 @@ function runInPage(ctx) {
88
108
  if (!reasons.length) continue;
89
109
 
90
110
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
91
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
111
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
92
112
 
93
113
  occurrences.push({
94
114
  selector: stableSelector,
@@ -110,9 +130,14 @@ function runInPage(ctx) {
110
130
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
111
131
  }
112
132
  if (occurrences.length) {
113
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
133
+ return {
134
+ ruleId: rule.ruleId,
135
+ outcome: 'fail',
136
+ severity: rule.defaultSeverity || 'serious',
137
+ occurrences
138
+ };
114
139
  }
115
140
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
116
141
  }
117
142
 
118
- module.exports = { id, meta, runInPage };
143
+ module.exports = { id, meta, runInPage, applicability };