testaro 39.0.3 → 40.0.2
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.js +31 -491
- package/tests/testaro.js +1 -1
package/package.json
CHANGED
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',
|