@surea11y/core 1.0.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 (164) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +145 -0
  4. package/bin/core.js +244 -0
  5. package/docs/BINDING_AUTHORS_GUIDE.md +41 -0
  6. package/docs/CLI.md +49 -0
  7. package/docs/ENGINE_OPTIONS.md +155 -0
  8. package/docs/I18N.md +47 -0
  9. package/docs/INTEGRATION.md +156 -0
  10. package/docs/LIMITATIONS.md +31 -0
  11. package/docs/OUTPUT_SCHEMA.md +237 -0
  12. package/docs/POLICY.md +71 -0
  13. package/docs/RULE_AUTHORING.md +375 -0
  14. package/docs/RULE_CATALOG.md +180 -0
  15. package/docs/RULE_TAXONOMY.md +145 -0
  16. package/docs/TROUBLESHOOTING.md +48 -0
  17. package/docs/WCAG_CONFORMANCE.md +49 -0
  18. package/package.json +60 -0
  19. package/src/catalogs/composites.wcag.js +490 -0
  20. package/src/checks/automatic/area-alt-present.js +225 -0
  21. package/src/checks/automatic/aria-allowed-attr.js +206 -0
  22. package/src/checks/automatic/aria-allowed-role.js +102 -0
  23. package/src/checks/automatic/aria-braille-equivalent.js +139 -0
  24. package/src/checks/automatic/aria-conditional-attr.js +110 -0
  25. package/src/checks/automatic/aria-deprecated-role.js +106 -0
  26. package/src/checks/automatic/aria-hidden-body.js +87 -0
  27. package/src/checks/automatic/aria-hidden-focus.js +480 -0
  28. package/src/checks/automatic/aria-prohibited-attr.js +156 -0
  29. package/src/checks/automatic/aria-prohibited-children.js +265 -0
  30. package/src/checks/automatic/aria-required-attr.js +154 -0
  31. package/src/checks/automatic/aria-required-children.js +274 -0
  32. package/src/checks/automatic/aria-required-parent.js +222 -0
  33. package/src/checks/automatic/aria-role-name-present.js +201 -0
  34. package/src/checks/automatic/aria-roles-valid.js +110 -0
  35. package/src/checks/automatic/aria-valid-attr-value.js +123 -0
  36. package/src/checks/automatic/aria-valid-attr.js +109 -0
  37. package/src/checks/automatic/autocomplete-valid.js +134 -0
  38. package/src/checks/automatic/avoid-inline-spacing.js +107 -0
  39. package/src/checks/automatic/binary-control-name-present.js +294 -0
  40. package/src/checks/automatic/button-name-present.js +146 -0
  41. package/src/checks/automatic/bypass-blocks-present.js +162 -0
  42. package/src/checks/automatic/canvas-text-alternative-present.js +140 -0
  43. package/src/checks/automatic/combobox-name-present.js +267 -0
  44. package/src/checks/automatic/contrast-computable.js +378 -0
  45. package/src/checks/automatic/contrast-enhanced.js +517 -0
  46. package/src/checks/automatic/contrast-minimum.js +512 -0
  47. package/src/checks/automatic/css-orientation-lock.js +206 -0
  48. package/src/checks/automatic/definition-list-children-valid.js +148 -0
  49. package/src/checks/automatic/deprecated-elements-not-used.js +91 -0
  50. package/src/checks/automatic/dialog-name-present.js +209 -0
  51. package/src/checks/automatic/dlitem-parent-valid.js +100 -0
  52. package/src/checks/automatic/duplicate-id-aria.js +126 -0
  53. package/src/checks/automatic/embed-text-alternative-present.js +190 -0
  54. package/src/checks/automatic/form-control-programmatic-label-present.js +409 -0
  55. package/src/checks/automatic/form-control-single-label.js +117 -0
  56. package/src/checks/automatic/html-xml-lang-mismatch.js +91 -0
  57. package/src/checks/automatic/iframe-focusable-content.js +141 -0
  58. package/src/checks/automatic/iframe-name-present.js +102 -0
  59. package/src/checks/automatic/iframe-title-unique.js +107 -0
  60. package/src/checks/automatic/img-alt-present.js +223 -0
  61. package/src/checks/automatic/input-image-alt-present.js +155 -0
  62. package/src/checks/automatic/label-in-name.js +326 -0
  63. package/src/checks/automatic/language-page-present.js +159 -0
  64. package/src/checks/automatic/link-in-text-block.js +218 -0
  65. package/src/checks/automatic/link-name-present.js +114 -0
  66. package/src/checks/automatic/list-children-valid.js +152 -0
  67. package/src/checks/automatic/listbox-name-present.js +236 -0
  68. package/src/checks/automatic/listitem-parent-valid.js +118 -0
  69. package/src/checks/automatic/menuitem-name-present.js +201 -0
  70. package/src/checks/automatic/meta-refresh-no-exceptions.js +105 -0
  71. package/src/checks/automatic/meta-refresh-timing-absent.js +107 -0
  72. package/src/checks/automatic/meta-viewport-zoom-enabled.js +118 -0
  73. package/src/checks/automatic/meter-name-present.js +160 -0
  74. package/src/checks/automatic/nested-interactive-controls-absent.js +135 -0
  75. package/src/checks/automatic/object-text-alternative-present.js +193 -0
  76. package/src/checks/automatic/option-name-present.js +157 -0
  77. package/src/checks/automatic/page-title-present.js +86 -0
  78. package/src/checks/automatic/progressbar-name-present.js +165 -0
  79. package/src/checks/automatic/role-img-alt-present.js +206 -0
  80. package/src/checks/automatic/searchbox-name-present.js +236 -0
  81. package/src/checks/automatic/server-side-image-map-absent.js +88 -0
  82. package/src/checks/automatic/slider-name-present.js +276 -0
  83. package/src/checks/automatic/spinbutton-name-present.js +236 -0
  84. package/src/checks/automatic/summary-name-present.js +153 -0
  85. package/src/checks/automatic/svg-image-text-alternative-present.js +220 -0
  86. package/src/checks/automatic/svg-text-alternative-present.js +298 -0
  87. package/src/checks/automatic/tab-name-present.js +200 -0
  88. package/src/checks/automatic/table-headers-attr-valid.js +122 -0
  89. package/src/checks/automatic/table-th-has-data-cells.js +117 -0
  90. package/src/checks/automatic/target-size-minimum.js +605 -0
  91. package/src/checks/automatic/td-has-header.js +151 -0
  92. package/src/checks/automatic/textbox-name-present.js +236 -0
  93. package/src/checks/automatic/tooltip-name-present.js +158 -0
  94. package/src/checks/automatic/treeitem-name-present.js +157 -0
  95. package/src/checks/automatic/valid-lang.js +100 -0
  96. package/src/checks/automatic/video-poster-text-alternative-present.js +193 -0
  97. package/src/checks/manual/accesskeys-manual.js +93 -0
  98. package/src/checks/manual/area-alt-decorative-manual.js +247 -0
  99. package/src/checks/manual/area-alt-quality-manual.js +204 -0
  100. package/src/checks/manual/aria-checked-state-mismatch-manual.js +141 -0
  101. package/src/checks/manual/aria-text-manual.js +109 -0
  102. package/src/checks/manual/canvas-text-alternative-quality-manual.js +170 -0
  103. package/src/checks/manual/css-hidden-focus.js +259 -0
  104. package/src/checks/manual/embed-text-alternative-quality-manual.js +204 -0
  105. package/src/checks/manual/empty-heading-manual.js +182 -0
  106. package/src/checks/manual/empty-table-header-manual.js +163 -0
  107. package/src/checks/manual/focus-order-semantics-manual.js +117 -0
  108. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +291 -0
  109. package/src/checks/manual/heading-order-manual.js +130 -0
  110. package/src/checks/manual/identical-links-same-purpose-manual.js +142 -0
  111. package/src/checks/manual/image-redundant-alt-manual.js +118 -0
  112. package/src/checks/manual/img-alt-decorative-manual.js +148 -0
  113. package/src/checks/manual/img-alt-quality-manual.js +182 -0
  114. package/src/checks/manual/input-image-alt-decorative-manual.js +144 -0
  115. package/src/checks/manual/input-image-alt-quality-manual.js +144 -0
  116. package/src/checks/manual/label-title-only-manual.js +115 -0
  117. package/src/checks/manual/landmark-banner-is-top-level-manual.js +180 -0
  118. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +169 -0
  119. package/src/checks/manual/landmark-main-is-top-level-manual.js +167 -0
  120. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +177 -0
  121. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +169 -0
  122. package/src/checks/manual/landmark-no-duplicate-main-manual.js +132 -0
  123. package/src/checks/manual/landmark-one-main-manual.js +151 -0
  124. package/src/checks/manual/landmark-unique-manual.js +252 -0
  125. package/src/checks/manual/link-name-quality-manual.js +143 -0
  126. package/src/checks/manual/media-transcript-present-manual.js +373 -0
  127. package/src/checks/manual/meta-viewport-large-manual.js +119 -0
  128. package/src/checks/manual/mouse-only-event-handlers-manual.js +134 -0
  129. package/src/checks/manual/no-autoplay-audio-manual.js +116 -0
  130. package/src/checks/manual/object-text-alternative-quality-manual.js +194 -0
  131. package/src/checks/manual/p-as-heading-manual.js +163 -0
  132. package/src/checks/manual/page-has-heading-one-manual.js +110 -0
  133. package/src/checks/manual/page-title-patterns-manual.js +262 -0
  134. package/src/checks/manual/presentation-role-conflict-manual.js +159 -0
  135. package/src/checks/manual/region-manual.js +183 -0
  136. package/src/checks/manual/scope-attr-valid-manual.js +93 -0
  137. package/src/checks/manual/scrollable-region-focusable-manual.js +168 -0
  138. package/src/checks/manual/skip-link-manual.js +150 -0
  139. package/src/checks/manual/svg-text-alternative-quality-manual.js +209 -0
  140. package/src/checks/manual/tabindex-manual.js +94 -0
  141. package/src/checks/manual/table-duplicate-name-manual.js +99 -0
  142. package/src/checks/manual/table-fake-caption-manual.js +122 -0
  143. package/src/checks/manual/video-caption-manual.js +118 -0
  144. package/src/checks/manual-review.js +95 -0
  145. package/src/checks/rules-and-tags.full.csv +19 -0
  146. package/src/checks/rules-and-tags.full.json +259 -0
  147. package/src/core/aria-helpers.js +906 -0
  148. package/src/core/contrast-helpers.js +1147 -0
  149. package/src/core/dom-helpers.js +4085 -0
  150. package/src/core/dom-runner.js +627 -0
  151. package/src/core/frame-messaging.js +210 -0
  152. package/src/core/frame-scan.js +178 -0
  153. package/src/core/rollup-composites.js +135 -0
  154. package/src/core/rule-meta.js +140 -0
  155. package/src/core.js +79055 -0
  156. package/src/coverage/wcag-facets.js +1079 -0
  157. package/src/coverage/wcag-version-map.js +84 -0
  158. package/src/i18n/en.js +919 -0
  159. package/src/i18n/fr.js +527 -0
  160. package/src/index.js +4 -0
  161. package/src/policy/contracts.js +18 -0
  162. package/src/policy/resolvePolicy.js +55 -0
  163. package/src/policy/schemas/engine-options.schema.json +103 -0
  164. package/src/policy/schemas/policy-contract.schema.json +40 -0
@@ -0,0 +1,94 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @check tabindex
5
+ * @atomic true
6
+ * @summary tabindex should not be greater than 0
7
+ * @standard Best Practices (a widely-used reference engine's classification; no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
8
+ * @applicability
9
+ * Applies to elements with a tabindex attribute whose value parses as
10
+ * a valid integer.
11
+ * @expectation
12
+ * The tabindex value is 0 or negative. A positive tabindex reorders
13
+ * keyboard tab order explicitly, which is fragile to maintain as a
14
+ * page changes and usually indicates the natural DOM order should be
15
+ * fixed instead.
16
+ * @implementation-notes
17
+ * - Not WCAG-normative — authored as an advisory, cantTell-capped
18
+ * `type: 'manual'` rule; see landmark-banner-is-top-level's
19
+ * header comment for the shared rationale/precedent.
20
+ */
21
+
22
+ const id = 'tabindex';
23
+
24
+ const meta = {
25
+ title: 'tabindex should not be greater than 0',
26
+ description: 'Checks that tabindex values are 0 or negative, not a positive number.',
27
+ i18n: {
28
+ titleKey: 'tabindex_title',
29
+ descriptionKey: 'tabindex_description'
30
+ },
31
+ helpUrl: null,
32
+ tags: ['best-practice', 'keyboard', 'structure', 'atomic', 'manual'],
33
+ wcagSc: [],
34
+ normativeMappings: [],
35
+ defaultSeverity: 'minor',
36
+ category: 'operable',
37
+ type: 'manual',
38
+ defaultConfidence: 'medium',
39
+ coverage: {}
40
+ };
41
+
42
+ function runInPage(ctx) {
43
+ const { document, helpers, rule } = ctx;
44
+
45
+ let nodes = [];
46
+ try {
47
+ nodes = document.querySelectorAll('[tabindex]');
48
+ } catch {
49
+ nodes = [];
50
+ }
51
+
52
+ const occurrences = [];
53
+ let applicableCount = 0;
54
+
55
+ for (const el of nodes) {
56
+ if (!el || !el.getAttribute) continue;
57
+ const raw = String(el.getAttribute('tabindex') || '').trim();
58
+ if (!raw) continue;
59
+ const n = Number(raw);
60
+ if (!Number.isInteger(n)) continue;
61
+
62
+ applicableCount += 1;
63
+
64
+ if (n <= 0) continue;
65
+
66
+ const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
67
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
68
+
69
+ occurrences.push({
70
+ selector: stableSelector,
71
+ html,
72
+ summary: 'This element has a positive tabindex, overriding the natural tab order.',
73
+ hint: 'Use tabindex="0" (or a negative value to remove from tab order) instead of a positive number; fix the DOM order if a different tab order is needed.',
74
+ i18n: {
75
+ summaryKey: 'tabindex_summary_cantTell',
76
+ hintKey: 'tabindex_hint_cantTell',
77
+ params: { value: String(n) }
78
+ },
79
+ data: {
80
+ details: { reasonCode: 'TABINDEX_POSITIVE', value: n }
81
+ }
82
+ });
83
+ }
84
+
85
+ if (applicableCount === 0) {
86
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
87
+ }
88
+ if (occurrences.length) {
89
+ return { ruleId: rule.ruleId, outcome: 'cantTell', severity: rule.defaultSeverity || 'minor', occurrences };
90
+ }
91
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
92
+ }
93
+
94
+ module.exports = { id, meta, runInPage };
@@ -0,0 +1,99 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @check table-duplicate-name
5
+ * @atomic true
6
+ * @summary A table's caption must not duplicate its summary attribute
7
+ * @standard Best Practices (a widely-used reference engine's classification; no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
8
+ * @applicability
9
+ * Applies to <table> elements that have both a <caption> with text
10
+ * content and a (deprecated but still encountered) summary attribute.
11
+ * @expectation
12
+ * The caption text and the summary attribute text are not identical
13
+ * (case-insensitive, normalized). When both are present and say the
14
+ * same thing, assistive technology that surfaces both announces the
15
+ * same text twice for one table.
16
+ * @implementation-notes
17
+ * - Not WCAG-normative — authored as an advisory, cantTell-capped
18
+ * `type: 'manual'` rule; see landmark-banner-is-top-level's
19
+ * header comment for the shared rationale/precedent.
20
+ * - Narrowly scoped to caption-vs-summary duplication specifically
21
+ * (matching a reference engine's table-duplicate-name check), not a general "table
22
+ * name quality" check.
23
+ */
24
+
25
+ const id = 'table-duplicate-name';
26
+
27
+ const meta = {
28
+ title: 'Table caption must not duplicate its summary attribute',
29
+ description: 'Checks that a <table>\'s <caption> text is not identical to its (deprecated) summary attribute.',
30
+ i18n: {
31
+ titleKey: 'tableDuplicateName_title',
32
+ descriptionKey: 'tableDuplicateName_description'
33
+ },
34
+ helpUrl: null,
35
+ tags: ['best-practice', 'tables', 'structure', 'atomic', 'manual'],
36
+ wcagSc: [],
37
+ normativeMappings: [],
38
+ defaultSeverity: 'minor',
39
+ category: 'perceivable',
40
+ type: 'manual',
41
+ defaultConfidence: 'medium',
42
+ coverage: {}
43
+ };
44
+
45
+ function runInPage(ctx) {
46
+ const { document, root, helpers, rule } = ctx;
47
+ const safeRoot = root || document;
48
+
49
+ function normalizeWs(s) {
50
+ return String(s || '').replace(/\s+/g, ' ').trim();
51
+ }
52
+
53
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('table[summary]', safeRoot) : helpers.queryAll('table[summary]', safeRoot);
54
+
55
+ const occurrences = [];
56
+ let applicableCount = 0;
57
+
58
+ for (const el of nodes) {
59
+ if (!el || !el.getAttribute) continue;
60
+ const summary = normalizeWs(el.getAttribute('summary'));
61
+ if (!summary) continue;
62
+
63
+ const captionEl = el.querySelector ? el.querySelector('caption') : null;
64
+ const captionText = captionEl ? normalizeWs(captionEl.textContent) : '';
65
+ if (!captionText) continue;
66
+
67
+ applicableCount += 1;
68
+
69
+ if (captionText.toLowerCase() !== summary.toLowerCase()) continue;
70
+
71
+ const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
72
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
73
+
74
+ occurrences.push({
75
+ selector: stableSelector,
76
+ html,
77
+ summary: 'This table\'s caption duplicates its summary attribute.',
78
+ hint: 'Remove the redundant summary attribute, or make it provide different information than the caption.',
79
+ i18n: {
80
+ summaryKey: 'tableDuplicateName_summary_cantTell',
81
+ hintKey: 'tableDuplicateName_hint_cantTell',
82
+ params: {}
83
+ },
84
+ data: {
85
+ details: { reasonCode: 'TABLE_CAPTION_SUMMARY_DUPLICATE' }
86
+ }
87
+ });
88
+ }
89
+
90
+ if (applicableCount === 0) {
91
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
92
+ }
93
+ if (occurrences.length) {
94
+ return { ruleId: rule.ruleId, outcome: 'cantTell', severity: rule.defaultSeverity || 'minor', occurrences };
95
+ }
96
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
97
+ }
98
+
99
+ module.exports = { id, meta, runInPage };
@@ -0,0 +1,122 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @check table-fake-caption
5
+ * @atomic true
6
+ * @summary A table's first row should not stand in for a real <caption>
7
+ * @standard WCAG 2.2
8
+ * @sc 1.3.1
9
+ * @applicability
10
+ * `<table>` elements with no `<caption>` child, at least two rows, and
11
+ * a first row containing exactly one non-empty-text cell while at
12
+ * least one other row has more than one cell.
13
+ * @expectation
14
+ * A single lone cell in the first row, sitting above rows that clearly
15
+ * have multiple columns, strongly suggests the author is using it as a
16
+ * visual caption/title rather than as a real table cell. Structure
17
+ * conveyed only through this positional convention is not
18
+ * programmatically associated with the table the way a real
19
+ * `<caption>` element is (1.3.1).
20
+ * @implementation-notes
21
+ * - Heuristic (a legitimately narrow first data row is possible, if
22
+ * uncommon), so authored as `type: 'manual'` (cantTell-capped, never
23
+ * fail) rather than a hard fail — same conservative posture as
24
+ * `p-as-heading`.
25
+ * - Does not attempt full colspan/rowspan-aware column counting; "more
26
+ * than one cell" on another row is used as the multi-column signal,
27
+ * which is simple, deterministic, and sufficient for the heuristic's
28
+ * purpose without risking a wrong column-index computation.
29
+ */
30
+
31
+ const id = 'table-fake-caption';
32
+
33
+ const meta = {
34
+ title: "A table's first row should not stand in for a real <caption>",
35
+ description: 'Flags tables with no <caption> whose first row has a single non-empty cell while other rows have multiple cells, for manual review of whether that cell is acting as a fake caption.',
36
+ i18n: {
37
+ titleKey: 'tableFakeCaption_title',
38
+ descriptionKey: 'tableFakeCaption_description'
39
+ },
40
+ helpUrl: null,
41
+ tags: ['wcag2a', 'wcag131', 'structure', 'atomic', 'manual'],
42
+ wcagSc: ['1.3.1'],
43
+ normativeMappings: [
44
+ { standard: 'WCAG', version: '2.2', requirement: '1.3.1', title: 'Info and Relationships', conformanceLevel: 'A' }
45
+ ],
46
+ defaultSeverity: 'minor',
47
+ category: 'perceivable',
48
+ type: 'manual',
49
+ defaultConfidence: 'low',
50
+ coverage: { facetsBySc: { '1.3.1': ['table-fake-caption-evidence'] } }
51
+ };
52
+
53
+ function runInPage(ctx) {
54
+ const { document, root, helpers, rule } = ctx;
55
+ const safeRoot = root || document;
56
+
57
+ function trim(v) { return (v == null ? '' : String(v)).trim(); }
58
+
59
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('table', safeRoot) : helpers.queryAll('table', safeRoot);
60
+
61
+ const occurrences = [];
62
+ let applicableCount = 0;
63
+
64
+ for (const table of nodes) {
65
+ if (!table || !table.rows) continue;
66
+
67
+ const hasCaption = !!(table.querySelector && table.querySelector('caption'));
68
+ if (hasCaption) continue;
69
+
70
+ const rows = Array.from(table.rows);
71
+ if (rows.length < 2) continue;
72
+
73
+ applicableCount += 1;
74
+
75
+ const firstRow = rows[0];
76
+ const firstRowCells = Array.from(firstRow.cells || []);
77
+ if (firstRowCells.length !== 1) continue;
78
+
79
+ const candidateCell = firstRowCells[0];
80
+ const candidateText = trim(candidateCell.textContent || '');
81
+ if (!candidateText) continue;
82
+
83
+ const hasMultiCellRow = rows.slice(1).some((r) => Array.from(r.cells || []).length > 1);
84
+ if (!hasMultiCellRow) continue;
85
+
86
+ const stableSelector = helpers.buildSelector ? helpers.buildSelector(table) : 'html';
87
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(table) : (table.outerHTML || '');
88
+
89
+ const baseOccurrence = {
90
+ selector: stableSelector,
91
+ html,
92
+ summary: 'This table has no <caption>, but its first row is a single cell sitting above multi-cell rows — it may be acting as a fake caption.',
93
+ hint: 'If this cell is meant to describe the table, use a real <caption> element instead of a lone first-row cell.',
94
+ i18n: {
95
+ summaryKey: 'tableFakeCaption_summary_cantTell',
96
+ hintKey: 'tableFakeCaption_hint_cantTell',
97
+ params: {}
98
+ },
99
+ data: {
100
+ details: { reasonCode: 'SINGLE_CELL_FIRST_ROW_NO_CAPTION' }
101
+ }
102
+ };
103
+
104
+ if (helpers && typeof helpers.reportOccurrence === 'function') {
105
+ occurrences.push(helpers.reportOccurrence(table, baseOccurrence));
106
+ } else {
107
+ occurrences.push(baseOccurrence);
108
+ }
109
+ }
110
+
111
+ if (applicableCount === 0) {
112
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
113
+ }
114
+
115
+ if (occurrences.length) {
116
+ return { ruleId: rule.ruleId, outcome: 'cantTell', severity: rule.defaultSeverity || 'minor', occurrences };
117
+ }
118
+
119
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
120
+ }
121
+
122
+ module.exports = { id, meta, runInPage };
@@ -0,0 +1,118 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @check video-caption
5
+ * @atomic true
6
+ * @summary Prerecorded <video> should provide a captions track
7
+ * @standard WCAG 2.2
8
+ * @sc 1.2.2
9
+ * @applicability
10
+ * Any <video> element in the composed DOM.
11
+ * @expectation
12
+ * SC 1.2.2 requires captions for prerecorded synchronized media, but
13
+ * only when the video actually has an audio track that conveys
14
+ * information (a silent/decorative video needs none) — which cannot be
15
+ * verified from static markup alone (jsdom does not decode media).
16
+ * This rule is therefore `type: 'manual'` (cantTell-capped, never
17
+ * fail), matching the precedent set by
18
+ * `media-alternative-transcript-evidence` for the same class
19
+ * of "normatively mapped but not staticaly verifiable" gap. A <video>
20
+ * with a `<track kind="captions">` (or `kind="subtitles"`, commonly
21
+ * used interchangeably in the wild even though captions and subtitles
22
+ * serve technically distinct purposes) whose `src` is non-empty is not
23
+ * flagged; everything else is flagged for human review.
24
+ * @implementation-notes
25
+ * - Does not attempt to verify the referenced track file's content —
26
+ * only that a captions/subtitles track is declared with a non-empty
27
+ * `src`.
28
+ */
29
+
30
+ const id = 'video-caption';
31
+
32
+ const meta = {
33
+ title: 'Prerecorded video should provide a captions track',
34
+ description:
35
+ 'Flags <video> elements with no <track kind="captions"|"subtitles"> child, for manual review of whether the video has an audio track that needs captions.',
36
+ i18n: {
37
+ titleKey: 'videoCaption_title',
38
+ descriptionKey: 'videoCaption_description'
39
+ },
40
+ helpUrl: null,
41
+ tags: ['wcag2a', 'wcag122', 'media', 'timebasedmedia', 'atomic', 'manual'],
42
+ wcagSc: ['1.2.2'],
43
+ normativeMappings: [
44
+ { standard: 'WCAG', version: '2.2', requirement: '1.2.2', title: 'Captions (Prerecorded)', conformanceLevel: 'A' }
45
+ ],
46
+ defaultSeverity: 'moderate',
47
+ category: 'perceivable',
48
+ type: 'manual',
49
+ defaultConfidence: 'low',
50
+ coverage: { facetsBySc: { '1.2.2': ['video-captions-track-evidence'] } }
51
+ };
52
+
53
+ function runInPage(ctx) {
54
+ const { document, root, helpers, rule } = ctx;
55
+ const safeRoot = root || document;
56
+
57
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('video', safeRoot) : helpers.queryAll('video', safeRoot);
58
+
59
+ const occurrences = [];
60
+ let applicableCount = 0;
61
+
62
+ for (const el of nodes) {
63
+ if (!el || !el.querySelectorAll) continue;
64
+
65
+ applicableCount += 1;
66
+
67
+ let hasCaptionsTrack = false;
68
+ const tracks = el.querySelectorAll('track');
69
+ for (const t of tracks) {
70
+ const kind = (t.getAttribute('kind') || '').trim().toLowerCase();
71
+ const src = (t.getAttribute('src') || '').trim();
72
+ if ((kind === 'captions' || kind === 'subtitles') && src) {
73
+ hasCaptionsTrack = true;
74
+ break;
75
+ }
76
+ }
77
+
78
+ if (hasCaptionsTrack) continue;
79
+
80
+ const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
81
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
82
+
83
+ const baseOccurrence = {
84
+ selector: stableSelector,
85
+ html,
86
+ summary: 'This video has no captions (or subtitles) track.',
87
+ hint: 'If this video has an audio track that conveys information, add a <track kind="captions" src="..."> with the captioned content.',
88
+ i18n: {
89
+ summaryKey: 'videoCaption_summary_cantTell',
90
+ hintKey: 'videoCaption_hint_cantTell',
91
+ params: {}
92
+ },
93
+ data: {
94
+ details: { reasonCode: 'CAPTIONS_TRACK_NOT_DETECTED' }
95
+ }
96
+ };
97
+
98
+ if (helpers && typeof helpers.reportOccurrence === 'function') {
99
+ occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
100
+ } else {
101
+ occurrences.push(baseOccurrence);
102
+ }
103
+ }
104
+
105
+ if (applicableCount === 0) {
106
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
107
+ }
108
+
109
+ if (occurrences.length) {
110
+ return { ruleId: rule.ruleId, outcome: 'cantTell', severity: rule.defaultSeverity || 'moderate', occurrences };
111
+ }
112
+
113
+ // Manual rules may only emit cantTell/notApplicable (never pass/fail):
114
+ // every <video> already has a captions/subtitles track.
115
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
116
+ }
117
+
118
+ module.exports = { id, meta, runInPage };
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @check manual-review
5
+ * Manual review for keyboard navigation and focus order.
6
+ */
7
+
8
+ const id = 'manual-review';
9
+
10
+ const meta = {
11
+ ruleId: 'manual-review',
12
+ title: 'Manual review: keyboard navigation and focus order',
13
+ description: 'Flags that a manual review of keyboard navigation and focus order is required.',
14
+ i18n: { titleKey: 'manualReview_title', descriptionKey: 'manualReview_description' },
15
+
16
+ helpUrl: null,
17
+ tags: ['wcag2a', 'wcag2aa', 'wcag211', 'wcag243', 'wcag247', 'nontext', 'atomic', 'manual'],
18
+ defaultSeverity: 'moderate',
19
+ category: 'operable',
20
+ type: 'manual',
21
+ defaultConfidence: 'medium',
22
+ normativeMappings: [
23
+ { standard: 'WCAG', version: '2.2', requirement: '2.1.1', title: 'Keyboard', conformanceLevel: 'A', url: 'https://www.w3.org/TR/WCAG22/#keyboard' },
24
+ { standard: 'WCAG', version: '2.2', requirement: '2.4.3', title: 'Focus Order', conformanceLevel: 'A', url: 'https://www.w3.org/TR/WCAG22/#focus-order' },
25
+ { standard: 'WCAG', version: '2.2', requirement: '2.4.7', title: 'Focus Visible', conformanceLevel: 'AA', url: 'https://www.w3.org/TR/WCAG22/#focus-visible' },
26
+
27
+ { standard: 'EN 301 549', version: 'V3.2.1', requirement: '9.2.1.1', title: 'Keyboard' },
28
+ { standard: 'EN 301 549', version: 'V3.2.1', requirement: '9.2.4.3', title: 'Focus Order' },
29
+ { standard: 'EN 301 549', version: 'V3.2.1', requirement: '9.2.4.7', title: 'Focus Visible' },
30
+
31
+ { standard: 'WCAG', version: '2.2', type: 'Understanding', requirement: '2.1.1', title: 'Understanding Keyboard', url: 'https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html' },
32
+ { standard: 'WCAG', version: '2.2', type: 'Understanding', requirement: '2.4.3', title: 'Understanding Focus Order', url: 'https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html' },
33
+ { standard: 'WCAG', version: '2.2', type: 'Understanding', requirement: '2.4.7', title: 'Understanding Focus Visible', url: 'https://www.w3.org/WAI/WCAG22/Understanding/focus-visible.html' }
34
+ ]
35
+ };
36
+
37
+ /**
38
+ * NOTE (engine constraint):
39
+ * runInPage() is serialized and executed from source (fnSource) by a11yCore-core,
40
+ * so it must NOT reference outer-scope variables like `meta` or `id`.
41
+ * Only use `ctx.*`, locals, and DOM APIs.
42
+ */
43
+ function runInPage(ctx) {
44
+ const { document, root, helpers, rule } = ctx;
45
+
46
+ const getOuterHtmlSnippet = helpers && helpers.getOuterHtmlSnippet
47
+ ? helpers.getOuterHtmlSnippet
48
+ : (el) => (el && el.outerHTML) || '';
49
+
50
+ const contextSelector = ctx.contextSelector || null;
51
+
52
+ let rootEl = root || null;
53
+
54
+ if (!rootEl) {
55
+ if (contextSelector) {
56
+ try {
57
+ rootEl = document.querySelector(contextSelector);
58
+ } catch {
59
+ // invalid selector, fallback to full document
60
+ rootEl = null;
61
+ }
62
+ }
63
+ if (!rootEl) {
64
+ rootEl = document.documentElement || document.body || document.querySelector('html');
65
+ }
66
+ }
67
+
68
+ const fallbackRoot =
69
+ rootEl ||
70
+ document.documentElement ||
71
+ document.body ||
72
+ document.querySelector('html');
73
+
74
+ const html = getOuterHtmlSnippet(fallbackRoot);
75
+
76
+ return {
77
+ ruleId: rule.ruleId,
78
+ outcome: 'cantTell',
79
+ severity: rule.defaultSeverity || 'moderate',
80
+ occurrences: [
81
+ {
82
+ selector: contextSelector || 'html',
83
+ html,
84
+ summary: 'Manual review required for keyboard navigation and focus order.',
85
+ i18n: { summaryKey: 'manualReview_summary_cantTell', hintKey: 'manualReview_hint_cantTell', params: {} },
86
+ }
87
+ ]
88
+ };
89
+ }
90
+
91
+ module.exports = {
92
+ id,
93
+ meta,
94
+ runInPage
95
+ };
@@ -0,0 +1,19 @@
1
+ ruleId,type,title,tags,file
2
+ area-alt-decorative,manual,<area> with alt=\,wcag2a wcag111 nontext images imagemap manual atomic,area-alt-decorative-manual.js
3
+ area-alt-present,automatic,&lt;area&gt; must have an alt attribute,wcag2a wcag111 nontext images imagemap atomic automatic,area-alt-present.js
4
+ area-alt-quality,manual,<area> alt text must be appropriate (manual review),wcag2a wcag111 nontext images imagemap manual atomic,area-alt-quality-manual.js
5
+ canvas-text-alternative-present,automatic,<canvas> must provide a text alternative,wcag2a wcag111 nontext canvas atomic automatic,canvas-text-alternative-present.js
6
+ canvas-text-alternative-quality,manual,<canvas> text alternative must be appropriate (manual review),wcag2a wcag111 nontext canvas manual atomic,canvas-text-alternative-quality-manual.js
7
+ embed-text-alternative-present,automatic,<embed> must provide a text alternative,wcag2a wcag111 nontext embed atomic automatic,embed-text-alternative-present.js
8
+ embed-text-alternative-quality,manual,<embed> text alternative must be appropriate (manual review),wcag2a wcag111 nontext embed manual atomic,embed-text-alternative-quality-manual.js
9
+ img-alt-decorative,manual,<img> with alt=\,wcag2a wcag111 nontext images manual atomic,img-alt-decorative-manual.js
10
+ img-alt-present,automatic,<img> must have an alt attribute,wcag2a wcag111 nontext images atomic automatic,img-alt-present.js
11
+ img-alt-quality,manual,<img> alt text must be appropriate (manual review),wcag2a wcag111 nontext images manual atomic,img-alt-quality-manual.js
12
+ input-image-alt-decorative,manual,<input type=\,wcag2a wcag111 forms images manual atomic,input-image-alt-decorative-manual.js
13
+ input-image-alt-present,automatic,<input type=,wcag2a wcag111 nontext images atomic automatic,input-image-alt-present.js
14
+ input-image-alt-quality,manual,<input type=\,wcag2a wcag111 forms images manual atomic,input-image-alt-quality-manual.js
15
+ object-text-alternative-present,automatic,<object> must provide a text alternative,wcag2a wcag111 nontext object atomic automatic,object-text-alternative-present.js
16
+ object-text-alternative-quality,manual,<object> text alternative must be appropriate (manual review),wcag2a wcag111 nontext object manual atomic,object-text-alternative-quality-manual.js
17
+ svg-image-text-alternative-present,automatic,SVG <image> must have a text alternative,wcag2a wcag111 nontext svg image atomic automatic,svg-image-text-alternative-present.js
18
+ svg-text-alternative-present,automatic,<svg> must provide a text alternative,wcag2a wcag111 svg nontext images atomic automatic,svg-text-alternative-present.js
19
+ video-poster-text-alternative-present,automatic,<video> poster must have a text alternative,wcag2a wcag111 media video atomic automatic,video-poster-text-alternative-present.js