@surea11y/core 1.4.1 → 1.5.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 +47 -7
- package/README.md +19 -3
- package/docs/API_STABILITY.md +2 -2
- package/docs/ENGINE_OPTIONS.md +14 -10
- package/docs/I18N.md +176 -20
- package/docs/INTEGRATION.md +28 -6
- package/docs/LIMITATIONS.md +1 -1
- package/docs/OUTPUT_SCHEMA.md +13 -3
- package/docs/REPORT.md +2 -0
- package/docs/RULE_AUTHORING.md +53 -0
- package/docs/TROUBLESHOOTING.md +2 -2
- package/package.json +6 -1
- package/src/checks/automatic/aria-allowed-attr.js +27 -30
- package/src/checks/automatic/aria-allowed-role.js +14 -16
- package/src/checks/automatic/aria-braille-equivalent.js +17 -19
- package/src/checks/automatic/aria-conditional-attr.js +17 -19
- package/src/checks/automatic/aria-deprecated-role.js +62 -49
- package/src/checks/automatic/aria-hidden-body.js +2 -9
- package/src/checks/automatic/aria-hidden-focus.js +99 -18
- package/src/checks/automatic/aria-prohibited-attr.js +54 -55
- package/src/checks/automatic/aria-prohibited-children.js +26 -26
- package/src/checks/automatic/aria-required-attr.js +14 -17
- package/src/checks/automatic/aria-required-children.js +17 -20
- package/src/checks/automatic/aria-required-parent.js +17 -20
- package/src/checks/automatic/aria-roles-valid.js +37 -23
- package/src/checks/automatic/aria-valid-attr-value.js +18 -21
- package/src/checks/automatic/aria-valid-attr.js +14 -17
- package/src/checks/automatic/autocomplete-valid.js +15 -17
- package/src/checks/automatic/avoid-inline-spacing.js +14 -16
- package/src/checks/automatic/binary-control-name-present.js +19 -21
- package/src/checks/automatic/button-name-present.js +23 -28
- package/src/checks/automatic/combobox-name-present.js +15 -17
- package/src/checks/automatic/css-orientation-lock.js +22 -22
- package/src/checks/automatic/definition-list-children-valid.js +18 -21
- package/src/checks/automatic/deprecated-elements-not-used.js +14 -16
- package/src/checks/automatic/dialog-name-present.js +16 -18
- package/src/checks/automatic/dlitem-parent-valid.js +15 -17
- package/src/checks/automatic/duplicate-id-aria.js +45 -37
- package/src/checks/automatic/form-control-single-label.js +38 -40
- package/src/checks/automatic/html-xml-lang-mismatch.js +2 -9
- package/src/checks/automatic/iframe-focusable-content.js +29 -31
- package/src/checks/automatic/iframe-name-present.js +15 -17
- package/src/checks/automatic/iframe-title-unique.js +18 -23
- package/src/checks/automatic/label-in-name.js +31 -36
- package/src/checks/automatic/link-in-text-block.js +19 -21
- package/src/checks/automatic/link-name-present.js +25 -30
- package/src/checks/automatic/list-children-valid.js +15 -17
- package/src/checks/automatic/listbox-name-present.js +15 -17
- package/src/checks/automatic/listitem-parent-valid.js +14 -17
- package/src/checks/automatic/menuitem-name-present.js +16 -18
- package/src/checks/automatic/meta-refresh-no-exceptions.js +15 -18
- package/src/checks/automatic/meta-refresh-timing-absent.js +14 -17
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +14 -17
- package/src/checks/automatic/meter-name-present.js +16 -18
- package/src/checks/automatic/nested-interactive-controls-absent.js +17 -19
- package/src/checks/automatic/option-name-present.js +16 -18
- package/src/checks/automatic/progressbar-name-present.js +19 -21
- package/src/checks/automatic/searchbox-name-present.js +19 -17
- package/src/checks/automatic/server-side-image-map-absent.js +15 -18
- package/src/checks/automatic/slider-name-present.js +15 -17
- package/src/checks/automatic/spinbutton-name-present.js +19 -17
- package/src/checks/automatic/summary-name-present.js +16 -18
- package/src/checks/automatic/tab-name-present.js +16 -18
- package/src/checks/automatic/table-headers-attr-valid.js +15 -17
- package/src/checks/automatic/table-th-has-data-cells.js +15 -19
- package/src/checks/automatic/target-size-minimum.js +104 -81
- package/src/checks/automatic/td-has-header.js +15 -20
- package/src/checks/automatic/textbox-name-present.js +15 -17
- package/src/checks/automatic/tooltip-name-present.js +16 -18
- package/src/checks/automatic/treeitem-name-present.js +16 -18
- package/src/checks/automatic/valid-lang.js +15 -17
- package/src/checks/manual/accesskeys-manual.js +18 -19
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +19 -22
- package/src/checks/manual/bypass-blocks-present-manual.js +4 -12
- package/src/checks/manual/empty-heading-manual.js +15 -17
- package/src/checks/manual/empty-table-header-manual.js +27 -30
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -9
- package/src/checks/manual/heading-order-manual.js +17 -22
- package/src/checks/manual/image-redundant-alt-manual.js +14 -17
- package/src/checks/manual/label-title-only-manual.js +15 -17
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +14 -17
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +14 -17
- package/src/checks/manual/landmark-main-is-top-level-manual.js +14 -17
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +2 -7
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +2 -7
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +2 -7
- package/src/checks/manual/landmark-one-main-manual.js +2 -9
- package/src/checks/manual/landmark-unique-manual.js +22 -27
- package/src/checks/manual/link-name-quality-manual.js +15 -17
- package/src/checks/manual/meta-viewport-large-manual.js +14 -17
- package/src/checks/manual/mouse-only-event-handlers-manual.js +17 -19
- package/src/checks/manual/page-has-heading-one-manual.js +2 -9
- package/src/checks/manual/presentation-role-conflict-manual.js +19 -21
- package/src/checks/manual/region-manual.js +13 -6
- package/src/checks/manual/scope-attr-valid-manual.js +14 -17
- package/src/checks/manual/skip-link-manual.js +39 -47
- package/src/checks/manual/tabindex-manual.js +14 -17
- package/src/checks/manual/table-duplicate-name-manual.js +14 -17
- package/src/core.js +4203 -3604
- package/src/report.js +14 -0
- package/surea11y.browser.js +1960 -3671
- package/surea11y.i18n.de.js +22 -0
- package/surea11y.i18n.es.js +22 -0
- package/surea11y.i18n.fr.js +22 -0
|
@@ -148,8 +148,6 @@ function runInPage(ctx) {
|
|
|
148
148
|
const name = getAccessibleNameText(el);
|
|
149
149
|
if (name) continue;
|
|
150
150
|
|
|
151
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
152
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
153
151
|
const eligInfo = helpers.getEligibilityInfo
|
|
154
152
|
? (() => {
|
|
155
153
|
try {
|
|
@@ -160,21 +158,21 @@ function runInPage(ctx) {
|
|
|
160
158
|
})()
|
|
161
159
|
: null;
|
|
162
160
|
|
|
163
|
-
occurrences.push(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
161
|
+
occurrences.push(
|
|
162
|
+
helpers.reportOccurrence(el, {
|
|
163
|
+
summary: 'This heading has no accessible name.',
|
|
164
|
+
hint: 'Add text content (or aria-label/aria-labelledby) to this heading, or remove it if it is not needed.',
|
|
165
|
+
i18n: {
|
|
166
|
+
summaryKey: 'emptyHeading_summary_cantTell',
|
|
167
|
+
hintKey: 'emptyHeading_hint_cantTell',
|
|
168
|
+
params: {}
|
|
169
|
+
},
|
|
170
|
+
data: {
|
|
171
|
+
details: { reasonCode: 'HEADING_EMPTY' },
|
|
172
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
);
|
|
178
176
|
}
|
|
179
177
|
|
|
180
178
|
if (applicableCount === 0) {
|
|
@@ -141,43 +141,40 @@ function runInPage(ctx) {
|
|
|
141
141
|
|
|
142
142
|
if (getVisibleText(el)) continue;
|
|
143
143
|
|
|
144
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
145
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
146
|
-
|
|
147
144
|
const ariaName = getAriaOnlyName(el);
|
|
148
145
|
if (ariaName) {
|
|
149
|
-
occurrences.push(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
'
|
|
154
|
-
|
|
146
|
+
occurrences.push(
|
|
147
|
+
helpers.reportOccurrence(el, {
|
|
148
|
+
summary:
|
|
149
|
+
'This table header cell has no visible text — its only accessible name comes from aria-label/aria-labelledby, which real screen-reader/browser combinations (e.g. NVDA+Firefox, iOS VoiceOver+Safari) are known to ignore on <th> elements.',
|
|
150
|
+
hint: 'Add visible text content to this header cell (in addition to, or instead of, aria-label/aria-labelledby) — visible text is the only naming mechanism confirmed to work across tested screen readers.',
|
|
151
|
+
i18n: {
|
|
152
|
+
summaryKey: 'emptyTableHeader_summary_cantTell_ariaOnly',
|
|
153
|
+
hintKey: 'emptyTableHeader_hint_cantTell_ariaOnly',
|
|
154
|
+
params: {}
|
|
155
|
+
},
|
|
156
|
+
data: {
|
|
157
|
+
details: { reasonCode: 'TABLE_HEADER_NAME_NOT_VISIBLE_TEXT', ariaName }
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
occurrences.push(
|
|
165
|
+
helpers.reportOccurrence(el, {
|
|
166
|
+
summary: 'This table header cell has no accessible name.',
|
|
167
|
+
hint: 'Add text content (or aria-label/aria-labelledby) to this header cell, or remove it if it is not needed.',
|
|
155
168
|
i18n: {
|
|
156
|
-
summaryKey: '
|
|
157
|
-
hintKey: '
|
|
169
|
+
summaryKey: 'emptyTableHeader_summary_cantTell',
|
|
170
|
+
hintKey: 'emptyTableHeader_hint_cantTell',
|
|
158
171
|
params: {}
|
|
159
172
|
},
|
|
160
173
|
data: {
|
|
161
|
-
details: { reasonCode: '
|
|
174
|
+
details: { reasonCode: 'TABLE_HEADER_EMPTY' }
|
|
162
175
|
}
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
occurrences.push({
|
|
168
|
-
selector: stableSelector,
|
|
169
|
-
html,
|
|
170
|
-
summary: 'This table header cell has no accessible name.',
|
|
171
|
-
hint: 'Add text content (or aria-label/aria-labelledby) to this header cell, or remove it if it is not needed.',
|
|
172
|
-
i18n: {
|
|
173
|
-
summaryKey: 'emptyTableHeader_summary_cantTell',
|
|
174
|
-
hintKey: 'emptyTableHeader_hint_cantTell',
|
|
175
|
-
params: {}
|
|
176
|
-
},
|
|
177
|
-
data: {
|
|
178
|
-
details: { reasonCode: 'TABLE_HEADER_EMPTY' }
|
|
179
|
-
}
|
|
180
|
-
});
|
|
176
|
+
})
|
|
177
|
+
);
|
|
181
178
|
}
|
|
182
179
|
|
|
183
180
|
if (applicableCount === 0) {
|
|
@@ -185,20 +185,13 @@ function runInPage(ctx) {
|
|
|
185
185
|
|
|
186
186
|
const reasonCode =
|
|
187
187
|
method === 'title' ? 'label_from_title_primary' : 'label_from_placeholder_primary';
|
|
188
|
-
const methodLabel =
|
|
189
|
-
method === 'title'
|
|
190
|
-
? 'title'
|
|
191
|
-
: method === 'placeholder'
|
|
192
|
-
? 'placeholder'
|
|
193
|
-
: 'title or placeholder';
|
|
194
|
-
|
|
195
188
|
const baseOccurrence = {
|
|
196
|
-
summary:
|
|
189
|
+
summary: `Form control’s primary label is derived from ${method}.`,
|
|
197
190
|
hint: 'Prefer a persistent <label> or aria-labelledby. Avoid relying on placeholder/title as the primary label.',
|
|
198
191
|
i18n: {
|
|
199
192
|
summaryKey: 'formControl_programmaticLabelQuality_summary_cantTell',
|
|
200
193
|
hintKey: 'formControl_programmaticLabelQuality_hint_cantTell',
|
|
201
|
-
params: { element: (el.tagName || '').toLowerCase(), method
|
|
194
|
+
params: { element: (el.tagName || '').toLowerCase(), method }
|
|
202
195
|
},
|
|
203
196
|
data: {
|
|
204
197
|
visibilityFilter: vf || { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
@@ -125,29 +125,24 @@ function runInPage(ctx) {
|
|
|
125
125
|
const { el, level } = headings[i];
|
|
126
126
|
|
|
127
127
|
if (level > highestSoFar + 1) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
data: {
|
|
144
|
-
details: {
|
|
145
|
-
reasonCode: 'HEADING_ORDER_SKIPPED_LEVEL',
|
|
146
|
-
fromLevel: highestSoFar,
|
|
147
|
-
toLevel: level
|
|
128
|
+
occurrences.push(
|
|
129
|
+
helpers.reportOccurrence(el, {
|
|
130
|
+
summary: `This heading jumps from level ${highestSoFar} to level ${level}, skipping a level.`,
|
|
131
|
+
hint: 'Use consecutive heading levels (do not skip a level when going deeper) so the document outline stays predictable.',
|
|
132
|
+
i18n: {
|
|
133
|
+
summaryKey: 'headingOrder_summary_cantTell',
|
|
134
|
+
hintKey: 'headingOrder_hint_cantTell',
|
|
135
|
+
params: { fromLevel: String(highestSoFar), toLevel: String(level) }
|
|
136
|
+
},
|
|
137
|
+
data: {
|
|
138
|
+
details: {
|
|
139
|
+
reasonCode: 'HEADING_ORDER_SKIPPED_LEVEL',
|
|
140
|
+
fromLevel: highestSoFar,
|
|
141
|
+
toLevel: level
|
|
142
|
+
}
|
|
148
143
|
}
|
|
149
|
-
}
|
|
150
|
-
|
|
144
|
+
})
|
|
145
|
+
);
|
|
151
146
|
}
|
|
152
147
|
|
|
153
148
|
if (level > highestSoFar) highestSoFar = level;
|
|
@@ -111,23 +111,20 @@ function runInPage(ctx) {
|
|
|
111
111
|
|
|
112
112
|
if (otherText.toLowerCase() !== alt.toLowerCase()) continue;
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
details: { reasonCode: 'IMAGE_ALT_REDUNDANT', alt }
|
|
129
|
-
}
|
|
130
|
-
});
|
|
114
|
+
occurrences.push(
|
|
115
|
+
helpers.reportOccurrence(el, {
|
|
116
|
+
summary: "This image's alt text duplicates other visible text right next to it.",
|
|
117
|
+
hint: 'Make the alt text empty (alt="") if the image is purely decorative alongside the text, or remove the redundant duplication.',
|
|
118
|
+
i18n: {
|
|
119
|
+
summaryKey: 'imageRedundantAlt_summary_cantTell',
|
|
120
|
+
hintKey: 'imageRedundantAlt_hint_cantTell',
|
|
121
|
+
params: { alt }
|
|
122
|
+
},
|
|
123
|
+
data: {
|
|
124
|
+
details: { reasonCode: 'IMAGE_ALT_REDUNDANT', alt }
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
);
|
|
131
128
|
}
|
|
132
129
|
|
|
133
130
|
if (applicableCount === 0) {
|
|
@@ -94,23 +94,21 @@ function runInPage(ctx) {
|
|
|
94
94
|
if (!nameInfo || nameInfo.mechanism !== 'title') continue;
|
|
95
95
|
|
|
96
96
|
const tag = el.tagName.toLowerCase();
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
});
|
|
97
|
+
|
|
98
|
+
occurrences.push(
|
|
99
|
+
helpers.reportOccurrence(el, {
|
|
100
|
+
summary: 'This form control relies on the title attribute as its only label.',
|
|
101
|
+
hint: 'Add a visible <label> (or aria-label/aria-labelledby) in addition to, or instead of, the title attribute.',
|
|
102
|
+
i18n: {
|
|
103
|
+
summaryKey: 'labelTitleOnly_summary_cantTell',
|
|
104
|
+
hintKey: 'labelTitleOnly_hint_cantTell',
|
|
105
|
+
params: { element: tag }
|
|
106
|
+
},
|
|
107
|
+
data: {
|
|
108
|
+
details: { reasonCode: 'LABEL_TITLE_ONLY', element: tag }
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
);
|
|
114
112
|
}
|
|
115
113
|
|
|
116
114
|
if (applicableCount === 0) {
|
|
@@ -209,23 +209,20 @@ function runInPage(ctx) {
|
|
|
209
209
|
for (const el of banners) {
|
|
210
210
|
if (!hasLandmarkAncestor(el)) continue;
|
|
211
211
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
details: { reasonCode: 'LANDMARK_BANNER_NOT_TOP_LEVEL' }
|
|
227
|
-
}
|
|
228
|
-
});
|
|
212
|
+
occurrences.push(
|
|
213
|
+
helpers.reportOccurrence(el, {
|
|
214
|
+
summary: 'This banner landmark is nested inside another landmark region.',
|
|
215
|
+
hint: 'Move the banner landmark (header/role="banner") so it is not contained by another landmark; a banner should be a top-level region of the page.',
|
|
216
|
+
i18n: {
|
|
217
|
+
summaryKey: 'landmarkBannerIsTopLevel_summary_cantTell',
|
|
218
|
+
hintKey: 'landmarkBannerIsTopLevel_hint_cantTell',
|
|
219
|
+
params: {}
|
|
220
|
+
},
|
|
221
|
+
data: {
|
|
222
|
+
details: { reasonCode: 'LANDMARK_BANNER_NOT_TOP_LEVEL' }
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
);
|
|
229
226
|
}
|
|
230
227
|
|
|
231
228
|
if (occurrences.length) {
|
|
@@ -202,23 +202,20 @@ function runInPage(ctx) {
|
|
|
202
202
|
for (const el of contentinfos) {
|
|
203
203
|
if (!hasLandmarkAncestor(el)) continue;
|
|
204
204
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
details: { reasonCode: 'LANDMARK_CONTENTINFO_NOT_TOP_LEVEL' }
|
|
220
|
-
}
|
|
221
|
-
});
|
|
205
|
+
occurrences.push(
|
|
206
|
+
helpers.reportOccurrence(el, {
|
|
207
|
+
summary: 'This contentinfo landmark is nested inside another landmark region.',
|
|
208
|
+
hint: 'Move the contentinfo landmark (footer/role="contentinfo") so it is not contained by another landmark; contentinfo should be a top-level region of the page.',
|
|
209
|
+
i18n: {
|
|
210
|
+
summaryKey: 'landmarkContentinfoIsTopLevel_summary_cantTell',
|
|
211
|
+
hintKey: 'landmarkContentinfoIsTopLevel_hint_cantTell',
|
|
212
|
+
params: {}
|
|
213
|
+
},
|
|
214
|
+
data: {
|
|
215
|
+
details: { reasonCode: 'LANDMARK_CONTENTINFO_NOT_TOP_LEVEL' }
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
);
|
|
222
219
|
}
|
|
223
220
|
|
|
224
221
|
if (occurrences.length) {
|
|
@@ -192,23 +192,20 @@ function runInPage(ctx) {
|
|
|
192
192
|
for (const el of mains) {
|
|
193
193
|
if (!hasLandmarkAncestor(el)) continue;
|
|
194
194
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
details: { reasonCode: 'LANDMARK_MAIN_NOT_TOP_LEVEL' }
|
|
210
|
-
}
|
|
211
|
-
});
|
|
195
|
+
occurrences.push(
|
|
196
|
+
helpers.reportOccurrence(el, {
|
|
197
|
+
summary: 'This main landmark is nested inside another landmark region.',
|
|
198
|
+
hint: 'Move the main landmark (<main>/role="main") so it is not contained by another landmark; main should be a top-level region of the page.',
|
|
199
|
+
i18n: {
|
|
200
|
+
summaryKey: 'landmarkMainIsTopLevel_summary_cantTell',
|
|
201
|
+
hintKey: 'landmarkMainIsTopLevel_hint_cantTell',
|
|
202
|
+
params: {}
|
|
203
|
+
},
|
|
204
|
+
data: {
|
|
205
|
+
details: { reasonCode: 'LANDMARK_MAIN_NOT_TOP_LEVEL' }
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
);
|
|
212
209
|
}
|
|
213
210
|
|
|
214
211
|
if (occurrences.length) {
|
|
@@ -171,12 +171,7 @@ function runInPage(ctx) {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
const occurrences = banners.map((el) => {
|
|
174
|
-
|
|
175
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
176
|
-
|
|
177
|
-
return {
|
|
178
|
-
selector: stableSelector,
|
|
179
|
-
html,
|
|
174
|
+
return helpers.reportOccurrence(el, {
|
|
180
175
|
summary: 'This page has more than one banner landmark.',
|
|
181
176
|
hint: 'Keep only one banner landmark (header/role="banner") per page.',
|
|
182
177
|
i18n: {
|
|
@@ -187,7 +182,7 @@ function runInPage(ctx) {
|
|
|
187
182
|
data: {
|
|
188
183
|
details: { reasonCode: 'LANDMARK_DUPLICATE_BANNER', count: banners.length }
|
|
189
184
|
}
|
|
190
|
-
};
|
|
185
|
+
});
|
|
191
186
|
});
|
|
192
187
|
|
|
193
188
|
return {
|
|
@@ -163,12 +163,7 @@ function runInPage(ctx) {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
const occurrences = contentinfos.map((el) => {
|
|
166
|
-
|
|
167
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
selector: stableSelector,
|
|
171
|
-
html,
|
|
166
|
+
return helpers.reportOccurrence(el, {
|
|
172
167
|
summary: 'This page has more than one contentinfo landmark.',
|
|
173
168
|
hint: 'Keep only one contentinfo landmark (footer/role="contentinfo") per page.',
|
|
174
169
|
i18n: {
|
|
@@ -179,7 +174,7 @@ function runInPage(ctx) {
|
|
|
179
174
|
data: {
|
|
180
175
|
details: { reasonCode: 'LANDMARK_DUPLICATE_CONTENTINFO', count: contentinfos.length }
|
|
181
176
|
}
|
|
182
|
-
};
|
|
177
|
+
});
|
|
183
178
|
});
|
|
184
179
|
|
|
185
180
|
return {
|
|
@@ -108,12 +108,7 @@ function runInPage(ctx) {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
const occurrences = mains.map((el) => {
|
|
111
|
-
|
|
112
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
113
|
-
|
|
114
|
-
return {
|
|
115
|
-
selector: stableSelector,
|
|
116
|
-
html,
|
|
111
|
+
return helpers.reportOccurrence(el, {
|
|
117
112
|
summary: 'This page has more than one main landmark.',
|
|
118
113
|
hint: 'Keep only one main landmark (<main>/role="main") per page.',
|
|
119
114
|
i18n: {
|
|
@@ -124,7 +119,7 @@ function runInPage(ctx) {
|
|
|
124
119
|
data: {
|
|
125
120
|
details: { reasonCode: 'LANDMARK_DUPLICATE_MAIN', count: mains.length }
|
|
126
121
|
}
|
|
127
|
-
};
|
|
122
|
+
});
|
|
128
123
|
});
|
|
129
124
|
|
|
130
125
|
return {
|
|
@@ -123,19 +123,12 @@ function runInPage(ctx) {
|
|
|
123
123
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(body) : 'body';
|
|
127
|
-
const html = helpers.getOuterHtmlSnippet
|
|
128
|
-
? helpers.getOuterHtmlSnippet(body)
|
|
129
|
-
: (body.outerHTML || '').slice(0, 200);
|
|
130
|
-
|
|
131
126
|
return {
|
|
132
127
|
ruleId: rule.ruleId,
|
|
133
128
|
outcome: 'cantTell',
|
|
134
129
|
severity: rule.defaultSeverity || 'minor',
|
|
135
130
|
occurrences: [
|
|
136
|
-
{
|
|
137
|
-
selector: stableSelector,
|
|
138
|
-
html,
|
|
131
|
+
helpers.reportOccurrence(body, {
|
|
139
132
|
summary: 'This page has no main landmark.',
|
|
140
133
|
hint: 'Add a main landmark (<main> or role="main") around the page\'s primary content.',
|
|
141
134
|
i18n: {
|
|
@@ -146,7 +139,7 @@ function runInPage(ctx) {
|
|
|
146
139
|
data: {
|
|
147
140
|
details: { reasonCode: 'LANDMARK_MAIN_MISSING' }
|
|
148
141
|
}
|
|
149
|
-
}
|
|
142
|
+
})
|
|
150
143
|
]
|
|
151
144
|
};
|
|
152
145
|
}
|
|
@@ -219,34 +219,29 @@ function runInPage(ctx) {
|
|
|
219
219
|
if (group.length <= 1) continue;
|
|
220
220
|
|
|
221
221
|
for (const { el } of group) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
details: {
|
|
243
|
-
reasonCode: 'LANDMARK_NOT_UNIQUE',
|
|
244
|
-
role,
|
|
245
|
-
name: normalizedName,
|
|
246
|
-
groupSize: group.length
|
|
222
|
+
occurrences.push(
|
|
223
|
+
helpers.reportOccurrence(el, {
|
|
224
|
+
summary: normalizedName
|
|
225
|
+
? `This ${role} landmark shares its accessible name with another ${role} landmark.`
|
|
226
|
+
: `This ${role} landmark has no accessible name, and more than one unnamed ${role} landmark exists on this page.`,
|
|
227
|
+
hint: `Give each ${role} landmark a distinct name via aria-label or aria-labelledby.`,
|
|
228
|
+
i18n: {
|
|
229
|
+
summaryKey: normalizedName
|
|
230
|
+
? 'landmarkUnique_summary_cantTell_duplicateName'
|
|
231
|
+
: 'landmarkUnique_summary_cantTell_bothUnnamed',
|
|
232
|
+
hintKey: 'landmarkUnique_hint_cantTell',
|
|
233
|
+
params: { role }
|
|
234
|
+
},
|
|
235
|
+
data: {
|
|
236
|
+
details: {
|
|
237
|
+
reasonCode: 'LANDMARK_NOT_UNIQUE',
|
|
238
|
+
role,
|
|
239
|
+
name: normalizedName,
|
|
240
|
+
groupSize: group.length
|
|
241
|
+
}
|
|
247
242
|
}
|
|
248
|
-
}
|
|
249
|
-
|
|
243
|
+
})
|
|
244
|
+
);
|
|
250
245
|
}
|
|
251
246
|
}
|
|
252
247
|
}
|
|
@@ -133,27 +133,25 @@ function runInPage(ctx) {
|
|
|
133
133
|
|
|
134
134
|
if (!GENERIC_LINK_TEXT.has(normalized)) continue;
|
|
135
135
|
|
|
136
|
-
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
137
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
138
136
|
const eligInfo = helpers.getEligibilityInfo
|
|
139
137
|
? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' })
|
|
140
138
|
: null;
|
|
141
139
|
|
|
142
|
-
occurrences.push(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
140
|
+
occurrences.push(
|
|
141
|
+
helpers.reportOccurrence(el, {
|
|
142
|
+
summary: `This link's accessible name ("${rawName.trim()}") is a generic, non-descriptive phrase.`,
|
|
143
|
+
hint: 'Make the link text itself describe its destination/purpose (e.g. "Download the 2026 pricing guide" instead of "Download"), or confirm the surrounding context already makes the purpose clear.',
|
|
144
|
+
i18n: {
|
|
145
|
+
summaryKey: 'linkNameQuality_summary_cantTell',
|
|
146
|
+
hintKey: 'linkNameQuality_hint_cantTell',
|
|
147
|
+
params: { name: rawName.trim() }
|
|
148
|
+
},
|
|
149
|
+
data: {
|
|
150
|
+
details: { reasonCode: 'GENERIC_LINK_TEXT', normalizedName: normalized },
|
|
151
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
);
|
|
157
155
|
}
|
|
158
156
|
|
|
159
157
|
if (applicableCount === 0) {
|
|
@@ -104,23 +104,20 @@ function runInPage(ctx) {
|
|
|
104
104
|
|
|
105
105
|
if (!reasons.length) continue;
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
details: { reasonCode: 'VIEWPORT_ZOOM_BELOW_500', reasons }
|
|
122
|
-
}
|
|
123
|
-
});
|
|
107
|
+
occurrences.push(
|
|
108
|
+
helpers.reportOccurrence(el, {
|
|
109
|
+
summary: 'This viewport meta tag restricts zoom below the 500% best-practice target.',
|
|
110
|
+
hint: 'Remove user-scalable=no and raise maximum-scale to at least 5 (500%) if possible.',
|
|
111
|
+
i18n: {
|
|
112
|
+
summaryKey: 'metaViewportLarge_summary_cantTell',
|
|
113
|
+
hintKey: 'metaViewportLarge_hint_cantTell',
|
|
114
|
+
params: { reasons: reasons.join(', ') }
|
|
115
|
+
},
|
|
116
|
+
data: {
|
|
117
|
+
details: { reasonCode: 'VIEWPORT_ZOOM_BELOW_500', reasons }
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
);
|
|
124
121
|
}
|
|
125
122
|
|
|
126
123
|
if (applicableCount === 0) {
|