recker 1.0.28 → 1.0.29
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/dist/cli/tui/shell.d.ts +1 -0
- package/dist/cli/tui/shell.js +339 -5
- package/dist/scrape/index.d.ts +2 -0
- package/dist/scrape/index.js +1 -0
- package/dist/scrape/spider.d.ts +61 -0
- package/dist/scrape/spider.js +250 -0
- package/dist/seo/analyzer.js +27 -0
- package/dist/seo/index.d.ts +3 -1
- package/dist/seo/index.js +1 -0
- package/dist/seo/rules/accessibility.js +620 -54
- package/dist/seo/rules/best-practices.d.ts +2 -0
- package/dist/seo/rules/best-practices.js +188 -0
- package/dist/seo/rules/crawl.d.ts +2 -0
- package/dist/seo/rules/crawl.js +307 -0
- package/dist/seo/rules/cwv.d.ts +2 -0
- package/dist/seo/rules/cwv.js +337 -0
- package/dist/seo/rules/ecommerce.d.ts +2 -0
- package/dist/seo/rules/ecommerce.js +252 -0
- package/dist/seo/rules/i18n.d.ts +2 -0
- package/dist/seo/rules/i18n.js +222 -0
- package/dist/seo/rules/index.d.ts +32 -0
- package/dist/seo/rules/index.js +71 -0
- package/dist/seo/rules/internal-linking.d.ts +2 -0
- package/dist/seo/rules/internal-linking.js +375 -0
- package/dist/seo/rules/local.d.ts +2 -0
- package/dist/seo/rules/local.js +265 -0
- package/dist/seo/rules/pwa.d.ts +2 -0
- package/dist/seo/rules/pwa.js +302 -0
- package/dist/seo/rules/readability.d.ts +2 -0
- package/dist/seo/rules/readability.js +255 -0
- package/dist/seo/rules/security.js +406 -28
- package/dist/seo/rules/social.d.ts +2 -0
- package/dist/seo/rules/social.js +373 -0
- package/dist/seo/rules/types.d.ts +155 -0
- package/dist/seo/seo-spider.d.ts +47 -0
- package/dist/seo/seo-spider.js +362 -0
- package/dist/seo/types.d.ts +24 -0
- package/package.json +1 -1
|
@@ -1,128 +1,694 @@
|
|
|
1
1
|
import { createResult } from './types.js';
|
|
2
2
|
export const accessibilityRules = [
|
|
3
3
|
{
|
|
4
|
-
id: 'a11y-buttons-
|
|
5
|
-
name: '
|
|
4
|
+
id: 'a11y-buttons-accessible-name',
|
|
5
|
+
name: 'Buttons Accessible Name',
|
|
6
6
|
category: 'accessibility',
|
|
7
|
-
severity: '
|
|
8
|
-
description: 'Buttons
|
|
7
|
+
severity: 'error',
|
|
8
|
+
description: 'Buttons must have an accessible name (text content, aria-label, or title)',
|
|
9
9
|
check: (ctx) => {
|
|
10
|
-
|
|
10
|
+
if (ctx.buttonsWithoutAriaLabel === undefined)
|
|
11
|
+
return null;
|
|
12
|
+
const count = ctx.buttonsWithoutAriaLabel;
|
|
11
13
|
if (count > 0) {
|
|
12
|
-
return createResult({ id: 'a11y-buttons-
|
|
14
|
+
return createResult({ id: 'a11y-buttons-accessible-name', name: 'Buttons Accessible Name', category: 'accessibility', severity: 'error' }, 'fail', `${count} button(s) do not have an accessible name`, {
|
|
15
|
+
value: count,
|
|
16
|
+
recommendation: 'Add text content, aria-label, aria-labelledby, or title to all buttons',
|
|
17
|
+
evidence: {
|
|
18
|
+
found: count,
|
|
19
|
+
expected: 0,
|
|
20
|
+
impact: 'Screen reader users cannot determine the button purpose',
|
|
21
|
+
learnMore: 'https://dequeuniversity.com/rules/axe/4.4/button-name',
|
|
22
|
+
},
|
|
23
|
+
});
|
|
13
24
|
}
|
|
14
|
-
return
|
|
25
|
+
return createResult({ id: 'a11y-buttons-accessible-name', name: 'Buttons Accessible Name', category: 'accessibility', severity: 'error' }, 'pass', 'All buttons have accessible names');
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'a11y-images-alt',
|
|
30
|
+
name: 'Image Alt Attributes',
|
|
31
|
+
category: 'accessibility',
|
|
32
|
+
severity: 'error',
|
|
33
|
+
description: 'Image elements must have [alt] attributes',
|
|
34
|
+
check: (ctx) => {
|
|
35
|
+
if (ctx.imagesWithoutAlt === undefined)
|
|
36
|
+
return null;
|
|
37
|
+
const count = ctx.imagesWithoutAlt;
|
|
38
|
+
if (count > 0) {
|
|
39
|
+
return createResult({ id: 'a11y-images-alt', name: 'Image Alt Attributes', category: 'accessibility', severity: 'error' }, 'fail', `${count} image(s) do not have [alt] attributes`, {
|
|
40
|
+
value: count,
|
|
41
|
+
recommendation: 'Add alt="" for decorative images or descriptive alt text for informative images',
|
|
42
|
+
evidence: {
|
|
43
|
+
found: count,
|
|
44
|
+
expected: 0,
|
|
45
|
+
impact: 'Screen readers cannot convey image content to blind users',
|
|
46
|
+
learnMore: 'https://dequeuniversity.com/rules/axe/4.4/image-alt',
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return createResult({ id: 'a11y-images-alt', name: 'Image Alt Attributes', category: 'accessibility', severity: 'error' }, 'pass', 'All images have [alt] attributes');
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'a11y-links-discernible-name',
|
|
55
|
+
name: 'Links Discernible Name',
|
|
56
|
+
category: 'accessibility',
|
|
57
|
+
severity: 'error',
|
|
58
|
+
description: 'Links must have a discernible name',
|
|
59
|
+
check: (ctx) => {
|
|
60
|
+
if (ctx.linksWithoutText === undefined && ctx.linksWithoutAriaLabel === undefined)
|
|
61
|
+
return null;
|
|
62
|
+
const linksNoText = ctx.linksWithoutText ?? 0;
|
|
63
|
+
const linksNoAria = ctx.linksWithoutAriaLabel ?? 0;
|
|
64
|
+
const count = Math.max(linksNoText, linksNoAria);
|
|
65
|
+
if (count > 0) {
|
|
66
|
+
return createResult({ id: 'a11y-links-discernible-name', name: 'Links Discernible Name', category: 'accessibility', severity: 'error' }, 'fail', `${count} link(s) do not have a discernible name`, {
|
|
67
|
+
value: count,
|
|
68
|
+
recommendation: 'Add text content, aria-label, aria-labelledby, or title to links',
|
|
69
|
+
evidence: {
|
|
70
|
+
found: count,
|
|
71
|
+
expected: 0,
|
|
72
|
+
impact: 'Screen reader users cannot understand link destinations',
|
|
73
|
+
learnMore: 'https://dequeuniversity.com/rules/axe/4.4/link-name',
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return createResult({ id: 'a11y-links-discernible-name', name: 'Links Discernible Name', category: 'accessibility', severity: 'error' }, 'pass', 'All links have discernible names');
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'a11y-form-labels',
|
|
82
|
+
name: 'Form Input Labels',
|
|
83
|
+
category: 'accessibility',
|
|
84
|
+
severity: 'error',
|
|
85
|
+
description: 'Form elements must have associated labels',
|
|
86
|
+
check: (ctx) => {
|
|
87
|
+
if (ctx.inputsWithoutLabel === undefined)
|
|
88
|
+
return null;
|
|
89
|
+
const count = ctx.inputsWithoutLabel;
|
|
90
|
+
if (count > 0) {
|
|
91
|
+
return createResult({ id: 'a11y-form-labels', name: 'Form Input Labels', category: 'accessibility', severity: 'error' }, 'fail', `${count} form input(s) without associated label`, {
|
|
92
|
+
value: count,
|
|
93
|
+
recommendation: 'Add <label for="id">, aria-label, or aria-labelledby to all form inputs',
|
|
94
|
+
evidence: {
|
|
95
|
+
found: count,
|
|
96
|
+
expected: 0,
|
|
97
|
+
impact: 'Users cannot identify input purpose, critical for screen reader users',
|
|
98
|
+
learnMore: 'https://dequeuniversity.com/rules/axe/4.4/label',
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return createResult({ id: 'a11y-form-labels', name: 'Form Input Labels', category: 'accessibility', severity: 'error' }, 'pass', 'All form inputs have associated labels');
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'a11y-heading-order',
|
|
107
|
+
name: 'Heading Order',
|
|
108
|
+
category: 'accessibility',
|
|
109
|
+
severity: 'warning',
|
|
110
|
+
description: 'Heading elements should be in sequentially-descending order',
|
|
111
|
+
check: (ctx) => {
|
|
112
|
+
if (ctx.headingHierarchyValid === undefined)
|
|
113
|
+
return null;
|
|
114
|
+
if (!ctx.headingHierarchyValid) {
|
|
115
|
+
return createResult({ id: 'a11y-heading-order', name: 'Heading Order', category: 'accessibility', severity: 'warning' }, 'warn', 'Heading elements are not in sequentially-descending order', {
|
|
116
|
+
recommendation: 'Ensure headings follow a logical order (H1 → H2 → H3) without skipping levels',
|
|
117
|
+
evidence: {
|
|
118
|
+
found: ctx.headingSkippedLevels?.join(', ') || 'Skipped levels detected',
|
|
119
|
+
expected: 'Sequential heading hierarchy',
|
|
120
|
+
impact: 'Impacts keyboard navigation for screen reader users',
|
|
121
|
+
learnMore: 'https://dequeuniversity.com/rules/axe/4.4/heading-order',
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return createResult({ id: 'a11y-heading-order', name: 'Heading Order', category: 'accessibility', severity: 'warning' }, 'pass', 'Headings are in sequentially-descending order');
|
|
15
126
|
},
|
|
16
127
|
},
|
|
17
128
|
{
|
|
18
|
-
id: 'a11y-
|
|
19
|
-
name: '
|
|
129
|
+
id: 'a11y-tabindex',
|
|
130
|
+
name: 'Tabindex Values',
|
|
20
131
|
category: 'accessibility',
|
|
21
132
|
severity: 'warning',
|
|
22
|
-
description: '
|
|
133
|
+
description: 'No element should have a [tabindex] value greater than 0',
|
|
23
134
|
check: (ctx) => {
|
|
24
|
-
|
|
135
|
+
if (ctx.elementsWithHighTabindex === undefined)
|
|
136
|
+
return null;
|
|
137
|
+
const count = ctx.elementsWithHighTabindex;
|
|
25
138
|
if (count > 0) {
|
|
26
|
-
return createResult({ id: 'a11y-
|
|
139
|
+
return createResult({ id: 'a11y-tabindex', name: 'Tabindex Values', category: 'accessibility', severity: 'warning' }, 'warn', `${count} element(s) have tabindex > 0`, {
|
|
140
|
+
value: count,
|
|
141
|
+
recommendation: 'Remove positive tabindex values; use tabindex="0" or "-1" instead',
|
|
142
|
+
evidence: {
|
|
143
|
+
found: count,
|
|
144
|
+
expected: 0,
|
|
145
|
+
impact: 'Creates confusing focus order for keyboard users',
|
|
146
|
+
learnMore: 'https://dequeuniversity.com/rules/axe/4.4/tabindex',
|
|
147
|
+
},
|
|
148
|
+
});
|
|
27
149
|
}
|
|
28
|
-
return
|
|
150
|
+
return createResult({ id: 'a11y-tabindex', name: 'Tabindex Values', category: 'accessibility', severity: 'warning' }, 'pass', 'No elements have tabindex > 0');
|
|
29
151
|
},
|
|
30
152
|
},
|
|
31
153
|
{
|
|
32
|
-
id: 'a11y-
|
|
33
|
-
name: '
|
|
154
|
+
id: 'a11y-aria-valid-attrs',
|
|
155
|
+
name: 'Valid ARIA Attributes',
|
|
34
156
|
category: 'accessibility',
|
|
35
157
|
severity: 'error',
|
|
36
|
-
description: '
|
|
158
|
+
description: '[aria-*] attributes must be valid and not misspelled',
|
|
37
159
|
check: (ctx) => {
|
|
38
|
-
|
|
160
|
+
if (ctx.invalidAriaAttributes === undefined)
|
|
161
|
+
return null;
|
|
162
|
+
const count = ctx.invalidAriaAttributes;
|
|
39
163
|
if (count > 0) {
|
|
40
|
-
return createResult({ id: 'a11y-
|
|
164
|
+
return createResult({ id: 'a11y-aria-valid-attrs', name: 'Valid ARIA Attributes', category: 'accessibility', severity: 'error' }, 'fail', `${count} invalid or misspelled aria-* attribute(s) found`, {
|
|
165
|
+
value: count,
|
|
166
|
+
recommendation: 'Verify ARIA attributes are spelled correctly and valid',
|
|
167
|
+
evidence: {
|
|
168
|
+
found: count,
|
|
169
|
+
expected: 0,
|
|
170
|
+
impact: 'Invalid ARIA provides no accessibility benefit',
|
|
171
|
+
learnMore: 'https://dequeuniversity.com/rules/axe/4.4/aria-valid-attr',
|
|
172
|
+
},
|
|
173
|
+
});
|
|
41
174
|
}
|
|
42
|
-
return
|
|
175
|
+
return createResult({ id: 'a11y-aria-valid-attrs', name: 'Valid ARIA Attributes', category: 'accessibility', severity: 'error' }, 'pass', 'All ARIA attributes are valid');
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: 'a11y-aria-valid-values',
|
|
180
|
+
name: 'ARIA Attribute Values',
|
|
181
|
+
category: 'accessibility',
|
|
182
|
+
severity: 'error',
|
|
183
|
+
description: '[aria-*] attributes must have valid values',
|
|
184
|
+
check: (ctx) => {
|
|
185
|
+
if (ctx.invalidAriaValues === undefined)
|
|
186
|
+
return null;
|
|
187
|
+
const count = ctx.invalidAriaValues;
|
|
188
|
+
if (count > 0) {
|
|
189
|
+
return createResult({ id: 'a11y-aria-valid-values', name: 'ARIA Attribute Values', category: 'accessibility', severity: 'error' }, 'fail', `${count} aria-* attribute(s) have invalid values`, {
|
|
190
|
+
value: count,
|
|
191
|
+
recommendation: 'Use valid values for ARIA attributes (e.g., aria-live="polite")',
|
|
192
|
+
evidence: {
|
|
193
|
+
found: count,
|
|
194
|
+
expected: 0,
|
|
195
|
+
impact: 'Invalid values prevent assistive technologies from working correctly',
|
|
196
|
+
learnMore: 'https://dequeuniversity.com/rules/axe/4.4/aria-valid-attr-value',
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return createResult({ id: 'a11y-aria-valid-values', name: 'ARIA Attribute Values', category: 'accessibility', severity: 'error' }, 'pass', 'All ARIA attribute values are valid');
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: 'a11y-aria-roles',
|
|
205
|
+
name: 'Valid ARIA Roles',
|
|
206
|
+
category: 'accessibility',
|
|
207
|
+
severity: 'error',
|
|
208
|
+
description: '[role] values must be valid',
|
|
209
|
+
check: (ctx) => {
|
|
210
|
+
if (ctx.invalidAriaRoles === undefined)
|
|
211
|
+
return null;
|
|
212
|
+
const count = ctx.invalidAriaRoles;
|
|
213
|
+
if (count > 0) {
|
|
214
|
+
return createResult({ id: 'a11y-aria-roles', name: 'Valid ARIA Roles', category: 'accessibility', severity: 'error' }, 'fail', `${count} invalid [role] value(s) found`, {
|
|
215
|
+
value: count,
|
|
216
|
+
recommendation: 'Use valid ARIA role values (e.g., button, dialog, navigation)',
|
|
217
|
+
evidence: {
|
|
218
|
+
found: count,
|
|
219
|
+
expected: 0,
|
|
220
|
+
impact: 'Invalid roles confuse assistive technologies',
|
|
221
|
+
learnMore: 'https://dequeuniversity.com/rules/axe/4.4/aria-roles',
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return createResult({ id: 'a11y-aria-roles', name: 'Valid ARIA Roles', category: 'accessibility', severity: 'error' }, 'pass', 'All [role] values are valid');
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: 'a11y-aria-required-attrs',
|
|
230
|
+
name: 'Required ARIA Attributes',
|
|
231
|
+
category: 'accessibility',
|
|
232
|
+
severity: 'error',
|
|
233
|
+
description: '[role]s must have all required [aria-*] attributes',
|
|
234
|
+
check: (ctx) => {
|
|
235
|
+
if (ctx.missingRequiredAriaAttrs === undefined)
|
|
236
|
+
return null;
|
|
237
|
+
const count = ctx.missingRequiredAriaAttrs;
|
|
238
|
+
if (count > 0) {
|
|
239
|
+
return createResult({ id: 'a11y-aria-required-attrs', name: 'Required ARIA Attributes', category: 'accessibility', severity: 'error' }, 'fail', `${count} element(s) with roles missing required aria-* attributes`, {
|
|
240
|
+
value: count,
|
|
241
|
+
recommendation: 'Add required ARIA attributes for each role (e.g., role="slider" requires aria-valuenow)',
|
|
242
|
+
evidence: {
|
|
243
|
+
found: count,
|
|
244
|
+
expected: 0,
|
|
245
|
+
impact: 'Missing attributes break assistive technology functionality',
|
|
246
|
+
learnMore: 'https://dequeuniversity.com/rules/axe/4.4/aria-required-attr',
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return createResult({ id: 'a11y-aria-required-attrs', name: 'Required ARIA Attributes', category: 'accessibility', severity: 'error' }, 'pass', 'All roles have required ARIA attributes');
|
|
43
251
|
},
|
|
44
252
|
},
|
|
45
253
|
{
|
|
46
|
-
id: 'a11y-
|
|
47
|
-
name: '
|
|
254
|
+
id: 'a11y-aria-hidden-body',
|
|
255
|
+
name: 'ARIA Hidden Body',
|
|
256
|
+
category: 'accessibility',
|
|
257
|
+
severity: 'error',
|
|
258
|
+
description: '[aria-hidden="true"] must not be present on the document <body>',
|
|
259
|
+
check: (ctx) => {
|
|
260
|
+
if (ctx.hasAriaHiddenBody === undefined)
|
|
261
|
+
return null;
|
|
262
|
+
if (ctx.hasAriaHiddenBody) {
|
|
263
|
+
return createResult({ id: 'a11y-aria-hidden-body', name: 'ARIA Hidden Body', category: 'accessibility', severity: 'error' }, 'fail', 'aria-hidden="true" is present on document <body>', {
|
|
264
|
+
recommendation: 'Remove aria-hidden from <body> element',
|
|
265
|
+
evidence: {
|
|
266
|
+
found: 'aria-hidden="true" on body',
|
|
267
|
+
expected: 'No aria-hidden on body',
|
|
268
|
+
impact: 'Entire page content hidden from assistive technologies',
|
|
269
|
+
learnMore: 'https://dequeuniversity.com/rules/axe/4.4/aria-hidden-body',
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return createResult({ id: 'a11y-aria-hidden-body', name: 'ARIA Hidden Body', category: 'accessibility', severity: 'error' }, 'pass', 'aria-hidden is not present on <body>');
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
id: 'a11y-aria-hidden-focus',
|
|
278
|
+
name: 'ARIA Hidden Focusable',
|
|
279
|
+
category: 'accessibility',
|
|
280
|
+
severity: 'error',
|
|
281
|
+
description: '[aria-hidden="true"] elements must not contain focusable descendants',
|
|
282
|
+
check: (ctx) => {
|
|
283
|
+
if (ctx.ariaHiddenFocusableCount === undefined)
|
|
284
|
+
return null;
|
|
285
|
+
const count = ctx.ariaHiddenFocusableCount;
|
|
286
|
+
if (count > 0) {
|
|
287
|
+
return createResult({ id: 'a11y-aria-hidden-focus', name: 'ARIA Hidden Focusable', category: 'accessibility', severity: 'error' }, 'fail', `${count} aria-hidden element(s) contain focusable descendants`, {
|
|
288
|
+
value: count,
|
|
289
|
+
recommendation: 'Remove focusable elements from aria-hidden containers or remove aria-hidden',
|
|
290
|
+
evidence: {
|
|
291
|
+
found: count,
|
|
292
|
+
expected: 0,
|
|
293
|
+
impact: 'Focus can move to invisible elements, confusing users',
|
|
294
|
+
learnMore: 'https://dequeuniversity.com/rules/axe/4.4/aria-hidden-focus',
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
return createResult({ id: 'a11y-aria-hidden-focus', name: 'ARIA Hidden Focusable', category: 'accessibility', severity: 'error' }, 'pass', 'No focusable elements inside aria-hidden containers');
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
id: 'a11y-aria-deprecated',
|
|
303
|
+
name: 'Deprecated ARIA Roles',
|
|
48
304
|
category: 'accessibility',
|
|
49
305
|
severity: 'warning',
|
|
50
|
-
description: '
|
|
306
|
+
description: 'Deprecated ARIA roles should not be used',
|
|
51
307
|
check: (ctx) => {
|
|
52
|
-
|
|
308
|
+
if (ctx.deprecatedAriaRoles === undefined)
|
|
309
|
+
return null;
|
|
310
|
+
const count = ctx.deprecatedAriaRoles;
|
|
53
311
|
if (count > 0) {
|
|
54
|
-
return createResult({ id: 'a11y-
|
|
312
|
+
return createResult({ id: 'a11y-aria-deprecated', name: 'Deprecated ARIA Roles', category: 'accessibility', severity: 'warning' }, 'warn', `${count} deprecated ARIA role(s) found`, {
|
|
313
|
+
value: count,
|
|
314
|
+
recommendation: 'Replace deprecated roles with current alternatives',
|
|
315
|
+
evidence: {
|
|
316
|
+
found: count,
|
|
317
|
+
expected: 0,
|
|
318
|
+
impact: 'Deprecated roles may not work in future browsers',
|
|
319
|
+
learnMore: 'https://www.w3.org/TR/wai-aria-1.2/',
|
|
320
|
+
},
|
|
321
|
+
});
|
|
55
322
|
}
|
|
56
|
-
return
|
|
323
|
+
return createResult({ id: 'a11y-aria-deprecated', name: 'Deprecated ARIA Roles', category: 'accessibility', severity: 'warning' }, 'pass', 'No deprecated ARIA roles used');
|
|
57
324
|
},
|
|
58
325
|
},
|
|
59
326
|
{
|
|
60
|
-
id: 'a11y-
|
|
61
|
-
name: '
|
|
327
|
+
id: 'a11y-aria-ids-unique',
|
|
328
|
+
name: 'ARIA IDs Unique',
|
|
329
|
+
category: 'accessibility',
|
|
330
|
+
severity: 'error',
|
|
331
|
+
description: 'ARIA IDs must be unique',
|
|
332
|
+
check: (ctx) => {
|
|
333
|
+
if (ctx.duplicateAriaIds === undefined)
|
|
334
|
+
return null;
|
|
335
|
+
const count = ctx.duplicateAriaIds;
|
|
336
|
+
if (count > 0) {
|
|
337
|
+
return createResult({ id: 'a11y-aria-ids-unique', name: 'ARIA IDs Unique', category: 'accessibility', severity: 'error' }, 'fail', `${count} duplicate ARIA ID(s) found`, {
|
|
338
|
+
value: count,
|
|
339
|
+
recommendation: 'Ensure all IDs referenced by aria-labelledby, aria-describedby, etc. are unique',
|
|
340
|
+
evidence: {
|
|
341
|
+
found: count,
|
|
342
|
+
expected: 0,
|
|
343
|
+
impact: 'References to duplicate IDs produce unpredictable behavior',
|
|
344
|
+
learnMore: 'https://dequeuniversity.com/rules/axe/4.4/duplicate-id-aria',
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
return createResult({ id: 'a11y-aria-ids-unique', name: 'ARIA IDs Unique', category: 'accessibility', severity: 'error' }, 'pass', 'All ARIA IDs are unique');
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
id: 'a11y-dialog-name',
|
|
353
|
+
name: 'Dialog Accessible Name',
|
|
354
|
+
category: 'accessibility',
|
|
355
|
+
severity: 'error',
|
|
356
|
+
description: 'Elements with role="dialog" or role="alertdialog" must have accessible names',
|
|
357
|
+
check: (ctx) => {
|
|
358
|
+
if (ctx.dialogsWithoutName === undefined)
|
|
359
|
+
return null;
|
|
360
|
+
const count = ctx.dialogsWithoutName;
|
|
361
|
+
if (count > 0) {
|
|
362
|
+
return createResult({ id: 'a11y-dialog-name', name: 'Dialog Accessible Name', category: 'accessibility', severity: 'error' }, 'fail', `${count} dialog(s) without accessible name`, {
|
|
363
|
+
value: count,
|
|
364
|
+
recommendation: 'Add aria-label or aria-labelledby to dialog elements',
|
|
365
|
+
evidence: {
|
|
366
|
+
found: count,
|
|
367
|
+
expected: 0,
|
|
368
|
+
impact: 'Screen reader users cannot identify dialog purpose',
|
|
369
|
+
learnMore: 'https://dequeuniversity.com/rules/axe/4.4/aria-dialog-name',
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
return createResult({ id: 'a11y-dialog-name', name: 'Dialog Accessible Name', category: 'accessibility', severity: 'error' }, 'pass', 'All dialogs have accessible names');
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
id: 'a11y-main-landmark',
|
|
378
|
+
name: 'Main Landmark',
|
|
379
|
+
category: 'accessibility',
|
|
380
|
+
severity: 'warning',
|
|
381
|
+
description: 'Document should have a main landmark',
|
|
382
|
+
check: (ctx) => {
|
|
383
|
+
if (ctx.hasMain === undefined)
|
|
384
|
+
return null;
|
|
385
|
+
if (!ctx.hasMain) {
|
|
386
|
+
return createResult({ id: 'a11y-main-landmark', name: 'Main Landmark', category: 'accessibility', severity: 'warning' }, 'warn', 'Document does not have a <main> landmark', {
|
|
387
|
+
recommendation: 'Add a <main> element or role="main" to identify the main content area',
|
|
388
|
+
evidence: {
|
|
389
|
+
expected: '<main> or role="main"',
|
|
390
|
+
impact: 'Screen reader users cannot quickly navigate to main content',
|
|
391
|
+
learnMore: 'https://dequeuniversity.com/rules/axe/4.4/landmark-one-main',
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
return createResult({ id: 'a11y-main-landmark', name: 'Main Landmark', category: 'accessibility', severity: 'warning' }, 'pass', 'Document has a main landmark');
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
id: 'a11y-skip-link',
|
|
400
|
+
name: 'Skip Link',
|
|
401
|
+
category: 'accessibility',
|
|
402
|
+
severity: 'info',
|
|
403
|
+
description: 'Page should contain a heading, skip link, or landmark region',
|
|
404
|
+
check: (ctx) => {
|
|
405
|
+
if (ctx.hasSkipLink === undefined && ctx.hasMain === undefined && ctx.h1Count === undefined)
|
|
406
|
+
return null;
|
|
407
|
+
const hasSkip = ctx.hasSkipLink ?? false;
|
|
408
|
+
const hasMain = ctx.hasMain ?? false;
|
|
409
|
+
const hasH1 = (ctx.h1Count ?? 0) > 0;
|
|
410
|
+
if (!hasSkip && !hasMain && !hasH1) {
|
|
411
|
+
return createResult({ id: 'a11y-skip-link', name: 'Skip Link', category: 'accessibility', severity: 'info' }, 'info', 'No skip link, main landmark, or heading found', {
|
|
412
|
+
recommendation: 'Add a skip link, <main> landmark, or at least one heading for navigation',
|
|
413
|
+
evidence: {
|
|
414
|
+
expected: 'Skip link, <main>, or heading',
|
|
415
|
+
impact: 'Keyboard users must tab through all navigation to reach content',
|
|
416
|
+
learnMore: 'https://dequeuniversity.com/rules/axe/4.4/bypass',
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
return createResult({ id: 'a11y-skip-link', name: 'Skip Link', category: 'accessibility', severity: 'info' }, 'pass', 'Page has navigation bypass mechanism');
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
id: 'a11y-table-caption',
|
|
425
|
+
name: 'Table Caption',
|
|
62
426
|
category: 'accessibility',
|
|
63
427
|
severity: 'info',
|
|
64
428
|
description: 'Data tables should have caption or aria-label',
|
|
65
429
|
check: (ctx) => {
|
|
66
|
-
|
|
430
|
+
if (ctx.tablesWithoutCaption === undefined)
|
|
431
|
+
return null;
|
|
432
|
+
const count = ctx.tablesWithoutCaption;
|
|
67
433
|
if (count > 0) {
|
|
68
|
-
return createResult({ id: 'a11y-
|
|
434
|
+
return createResult({ id: 'a11y-table-caption', name: 'Table Caption', category: 'accessibility', severity: 'info' }, 'info', `${count} table(s) without caption or aria-label`, {
|
|
435
|
+
value: count,
|
|
436
|
+
recommendation: 'Add <caption> or aria-label to data tables',
|
|
437
|
+
evidence: {
|
|
438
|
+
found: count,
|
|
439
|
+
expected: 0,
|
|
440
|
+
impact: 'Screen reader users cannot understand table purpose',
|
|
441
|
+
},
|
|
442
|
+
});
|
|
69
443
|
}
|
|
70
|
-
return
|
|
444
|
+
return createResult({ id: 'a11y-table-caption', name: 'Table Caption', category: 'accessibility', severity: 'info' }, 'pass', 'All tables have captions or labels');
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
id: 'a11y-iframe-title',
|
|
449
|
+
name: 'Iframe Title',
|
|
450
|
+
category: 'accessibility',
|
|
451
|
+
severity: 'warning',
|
|
452
|
+
description: '<frame> or <iframe> elements must have a title',
|
|
453
|
+
check: (ctx) => {
|
|
454
|
+
if (ctx.iframesWithoutTitle === undefined)
|
|
455
|
+
return null;
|
|
456
|
+
const count = ctx.iframesWithoutTitle;
|
|
457
|
+
if (count > 0) {
|
|
458
|
+
return createResult({ id: 'a11y-iframe-title', name: 'Iframe Title', category: 'accessibility', severity: 'warning' }, 'warn', `${count} iframe(s) without title attribute`, {
|
|
459
|
+
value: count,
|
|
460
|
+
recommendation: 'Add title attribute to describe iframe content',
|
|
461
|
+
evidence: {
|
|
462
|
+
found: count,
|
|
463
|
+
expected: 0,
|
|
464
|
+
impact: 'Screen reader users cannot identify iframe purpose',
|
|
465
|
+
learnMore: 'https://dequeuniversity.com/rules/axe/4.4/frame-title',
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
return createResult({ id: 'a11y-iframe-title', name: 'Iframe Title', category: 'accessibility', severity: 'warning' }, 'pass', 'All iframes have title attributes');
|
|
71
470
|
},
|
|
72
471
|
},
|
|
73
472
|
{
|
|
74
473
|
id: 'a11y-svg-title',
|
|
75
|
-
name: 'SVG
|
|
474
|
+
name: 'SVG Title',
|
|
76
475
|
category: 'accessibility',
|
|
77
476
|
severity: 'warning',
|
|
78
477
|
description: 'SVGs should have <title> or aria-label for accessibility',
|
|
79
478
|
check: (ctx) => {
|
|
80
|
-
|
|
479
|
+
if (ctx.svgsWithoutTitle === undefined)
|
|
480
|
+
return null;
|
|
481
|
+
const count = ctx.svgsWithoutTitle;
|
|
81
482
|
if (count > 0) {
|
|
82
|
-
return createResult({ id: 'a11y-svg-title', name: 'SVG
|
|
483
|
+
return createResult({ id: 'a11y-svg-title', name: 'SVG Title', category: 'accessibility', severity: 'warning' }, 'warn', `${count} SVG(s) without accessible title`, {
|
|
484
|
+
value: count,
|
|
485
|
+
recommendation: 'Add <title> element inside SVG or aria-label attribute',
|
|
486
|
+
evidence: {
|
|
487
|
+
found: count,
|
|
488
|
+
expected: 0,
|
|
489
|
+
impact: 'Screen readers cannot describe SVG content',
|
|
490
|
+
},
|
|
491
|
+
});
|
|
83
492
|
}
|
|
84
|
-
return
|
|
493
|
+
return createResult({ id: 'a11y-svg-title', name: 'SVG Title', category: 'accessibility', severity: 'warning' }, 'pass', 'All SVGs have accessible titles');
|
|
85
494
|
},
|
|
86
495
|
},
|
|
87
496
|
{
|
|
88
|
-
id: 'a11y-
|
|
89
|
-
name: '
|
|
497
|
+
id: 'a11y-viewport-zoom',
|
|
498
|
+
name: 'Viewport Zoom',
|
|
90
499
|
category: 'accessibility',
|
|
91
|
-
severity: '
|
|
92
|
-
description: '
|
|
500
|
+
severity: 'error',
|
|
501
|
+
description: '[user-scalable="no"] should not be used and maximum-scale should not be less than 5',
|
|
93
502
|
check: (ctx) => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
503
|
+
if (ctx.viewportContent === undefined)
|
|
504
|
+
return null;
|
|
505
|
+
const viewport = ctx.viewportContent.toLowerCase();
|
|
506
|
+
const hasUserScalableNo = viewport.includes('user-scalable=no') || viewport.includes('user-scalable=0');
|
|
507
|
+
const maxScaleMatch = viewport.match(/maximum-scale\s*=\s*([\d.]+)/);
|
|
508
|
+
const maxScale = maxScaleMatch ? parseFloat(maxScaleMatch[1]) : null;
|
|
509
|
+
if (hasUserScalableNo || (maxScale !== null && maxScale < 5)) {
|
|
510
|
+
return createResult({ id: 'a11y-viewport-zoom', name: 'Viewport Zoom', category: 'accessibility', severity: 'error' }, 'fail', 'Viewport prevents zooming', {
|
|
511
|
+
recommendation: 'Remove user-scalable=no and ensure maximum-scale is at least 5',
|
|
512
|
+
evidence: {
|
|
513
|
+
found: ctx.viewportContent,
|
|
514
|
+
expected: 'No user-scalable=no, maximum-scale >= 5',
|
|
515
|
+
impact: 'Users with low vision cannot zoom to read content',
|
|
516
|
+
learnMore: 'https://dequeuniversity.com/rules/axe/4.4/meta-viewport',
|
|
517
|
+
},
|
|
518
|
+
});
|
|
98
519
|
}
|
|
99
|
-
return
|
|
520
|
+
return createResult({ id: 'a11y-viewport-zoom', name: 'Viewport Zoom', category: 'accessibility', severity: 'error' }, 'pass', 'Viewport allows zooming');
|
|
100
521
|
},
|
|
101
522
|
},
|
|
102
523
|
{
|
|
103
|
-
id: 'a11y-
|
|
104
|
-
name: '
|
|
524
|
+
id: 'a11y-document-title',
|
|
525
|
+
name: 'Document Title',
|
|
105
526
|
category: 'accessibility',
|
|
106
527
|
severity: 'error',
|
|
107
|
-
description: '
|
|
528
|
+
description: 'Document must have a <title> element',
|
|
108
529
|
check: (ctx) => {
|
|
109
|
-
if (ctx.
|
|
110
|
-
return
|
|
530
|
+
if (ctx.title === undefined)
|
|
531
|
+
return null;
|
|
532
|
+
if (!ctx.title || ctx.title.trim().length === 0) {
|
|
533
|
+
return createResult({ id: 'a11y-document-title', name: 'Document Title', category: 'accessibility', severity: 'error' }, 'fail', 'Document does not have a <title> element', {
|
|
534
|
+
recommendation: 'Add a descriptive <title> element to the document',
|
|
535
|
+
evidence: {
|
|
536
|
+
expected: '<title>Page Title</title>',
|
|
537
|
+
impact: 'Screen reader users cannot identify the page',
|
|
538
|
+
learnMore: 'https://dequeuniversity.com/rules/axe/4.4/document-title',
|
|
539
|
+
},
|
|
540
|
+
});
|
|
111
541
|
}
|
|
112
|
-
return
|
|
542
|
+
return createResult({ id: 'a11y-document-title', name: 'Document Title', category: 'accessibility', severity: 'error' }, 'pass', 'Document has a title element');
|
|
113
543
|
},
|
|
114
544
|
},
|
|
115
545
|
{
|
|
116
|
-
id: 'a11y-
|
|
117
|
-
name: '
|
|
546
|
+
id: 'a11y-html-lang',
|
|
547
|
+
name: 'HTML Lang Attribute',
|
|
118
548
|
category: 'accessibility',
|
|
119
549
|
severity: 'error',
|
|
120
|
-
description: '
|
|
550
|
+
description: '<html> element must have a [lang] attribute',
|
|
121
551
|
check: (ctx) => {
|
|
122
|
-
if (ctx.
|
|
123
|
-
return
|
|
552
|
+
if (ctx.hasLang === undefined)
|
|
553
|
+
return null;
|
|
554
|
+
if (!ctx.hasLang) {
|
|
555
|
+
return createResult({ id: 'a11y-html-lang', name: 'HTML Lang Attribute', category: 'accessibility', severity: 'error' }, 'fail', '<html> element does not have a [lang] attribute', {
|
|
556
|
+
recommendation: 'Add lang attribute to html element (e.g., <html lang="en">)',
|
|
557
|
+
evidence: {
|
|
558
|
+
expected: '<html lang="en">',
|
|
559
|
+
impact: 'Screen readers may pronounce content incorrectly',
|
|
560
|
+
learnMore: 'https://dequeuniversity.com/rules/axe/4.4/html-has-lang',
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
return createResult({ id: 'a11y-html-lang', name: 'HTML Lang Attribute', category: 'accessibility', severity: 'error' }, 'pass', 'HTML element has lang attribute');
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
id: 'a11y-html-lang-valid',
|
|
569
|
+
name: 'Valid Lang Attribute',
|
|
570
|
+
category: 'accessibility',
|
|
571
|
+
severity: 'error',
|
|
572
|
+
description: '<html> element must have a valid value for its [lang] attribute',
|
|
573
|
+
check: (ctx) => {
|
|
574
|
+
if (!ctx.hasLang || !ctx.langValue)
|
|
575
|
+
return null;
|
|
576
|
+
const validLangPattern = /^[a-z]{2,3}(-[A-Za-z]{2,4})?(-[A-Za-z0-9]{2,})?$/i;
|
|
577
|
+
if (!validLangPattern.test(ctx.langValue)) {
|
|
578
|
+
return createResult({ id: 'a11y-html-lang-valid', name: 'Valid Lang Attribute', category: 'accessibility', severity: 'error' }, 'fail', `Invalid lang attribute value: ${ctx.langValue}`, {
|
|
579
|
+
value: ctx.langValue,
|
|
580
|
+
recommendation: 'Use a valid BCP 47 language tag (e.g., "en", "en-US", "pt-BR")',
|
|
581
|
+
evidence: {
|
|
582
|
+
found: ctx.langValue,
|
|
583
|
+
expected: 'Valid BCP 47 language tag',
|
|
584
|
+
learnMore: 'https://dequeuniversity.com/rules/axe/4.4/html-lang-valid',
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
return createResult({ id: 'a11y-html-lang-valid', name: 'Valid Lang Attribute', category: 'accessibility', severity: 'error' }, 'pass', `Valid lang attribute: ${ctx.langValue}`);
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
id: 'a11y-video-captions',
|
|
593
|
+
name: 'Video Captions',
|
|
594
|
+
category: 'accessibility',
|
|
595
|
+
severity: 'warning',
|
|
596
|
+
description: '<video> elements should contain a <track> element with [kind="captions"]',
|
|
597
|
+
check: (ctx) => {
|
|
598
|
+
if (ctx.videoCount === undefined || ctx.videosWithCaptions === undefined)
|
|
599
|
+
return null;
|
|
600
|
+
const videos = ctx.videoCount;
|
|
601
|
+
const withCaptions = ctx.videosWithCaptions;
|
|
602
|
+
if (videos > 0 && withCaptions < videos) {
|
|
603
|
+
return createResult({ id: 'a11y-video-captions', name: 'Video Captions', category: 'accessibility', severity: 'warning' }, 'warn', `${videos - withCaptions} of ${videos} video(s) missing captions track`, {
|
|
604
|
+
value: videos - withCaptions,
|
|
605
|
+
recommendation: 'Add <track kind="captions" src="..."> to video elements',
|
|
606
|
+
evidence: {
|
|
607
|
+
found: `${withCaptions} with captions`,
|
|
608
|
+
expected: `${videos} with captions`,
|
|
609
|
+
impact: 'Deaf users cannot access video content',
|
|
610
|
+
learnMore: 'https://dequeuniversity.com/rules/axe/4.4/video-caption',
|
|
611
|
+
},
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
if (videos > 0) {
|
|
615
|
+
return createResult({ id: 'a11y-video-captions', name: 'Video Captions', category: 'accessibility', severity: 'warning' }, 'pass', `All ${videos} video(s) have captions`);
|
|
124
616
|
}
|
|
125
617
|
return null;
|
|
126
618
|
},
|
|
127
619
|
},
|
|
620
|
+
{
|
|
621
|
+
id: 'a11y-list-structure',
|
|
622
|
+
name: 'List Structure',
|
|
623
|
+
category: 'accessibility',
|
|
624
|
+
severity: 'warning',
|
|
625
|
+
description: 'Lists must contain only <li> elements and script supporting elements',
|
|
626
|
+
check: (ctx) => {
|
|
627
|
+
if (ctx.invalidListStructure === undefined)
|
|
628
|
+
return null;
|
|
629
|
+
const count = ctx.invalidListStructure;
|
|
630
|
+
if (count > 0) {
|
|
631
|
+
return createResult({ id: 'a11y-list-structure', name: 'List Structure', category: 'accessibility', severity: 'warning' }, 'warn', `${count} list(s) have invalid structure`, {
|
|
632
|
+
value: count,
|
|
633
|
+
recommendation: 'Ensure lists (<ul>, <ol>) only contain <li>, <script>, or <template> children',
|
|
634
|
+
evidence: {
|
|
635
|
+
found: count,
|
|
636
|
+
expected: 0,
|
|
637
|
+
impact: 'Screen readers may not announce lists correctly',
|
|
638
|
+
learnMore: 'https://dequeuniversity.com/rules/axe/4.4/list',
|
|
639
|
+
},
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
return createResult({ id: 'a11y-list-structure', name: 'List Structure', category: 'accessibility', severity: 'warning' }, 'pass', 'All lists have valid structure');
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
id: 'a11y-headings-content',
|
|
647
|
+
name: 'Heading Content',
|
|
648
|
+
category: 'accessibility',
|
|
649
|
+
severity: 'warning',
|
|
650
|
+
description: 'All heading elements must contain content',
|
|
651
|
+
check: (ctx) => {
|
|
652
|
+
if (ctx.emptyHeadings === undefined)
|
|
653
|
+
return null;
|
|
654
|
+
const count = ctx.emptyHeadings;
|
|
655
|
+
if (count > 0) {
|
|
656
|
+
return createResult({ id: 'a11y-headings-content', name: 'Heading Content', category: 'accessibility', severity: 'warning' }, 'warn', `${count} empty heading element(s) found`, {
|
|
657
|
+
value: count,
|
|
658
|
+
recommendation: 'Add text content to all heading elements or remove empty headings',
|
|
659
|
+
evidence: {
|
|
660
|
+
found: count,
|
|
661
|
+
expected: 0,
|
|
662
|
+
impact: 'Empty headings confuse screen reader navigation',
|
|
663
|
+
learnMore: 'https://dequeuniversity.com/rules/axe/4.4/empty-heading',
|
|
664
|
+
},
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
return createResult({ id: 'a11y-headings-content', name: 'Heading Content', category: 'accessibility', severity: 'warning' }, 'pass', 'All headings contain content');
|
|
668
|
+
},
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
id: 'a11y-images-redundant-alt',
|
|
672
|
+
name: 'Redundant Alt Text',
|
|
673
|
+
category: 'accessibility',
|
|
674
|
+
severity: 'info',
|
|
675
|
+
description: 'Image alt attributes should not be redundant',
|
|
676
|
+
check: (ctx) => {
|
|
677
|
+
if (ctx.imagesWithRedundantAlt === undefined)
|
|
678
|
+
return null;
|
|
679
|
+
const count = ctx.imagesWithRedundantAlt;
|
|
680
|
+
if (count > 0) {
|
|
681
|
+
return createResult({ id: 'a11y-images-redundant-alt', name: 'Redundant Alt Text', category: 'accessibility', severity: 'info' }, 'info', `${count} image(s) have redundant alt text (e.g., "image of", "picture of")`, {
|
|
682
|
+
value: count,
|
|
683
|
+
recommendation: 'Remove redundant phrases like "image of" or "picture of" from alt text',
|
|
684
|
+
evidence: {
|
|
685
|
+
found: count,
|
|
686
|
+
expected: 0,
|
|
687
|
+
impact: 'Screen readers already announce images; redundant text is repetitive',
|
|
688
|
+
},
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
return createResult({ id: 'a11y-images-redundant-alt', name: 'Redundant Alt Text', category: 'accessibility', severity: 'info' }, 'pass', 'No redundant alt text found');
|
|
692
|
+
},
|
|
693
|
+
},
|
|
128
694
|
];
|