@surea11y/core 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -7
- package/LICENSE +373 -21
- package/README.md +67 -1
- package/bin/core.js +144 -19
- package/docs/API_STABILITY.md +1 -1
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/CLI.md +54 -1
- package/docs/ENGINE_OPTIONS.md +2 -0
- package/docs/INTEGRATION.md +18 -0
- package/docs/OUTPUT_SCHEMA.md +1 -1
- package/docs/RULE_CATALOG.md +1 -1
- package/docs/SARIF.md +59 -0
- package/package.json +15 -4
- package/src/baseline.js +0 -0
- package/src/catalogs/composites.wcag.js +414 -450
- package/src/checks/automatic/area-alt-present.js +59 -25
- package/src/checks/automatic/aria-allowed-attr.js +193 -33
- package/src/checks/automatic/aria-allowed-role.js +21 -7
- package/src/checks/automatic/aria-braille-equivalent.js +32 -10
- package/src/checks/automatic/aria-conditional-attr.js +24 -7
- package/src/checks/automatic/aria-deprecated-role.js +22 -8
- package/src/checks/automatic/aria-hidden-body.js +42 -19
- package/src/checks/automatic/aria-hidden-focus.js +408 -53
- package/src/checks/automatic/aria-prohibited-attr.js +296 -22
- package/src/checks/automatic/aria-prohibited-children.js +55 -16
- package/src/checks/automatic/aria-required-attr.js +23 -8
- package/src/checks/automatic/aria-required-children.js +37 -14
- package/src/checks/automatic/aria-required-parent.js +48 -14
- package/src/checks/automatic/aria-role-name-present.js +47 -21
- package/src/checks/automatic/aria-roles-valid.js +22 -12
- package/src/checks/automatic/aria-valid-attr-value.js +28 -7
- package/src/checks/automatic/aria-valid-attr.js +17 -5
- package/src/checks/automatic/autocomplete-valid.js +74 -16
- package/src/checks/automatic/avoid-inline-spacing.js +20 -6
- package/src/checks/automatic/binary-control-name-present.js +60 -50
- package/src/checks/automatic/button-name-present.js +48 -18
- package/src/checks/automatic/bypass-blocks-present.js +42 -25
- package/src/checks/automatic/canvas-text-alternative-present.js +57 -26
- package/src/checks/automatic/combobox-name-present.js +38 -45
- package/src/checks/automatic/contrast-computable.js +361 -341
- package/src/checks/automatic/contrast-enhanced.js +487 -466
- package/src/checks/automatic/contrast-minimum.js +486 -465
- package/src/checks/automatic/css-orientation-lock.js +30 -8
- package/src/checks/automatic/definition-list-children-valid.js +40 -19
- package/src/checks/automatic/deprecated-elements-not-used.js +21 -7
- package/src/checks/automatic/dialog-name-present.js +37 -75
- package/src/checks/automatic/dlitem-parent-valid.js +23 -8
- package/src/checks/automatic/duplicate-id-aria.js +24 -6
- package/src/checks/automatic/embed-text-alternative-present.js +86 -35
- package/src/checks/automatic/form-control-programmatic-label-present.js +79 -196
- package/src/checks/automatic/form-control-single-label.js +47 -10
- package/src/checks/automatic/html-xml-lang-mismatch.js +34 -18
- package/src/checks/automatic/iframe-focusable-content.js +26 -11
- package/src/checks/automatic/iframe-name-present.js +31 -9
- package/src/checks/automatic/iframe-title-unique.js +29 -8
- package/src/checks/automatic/img-alt-present.js +47 -43
- package/src/checks/automatic/input-image-alt-present.js +143 -112
- package/src/checks/automatic/label-in-name.js +50 -22
- package/src/checks/automatic/language-page-present.js +109 -109
- package/src/checks/automatic/link-in-text-block.js +59 -19
- package/src/checks/automatic/link-name-present.js +45 -14
- package/src/checks/automatic/list-children-valid.js +26 -9
- package/src/checks/automatic/listbox-name-present.js +39 -19
- package/src/checks/automatic/listitem-parent-valid.js +18 -6
- package/src/checks/automatic/menuitem-name-present.js +39 -61
- package/src/checks/automatic/meta-refresh-no-exceptions.js +28 -7
- package/src/checks/automatic/meta-refresh-timing-absent.js +20 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +24 -7
- package/src/checks/automatic/meter-name-present.js +36 -33
- package/src/checks/automatic/nested-interactive-controls-absent.js +31 -10
- package/src/checks/automatic/object-text-alternative-present.js +91 -39
- package/src/checks/automatic/option-name-present.js +38 -21
- package/src/checks/automatic/page-title-present.js +17 -6
- package/src/checks/automatic/progressbar-name-present.js +41 -34
- package/src/checks/automatic/role-img-alt-present.js +209 -157
- package/src/checks/automatic/searchbox-name-present.js +39 -19
- package/src/checks/automatic/server-side-image-map-absent.js +23 -8
- package/src/checks/automatic/slider-name-present.js +40 -47
- package/src/checks/automatic/spinbutton-name-present.js +39 -19
- package/src/checks/automatic/summary-name-present.js +37 -17
- package/src/checks/automatic/svg-image-text-alternative-present.js +114 -47
- package/src/checks/automatic/svg-text-alternative-present.js +246 -226
- package/src/checks/automatic/tab-name-present.js +37 -60
- package/src/checks/automatic/table-headers-attr-valid.js +24 -8
- package/src/checks/automatic/table-th-has-data-cells.js +22 -8
- package/src/checks/automatic/target-size-minimum.js +118 -48
- package/src/checks/automatic/td-has-header.js +29 -11
- package/src/checks/automatic/textbox-name-present.js +39 -19
- package/src/checks/automatic/tooltip-name-present.js +37 -18
- package/src/checks/automatic/treeitem-name-present.js +38 -21
- package/src/checks/automatic/valid-lang.js +20 -6
- package/src/checks/automatic/video-poster-text-alternative-present.js +79 -36
- package/src/checks/manual/accesskeys-manual.js +14 -5
- package/src/checks/manual/area-alt-decorative-manual.js +192 -193
- package/src/checks/manual/area-alt-quality-manual.js +182 -141
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +34 -11
- package/src/checks/manual/aria-text-manual.js +14 -6
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +149 -114
- package/src/checks/manual/css-hidden-focus.js +196 -165
- package/src/checks/manual/embed-text-alternative-quality-manual.js +171 -160
- package/src/checks/manual/empty-heading-manual.js +24 -7
- package/src/checks/manual/empty-table-header-manual.js +17 -6
- package/src/checks/manual/focus-order-semantics-manual.js +45 -10
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +207 -246
- package/src/checks/manual/heading-order-manual.js +22 -7
- package/src/checks/manual/identical-links-same-purpose-manual.js +34 -12
- package/src/checks/manual/image-redundant-alt-manual.js +17 -7
- package/src/checks/manual/img-alt-decorative-manual.js +131 -96
- package/src/checks/manual/img-alt-quality-manual.js +176 -127
- package/src/checks/manual/input-image-alt-decorative-manual.js +125 -92
- package/src/checks/manual/input-image-alt-quality-manual.js +125 -92
- package/src/checks/manual/label-title-only-manual.js +14 -5
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +66 -16
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +53 -16
- package/src/checks/manual/landmark-main-is-top-level-manual.js +35 -10
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +29 -10
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +29 -10
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
- package/src/checks/manual/landmark-one-main-manual.js +26 -20
- package/src/checks/manual/landmark-unique-manual.js +41 -15
- package/src/checks/manual/link-name-quality-manual.js +43 -12
- package/src/checks/manual/media-transcript-present-manual.js +35 -22
- package/src/checks/manual/meta-viewport-large-manual.js +16 -5
- package/src/checks/manual/mouse-only-event-handlers-manual.js +38 -11
- package/src/checks/manual/no-autoplay-audio-manual.js +20 -6
- package/src/checks/manual/object-text-alternative-quality-manual.js +175 -154
- package/src/checks/manual/p-as-heading-manual.js +22 -7
- package/src/checks/manual/page-has-heading-one-manual.js +31 -22
- package/src/checks/manual/page-title-patterns-manual.js +78 -50
- package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
- package/src/checks/manual/region-manual.js +241 -48
- package/src/checks/manual/scope-attr-valid-manual.js +10 -3
- package/src/checks/manual/scrollable-region-focusable-manual.js +37 -11
- package/src/checks/manual/skip-link-manual.js +35 -12
- package/src/checks/manual/svg-text-alternative-quality-manual.js +206 -165
- package/src/checks/manual/tabindex-manual.js +10 -3
- package/src/checks/manual/table-duplicate-name-manual.js +17 -7
- package/src/checks/manual/table-fake-caption-manual.js +24 -7
- package/src/checks/manual/video-caption-manual.js +15 -4
- package/src/checks/manual-review.js +56 -12
- package/src/core/aria-helpers.js +1127 -886
- package/src/core/contrast-helpers.js +1217 -1062
- package/src/core/dom-helpers.js +4175 -3917
- package/src/core/dom-runner.js +720 -604
- package/src/core/frame-messaging.js +189 -138
- package/src/core/frame-scan.js +94 -82
- package/src/core/rollup-composites.js +94 -102
- package/src/core/rule-meta.js +58 -41
- package/src/core.js +36552 -29111
- package/src/i18n/en.js +1194 -889
- package/src/i18n/fr.js +1136 -795
- package/src/policy/contracts.js +13 -13
- package/src/policy/resolvePolicy.js +48 -44
- package/src/report.js +63 -43
- package/src/sarif.js +175 -0
- package/surea11y.browser.js +36042 -0
|
@@ -27,7 +27,8 @@ const id = 'html-xml-lang-mismatch';
|
|
|
27
27
|
|
|
28
28
|
const meta = {
|
|
29
29
|
title: 'lang and xml:lang must not disagree',
|
|
30
|
-
description:
|
|
30
|
+
description:
|
|
31
|
+
"Checks that the <html> element's lang and xml:lang attributes declare the same primary language, when both are present.",
|
|
31
32
|
i18n: {
|
|
32
33
|
titleKey: 'htmlXmlLangMismatch_title',
|
|
33
34
|
descriptionKey: 'htmlXmlLangMismatch_description'
|
|
@@ -36,7 +37,13 @@ const meta = {
|
|
|
36
37
|
tags: ['wcag2a', 'wcag311', 'structure', 'language', 'atomic', 'automatic'],
|
|
37
38
|
wcagSc: ['3.1.1'],
|
|
38
39
|
normativeMappings: [
|
|
39
|
-
{
|
|
40
|
+
{
|
|
41
|
+
standard: 'WCAG',
|
|
42
|
+
version: '2.2',
|
|
43
|
+
requirement: '3.1.1',
|
|
44
|
+
title: 'Language of Page',
|
|
45
|
+
conformanceLevel: 'A'
|
|
46
|
+
}
|
|
40
47
|
],
|
|
41
48
|
defaultSeverity: 'serious',
|
|
42
49
|
category: 'understandable',
|
|
@@ -57,7 +64,7 @@ function runInPage(ctx) {
|
|
|
57
64
|
const { document, helpers, rule } = ctx;
|
|
58
65
|
|
|
59
66
|
const html = document && document.documentElement;
|
|
60
|
-
const tag =
|
|
67
|
+
const tag = html && html.tagName ? String(html.tagName).toLowerCase() : '';
|
|
61
68
|
if (!html || tag !== 'html') {
|
|
62
69
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
63
70
|
}
|
|
@@ -76,24 +83,33 @@ function runInPage(ctx) {
|
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(html) : 'html';
|
|
79
|
-
const htmlSnippet = helpers.getOuterHtmlSnippet
|
|
86
|
+
const htmlSnippet = helpers.getOuterHtmlSnippet
|
|
87
|
+
? helpers.getOuterHtmlSnippet(html)
|
|
88
|
+
: (html.outerHTML || '').slice(0, 200);
|
|
80
89
|
|
|
81
|
-
const occurrences = [
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
const occurrences = [
|
|
91
|
+
{
|
|
92
|
+
selector: stableSelector,
|
|
93
|
+
html: htmlSnippet,
|
|
94
|
+
summary: `The lang ("${lang}") and xml:lang ("${xmlLang}") attributes declare different languages.`,
|
|
95
|
+
hint: 'Make lang and xml:lang declare the same primary language, or remove the deprecated xml:lang attribute.',
|
|
96
|
+
i18n: {
|
|
97
|
+
summaryKey: 'htmlXmlLangMismatch_summary_fail',
|
|
98
|
+
hintKey: 'htmlXmlLangMismatch_hint_fail',
|
|
99
|
+
params: { lang, xmlLang }
|
|
100
|
+
},
|
|
101
|
+
data: {
|
|
102
|
+
details: { reasonCode: 'HTML_XML_LANG_MISMATCH', lang, xmlLang }
|
|
103
|
+
}
|
|
93
104
|
}
|
|
94
|
-
|
|
105
|
+
];
|
|
95
106
|
|
|
96
|
-
return {
|
|
107
|
+
return {
|
|
108
|
+
ruleId: rule.ruleId,
|
|
109
|
+
outcome: 'fail',
|
|
110
|
+
severity: rule.defaultSeverity || 'serious',
|
|
111
|
+
occurrences
|
|
112
|
+
};
|
|
97
113
|
}
|
|
98
114
|
|
|
99
115
|
module.exports = { id, meta, runInPage, applicability };
|
|
@@ -36,7 +36,8 @@ const id = 'iframe-focusable-content';
|
|
|
36
36
|
|
|
37
37
|
const meta = {
|
|
38
38
|
title: 'Frames with tabindex="-1" must not contain focusable content',
|
|
39
|
-
description:
|
|
39
|
+
description:
|
|
40
|
+
'Checks that same-origin <iframe>/<frame> elements with tabindex="-1" do not contain focusable content, since browsers do not propagate that restriction into the frame’s embedded document.',
|
|
40
41
|
i18n: {
|
|
41
42
|
titleKey: 'iframeFocusableContent_title',
|
|
42
43
|
descriptionKey: 'iframeFocusableContent_description'
|
|
@@ -45,7 +46,13 @@ const meta = {
|
|
|
45
46
|
tags: ['wcag2a', 'wcag211', 'structure', 'atomic', 'automatic', 'keyboard', 'iframe'],
|
|
46
47
|
wcagSc: ['2.1.1'],
|
|
47
48
|
normativeMappings: [
|
|
48
|
-
{
|
|
49
|
+
{
|
|
50
|
+
standard: 'WCAG',
|
|
51
|
+
version: '2.2',
|
|
52
|
+
requirement: '2.1.1',
|
|
53
|
+
title: 'Keyboard',
|
|
54
|
+
conformanceLevel: 'A'
|
|
55
|
+
}
|
|
49
56
|
],
|
|
50
57
|
defaultSeverity: 'moderate',
|
|
51
58
|
category: 'operable',
|
|
@@ -55,15 +62,15 @@ const meta = {
|
|
|
55
62
|
};
|
|
56
63
|
|
|
57
64
|
function runInPage(ctx) {
|
|
58
|
-
const {
|
|
65
|
+
const { helpers, rule } = ctx;
|
|
59
66
|
|
|
60
67
|
function hasFocusableCandidate(doc) {
|
|
61
68
|
if (!doc || !doc.querySelectorAll) return false;
|
|
62
|
-
let els
|
|
69
|
+
let els;
|
|
63
70
|
try {
|
|
64
71
|
els = doc.querySelectorAll(
|
|
65
72
|
'a[href], area[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), ' +
|
|
66
|
-
|
|
73
|
+
'select:not([disabled]), textarea:not([disabled]), iframe, [contenteditable="true"], [tabindex]'
|
|
67
74
|
);
|
|
68
75
|
} catch {
|
|
69
76
|
return false;
|
|
@@ -87,7 +94,9 @@ function runInPage(ctx) {
|
|
|
87
94
|
return !Number.isNaN(n) && n < 0;
|
|
88
95
|
}
|
|
89
96
|
|
|
90
|
-
const nodes = helpers.queryAllSmart
|
|
97
|
+
const nodes = helpers.queryAllSmart
|
|
98
|
+
? helpers.queryAllSmart('iframe, frame')
|
|
99
|
+
: helpers.queryAll('iframe, frame');
|
|
91
100
|
|
|
92
101
|
const occurrences = [];
|
|
93
102
|
let applicableCount = 0;
|
|
@@ -96,7 +105,7 @@ function runInPage(ctx) {
|
|
|
96
105
|
if (!el || !el.getAttribute) continue;
|
|
97
106
|
if (!getNegativeTabIndex(el)) continue;
|
|
98
107
|
|
|
99
|
-
let contentDoc
|
|
108
|
+
let contentDoc;
|
|
100
109
|
try {
|
|
101
110
|
contentDoc = el.contentDocument || null;
|
|
102
111
|
} catch {
|
|
@@ -110,12 +119,13 @@ function runInPage(ctx) {
|
|
|
110
119
|
|
|
111
120
|
const tag = el.tagName.toLowerCase();
|
|
112
121
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
113
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
122
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
114
123
|
|
|
115
124
|
occurrences.push({
|
|
116
125
|
selector: stableSelector,
|
|
117
126
|
html,
|
|
118
|
-
summary:
|
|
127
|
+
summary:
|
|
128
|
+
'This frame has tabindex="-1" but its content contains focusable elements, which remain reachable by keyboard.',
|
|
119
129
|
hint: 'Remove focusable content from the frame, or remove tabindex="-1" if the frame is meant to be reachable.',
|
|
120
130
|
i18n: {
|
|
121
131
|
summaryKey: 'iframeFocusableContent_summary_fail',
|
|
@@ -132,9 +142,14 @@ function runInPage(ctx) {
|
|
|
132
142
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
133
143
|
}
|
|
134
144
|
if (occurrences.length) {
|
|
135
|
-
return {
|
|
145
|
+
return {
|
|
146
|
+
ruleId: rule.ruleId,
|
|
147
|
+
outcome: 'fail',
|
|
148
|
+
severity: rule.defaultSeverity || 'moderate',
|
|
149
|
+
occurrences
|
|
150
|
+
};
|
|
136
151
|
}
|
|
137
152
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
138
153
|
}
|
|
139
154
|
|
|
140
|
-
module.exports = { id, meta, runInPage };
|
|
155
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -25,7 +25,8 @@ const id = 'iframe-name-present';
|
|
|
25
25
|
|
|
26
26
|
const meta = {
|
|
27
27
|
title: 'Frames have an accessible name',
|
|
28
|
-
description:
|
|
28
|
+
description:
|
|
29
|
+
'Checks that <iframe>/<frame> elements expose a non-empty accessible name via aria-label, aria-labelledby, or the title attribute.',
|
|
29
30
|
i18n: {
|
|
30
31
|
titleKey: 'iframeNamePresent_title',
|
|
31
32
|
descriptionKey: 'iframeNamePresent_description'
|
|
@@ -34,7 +35,13 @@ const meta = {
|
|
|
34
35
|
tags: ['wcag2a', 'wcag412', 'structure', 'atomic', 'automatic', 'name', 'iframe'],
|
|
35
36
|
wcagSc: ['4.1.2'],
|
|
36
37
|
normativeMappings: [
|
|
37
|
-
{
|
|
38
|
+
{
|
|
39
|
+
standard: 'WCAG',
|
|
40
|
+
version: '2.2',
|
|
41
|
+
requirement: '4.1.2',
|
|
42
|
+
title: 'Name, Role, Value',
|
|
43
|
+
conformanceLevel: 'A'
|
|
44
|
+
}
|
|
38
45
|
],
|
|
39
46
|
defaultSeverity: 'serious',
|
|
40
47
|
category: 'robust',
|
|
@@ -44,9 +51,11 @@ const meta = {
|
|
|
44
51
|
};
|
|
45
52
|
|
|
46
53
|
function runInPage(ctx) {
|
|
47
|
-
const {
|
|
54
|
+
const { helpers, rule } = ctx;
|
|
48
55
|
|
|
49
|
-
const nodes = helpers.queryAllSmart
|
|
56
|
+
const nodes = helpers.queryAllSmart
|
|
57
|
+
? helpers.queryAllSmart('iframe, frame')
|
|
58
|
+
: helpers.queryAll('iframe, frame');
|
|
50
59
|
|
|
51
60
|
const occurrences = [];
|
|
52
61
|
let applicableCount = 0;
|
|
@@ -62,15 +71,23 @@ function runInPage(ctx) {
|
|
|
62
71
|
|
|
63
72
|
applicableCount += 1;
|
|
64
73
|
|
|
65
|
-
const nameInfo = helpers.getAccessibleNameInfo
|
|
74
|
+
const nameInfo = helpers.getAccessibleNameInfo
|
|
75
|
+
? helpers.getAccessibleNameInfo(el, ctx, { maxRefs: 8 })
|
|
76
|
+
: null;
|
|
66
77
|
if (nameInfo && nameInfo.present && nameInfo.value) continue;
|
|
67
78
|
|
|
68
79
|
const eligInfo = helpers.getEligibilityInfo
|
|
69
|
-
? (() => {
|
|
80
|
+
? (() => {
|
|
81
|
+
try {
|
|
82
|
+
return helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
})()
|
|
70
87
|
: null;
|
|
71
88
|
const tag = el.tagName.toLowerCase();
|
|
72
89
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
73
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
90
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
74
91
|
|
|
75
92
|
occurrences.push({
|
|
76
93
|
selector: stableSelector,
|
|
@@ -93,9 +110,14 @@ function runInPage(ctx) {
|
|
|
93
110
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
94
111
|
}
|
|
95
112
|
if (occurrences.length) {
|
|
96
|
-
return {
|
|
113
|
+
return {
|
|
114
|
+
ruleId: rule.ruleId,
|
|
115
|
+
outcome: 'fail',
|
|
116
|
+
severity: rule.defaultSeverity || 'serious',
|
|
117
|
+
occurrences
|
|
118
|
+
};
|
|
97
119
|
}
|
|
98
120
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
99
121
|
}
|
|
100
122
|
|
|
101
|
-
module.exports = { id, meta, runInPage };
|
|
123
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -28,7 +28,8 @@ const id = 'iframe-title-unique';
|
|
|
28
28
|
|
|
29
29
|
const meta = {
|
|
30
30
|
title: 'Frame titles must be unique',
|
|
31
|
-
description:
|
|
31
|
+
description:
|
|
32
|
+
'Checks that no two <iframe>/<frame> elements in scope share the same title attribute value.',
|
|
32
33
|
i18n: {
|
|
33
34
|
titleKey: 'iframeTitleUnique_title',
|
|
34
35
|
descriptionKey: 'iframeTitleUnique_description'
|
|
@@ -37,7 +38,13 @@ const meta = {
|
|
|
37
38
|
tags: ['wcag2a', 'wcag412', 'structure', 'atomic', 'automatic', 'name', 'iframe'],
|
|
38
39
|
wcagSc: ['4.1.2'],
|
|
39
40
|
normativeMappings: [
|
|
40
|
-
{
|
|
41
|
+
{
|
|
42
|
+
standard: 'WCAG',
|
|
43
|
+
version: '2.2',
|
|
44
|
+
requirement: '4.1.2',
|
|
45
|
+
title: 'Name, Role, Value',
|
|
46
|
+
conformanceLevel: 'A'
|
|
47
|
+
}
|
|
41
48
|
],
|
|
42
49
|
defaultSeverity: 'moderate',
|
|
43
50
|
category: 'robust',
|
|
@@ -47,9 +54,11 @@ const meta = {
|
|
|
47
54
|
};
|
|
48
55
|
|
|
49
56
|
function runInPage(ctx) {
|
|
50
|
-
const {
|
|
57
|
+
const { helpers, rule } = ctx;
|
|
51
58
|
|
|
52
|
-
const nodes = helpers.queryAllSmart
|
|
59
|
+
const nodes = helpers.queryAllSmart
|
|
60
|
+
? helpers.queryAllSmart('iframe, frame')
|
|
61
|
+
: helpers.queryAll('iframe, frame');
|
|
53
62
|
|
|
54
63
|
const groups = new Map(); // trimmed title -> elements[]
|
|
55
64
|
let applicableCount = 0;
|
|
@@ -79,7 +88,9 @@ function runInPage(ctx) {
|
|
|
79
88
|
for (const el of els) {
|
|
80
89
|
const tag = el.tagName.toLowerCase();
|
|
81
90
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
82
|
-
const html = helpers.getOuterHtmlSnippet
|
|
91
|
+
const html = helpers.getOuterHtmlSnippet
|
|
92
|
+
? helpers.getOuterHtmlSnippet(el)
|
|
93
|
+
: el.outerHTML || '';
|
|
83
94
|
|
|
84
95
|
occurrences.push({
|
|
85
96
|
selector: stableSelector,
|
|
@@ -92,16 +103,26 @@ function runInPage(ctx) {
|
|
|
92
103
|
params: { element: tag, title }
|
|
93
104
|
},
|
|
94
105
|
data: {
|
|
95
|
-
details: {
|
|
106
|
+
details: {
|
|
107
|
+
reasonCode: 'IFRAME_TITLE_DUPLICATE',
|
|
108
|
+
element: tag,
|
|
109
|
+
title,
|
|
110
|
+
duplicateCount: els.length
|
|
111
|
+
}
|
|
96
112
|
}
|
|
97
113
|
});
|
|
98
114
|
}
|
|
99
115
|
}
|
|
100
116
|
|
|
101
117
|
if (occurrences.length) {
|
|
102
|
-
return {
|
|
118
|
+
return {
|
|
119
|
+
ruleId: rule.ruleId,
|
|
120
|
+
outcome: 'fail',
|
|
121
|
+
severity: rule.defaultSeverity || 'moderate',
|
|
122
|
+
occurrences
|
|
123
|
+
};
|
|
103
124
|
}
|
|
104
125
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
105
126
|
}
|
|
106
127
|
|
|
107
|
-
module.exports = { id, meta, runInPage };
|
|
128
|
+
module.exports = { id, meta, runInPage };
|
|
@@ -20,7 +20,8 @@ const id = 'img-alt-present';
|
|
|
20
20
|
|
|
21
21
|
const meta = {
|
|
22
22
|
title: '<img> must have an alt attribute',
|
|
23
|
-
description:
|
|
23
|
+
description:
|
|
24
|
+
'Checks that <img> elements provide an alt attribute to support a text alternative mechanism.',
|
|
24
25
|
i18n: {
|
|
25
26
|
titleKey: 'img_altPresent_title',
|
|
26
27
|
descriptionKey: 'img_altPresent_description'
|
|
@@ -29,7 +30,13 @@ const meta = {
|
|
|
29
30
|
tags: ['wcag2a', 'wcag111', 'nontext', 'images', 'atomic', 'automatic'],
|
|
30
31
|
wcagSc: ['1.1.1'],
|
|
31
32
|
normativeMappings: [
|
|
32
|
-
{
|
|
33
|
+
{
|
|
34
|
+
standard: 'WCAG',
|
|
35
|
+
version: '2.2',
|
|
36
|
+
requirement: '1.1.1',
|
|
37
|
+
title: 'Non-text Content',
|
|
38
|
+
conformanceLevel: 'A'
|
|
39
|
+
}
|
|
33
40
|
],
|
|
34
41
|
defaultSeverity: 'serious',
|
|
35
42
|
category: 'perceivable',
|
|
@@ -46,12 +53,17 @@ function runInPage(ctx) {
|
|
|
46
53
|
const { document, root, helpers, rule } = ctx;
|
|
47
54
|
const safeRoot = root || document;
|
|
48
55
|
|
|
49
|
-
const queryAllSmart =
|
|
50
|
-
|
|
56
|
+
const queryAllSmart =
|
|
57
|
+
helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
58
|
+
const getEligibilityInfo =
|
|
59
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
51
60
|
|
|
52
|
-
const isAccTreeEligible =
|
|
53
|
-
|
|
54
|
-
const
|
|
61
|
+
const isAccTreeEligible =
|
|
62
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
63
|
+
const getFocusableInfo =
|
|
64
|
+
helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
|
|
65
|
+
const getAriaNameInfo =
|
|
66
|
+
helpers && typeof helpers.getAriaNameInfo === 'function' ? helpers.getAriaNameInfo : null;
|
|
55
67
|
|
|
56
68
|
// Prefer tagName collection when available (cheap), otherwise fall back.
|
|
57
69
|
function getImgsCollection() {
|
|
@@ -73,7 +85,8 @@ function runInPage(ctx) {
|
|
|
73
85
|
}
|
|
74
86
|
|
|
75
87
|
try {
|
|
76
|
-
if (safeRoot && typeof safeRoot.querySelectorAll === 'function')
|
|
88
|
+
if (safeRoot && typeof safeRoot.querySelectorAll === 'function')
|
|
89
|
+
return safeRoot.querySelectorAll('img');
|
|
77
90
|
} catch {
|
|
78
91
|
// fall through
|
|
79
92
|
}
|
|
@@ -94,19 +107,6 @@ function runInPage(ctx) {
|
|
|
94
107
|
// Helper-safe trim
|
|
95
108
|
const trim = (v) => (v == null ? '' : String(v)).trim();
|
|
96
109
|
|
|
97
|
-
// Deterministic tabindex fallback (only used if helper missing)
|
|
98
|
-
function isFocusableFallback(el) {
|
|
99
|
-
try {
|
|
100
|
-
const tabindex = el.getAttribute('tabindex');
|
|
101
|
-
if (tabindex == null) return false;
|
|
102
|
-
const s = String(tabindex).trim();
|
|
103
|
-
if (!s) return false;
|
|
104
|
-
return !Number.isNaN(Number(s));
|
|
105
|
-
} catch {
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
110
|
for (let i = 0; i < imgLen; i++) {
|
|
111
111
|
const el = imgs[i];
|
|
112
112
|
if (!el || !el.getAttribute) continue;
|
|
@@ -125,7 +125,7 @@ function runInPage(ctx) {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
// Role (presentation/none) exclusion only when NOT focusable.
|
|
128
|
-
let role
|
|
128
|
+
let role;
|
|
129
129
|
try {
|
|
130
130
|
role = trim(el.getAttribute('role')).toLowerCase();
|
|
131
131
|
} catch {
|
|
@@ -133,17 +133,13 @@ function runInPage(ctx) {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
if (role === 'presentation' || role === 'none') {
|
|
136
|
-
let focusable
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
focusable = false;
|
|
144
|
-
}
|
|
145
|
-
} else {
|
|
146
|
-
focusable = isFocusableFallback(el);
|
|
136
|
+
let focusable;
|
|
137
|
+
try {
|
|
138
|
+
const fi = getFocusableInfo(el, ctx);
|
|
139
|
+
// Preserve existing behavior: prefer fi.focusable; fall back to fi.tabbable if present.
|
|
140
|
+
focusable = !!(fi && (fi.focusable || fi.tabbable));
|
|
141
|
+
} catch {
|
|
142
|
+
focusable = false;
|
|
147
143
|
}
|
|
148
144
|
|
|
149
145
|
if (!focusable) continue;
|
|
@@ -153,7 +149,7 @@ function runInPage(ctx) {
|
|
|
153
149
|
applicableCount += 1;
|
|
154
150
|
|
|
155
151
|
// alt attribute presence check (empty allowed)
|
|
156
|
-
let hasAlt
|
|
152
|
+
let hasAlt;
|
|
157
153
|
try {
|
|
158
154
|
hasAlt = el.getAttribute('alt') !== null;
|
|
159
155
|
} catch {
|
|
@@ -167,7 +163,7 @@ function runInPage(ctx) {
|
|
|
167
163
|
// image with a non-empty ARIA name is not missing a text alternative
|
|
168
164
|
// just because it lacks an alt attribute.
|
|
169
165
|
if (getAriaNameInfo) {
|
|
170
|
-
let ariaName
|
|
166
|
+
let ariaName;
|
|
171
167
|
try {
|
|
172
168
|
ariaName = getAriaNameInfo(el, ctx);
|
|
173
169
|
} catch {
|
|
@@ -188,7 +184,15 @@ function runInPage(ctx) {
|
|
|
188
184
|
const title = trim(el.getAttribute('title'));
|
|
189
185
|
if (title) continue;
|
|
190
186
|
|
|
191
|
-
const eligInfo = getEligibilityInfo
|
|
187
|
+
const eligInfo = getEligibilityInfo
|
|
188
|
+
? (() => {
|
|
189
|
+
try {
|
|
190
|
+
return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
|
|
191
|
+
} catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
})()
|
|
195
|
+
: null;
|
|
192
196
|
|
|
193
197
|
const baseOccurrence = {
|
|
194
198
|
summary: 'Missing alt attribute on <img>.',
|
|
@@ -201,12 +205,7 @@ function runInPage(ctx) {
|
|
|
201
205
|
data: { visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] } }
|
|
202
206
|
};
|
|
203
207
|
|
|
204
|
-
|
|
205
|
-
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
206
|
-
} else {
|
|
207
|
-
// Never compute selector/snippet in the rule.
|
|
208
|
-
occurrences.push({ __node: el, selector: '', html: '', ...baseOccurrence });
|
|
209
|
-
}
|
|
208
|
+
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
210
209
|
}
|
|
211
210
|
|
|
212
211
|
if (applicableCount === 0) {
|
|
@@ -217,7 +216,12 @@ function runInPage(ctx) {
|
|
|
217
216
|
return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
|
|
218
217
|
}
|
|
219
218
|
|
|
220
|
-
return {
|
|
219
|
+
return {
|
|
220
|
+
ruleId: rule.ruleId,
|
|
221
|
+
outcome: 'fail',
|
|
222
|
+
severity: rule.defaultSeverity || 'minor',
|
|
223
|
+
occurrences
|
|
224
|
+
};
|
|
221
225
|
}
|
|
222
226
|
|
|
223
227
|
module.exports = { id, meta, runInPage };
|