@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.
- package/CHANGELOG.md +49 -0
- package/LICENSE +21 -0
- package/README.md +145 -0
- package/bin/core.js +244 -0
- package/docs/BINDING_AUTHORS_GUIDE.md +41 -0
- package/docs/CLI.md +49 -0
- package/docs/ENGINE_OPTIONS.md +155 -0
- package/docs/I18N.md +47 -0
- package/docs/INTEGRATION.md +156 -0
- package/docs/LIMITATIONS.md +31 -0
- package/docs/OUTPUT_SCHEMA.md +237 -0
- package/docs/POLICY.md +71 -0
- package/docs/RULE_AUTHORING.md +375 -0
- package/docs/RULE_CATALOG.md +180 -0
- package/docs/RULE_TAXONOMY.md +145 -0
- package/docs/TROUBLESHOOTING.md +48 -0
- package/docs/WCAG_CONFORMANCE.md +49 -0
- package/package.json +60 -0
- package/src/catalogs/composites.wcag.js +490 -0
- package/src/checks/automatic/area-alt-present.js +225 -0
- package/src/checks/automatic/aria-allowed-attr.js +206 -0
- package/src/checks/automatic/aria-allowed-role.js +102 -0
- package/src/checks/automatic/aria-braille-equivalent.js +139 -0
- package/src/checks/automatic/aria-conditional-attr.js +110 -0
- package/src/checks/automatic/aria-deprecated-role.js +106 -0
- package/src/checks/automatic/aria-hidden-body.js +87 -0
- package/src/checks/automatic/aria-hidden-focus.js +480 -0
- package/src/checks/automatic/aria-prohibited-attr.js +156 -0
- package/src/checks/automatic/aria-prohibited-children.js +265 -0
- package/src/checks/automatic/aria-required-attr.js +154 -0
- package/src/checks/automatic/aria-required-children.js +274 -0
- package/src/checks/automatic/aria-required-parent.js +222 -0
- package/src/checks/automatic/aria-role-name-present.js +201 -0
- package/src/checks/automatic/aria-roles-valid.js +110 -0
- package/src/checks/automatic/aria-valid-attr-value.js +123 -0
- package/src/checks/automatic/aria-valid-attr.js +109 -0
- package/src/checks/automatic/autocomplete-valid.js +134 -0
- package/src/checks/automatic/avoid-inline-spacing.js +107 -0
- package/src/checks/automatic/binary-control-name-present.js +294 -0
- package/src/checks/automatic/button-name-present.js +146 -0
- package/src/checks/automatic/bypass-blocks-present.js +162 -0
- package/src/checks/automatic/canvas-text-alternative-present.js +140 -0
- package/src/checks/automatic/combobox-name-present.js +267 -0
- package/src/checks/automatic/contrast-computable.js +378 -0
- package/src/checks/automatic/contrast-enhanced.js +517 -0
- package/src/checks/automatic/contrast-minimum.js +512 -0
- package/src/checks/automatic/css-orientation-lock.js +206 -0
- package/src/checks/automatic/definition-list-children-valid.js +148 -0
- package/src/checks/automatic/deprecated-elements-not-used.js +91 -0
- package/src/checks/automatic/dialog-name-present.js +209 -0
- package/src/checks/automatic/dlitem-parent-valid.js +100 -0
- package/src/checks/automatic/duplicate-id-aria.js +126 -0
- package/src/checks/automatic/embed-text-alternative-present.js +190 -0
- package/src/checks/automatic/form-control-programmatic-label-present.js +409 -0
- package/src/checks/automatic/form-control-single-label.js +117 -0
- package/src/checks/automatic/html-xml-lang-mismatch.js +91 -0
- package/src/checks/automatic/iframe-focusable-content.js +141 -0
- package/src/checks/automatic/iframe-name-present.js +102 -0
- package/src/checks/automatic/iframe-title-unique.js +107 -0
- package/src/checks/automatic/img-alt-present.js +223 -0
- package/src/checks/automatic/input-image-alt-present.js +155 -0
- package/src/checks/automatic/label-in-name.js +326 -0
- package/src/checks/automatic/language-page-present.js +159 -0
- package/src/checks/automatic/link-in-text-block.js +218 -0
- package/src/checks/automatic/link-name-present.js +114 -0
- package/src/checks/automatic/list-children-valid.js +152 -0
- package/src/checks/automatic/listbox-name-present.js +236 -0
- package/src/checks/automatic/listitem-parent-valid.js +118 -0
- package/src/checks/automatic/menuitem-name-present.js +201 -0
- package/src/checks/automatic/meta-refresh-no-exceptions.js +105 -0
- package/src/checks/automatic/meta-refresh-timing-absent.js +107 -0
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +118 -0
- package/src/checks/automatic/meter-name-present.js +160 -0
- package/src/checks/automatic/nested-interactive-controls-absent.js +135 -0
- package/src/checks/automatic/object-text-alternative-present.js +193 -0
- package/src/checks/automatic/option-name-present.js +157 -0
- package/src/checks/automatic/page-title-present.js +86 -0
- package/src/checks/automatic/progressbar-name-present.js +165 -0
- package/src/checks/automatic/role-img-alt-present.js +206 -0
- package/src/checks/automatic/searchbox-name-present.js +236 -0
- package/src/checks/automatic/server-side-image-map-absent.js +88 -0
- package/src/checks/automatic/slider-name-present.js +276 -0
- package/src/checks/automatic/spinbutton-name-present.js +236 -0
- package/src/checks/automatic/summary-name-present.js +153 -0
- package/src/checks/automatic/svg-image-text-alternative-present.js +220 -0
- package/src/checks/automatic/svg-text-alternative-present.js +298 -0
- package/src/checks/automatic/tab-name-present.js +200 -0
- package/src/checks/automatic/table-headers-attr-valid.js +122 -0
- package/src/checks/automatic/table-th-has-data-cells.js +117 -0
- package/src/checks/automatic/target-size-minimum.js +605 -0
- package/src/checks/automatic/td-has-header.js +151 -0
- package/src/checks/automatic/textbox-name-present.js +236 -0
- package/src/checks/automatic/tooltip-name-present.js +158 -0
- package/src/checks/automatic/treeitem-name-present.js +157 -0
- package/src/checks/automatic/valid-lang.js +100 -0
- package/src/checks/automatic/video-poster-text-alternative-present.js +193 -0
- package/src/checks/manual/accesskeys-manual.js +93 -0
- package/src/checks/manual/area-alt-decorative-manual.js +247 -0
- package/src/checks/manual/area-alt-quality-manual.js +204 -0
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +141 -0
- package/src/checks/manual/aria-text-manual.js +109 -0
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +170 -0
- package/src/checks/manual/css-hidden-focus.js +259 -0
- package/src/checks/manual/embed-text-alternative-quality-manual.js +204 -0
- package/src/checks/manual/empty-heading-manual.js +182 -0
- package/src/checks/manual/empty-table-header-manual.js +163 -0
- package/src/checks/manual/focus-order-semantics-manual.js +117 -0
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +291 -0
- package/src/checks/manual/heading-order-manual.js +130 -0
- package/src/checks/manual/identical-links-same-purpose-manual.js +142 -0
- package/src/checks/manual/image-redundant-alt-manual.js +118 -0
- package/src/checks/manual/img-alt-decorative-manual.js +148 -0
- package/src/checks/manual/img-alt-quality-manual.js +182 -0
- package/src/checks/manual/input-image-alt-decorative-manual.js +144 -0
- package/src/checks/manual/input-image-alt-quality-manual.js +144 -0
- package/src/checks/manual/label-title-only-manual.js +115 -0
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +180 -0
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +169 -0
- package/src/checks/manual/landmark-main-is-top-level-manual.js +167 -0
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +177 -0
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +169 -0
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +132 -0
- package/src/checks/manual/landmark-one-main-manual.js +151 -0
- package/src/checks/manual/landmark-unique-manual.js +252 -0
- package/src/checks/manual/link-name-quality-manual.js +143 -0
- package/src/checks/manual/media-transcript-present-manual.js +373 -0
- package/src/checks/manual/meta-viewport-large-manual.js +119 -0
- package/src/checks/manual/mouse-only-event-handlers-manual.js +134 -0
- package/src/checks/manual/no-autoplay-audio-manual.js +116 -0
- package/src/checks/manual/object-text-alternative-quality-manual.js +194 -0
- package/src/checks/manual/p-as-heading-manual.js +163 -0
- package/src/checks/manual/page-has-heading-one-manual.js +110 -0
- package/src/checks/manual/page-title-patterns-manual.js +262 -0
- package/src/checks/manual/presentation-role-conflict-manual.js +159 -0
- package/src/checks/manual/region-manual.js +183 -0
- package/src/checks/manual/scope-attr-valid-manual.js +93 -0
- package/src/checks/manual/scrollable-region-focusable-manual.js +168 -0
- package/src/checks/manual/skip-link-manual.js +150 -0
- package/src/checks/manual/svg-text-alternative-quality-manual.js +209 -0
- package/src/checks/manual/tabindex-manual.js +94 -0
- package/src/checks/manual/table-duplicate-name-manual.js +99 -0
- package/src/checks/manual/table-fake-caption-manual.js +122 -0
- package/src/checks/manual/video-caption-manual.js +118 -0
- package/src/checks/manual-review.js +95 -0
- package/src/checks/rules-and-tags.full.csv +19 -0
- package/src/checks/rules-and-tags.full.json +259 -0
- package/src/core/aria-helpers.js +906 -0
- package/src/core/contrast-helpers.js +1147 -0
- package/src/core/dom-helpers.js +4085 -0
- package/src/core/dom-runner.js +627 -0
- package/src/core/frame-messaging.js +210 -0
- package/src/core/frame-scan.js +178 -0
- package/src/core/rollup-composites.js +135 -0
- package/src/core/rule-meta.js +140 -0
- package/src/core.js +79055 -0
- package/src/coverage/wcag-facets.js +1079 -0
- package/src/coverage/wcag-version-map.js +84 -0
- package/src/i18n/en.js +919 -0
- package/src/i18n/fr.js +527 -0
- package/src/index.js +4 -0
- package/src/policy/contracts.js +18 -0
- package/src/policy/resolvePolicy.js +55 -0
- package/src/policy/schemas/engine-options.schema.json +103 -0
- 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,<area> 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
|