testaro 39.0.3 → 40.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/testaro/role-old.js +552 -0
- package/testaro/role.js +31 -491
- package/tests/testaro.js +1 -1
package/package.json
CHANGED
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
/*
|
|
2
|
+
© 2021–2024 CVS Health and/or one of its affiliates. All rights reserved.
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/*
|
|
24
|
+
role
|
|
25
|
+
This test reports role assignments that violate either an applicable standard or an applicable
|
|
26
|
+
recommendation from WAI-ARIA. Invalid roles include those that are abstract and thus prohibited
|
|
27
|
+
from direct use, and those that are implicit in HTML elements and thus advised against. Roles
|
|
28
|
+
that explicitly confirm implicit roles are deemed redundant and can be scored as less serious
|
|
29
|
+
than roles that override implicit roles. The math role has been removed, because of poor
|
|
30
|
+
adoption and exclusion from HTML5. The img role has accessibility uses, so is not classified
|
|
31
|
+
as deprecated. See:
|
|
32
|
+
https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/Role_Img
|
|
33
|
+
https://www.w3.org/TR/html-aria/
|
|
34
|
+
https://www.w3.org/TR/wai-aria/#roles_categorization
|
|
35
|
+
*/
|
|
36
|
+
exports.reporter = async page => await page.evaluate(() => {
|
|
37
|
+
|
|
38
|
+
// CONSTANTS
|
|
39
|
+
|
|
40
|
+
// All roles implicit in HTML elements.
|
|
41
|
+
const badRoles = new Set([
|
|
42
|
+
'article',
|
|
43
|
+
'banner',
|
|
44
|
+
'button',
|
|
45
|
+
'cell',
|
|
46
|
+
'checkbox',
|
|
47
|
+
'columnheader',
|
|
48
|
+
'combobox',
|
|
49
|
+
'complementary',
|
|
50
|
+
'contentinfo',
|
|
51
|
+
'definition',
|
|
52
|
+
'figure',
|
|
53
|
+
'graphics-document',
|
|
54
|
+
'gridcell',
|
|
55
|
+
'group',
|
|
56
|
+
'heading',
|
|
57
|
+
'link',
|
|
58
|
+
'list',
|
|
59
|
+
'listbox',
|
|
60
|
+
'listitem',
|
|
61
|
+
'main',
|
|
62
|
+
'navigation',
|
|
63
|
+
'option',
|
|
64
|
+
'progressbar',
|
|
65
|
+
'radio',
|
|
66
|
+
'row',
|
|
67
|
+
'rowgroup',
|
|
68
|
+
'rowheader',
|
|
69
|
+
'searchbox',
|
|
70
|
+
'separator',
|
|
71
|
+
'slider',
|
|
72
|
+
'spinbutton',
|
|
73
|
+
'status',
|
|
74
|
+
'table',
|
|
75
|
+
'term',
|
|
76
|
+
'textbox'
|
|
77
|
+
]);
|
|
78
|
+
// All non-abstract roles.
|
|
79
|
+
const goodRoles = new Set([
|
|
80
|
+
'alert',
|
|
81
|
+
'alertdialog',
|
|
82
|
+
'application',
|
|
83
|
+
'article',
|
|
84
|
+
'banner',
|
|
85
|
+
'button',
|
|
86
|
+
'cell',
|
|
87
|
+
'checkbox',
|
|
88
|
+
'columnheader',
|
|
89
|
+
'combobox',
|
|
90
|
+
'complementary',
|
|
91
|
+
'contentinfo',
|
|
92
|
+
'definition',
|
|
93
|
+
'dialog',
|
|
94
|
+
'directory',
|
|
95
|
+
'document',
|
|
96
|
+
'feed',
|
|
97
|
+
'figure',
|
|
98
|
+
'form',
|
|
99
|
+
'grid',
|
|
100
|
+
'gridcell',
|
|
101
|
+
'group',
|
|
102
|
+
'heading',
|
|
103
|
+
'img',
|
|
104
|
+
'link',
|
|
105
|
+
'list',
|
|
106
|
+
'listbox',
|
|
107
|
+
'listitem',
|
|
108
|
+
'log',
|
|
109
|
+
'main',
|
|
110
|
+
'marquee',
|
|
111
|
+
'menu',
|
|
112
|
+
'menubar',
|
|
113
|
+
'menuitem',
|
|
114
|
+
'menuitemcheckbox',
|
|
115
|
+
'menuitemradio',
|
|
116
|
+
'navigation',
|
|
117
|
+
'none',
|
|
118
|
+
'note',
|
|
119
|
+
'option',
|
|
120
|
+
'presentation',
|
|
121
|
+
'progressbar',
|
|
122
|
+
'radio',
|
|
123
|
+
'radiogroup',
|
|
124
|
+
'region',
|
|
125
|
+
'row',
|
|
126
|
+
'rowgroup',
|
|
127
|
+
'rowheader',
|
|
128
|
+
'scrollbar',
|
|
129
|
+
'search',
|
|
130
|
+
'searchbox',
|
|
131
|
+
'separator',
|
|
132
|
+
'separator',
|
|
133
|
+
'slider',
|
|
134
|
+
'spinbutton',
|
|
135
|
+
'status',
|
|
136
|
+
'switch',
|
|
137
|
+
'tab',
|
|
138
|
+
'table',
|
|
139
|
+
'tablist',
|
|
140
|
+
'tabpanel',
|
|
141
|
+
'term',
|
|
142
|
+
'textbox',
|
|
143
|
+
'timer',
|
|
144
|
+
'toolbar',
|
|
145
|
+
'tooltip',
|
|
146
|
+
'tree',
|
|
147
|
+
'treegrid',
|
|
148
|
+
'treeitem',
|
|
149
|
+
]);
|
|
150
|
+
// Implicit roles
|
|
151
|
+
const implicitRoles = {
|
|
152
|
+
article: 'article',
|
|
153
|
+
aside: 'complementary',
|
|
154
|
+
button: 'button',
|
|
155
|
+
datalist: 'listbox',
|
|
156
|
+
dd: 'definition',
|
|
157
|
+
details: 'group',
|
|
158
|
+
dfn: 'term',
|
|
159
|
+
dialog: 'dialog',
|
|
160
|
+
dt: 'term',
|
|
161
|
+
fieldset: 'group',
|
|
162
|
+
figure: 'figure',
|
|
163
|
+
hr: 'separator',
|
|
164
|
+
html: 'document',
|
|
165
|
+
li: 'listitem',
|
|
166
|
+
main: 'main',
|
|
167
|
+
math: 'math',
|
|
168
|
+
menu: 'list',
|
|
169
|
+
nav: 'navigation',
|
|
170
|
+
ol: 'list',
|
|
171
|
+
output: 'status',
|
|
172
|
+
progress: 'progressbar',
|
|
173
|
+
summary: 'button',
|
|
174
|
+
SVG: 'graphics-document',
|
|
175
|
+
table: 'table',
|
|
176
|
+
tbody: 'rowgroup',
|
|
177
|
+
textarea: 'textbox',
|
|
178
|
+
tfoot: 'rowgroup',
|
|
179
|
+
thead: 'rowgroup',
|
|
180
|
+
tr: 'row',
|
|
181
|
+
ul: 'list'
|
|
182
|
+
};
|
|
183
|
+
const implicitAttributes = {
|
|
184
|
+
a: [
|
|
185
|
+
{
|
|
186
|
+
role: 'link',
|
|
187
|
+
attributes: {
|
|
188
|
+
href: /./
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
],
|
|
192
|
+
area: [
|
|
193
|
+
{
|
|
194
|
+
role: 'link',
|
|
195
|
+
attributes: {
|
|
196
|
+
href: /./
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
],
|
|
200
|
+
h1: [
|
|
201
|
+
{
|
|
202
|
+
role: 'heading',
|
|
203
|
+
attributes: {
|
|
204
|
+
'aria-level': /^1$/
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
],
|
|
208
|
+
h2: [
|
|
209
|
+
{
|
|
210
|
+
role: 'heading',
|
|
211
|
+
attributes: {
|
|
212
|
+
'aria-level': /^2$/
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
role: 'heading',
|
|
217
|
+
attributes: {
|
|
218
|
+
'aria-level': false
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
],
|
|
222
|
+
h3: [
|
|
223
|
+
{
|
|
224
|
+
role: 'heading',
|
|
225
|
+
attributes: {
|
|
226
|
+
'aria-level': /^3$/
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
],
|
|
230
|
+
h4: [
|
|
231
|
+
{
|
|
232
|
+
role: 'heading',
|
|
233
|
+
attributes: {
|
|
234
|
+
'aria-level': /^4$/
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
],
|
|
238
|
+
h5: [
|
|
239
|
+
{
|
|
240
|
+
role: 'heading',
|
|
241
|
+
attributes: {
|
|
242
|
+
'aria-level': /^5$/
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
],
|
|
246
|
+
h6: [
|
|
247
|
+
{
|
|
248
|
+
role: 'heading',
|
|
249
|
+
attributes: {
|
|
250
|
+
'aria-level': /^6$/
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
],
|
|
254
|
+
input: [
|
|
255
|
+
{
|
|
256
|
+
role: 'checkbox',
|
|
257
|
+
attributes: {
|
|
258
|
+
type: /^checkbox$/
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
role: 'button',
|
|
263
|
+
attributes: {
|
|
264
|
+
type: /^(?:button|image|reset|submit)$/
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
role: 'combobox',
|
|
269
|
+
attributes: {
|
|
270
|
+
type: /^(?:email|search|tel|text|url)$/,
|
|
271
|
+
list: true
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
role: 'combobox',
|
|
276
|
+
attributes: {
|
|
277
|
+
type: false,
|
|
278
|
+
list: true
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
role: 'radio',
|
|
283
|
+
attributes: {
|
|
284
|
+
type: /^radio$/
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
role: 'searchbox',
|
|
289
|
+
attributes: {
|
|
290
|
+
type: /^search$/,
|
|
291
|
+
list: false
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
role: 'slider',
|
|
296
|
+
attributes: {
|
|
297
|
+
type: /^range$/
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
role: 'spinbutton',
|
|
302
|
+
attributes: {
|
|
303
|
+
type: /^number$/
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
role: 'textbox',
|
|
308
|
+
attributes: {
|
|
309
|
+
type: /^(?:email|tel|text|url)$/,
|
|
310
|
+
list: false
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
role: 'textbox',
|
|
315
|
+
attributes: {
|
|
316
|
+
type: false,
|
|
317
|
+
list: false
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
role: 'checkbox',
|
|
322
|
+
attributes: {
|
|
323
|
+
type: /^checkbox$/
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
role: 'checkbox',
|
|
328
|
+
attributes: {
|
|
329
|
+
type: /^checkbox$/
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
img: [
|
|
334
|
+
{
|
|
335
|
+
role: 'presentation',
|
|
336
|
+
attributes: {
|
|
337
|
+
alt: /^$/
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
role: 'img',
|
|
342
|
+
attributes: {
|
|
343
|
+
alt: /./
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
role: 'img',
|
|
348
|
+
attributes: {
|
|
349
|
+
alt: false
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
],
|
|
353
|
+
select: [
|
|
354
|
+
{
|
|
355
|
+
role: 'listbox',
|
|
356
|
+
attributes: {
|
|
357
|
+
multiple: true
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
role: 'listbox',
|
|
362
|
+
attributes: {
|
|
363
|
+
size: /^(?:[2-9]|[1-9]\d+)$/
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
role: 'combobox',
|
|
368
|
+
attributes: {
|
|
369
|
+
multiple: false,
|
|
370
|
+
size: false
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
role: 'combobox',
|
|
375
|
+
attributes: {
|
|
376
|
+
multiple: false,
|
|
377
|
+
size: /^1$/
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
]
|
|
381
|
+
};
|
|
382
|
+
// Array of th and td elements with redundant roles.
|
|
383
|
+
const redundantCells = [];
|
|
384
|
+
const {body} = document;
|
|
385
|
+
// Elements with role attributes.
|
|
386
|
+
const roleElements = Array.from(body.querySelectorAll('[role]'));
|
|
387
|
+
// th and td elements with redundant roles.
|
|
388
|
+
const gridHeaders = Array.from(
|
|
389
|
+
body.querySelectorAll('table[role=grid] th, table[role=treegrid] th')
|
|
390
|
+
);
|
|
391
|
+
const gridCells = Array.from(
|
|
392
|
+
body.querySelectorAll('table[role=grid] td, table[role=treegrid] td')
|
|
393
|
+
);
|
|
394
|
+
const tableHeaders = Array.from(
|
|
395
|
+
body.querySelectorAll('table[role=table] th, table:not([role]) th')
|
|
396
|
+
);
|
|
397
|
+
const tableCells = Array.from(
|
|
398
|
+
body.querySelectorAll('table[role=table] td, table:not([role]) td')
|
|
399
|
+
);
|
|
400
|
+
// Initialized result summrary.
|
|
401
|
+
const data = {
|
|
402
|
+
roleElements: roleElements.length,
|
|
403
|
+
badRoleElements: 0,
|
|
404
|
+
redundantRoleElements: 0,
|
|
405
|
+
tagNames: {}
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// FUNCTIONS
|
|
409
|
+
|
|
410
|
+
// Initializes the result summary.
|
|
411
|
+
const dataInit = (data, tagName, role) => {
|
|
412
|
+
if (! data.tagNames[tagName]) {
|
|
413
|
+
data.tagNames[tagName] = {};
|
|
414
|
+
}
|
|
415
|
+
if (! data.tagNames[tagName][role]) {
|
|
416
|
+
data.tagNames[tagName][role] = {
|
|
417
|
+
bad: 0,
|
|
418
|
+
redundant: 0
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
// Adds table-element data to the result summary.
|
|
423
|
+
const tallyTableRedundancy = (elements, redundantRoles, tagName) => {
|
|
424
|
+
elements.forEach(element => {
|
|
425
|
+
const role = element.getAttribute('role');
|
|
426
|
+
if (redundantRoles.includes(role)) {
|
|
427
|
+
dataInit(data, tagName, role);
|
|
428
|
+
data.redundantRoleElements++;
|
|
429
|
+
data.tagNames[tagName][role].redundant++;
|
|
430
|
+
redundantCells.push(element);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
// OPERATION
|
|
436
|
+
|
|
437
|
+
// Get the good (non-abstract, non-element-implicit) roles.
|
|
438
|
+
goodRoles.forEach(role => {
|
|
439
|
+
if (badRoles.has(role)) {
|
|
440
|
+
goodRoles.delete(role);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
// Identify the table elements with redundant roles.
|
|
444
|
+
tallyTableRedundancy(gridHeaders, ['columnheader', 'rowheader', 'gridcell'], 'TH');
|
|
445
|
+
tallyTableRedundancy(gridCells, ['gridcell'], 'TD');
|
|
446
|
+
tallyTableRedundancy(tableHeaders, ['columnheader', 'rowheader', 'cell'], 'TH');
|
|
447
|
+
tallyTableRedundancy(tableCells, ['cell'], 'TD');
|
|
448
|
+
// Identify the additional elements with redundant roles and bad roles.
|
|
449
|
+
roleElements.filter(element => ! redundantCells.includes(element)).forEach(element => {
|
|
450
|
+
const role = element.getAttribute('role');
|
|
451
|
+
const tagName = element.tagName;
|
|
452
|
+
// If the role is not absolutely valid:
|
|
453
|
+
if (! goodRoles.has(role)) {
|
|
454
|
+
// If it is bad or redundant:
|
|
455
|
+
if (badRoles.has(role)) {
|
|
456
|
+
dataInit(data, tagName, role);
|
|
457
|
+
const lcTagName = tagName.toLowerCase();
|
|
458
|
+
// If it is simply redundant:
|
|
459
|
+
if (role === implicitRoles[lcTagName]) {
|
|
460
|
+
// Update the result summary.
|
|
461
|
+
data.redundantRoleElements++;
|
|
462
|
+
data.tagNames[tagName][role].redundant++;
|
|
463
|
+
}
|
|
464
|
+
// Otherwise, if it is attributionally redundant:
|
|
465
|
+
else if (
|
|
466
|
+
implicitAttributes[lcTagName] && implicitAttributes[lcTagName].some(
|
|
467
|
+
criterion => role === criterion.role && Object.keys(criterion.attributes).every(
|
|
468
|
+
attributeName => {
|
|
469
|
+
const rule = criterion.attributes[attributeName];
|
|
470
|
+
const exists = element.hasAttribute(attributeName);
|
|
471
|
+
const value = exists ? element.getAttribute(attributeName) : null;
|
|
472
|
+
if (rule === true) {
|
|
473
|
+
return exists;
|
|
474
|
+
}
|
|
475
|
+
else if (rule === false) {
|
|
476
|
+
return ! exists;
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
return rule.test(value);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
)
|
|
483
|
+
)
|
|
484
|
+
) {
|
|
485
|
+
// Update the results.
|
|
486
|
+
data.redundantRoleElements++;
|
|
487
|
+
data.tagNames[tagName][role].redundant++;
|
|
488
|
+
}
|
|
489
|
+
// Otherwise, i.e. if it is absolutely invalid:
|
|
490
|
+
else {
|
|
491
|
+
// Update the results.
|
|
492
|
+
data.badRoleElements++;
|
|
493
|
+
data.tagNames[tagName][role].bad++;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
// Otherwise, i.e. if it is absolutely invalid:
|
|
497
|
+
else {
|
|
498
|
+
// Update the results.
|
|
499
|
+
data.badRoleElements++;
|
|
500
|
+
dataInit(data, tagName, role);
|
|
501
|
+
data.tagNames[tagName][role].bad++;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
const standardInstances = [];
|
|
506
|
+
Object.keys(data.tagNames).forEach(tagName => {
|
|
507
|
+
Object.keys(data.tagNames[tagName]).forEach(role => {
|
|
508
|
+
const pairTotals = data.tagNames[tagName][role];
|
|
509
|
+
const redCount = pairTotals.redundant;
|
|
510
|
+
if (redCount) {
|
|
511
|
+
standardInstances.push({
|
|
512
|
+
ruleID: 'role',
|
|
513
|
+
what: `Elements have redundant explicit role ${role}`,
|
|
514
|
+
count: redCount,
|
|
515
|
+
ordinalSeverity: 1,
|
|
516
|
+
tagName,
|
|
517
|
+
id: '',
|
|
518
|
+
location: {
|
|
519
|
+
doc: '',
|
|
520
|
+
type: '',
|
|
521
|
+
spec: ''
|
|
522
|
+
},
|
|
523
|
+
excerpt: ''
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
const badCount = pairTotals.bad;
|
|
527
|
+
if (badCount) {
|
|
528
|
+
standardInstances.push({
|
|
529
|
+
ruleID: 'role',
|
|
530
|
+
what:
|
|
531
|
+
`Elements have invalid or native-replaceable explicit role ${role}`,
|
|
532
|
+
count: badCount,
|
|
533
|
+
ordinalSeverity: 3,
|
|
534
|
+
tagName,
|
|
535
|
+
id: '',
|
|
536
|
+
location: {
|
|
537
|
+
doc: '',
|
|
538
|
+
type: '',
|
|
539
|
+
spec: ''
|
|
540
|
+
},
|
|
541
|
+
excerpt: ''
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
// Return the result.
|
|
547
|
+
return {
|
|
548
|
+
data,
|
|
549
|
+
totals: [0, data.redundantRoleElements, 0, data.badRoleElements],
|
|
550
|
+
standardInstances
|
|
551
|
+
};
|
|
552
|
+
});
|
package/testaro/role.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
© 2021–
|
|
2
|
+
© 2021–2024 CVS Health and/or one of its affiliates. All rights reserved.
|
|
3
3
|
|
|
4
4
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
5
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -22,133 +22,18 @@
|
|
|
22
22
|
|
|
23
23
|
/*
|
|
24
24
|
role
|
|
25
|
-
This test reports
|
|
26
|
-
recommendation from WAI-ARIA. Invalid roles include those that are abstract and thus prohibited
|
|
27
|
-
from direct use, and those that are implicit in HTML elements and thus advised against. Roles
|
|
28
|
-
that explicitly confirm implicit roles are deemed redundant and can be scored as less serious
|
|
29
|
-
than roles that override implicit roles. The math role has been removed, because of poor
|
|
30
|
-
adoption and exclusion from HTML5. The img role has accessibility uses, so is not classified
|
|
31
|
-
as deprecated. See:
|
|
32
|
-
https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/Role_Img
|
|
33
|
-
https://www.w3.org/TR/html-aria/
|
|
34
|
-
https://www.w3.org/TR/wai-aria/#roles_categorization
|
|
25
|
+
This test reports elements with native-replacing explicit role attributes.
|
|
35
26
|
*/
|
|
36
|
-
exports.reporter = async page => await page.evaluate(() => {
|
|
37
27
|
|
|
38
|
-
|
|
28
|
+
// IMPORTS
|
|
29
|
+
|
|
30
|
+
// Module to perform common operations.
|
|
31
|
+
const {init, report} = require('../procs/testaro');
|
|
32
|
+
|
|
33
|
+
// CONSTANTS
|
|
39
34
|
|
|
40
|
-
// All roles implicit in HTML elements.
|
|
41
|
-
const badRoles = new Set([
|
|
42
|
-
'article',
|
|
43
|
-
'banner',
|
|
44
|
-
'button',
|
|
45
|
-
'cell',
|
|
46
|
-
'checkbox',
|
|
47
|
-
'columnheader',
|
|
48
|
-
'combobox',
|
|
49
|
-
'complementary',
|
|
50
|
-
'contentinfo',
|
|
51
|
-
'definition',
|
|
52
|
-
'figure',
|
|
53
|
-
'graphics-document',
|
|
54
|
-
'gridcell',
|
|
55
|
-
'group',
|
|
56
|
-
'heading',
|
|
57
|
-
'link',
|
|
58
|
-
'list',
|
|
59
|
-
'listbox',
|
|
60
|
-
'listitem',
|
|
61
|
-
'main',
|
|
62
|
-
'navigation',
|
|
63
|
-
'option',
|
|
64
|
-
'progressbar',
|
|
65
|
-
'radio',
|
|
66
|
-
'row',
|
|
67
|
-
'rowgroup',
|
|
68
|
-
'rowheader',
|
|
69
|
-
'searchbox',
|
|
70
|
-
'separator',
|
|
71
|
-
'slider',
|
|
72
|
-
'spinbutton',
|
|
73
|
-
'status',
|
|
74
|
-
'table',
|
|
75
|
-
'term',
|
|
76
|
-
'textbox'
|
|
77
|
-
]);
|
|
78
|
-
// All non-abstract roles.
|
|
79
|
-
const goodRoles = new Set([
|
|
80
|
-
'alert',
|
|
81
|
-
'alertdialog',
|
|
82
|
-
'application',
|
|
83
|
-
'article',
|
|
84
|
-
'banner',
|
|
85
|
-
'button',
|
|
86
|
-
'cell',
|
|
87
|
-
'checkbox',
|
|
88
|
-
'columnheader',
|
|
89
|
-
'combobox',
|
|
90
|
-
'complementary',
|
|
91
|
-
'contentinfo',
|
|
92
|
-
'definition',
|
|
93
|
-
'dialog',
|
|
94
|
-
'directory',
|
|
95
|
-
'document',
|
|
96
|
-
'feed',
|
|
97
|
-
'figure',
|
|
98
|
-
'form',
|
|
99
|
-
'grid',
|
|
100
|
-
'gridcell',
|
|
101
|
-
'group',
|
|
102
|
-
'heading',
|
|
103
|
-
'img',
|
|
104
|
-
'link',
|
|
105
|
-
'list',
|
|
106
|
-
'listbox',
|
|
107
|
-
'listitem',
|
|
108
|
-
'log',
|
|
109
|
-
'main',
|
|
110
|
-
'marquee',
|
|
111
|
-
'menu',
|
|
112
|
-
'menubar',
|
|
113
|
-
'menuitem',
|
|
114
|
-
'menuitemcheckbox',
|
|
115
|
-
'menuitemradio',
|
|
116
|
-
'navigation',
|
|
117
|
-
'none',
|
|
118
|
-
'note',
|
|
119
|
-
'option',
|
|
120
|
-
'presentation',
|
|
121
|
-
'progressbar',
|
|
122
|
-
'radio',
|
|
123
|
-
'radiogroup',
|
|
124
|
-
'region',
|
|
125
|
-
'row',
|
|
126
|
-
'rowgroup',
|
|
127
|
-
'rowheader',
|
|
128
|
-
'scrollbar',
|
|
129
|
-
'search',
|
|
130
|
-
'searchbox',
|
|
131
|
-
'separator',
|
|
132
|
-
'separator',
|
|
133
|
-
'slider',
|
|
134
|
-
'spinbutton',
|
|
135
|
-
'status',
|
|
136
|
-
'switch',
|
|
137
|
-
'tab',
|
|
138
|
-
'table',
|
|
139
|
-
'tablist',
|
|
140
|
-
'tabpanel',
|
|
141
|
-
'term',
|
|
142
|
-
'textbox',
|
|
143
|
-
'timer',
|
|
144
|
-
'toolbar',
|
|
145
|
-
'tooltip',
|
|
146
|
-
'tree',
|
|
147
|
-
'treegrid',
|
|
148
|
-
'treeitem',
|
|
149
|
-
]);
|
|
150
35
|
// Implicit roles
|
|
151
|
-
const
|
|
36
|
+
const roleImplications = {
|
|
152
37
|
article: 'article',
|
|
153
38
|
aside: 'complementary',
|
|
154
39
|
button: 'button',
|
|
@@ -180,373 +65,28 @@ exports.reporter = async page => await page.evaluate(() => {
|
|
|
180
65
|
tr: 'row',
|
|
181
66
|
ul: 'list'
|
|
182
67
|
};
|
|
183
|
-
const
|
|
184
|
-
a: [
|
|
185
|
-
{
|
|
186
|
-
role: 'link',
|
|
187
|
-
attributes: {
|
|
188
|
-
href: /./
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
],
|
|
192
|
-
area: [
|
|
193
|
-
{
|
|
194
|
-
role: 'link',
|
|
195
|
-
attributes: {
|
|
196
|
-
href: /./
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
],
|
|
200
|
-
h1: [
|
|
201
|
-
{
|
|
202
|
-
role: 'heading',
|
|
203
|
-
attributes: {
|
|
204
|
-
'aria-level': /^1$/
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
],
|
|
208
|
-
h2: [
|
|
209
|
-
{
|
|
210
|
-
role: 'heading',
|
|
211
|
-
attributes: {
|
|
212
|
-
'aria-level': /^2$/
|
|
213
|
-
}
|
|
214
|
-
},
|
|
215
|
-
{
|
|
216
|
-
role: 'heading',
|
|
217
|
-
attributes: {
|
|
218
|
-
'aria-level': false
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
],
|
|
222
|
-
h3: [
|
|
223
|
-
{
|
|
224
|
-
role: 'heading',
|
|
225
|
-
attributes: {
|
|
226
|
-
'aria-level': /^3$/
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
],
|
|
230
|
-
h4: [
|
|
231
|
-
{
|
|
232
|
-
role: 'heading',
|
|
233
|
-
attributes: {
|
|
234
|
-
'aria-level': /^4$/
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
],
|
|
238
|
-
h5: [
|
|
239
|
-
{
|
|
240
|
-
role: 'heading',
|
|
241
|
-
attributes: {
|
|
242
|
-
'aria-level': /^5$/
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
],
|
|
246
|
-
h6: [
|
|
247
|
-
{
|
|
248
|
-
role: 'heading',
|
|
249
|
-
attributes: {
|
|
250
|
-
'aria-level': /^6$/
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
],
|
|
254
|
-
input: [
|
|
255
|
-
{
|
|
256
|
-
role: 'checkbox',
|
|
257
|
-
attributes: {
|
|
258
|
-
type: /^checkbox$/
|
|
259
|
-
}
|
|
260
|
-
},
|
|
261
|
-
{
|
|
262
|
-
role: 'button',
|
|
263
|
-
attributes: {
|
|
264
|
-
type: /^(?:button|image|reset|submit)$/
|
|
265
|
-
}
|
|
266
|
-
},
|
|
267
|
-
{
|
|
268
|
-
role: 'combobox',
|
|
269
|
-
attributes: {
|
|
270
|
-
type: /^(?:email|search|tel|text|url)$/,
|
|
271
|
-
list: true
|
|
272
|
-
}
|
|
273
|
-
},
|
|
274
|
-
{
|
|
275
|
-
role: 'combobox',
|
|
276
|
-
attributes: {
|
|
277
|
-
type: false,
|
|
278
|
-
list: true
|
|
279
|
-
}
|
|
280
|
-
},
|
|
281
|
-
{
|
|
282
|
-
role: 'radio',
|
|
283
|
-
attributes: {
|
|
284
|
-
type: /^radio$/
|
|
285
|
-
}
|
|
286
|
-
},
|
|
287
|
-
{
|
|
288
|
-
role: 'searchbox',
|
|
289
|
-
attributes: {
|
|
290
|
-
type: /^search$/,
|
|
291
|
-
list: false
|
|
292
|
-
}
|
|
293
|
-
},
|
|
294
|
-
{
|
|
295
|
-
role: 'slider',
|
|
296
|
-
attributes: {
|
|
297
|
-
type: /^range$/
|
|
298
|
-
}
|
|
299
|
-
},
|
|
300
|
-
{
|
|
301
|
-
role: 'spinbutton',
|
|
302
|
-
attributes: {
|
|
303
|
-
type: /^number$/
|
|
304
|
-
}
|
|
305
|
-
},
|
|
306
|
-
{
|
|
307
|
-
role: 'textbox',
|
|
308
|
-
attributes: {
|
|
309
|
-
type: /^(?:email|tel|text|url)$/,
|
|
310
|
-
list: false
|
|
311
|
-
}
|
|
312
|
-
},
|
|
313
|
-
{
|
|
314
|
-
role: 'textbox',
|
|
315
|
-
attributes: {
|
|
316
|
-
type: false,
|
|
317
|
-
list: false
|
|
318
|
-
}
|
|
319
|
-
},
|
|
320
|
-
{
|
|
321
|
-
role: 'checkbox',
|
|
322
|
-
attributes: {
|
|
323
|
-
type: /^checkbox$/
|
|
324
|
-
}
|
|
325
|
-
},
|
|
326
|
-
{
|
|
327
|
-
role: 'checkbox',
|
|
328
|
-
attributes: {
|
|
329
|
-
type: /^checkbox$/
|
|
330
|
-
}
|
|
331
|
-
},
|
|
332
|
-
],
|
|
333
|
-
img: [
|
|
334
|
-
{
|
|
335
|
-
role: 'presentation',
|
|
336
|
-
attributes: {
|
|
337
|
-
alt: /^$/
|
|
338
|
-
}
|
|
339
|
-
},
|
|
340
|
-
{
|
|
341
|
-
role: 'img',
|
|
342
|
-
attributes: {
|
|
343
|
-
alt: /./
|
|
344
|
-
}
|
|
345
|
-
},
|
|
346
|
-
{
|
|
347
|
-
role: 'img',
|
|
348
|
-
attributes: {
|
|
349
|
-
alt: false
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
],
|
|
353
|
-
select: [
|
|
354
|
-
{
|
|
355
|
-
role: 'listbox',
|
|
356
|
-
attributes: {
|
|
357
|
-
multiple: true
|
|
358
|
-
}
|
|
359
|
-
},
|
|
360
|
-
{
|
|
361
|
-
role: 'listbox',
|
|
362
|
-
attributes: {
|
|
363
|
-
size: /^(?:[2-9]|[1-9]\d+)$/
|
|
364
|
-
}
|
|
365
|
-
},
|
|
366
|
-
{
|
|
367
|
-
role: 'combobox',
|
|
368
|
-
attributes: {
|
|
369
|
-
multiple: false,
|
|
370
|
-
size: false
|
|
371
|
-
}
|
|
372
|
-
},
|
|
373
|
-
{
|
|
374
|
-
role: 'combobox',
|
|
375
|
-
attributes: {
|
|
376
|
-
multiple: false,
|
|
377
|
-
size: /^1$/
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
]
|
|
381
|
-
};
|
|
382
|
-
// Array of th and td elements with redundant roles.
|
|
383
|
-
const redundantCells = [];
|
|
384
|
-
const {body} = document;
|
|
385
|
-
// Elements with role attributes.
|
|
386
|
-
const roleElements = Array.from(body.querySelectorAll('[role]'));
|
|
387
|
-
// th and td elements with redundant roles.
|
|
388
|
-
const gridHeaders = Array.from(
|
|
389
|
-
body.querySelectorAll('table[role=grid] th, table[role=treegrid] th')
|
|
390
|
-
);
|
|
391
|
-
const gridCells = Array.from(
|
|
392
|
-
body.querySelectorAll('table[role=grid] td, table[role=treegrid] td')
|
|
393
|
-
);
|
|
394
|
-
const tableHeaders = Array.from(
|
|
395
|
-
body.querySelectorAll('table[role=table] th, table:not([role]) th')
|
|
396
|
-
);
|
|
397
|
-
const tableCells = Array.from(
|
|
398
|
-
body.querySelectorAll('table[role=table] td, table:not([role]) td')
|
|
399
|
-
);
|
|
400
|
-
// Initialized result.
|
|
401
|
-
const data = {
|
|
402
|
-
roleElements: roleElements.length,
|
|
403
|
-
badRoleElements: 0,
|
|
404
|
-
redundantRoleElements: 0,
|
|
405
|
-
tagNames: {}
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
// FUNCTIONS
|
|
409
|
-
|
|
410
|
-
// Initializes the results.
|
|
411
|
-
const dataInit = (data, tagName, role) => {
|
|
412
|
-
if (! data.tagNames[tagName]) {
|
|
413
|
-
data.tagNames[tagName] = {};
|
|
414
|
-
}
|
|
415
|
-
if (! data.tagNames[tagName][role]) {
|
|
416
|
-
data.tagNames[tagName][role] = {
|
|
417
|
-
bad: 0,
|
|
418
|
-
redundant: 0
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
};
|
|
422
|
-
//
|
|
423
|
-
const tallyTableRedundancy = (elements, okRoles, tagName) => {
|
|
424
|
-
elements.forEach(element => {
|
|
425
|
-
const role = element.getAttribute('role');
|
|
426
|
-
if (okRoles.includes(role)) {
|
|
427
|
-
dataInit(data, tagName, role);
|
|
428
|
-
data.redundantRoleElements++;
|
|
429
|
-
data.tagNames[tagName][role].redundant++;
|
|
430
|
-
redundantCells.push(element);
|
|
431
|
-
}
|
|
432
|
-
});
|
|
433
|
-
};
|
|
68
|
+
const implicitRoles = new Set(Object.values(roleImplications));
|
|
434
69
|
|
|
435
|
-
|
|
70
|
+
// FUNCTIONS
|
|
436
71
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
roleElements.filter(element => ! redundantCells.includes(element)).forEach(element => {
|
|
450
|
-
const role = element.getAttribute('role');
|
|
451
|
-
const tagName = element.tagName;
|
|
452
|
-
// If the role is not absolutely valid:
|
|
453
|
-
if (! goodRoles.has(role)) {
|
|
454
|
-
// If it is bad or redundant:
|
|
455
|
-
if (badRoles.has(role)) {
|
|
456
|
-
dataInit(data, tagName, role);
|
|
457
|
-
const lcTagName = tagName.toLowerCase();
|
|
458
|
-
// If it is simply redundant:
|
|
459
|
-
if (role === implicitRoles[lcTagName]) {
|
|
460
|
-
// Update the results.
|
|
461
|
-
data.redundantRoleElements++;
|
|
462
|
-
data.tagNames[tagName][role].redundant++;
|
|
463
|
-
}
|
|
464
|
-
// Otherwise, if it is attributionally redundant:
|
|
465
|
-
else if (
|
|
466
|
-
implicitAttributes[lcTagName] && implicitAttributes[lcTagName].some(
|
|
467
|
-
criterion => role === criterion.role && Object.keys(criterion.attributes).every(
|
|
468
|
-
attributeName => {
|
|
469
|
-
const rule = criterion.attributes[attributeName];
|
|
470
|
-
const exists = element.hasAttribute(attributeName);
|
|
471
|
-
const value = exists ? element.getAttribute(attributeName) : null;
|
|
472
|
-
if (rule === true) {
|
|
473
|
-
return exists;
|
|
474
|
-
}
|
|
475
|
-
else if (rule === false) {
|
|
476
|
-
return ! exists;
|
|
477
|
-
}
|
|
478
|
-
else {
|
|
479
|
-
return rule.test(value);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
)
|
|
483
|
-
)
|
|
484
|
-
) {
|
|
485
|
-
// Update the results.
|
|
486
|
-
data.redundantRoleElements++;
|
|
487
|
-
data.tagNames[tagName][role].redundant++;
|
|
488
|
-
}
|
|
489
|
-
// Otherwise, i.e. if it is absolutely invalid:
|
|
490
|
-
else {
|
|
491
|
-
// Update the results.
|
|
492
|
-
data.badRoleElements++;
|
|
493
|
-
data.tagNames[tagName][role].bad++;
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
// Otherwise, i.e. if it is absolutely invalid:
|
|
497
|
-
else {
|
|
498
|
-
// Update the results.
|
|
499
|
-
data.badRoleElements++;
|
|
500
|
-
dataInit(data, tagName, role);
|
|
501
|
-
data.tagNames[tagName][role].bad++;
|
|
502
|
-
}
|
|
72
|
+
// Runs the test and returns the result.
|
|
73
|
+
exports.reporter = async (page, withItems) => {
|
|
74
|
+
// Get locators for all elements with explicit roles.
|
|
75
|
+
const all = await init(100, page, '[role]');
|
|
76
|
+
// For each locator:
|
|
77
|
+
for (const loc of all.allLocs) {
|
|
78
|
+
// Get the explicit role of the element.
|
|
79
|
+
const role = await loc.getAttribute('role');
|
|
80
|
+
// If it is implicit:
|
|
81
|
+
if (implicitRoles.has(role)) {
|
|
82
|
+
// Add the locator to the array of violators.
|
|
83
|
+
all.locs.push([loc, role]);
|
|
503
84
|
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
ruleID: 'role',
|
|
513
|
-
what: `Elements have redundant explicit role ${role}`,
|
|
514
|
-
count: redCount,
|
|
515
|
-
ordinalSeverity: 1,
|
|
516
|
-
tagName,
|
|
517
|
-
id: '',
|
|
518
|
-
location: {
|
|
519
|
-
doc: '',
|
|
520
|
-
type: '',
|
|
521
|
-
spec: ''
|
|
522
|
-
},
|
|
523
|
-
excerpt: ''
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
const badCount = pairTotals.bad;
|
|
527
|
-
if (badCount) {
|
|
528
|
-
standardInstances.push({
|
|
529
|
-
ruleID: 'role',
|
|
530
|
-
what:
|
|
531
|
-
`Elements have invalid or native-replaceable explicit role ${role}`,
|
|
532
|
-
count: badCount,
|
|
533
|
-
ordinalSeverity: 3,
|
|
534
|
-
tagName,
|
|
535
|
-
id: '',
|
|
536
|
-
location: {
|
|
537
|
-
doc: '',
|
|
538
|
-
type: '',
|
|
539
|
-
spec: ''
|
|
540
|
-
},
|
|
541
|
-
excerpt: ''
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
});
|
|
545
|
-
});
|
|
546
|
-
// Return the result.
|
|
547
|
-
return {
|
|
548
|
-
data,
|
|
549
|
-
totals: [0, data.redundantRoleElements, 0, data.badRoleElements],
|
|
550
|
-
standardInstances
|
|
551
|
-
};
|
|
552
|
-
});
|
|
85
|
+
}
|
|
86
|
+
// Populate and return the result.
|
|
87
|
+
const whats = [
|
|
88
|
+
'Element has an explicit __param__ role, but it is also an implicit HTML element role',
|
|
89
|
+
'Elements have roles assigned that are also implicit roles of HTML elements'
|
|
90
|
+
];
|
|
91
|
+
return await report(withItems, all, 'role', whats, 0);
|
|
92
|
+
};
|
package/tests/testaro.js
CHANGED
|
@@ -100,7 +100,7 @@ const evalRules = {
|
|
|
100
100
|
opFoc: 'operable elements that are not Tab-focusable',
|
|
101
101
|
pseudoP: 'adjacent br elements suspected of nonsemantically simulating p elements',
|
|
102
102
|
radioSet: 'radio buttons not grouped into standard field sets',
|
|
103
|
-
role: '
|
|
103
|
+
role: 'native-replacing explicit roles',
|
|
104
104
|
styleDiff: 'style inconsistencies',
|
|
105
105
|
tabNav: 'nonstandard keyboard navigation between elements with the tab role',
|
|
106
106
|
targetSize: 'buttons, inputs, and non-inline links smaller than 44 pixels wide and high',
|