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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "39.0.3",
3
+ "version": "40.0.0",
4
4
  "description": "Run 1000 web accessibility tests from 10 tools and get a standardized report",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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–2023 CVS Health and/or one of its affiliates. All rights reserved.
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 role assignment 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
25
+ This test reports elements with native-replacing explicit role attributes.
35
26
  */
36
- exports.reporter = async page => await page.evaluate(() => {
37
27
 
38
- // CONSTANTS
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 implicitRoles = {
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 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.
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
- // OPERATION
70
+ // FUNCTIONS
436
71
 
437
- // Remove the element-implicit roles from the non-abstract 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 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
- 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
- });
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: 'invalid and native-replacing explicit roles',
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',