testilo 3.6.2 → 3.7.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/compare.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  compare.js
3
3
  Testilo comparison script.
4
- Usage example: node compare 35k1r cp0
4
+ Usage example: node compare cp12a candidates
5
5
  */
6
6
 
7
7
  // ########## IMPORTS
@@ -14,8 +14,8 @@ const fs = require('fs/promises');
14
14
  // ########## CONSTANTS
15
15
 
16
16
  const comparisonDir = process.env.COMPARISONDIR || 'reports/comparative';
17
- const comparisonNameBase = process.argv[2];
18
- const compareProcID = process.argv[3];
17
+ const compareProcID = process.argv[2];
18
+ const comparisonNameBase = process.argv[3];
19
19
 
20
20
  // ########## FUNCTIONS
21
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testilo",
3
- "version": "3.6.2",
3
+ "version": "3.7.0",
4
4
  "description": "Client that scores and digests Testaro reports",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,47 @@
1
+ <!DOCTYPE HTML>
2
+ <html lang="en-US">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="author" content="Testilo">
7
+ <meta name="creator" content="Testilo">
8
+ <meta name="publisher" name="Testilo">
9
+ <meta name="description" content="comparison of accessibility scores">
10
+ <meta name="keywords" content="accessibility a11y web testing">
11
+ <title>Accessibility score comparison</title>
12
+ <link rel="icon" href="favicon.png">
13
+ <link rel="stylesheet" href="style.css">
14
+ </head>
15
+ <body>
16
+ <main>
17
+ <header>
18
+ <h1>Accessibility score comparison</h1>
19
+ </header>
20
+ <h2>Introduction</h2>
21
+ <p>The table below compares __pageCount__ web pages on <a href="https://www.w3.org/WAI/fundamentals/accessibility-intro/">accessibility</a>. The page names link to the pages on the web. The scores link to digests that explain in detail how the scores were computed.</p>
22
+ <p>The pages were:</p>
23
+ <ol id="summary">
24
+ <li>Tested by <a href="https://www.npmjs.com/package/testaro">Testaro</a> with procedure <code>tp12</code></li>
25
+ <li>Scored by <a href="https://www.npmjs.com/package/testilo">Testilo</a> with procedure <code>sp12a</code></li>
26
+ <li>Digested by Testilo with procedure <code>dp12a</code></li>
27
+ <li>Compared by Testilo with procedure <code>cp12a</code></li>
28
+ </ol>
29
+ <p>The Testaro procedure performs 808 tests on each page. Of these, 16 tests are custom tests defined by Testaro, and the others belong to packages of tests created by others.</p>
30
+ <h2>Comparison</h2>
31
+ <table class="allBorder">
32
+ <caption>Accessibility scores of web pages</caption>
33
+ <thead>
34
+ <tr><th scope="col">Page</th><th scope="col" colspan="2">Score (lower is better)</tr>
35
+ </thead>
36
+ <tbody class="linkSmaller secondCellRight">
37
+ __tableBody__
38
+ </tbody>
39
+ </table>
40
+ <h2>Discussion</h2>
41
+ <p>Tests and scoring formulae are fallible and subjective. The reported faults merit investigation as potential opportunities for improved accessibility. But some may not actually harm accessibility, and some other accessibility faults are not detectable with these tests. Different reasonable procedures could yield different test results and different scores. Testaro and Testilo can be customized to fit different definitions and weightings of types of accessibility.</p>
42
+ <footer>
43
+ <p class="date">Produced <time itemprop="datePublished" datetime="__dateISO__">__dateSlash__</time></p>
44
+ </footer>
45
+ </main>
46
+ </body>
47
+ </html>
@@ -0,0 +1,71 @@
1
+ /*
2
+ cp12a.js
3
+ Returns a query for an HTML page including a bar-graph table.
4
+ */
5
+
6
+ // ########## IMPORTS
7
+
8
+ // Module to keep secrets local.
9
+ require('dotenv').config();
10
+ // Module to access files.
11
+ const fs = require('fs/promises');
12
+
13
+ // ########## CONSTANTS
14
+
15
+ const reportDirScored = process.env.REPORTDIR_SCORED || 'reports/scored';
16
+ const query = {};
17
+
18
+ // ########## FUNCTIONS
19
+
20
+ // Returns data on the hosts in the report directory.
21
+ const getData = async () => {
22
+ const reportDirAbs = `${__dirname}/../../../${reportDirScored}`;
23
+ const reportFileNamesAll = await fs.readdir(reportDirAbs);
24
+ const reportFileNamesSource = reportFileNamesAll.filter(fileName => fileName.endsWith('.json'));
25
+ const pageCount = reportFileNamesSource.length;
26
+ const bodyData = [];
27
+ for (const fileName of reportFileNamesSource) {
28
+ const fileJSON = await fs.readFile(`${reportDirAbs}/${fileName}`, 'utf8');
29
+ const file = JSON.parse(fileJSON);
30
+ const {id, host, score} = file;
31
+ bodyData.push({
32
+ id,
33
+ org: host.what,
34
+ url: host.which,
35
+ score: score.summary.total
36
+ });
37
+ };
38
+ return {
39
+ pageCount,
40
+ bodyData
41
+ }
42
+ };
43
+ // Returns the maximum score.
44
+ const getMaxScore = tableData => tableData.reduce((max, item) => Math.max(max, item.score), 0);
45
+ // Converts report data to a table body.
46
+ const getTableBody = async bodyData => {
47
+ const maxScore = getMaxScore(bodyData);
48
+ const rows = bodyData
49
+ .sort((a, b) => a.score - b.score)
50
+ .map(item => {
51
+ const {id, org, url, score} = item;
52
+ const pageCell = `<th scope="row"><a href="${url}">${org}</a></th>`;
53
+ const numCell = `<td><a href="digests/${id}.html">${score}</a></td>`;
54
+ const barWidth = 100 * score / maxScore;
55
+ const bar = `<rect height="100%" width="${barWidth}%" fill="red"></rect>`;
56
+ const barCell = `<td aria-hidden="true"><svg width="100%" height="0.7em">${bar}</svg></td>`;
57
+ const row = `<tr>${pageCell}${numCell}${barCell}</tr>`;
58
+ return row;
59
+ });
60
+ return rows.join('\n ');
61
+ };
62
+ // Returns a query for a comparative table.
63
+ exports.getQuery = async () => {
64
+ const data = await getData();
65
+ query.pageCount = data.pageCount;
66
+ query.tableBody = await getTableBody(data.bodyData);
67
+ const date = new Date();
68
+ query.dateISO = date.toISOString().slice(0, 10);
69
+ query.dateSlash = query.dateISO.replace(/-/g, '/');
70
+ return query;
71
+ };
@@ -19,9 +19,9 @@ const specialMessages = {
19
19
 
20
20
  // Makes strings HTML-safe.
21
21
  const htmlEscape = textOrNumber => textOrNumber
22
- .toString()
23
- .replace(/&/g, '&amp;')
24
- .replace(/</g, '&lt;');
22
+ .toString()
23
+ .replace(/&/g, '&amp;')
24
+ .replace(/</g, '&lt;');
25
25
  // Gets a row of the score-summary table.
26
26
  const getScoreRow = (component, score) => `<tr><th>${component}</th><td>${score}</td></tr>`;
27
27
  // Gets the start of a paragraph about a special score.
@@ -62,13 +62,17 @@ const groups = {
62
62
  alfa: {
63
63
  r3: {
64
64
  quality: 1,
65
- what: 'Element ID is not unique'
65
+ what: 'Element id attribute value is not unique'
66
66
  }
67
67
  },
68
68
  axe: {
69
69
  'duplicate-id': {
70
70
  quality: 1,
71
- what: 'ID attribute value must be unique'
71
+ what: 'id attribute value is not unique'
72
+ },
73
+ 'duplicate-id-active': {
74
+ quality: 1,
75
+ what: 'id attribute value of the active element is not unique'
72
76
  }
73
77
  },
74
78
  htmlcs: {
@@ -80,18 +84,43 @@ const groups = {
80
84
  ibm: {
81
85
  RPT_Elem_UniqueId: {
82
86
  quality: 1,
83
- what: 'Element id attribute values must be unique within a document'
87
+ what: 'Element id attribute value is not unique within the document'
84
88
  }
85
89
  }
86
90
  }
87
91
  },
88
- textInputNoText: {
92
+ formFieldNoText: {
89
93
  weight: 4,
90
94
  packages: {
95
+ alfa: {
96
+ r8: {
97
+ quality: 1,
98
+ what: 'Form field has no accessible name'
99
+ }
100
+ }
101
+ }
102
+ },
103
+ inputNoText: {
104
+ weight: 4,
105
+ packages: {
106
+ axe: {
107
+ 'aria-input-field-name': {
108
+ quality: 1,
109
+ what: 'ARIA input field has no accessible name'
110
+ }
111
+ },
91
112
  htmlcs: {
92
113
  'e:AA.4_1_2.H91.InputText.Name': {
93
114
  quality: 1,
94
115
  what: 'Text input has no accessible name'
116
+ },
117
+ 'e:AA.4_1_2.H91.InputEmail.Name': {
118
+ quality: 1,
119
+ what: 'Email input has no accessible name'
120
+ },
121
+ 'e:AA.4_1_2.H91.InputNumber.Name': {
122
+ quality: 1,
123
+ what: 'Number input has no accessible name'
95
124
  }
96
125
  }
97
126
  }
@@ -120,7 +149,7 @@ const groups = {
120
149
  ibm: {
121
150
  'v:WCAG20_Input_ExplicitLabelImage': {
122
151
  quality: 1,
123
- what: 'Input element of type image should have a text alternative'
152
+ what: 'Input element of type image has no text alternative'
124
153
  }
125
154
  },
126
155
  wave: {
@@ -143,7 +172,7 @@ const groups = {
143
172
  axe: {
144
173
  'image-alt': {
145
174
  quality: 1,
146
- what: 'Images must have alternate text'
175
+ what: 'Image has no text alternative'
147
176
  }
148
177
  },
149
178
  htmlcs: {
@@ -161,7 +190,7 @@ const groups = {
161
190
  wave: {
162
191
  'e:alt_missing': {
163
192
  quality: 1,
164
- what: 'Missing alternative text'
193
+ what: 'Text alternative is missing'
165
194
  }
166
195
  }
167
196
  }
@@ -177,6 +206,28 @@ const groups = {
177
206
  }
178
207
  }
179
208
  },
209
+ imagesSameAlt: {
210
+ weight: 1,
211
+ packages: {
212
+ wave: {
213
+ 'a:alt_duplicate': {
214
+ quality: 1,
215
+ what: 'Two images near each other have the same text alternative'
216
+ }
217
+ }
218
+ }
219
+ },
220
+ imageTextLong: {
221
+ weight: 2,
222
+ packages: {
223
+ wave: {
224
+ 'a:alt_long': {
225
+ quality: 1,
226
+ what: 'Long text alternative'
227
+ }
228
+ }
229
+ }
230
+ },
180
231
  imageTextRisk: {
181
232
  weight: 1,
182
233
  packages: {
@@ -199,6 +250,21 @@ const groups = {
199
250
  }
200
251
  }
201
252
  },
253
+ decorativeElementExposed: {
254
+ weight: 1,
255
+ packages: {
256
+ alfa: {
257
+ r67: {
258
+ quality: 1,
259
+ what: 'Image marked as decorative is in the accessibility tree or has no none/presentation role'
260
+ },
261
+ r86: {
262
+ quality: 1,
263
+ what: 'Element marked as decorative is in the accessibility tree or has no none/presentation role'
264
+ }
265
+ }
266
+ }
267
+ },
202
268
  pageLanguage: {
203
269
  weight: 4,
204
270
  packages: {
@@ -215,9 +281,9 @@ const groups = {
215
281
  }
216
282
  },
217
283
  htmlcs: {
218
- 'e:H57': {
284
+ 'e:AA.3_1_1.H57.2': {
219
285
  quality: 1,
220
- what: 'Lang attribute of the document element'
286
+ what: 'html element has no lang or xml:lang attribute'
221
287
  }
222
288
  },
223
289
  ibm: {
@@ -342,13 +408,13 @@ const groups = {
342
408
  }
343
409
  }
344
410
  },
345
- eventKeyboard: {
346
- weight: 4,
411
+ eventKeyboardRisk: {
412
+ weight: 1,
347
413
  packages: {
348
414
  htmlcs: {
349
- 'w:G90': {
415
+ 'w:AA.2_1_1.G90': {
350
416
  quality: 1,
351
- what: 'Event handler functionality not available by keyboard'
417
+ what: 'Event handler functionality may not be available by keyboard'
352
418
  }
353
419
  },
354
420
  wave: {
@@ -372,6 +438,10 @@ const groups = {
372
438
  'a:label_orphaned': {
373
439
  quality: 1,
374
440
  what: 'Orphaned form label'
441
+ },
442
+ 'a:link_internal_broken': {
443
+ quality: 1,
444
+ what: 'Broken same-page link'
375
445
  }
376
446
  }
377
447
  }
@@ -433,20 +503,28 @@ const groups = {
433
503
  ibm: {
434
504
  'v:WCAG20_A_HasText': {
435
505
  quality: 1,
436
- what: 'Hyperlinks must have a text description'
506
+ what: 'Hyperlink has no text description'
437
507
  }
438
508
  },
439
509
  tenon: {
440
510
  57: {
441
511
  quality: 1,
442
512
  what: 'Link has no text inside it'
513
+ },
514
+ 91: {
515
+ quality: 1,
516
+ what: 'Link has a background image but no text inside it'
443
517
  }
444
518
  },
445
519
  wave: {
446
520
  'e:link_empty': {
447
521
  quality: 1,
448
522
  what: 'Link contains no text'
449
- }
523
+ },
524
+ 'e:alt_link_missing': {
525
+ quality: 1,
526
+ what: 'Linked image has no text alternative'
527
+ },
450
528
  }
451
529
  }
452
530
  },
@@ -461,6 +539,17 @@ const groups = {
461
539
  }
462
540
  }
463
541
  },
542
+ pdfLink: {
543
+ weight: 1,
544
+ packages: {
545
+ wave: {
546
+ 'a:link_pdf': {
547
+ quality: 1,
548
+ what: 'Link to PDF document'
549
+ }
550
+ }
551
+ }
552
+ },
464
553
  destinationLink: {
465
554
  weight: 2,
466
555
  packages: {
@@ -494,6 +583,17 @@ const groups = {
494
583
  }
495
584
  }
496
585
  },
586
+ linkDestinationsSame: {
587
+ weight: 2,
588
+ packages: {
589
+ tenon: {
590
+ 184: {
591
+ quality: 1,
592
+ what: 'Adjacent links point to the same destination'
593
+ }
594
+ }
595
+ }
596
+ },
497
597
  linkConfusionRisk: {
498
598
  weight: 1,
499
599
  packages: {
@@ -516,6 +616,17 @@ const groups = {
516
616
  }
517
617
  }
518
618
  },
619
+ formNewWindow: {
620
+ weight: 2,
621
+ packages: {
622
+ tenon: {
623
+ 214: {
624
+ quality: 1,
625
+ what: 'Form submission opens a new window'
626
+ }
627
+ }
628
+ }
629
+ },
519
630
  linkForcesNewWindow: {
520
631
  weight: 3,
521
632
  packages: {
@@ -527,7 +638,7 @@ const groups = {
527
638
  }
528
639
  }
529
640
  },
530
- newWindowSurpriseRisk: {
641
+ linkWindowSurpriseRisk: {
531
642
  weight: 1,
532
643
  packages: {
533
644
  htmlcs: {
@@ -550,10 +661,18 @@ const groups = {
550
661
  axe: {
551
662
  'aria-command-name': {
552
663
  quality: 1,
553
- what: 'ARIA commands must have an accessible name'
664
+ what: 'ARIA command has no accessible name'
665
+ },
666
+ 'button-name': {
667
+ quality: 1,
668
+ what: 'Button has no discernible text'
554
669
  }
555
670
  },
556
671
  htmlcs: {
672
+ 'e:AA.4_1_2.H91.A.Name': {
673
+ quality: 1,
674
+ what: 'Link with button role has no accessible name'
675
+ },
557
676
  'e:AA.4_1_2.H91.Button.Name': {
558
677
  quality: 1,
559
678
  what: 'Button element has no accessible name'
@@ -601,6 +720,28 @@ const groups = {
601
720
  }
602
721
  }
603
722
  },
723
+ cssBansRotate: {
724
+ weight: 4,
725
+ packages: {
726
+ axe: {
727
+ 'css-orientation-lock': {
728
+ quality: 1,
729
+ what: 'CSS media query locks display orientation'
730
+ }
731
+ }
732
+ }
733
+ },
734
+ textRotated: {
735
+ weight: 2,
736
+ packages: {
737
+ tenon: {
738
+ 271: {
739
+ quality: 1,
740
+ what: 'Text is needlessly rotated 60+ degrees or more, hurting comprehension'
741
+ }
742
+ }
743
+ }
744
+ },
604
745
  metaBansZoom: {
605
746
  weight: 4,
606
747
  packages: {
@@ -613,7 +754,11 @@ const groups = {
613
754
  axe: {
614
755
  'meta-viewport': {
615
756
  quality: 1,
616
- what: 'Zooming and scaling should not be disabled'
757
+ what: 'Zooming and scaling are disabled'
758
+ },
759
+ 'meta-viewport-large': {
760
+ quality: 1,
761
+ what: 'User cannot zoom and scale the text up to 500%'
617
762
  }
618
763
  }
619
764
  }
@@ -812,6 +957,10 @@ const groups = {
812
957
  weight: 4,
813
958
  packages: {
814
959
  alfa: {
960
+ r19: {
961
+ quality: 1,
962
+ what: 'ARIA state or property has an invalid value'
963
+ },
815
964
  r20: {
816
965
  quality: 1,
817
966
  what: 'ARIA attribute is not defined'
@@ -1006,10 +1155,16 @@ const groups = {
1006
1155
  headingEmpty: {
1007
1156
  weight: 3,
1008
1157
  packages: {
1158
+ alfa: {
1159
+ r64: {
1160
+ quality: 1,
1161
+ what: 'Heading has no non-empty accessible name'
1162
+ }
1163
+ },
1009
1164
  axe: {
1010
1165
  'empty-heading': {
1011
1166
  quality: 1,
1012
- what: 'Headings should not be empty'
1167
+ what: 'Heading empty'
1013
1168
  }
1014
1169
  },
1015
1170
  htmlcs: {
@@ -1032,6 +1187,17 @@ const groups = {
1032
1187
  }
1033
1188
  }
1034
1189
  },
1190
+ headingOfNothing: {
1191
+ weight: 2,
1192
+ packages: {
1193
+ alfa: {
1194
+ r78: {
1195
+ quality: 1,
1196
+ what: 'No content between two headings of the same level'
1197
+ }
1198
+ }
1199
+ }
1200
+ },
1035
1201
  imageTextRedundant: {
1036
1202
  weight: 1,
1037
1203
  packages: {
@@ -1044,7 +1210,19 @@ const groups = {
1044
1210
  ibm: {
1045
1211
  'v:WCAG20_Img_LinkTextNotRedundant': {
1046
1212
  quality: 1,
1047
- what: 'Text alternative for image within link should not repeat link text or adjacent link text'
1213
+ what: 'Text alternative for the image in a link repeats text of the same or an adjacent link'
1214
+ }
1215
+ },
1216
+ tenon: {
1217
+ 138: {
1218
+ quality: 1,
1219
+ what: 'Image link alternative text repeats text in the link'
1220
+ }
1221
+ },
1222
+ wave: {
1223
+ 'a:alt_redundant': {
1224
+ quality: 1,
1225
+ what: 'Redundant text alternative'
1048
1226
  }
1049
1227
  }
1050
1228
  }
@@ -1156,13 +1334,56 @@ const groups = {
1156
1334
  }
1157
1335
  }
1158
1336
  },
1337
+ justification: {
1338
+ weight: 1,
1339
+ packages: {
1340
+ wave: {
1341
+ 'a:text_justified': {
1342
+ quality: 1,
1343
+ what: 'Text is justified'
1344
+ }
1345
+ }
1346
+ }
1347
+ },
1348
+ nonSemanticText: {
1349
+ weight: 2,
1350
+ packages: {
1351
+ htmlcs: {
1352
+ 'w:AA.1_3_1.H49.B': {
1353
+ quality: 1,
1354
+ what: 'Bold text is coded nonsemantically'
1355
+ },
1356
+ 'w:AA.1_3_1.H49.Small': {
1357
+ quality: 1,
1358
+ what: 'Small text is coded nonsemantically'
1359
+ }
1360
+ }
1361
+ }
1362
+ },
1363
+ pseudoParagraphRisk: {
1364
+ weight: 1,
1365
+ packages: {
1366
+ tenon: {
1367
+ 242: {
1368
+ quality: 1,
1369
+ what: 'Multiple consecutive br elements may simulate paragraphs'
1370
+ }
1371
+ }
1372
+ }
1373
+ },
1159
1374
  pseudoHeadingRisk: {
1160
1375
  weight: 1,
1161
1376
  packages: {
1377
+ axe: {
1378
+ 'p-as-heading': {
1379
+ quality: 1,
1380
+ what: 'Styled p element may be misused as a heading'
1381
+ }
1382
+ },
1162
1383
  htmlcs: {
1163
1384
  'w:AA.1_3_1.H42': {
1164
1385
  quality: 1,
1165
- what: 'Heading coding should be used if intended as a heading'
1386
+ what: 'Heading coding is not used but the element may be intended as a heading'
1166
1387
  }
1167
1388
  },
1168
1389
  wave: {
@@ -1181,6 +1402,34 @@ const groups = {
1181
1402
  quality: 1,
1182
1403
  what: 'CSS underline on text that is not a link'
1183
1404
  }
1405
+ },
1406
+ wave: {
1407
+ 'a:underline': {
1408
+ quality: 1,
1409
+ what: 'CSS underline on text that is not a link'
1410
+ }
1411
+ }
1412
+ }
1413
+ },
1414
+ listChild: {
1415
+ weight: 4,
1416
+ packages: {
1417
+ axe: {
1418
+ 'list': {
1419
+ quality: 1,
1420
+ what: 'List element ul or ol has a child element other than li, script, and template'
1421
+ }
1422
+ }
1423
+ }
1424
+ },
1425
+ listItemOrphan: {
1426
+ weight: 4,
1427
+ packages: {
1428
+ axe: {
1429
+ listitem: {
1430
+ quality: 1,
1431
+ what: 'li element is not contained by a ul or ol element'
1432
+ }
1184
1433
  }
1185
1434
  }
1186
1435
  },
@@ -1212,13 +1461,17 @@ const groups = {
1212
1461
  axe: {
1213
1462
  'select-name': {
1214
1463
  quality: 1,
1215
- what: 'Select element must have an accessible name'
1464
+ what: 'Select element has no accessible name'
1216
1465
  }
1217
1466
  },
1218
1467
  htmlcs: {
1219
- 'w:H91': {
1468
+ 'e:AA.4_1_2.H91.Select.Name': {
1469
+ quality: 1,
1470
+ what: 'Select element has no accessible name'
1471
+ },
1472
+ 'w:AA.4_1_2.H91.Select.Value': {
1220
1473
  quality: 1,
1221
- what: 'Select element has no value available to an accessibility API'
1474
+ what: 'Select element value has no accessible name'
1222
1475
  }
1223
1476
  },
1224
1477
  wave: {
@@ -1288,12 +1541,24 @@ const groups = {
1288
1541
  quality: 1,
1289
1542
  what: 'Fieldset has no legend element'
1290
1543
  }
1544
+ },
1545
+ wave: {
1546
+ 'a:legend_missing': {
1547
+ quality: 1,
1548
+ what: 'Fieldset has no legend element'
1549
+ }
1291
1550
  }
1292
1551
  }
1293
1552
  },
1294
- fieldSetName: {
1553
+ groupName: {
1295
1554
  weight: 3,
1296
1555
  packages: {
1556
+ alfa: {
1557
+ r60: {
1558
+ quality: 1,
1559
+ what: 'Form-control group has no accessible name'
1560
+ }
1561
+ },
1297
1562
  htmlcs: {
1298
1563
  'e:AA.4_1_2.H91.Fieldset.Name': {
1299
1564
  quality: 1,
@@ -1302,6 +1567,17 @@ const groups = {
1302
1567
  }
1303
1568
  }
1304
1569
  },
1570
+ layoutTable: {
1571
+ weight: 2,
1572
+ packages: {
1573
+ wave: {
1574
+ 'a:table_layout': {
1575
+ quality: 1,
1576
+ what: 'Table element is misused to arrange content'
1577
+ }
1578
+ }
1579
+ }
1580
+ },
1305
1581
  tableCaption: {
1306
1582
  weight: 1,
1307
1583
  packages: {
@@ -1313,7 +1589,18 @@ const groups = {
1313
1589
  }
1314
1590
  }
1315
1591
  },
1316
- nameValue: {
1592
+ tableCellHeaderless: {
1593
+ weight: 4,
1594
+ packages: {
1595
+ alfa: {
1596
+ r77: {
1597
+ quality: 1,
1598
+ what: 'Table cell has no header'
1599
+ }
1600
+ }
1601
+ }
1602
+ },
1603
+ controlLabel: {
1317
1604
  weight: 4,
1318
1605
  packages: {
1319
1606
  htmlcs: {
@@ -1330,9 +1617,32 @@ const groups = {
1330
1617
  }
1331
1618
  }
1332
1619
  },
1620
+ titleAsLabel: {
1621
+ weight: 3,
1622
+ packages: {
1623
+ wave: {
1624
+ 'a:label_title': {
1625
+ quality: 1,
1626
+ what: 'Form control has a title but no label'
1627
+ }
1628
+ }
1629
+ }
1630
+ },
1333
1631
  invisibleLabel: {
1334
1632
  weight: 3,
1335
1633
  packages: {
1634
+ alfa: {
1635
+ r14: {
1636
+ quality: 1,
1637
+ what: 'Visible label is not in the accessible name'
1638
+ }
1639
+ },
1640
+ axe: {
1641
+ 'label-content-name-mismatch': {
1642
+ quality: 1,
1643
+ what: 'Element visible text is not part of its accessible name'
1644
+ }
1645
+ },
1336
1646
  htmlcs: {
1337
1647
  'w:AA.2_5_3.F96': {
1338
1648
  quality: 1,
@@ -1366,10 +1676,16 @@ const groups = {
1366
1676
  activeEmbedding: {
1367
1677
  weight: 3,
1368
1678
  packages: {
1679
+ axe: {
1680
+ 'nested-interactive': {
1681
+ quality: 1,
1682
+ what: 'Interactive controls are nested'
1683
+ }
1684
+ },
1369
1685
  testaro: {
1370
1686
  embAc: {
1371
1687
  quality: 1,
1372
- what: 'Active elements embedded in links or buttons'
1688
+ what: 'Active element is embedded in a link or button'
1373
1689
  }
1374
1690
  }
1375
1691
  }
@@ -1405,6 +1721,12 @@ const groups = {
1405
1721
  allCaps: {
1406
1722
  weight: 1,
1407
1723
  packages: {
1724
+ alfa: {
1725
+ r72: {
1726
+ quality: 1,
1727
+ what: 'Paragraph text is uppercased'
1728
+ }
1729
+ },
1408
1730
  tenon: {
1409
1731
  153: {
1410
1732
  quality: 1,
@@ -1413,13 +1735,52 @@ const groups = {
1413
1735
  }
1414
1736
  }
1415
1737
  },
1416
- textBeyondLandmarks: {
1738
+ allItalics: {
1739
+ weight: 1,
1740
+ packages: {
1741
+ tenon: {
1742
+ 154: {
1743
+ quality: 1,
1744
+ what: 'Long string of text is italic'
1745
+ }
1746
+ }
1747
+ }
1748
+ },
1749
+ contentBeyondLandmarks: {
1417
1750
  weight: 2,
1418
1751
  packages: {
1419
1752
  alfa: {
1420
1753
  r57: {
1421
1754
  quality: 1,
1422
- what: 'Perceivable text content not included in any landmark'
1755
+ what: 'Perceivable text content is not included in any landmark'
1756
+ }
1757
+ },
1758
+ axe: {
1759
+ region: {
1760
+ quality: 1,
1761
+ what: 'Some page content is not contained by landmarks'
1762
+ }
1763
+ }
1764
+ }
1765
+ },
1766
+ footerTopLandmark: {
1767
+ weight: 1,
1768
+ packages: {
1769
+ axe: {
1770
+ 'landmark-contentinfo-is-top-level': {
1771
+ quality: 1,
1772
+ what: 'contentinfo landmark (footer) is contained in another landmark'
1773
+ }
1774
+ }
1775
+ }
1776
+ },
1777
+ asideTopLandmark: {
1778
+ weight: 2,
1779
+ packages: {
1780
+ axe: {
1781
+ 'landmark-complementary-is-top-level': {
1782
+ quality: 1,
1783
+ what: 'complementary landmark (aside) is contained in another landmark'
1423
1784
  }
1424
1785
  }
1425
1786
  }
@@ -1435,10 +1796,14 @@ const groups = {
1435
1796
  }
1436
1797
  }
1437
1798
  },
1438
- multipleMain: {
1799
+ mainLandmark: {
1439
1800
  weight: 2,
1440
1801
  packages: {
1441
1802
  axe: {
1803
+ 'landmark-one-main': {
1804
+ quality: 1,
1805
+ what: 'page has no main landmark'
1806
+ },
1442
1807
  'landmark-no-duplicate-main': {
1443
1808
  quality: 1,
1444
1809
  what: 'page has more than 1 main landmark'
@@ -1446,6 +1811,39 @@ const groups = {
1446
1811
  }
1447
1812
  }
1448
1813
  },
1814
+ bannerLandmark: {
1815
+ weight: 2,
1816
+ packages: {
1817
+ axe: {
1818
+ 'landmark-no-duplicate-banner': {
1819
+ quality: 1,
1820
+ what: 'page has more than 1 banner landmark'
1821
+ }
1822
+ }
1823
+ }
1824
+ },
1825
+ footerMultiple: {
1826
+ weight: 2,
1827
+ packages: {
1828
+ axe: {
1829
+ 'landmark-no-duplicate-contentinfo': {
1830
+ quality: 1,
1831
+ what: 'page has more than 1 contentinfo landmark (footer)'
1832
+ }
1833
+ }
1834
+ }
1835
+ },
1836
+ landmarkConfusion: {
1837
+ weight: 3,
1838
+ packages: {
1839
+ axe: {
1840
+ 'landmark-unique': {
1841
+ quality: 1,
1842
+ what: 'Landmark has a role and an accessible name that are identical to another'
1843
+ }
1844
+ }
1845
+ }
1846
+ },
1449
1847
  focusableOperable: {
1450
1848
  weight: 3,
1451
1849
  packages: {
@@ -1481,6 +1879,31 @@ const groups = {
1481
1879
  'aria-hidden-focus': {
1482
1880
  quality: 1,
1483
1881
  what: 'ARIA hidden element is focusable or contains a focusable element'
1882
+ },
1883
+ 'presentation-role-conflict': {
1884
+ quality: 1,
1885
+ what: 'Element has a none/presentation role but is focusable or has a global ARIA state or property'
1886
+ }
1887
+ },
1888
+ tenon: {
1889
+ 189: {
1890
+ quality: 1,
1891
+ what: 'Element is typically used for interaction but has a presentation role'
1892
+ },
1893
+ 194: {
1894
+ quality: 1,
1895
+ what: 'Visible element is focusable but has a presentation role or aria-hidden=true attribute'
1896
+ }
1897
+ }
1898
+ }
1899
+ },
1900
+ labeledHidden: {
1901
+ weight: 2,
1902
+ packages: {
1903
+ htmlcs: {
1904
+ 'w:AA.1_3_1.F68.Hidden': {
1905
+ quality: 1,
1906
+ what: 'Hidden form field is needlessly labeled.'
1484
1907
  }
1485
1908
  }
1486
1909
  }
@@ -1540,13 +1963,47 @@ const groups = {
1540
1963
  }
1541
1964
  }
1542
1965
  },
1543
- linkUnderlines: {
1966
+ linkComprehensionRisk: {
1967
+ weight: 1,
1968
+ packages: {
1969
+ wave: {
1970
+ 'a:link_suspicious': {
1971
+ quality: 1,
1972
+ what: 'Suspicious link text'
1973
+ }
1974
+ }
1975
+ }
1976
+ },
1977
+ linkVague: {
1978
+ weight: 3,
1979
+ packages: {
1980
+ tenon: {
1981
+ 73: {
1982
+ quality: 1,
1983
+ what: 'Link text is too generic to communicate the purpose or destination'
1984
+ }
1985
+ }
1986
+ }
1987
+ },
1988
+ linkIndication: {
1544
1989
  weight: 2,
1545
1990
  packages: {
1991
+ alfa: {
1992
+ r62: {
1993
+ quality: 1,
1994
+ what: 'Inline link is not distinct from the surrounding text except by color'
1995
+ }
1996
+ },
1997
+ axe: {
1998
+ 'link-in-text-block': {
1999
+ quality: 1,
2000
+ what: 'Link is not distinct from surrounding text without reliance on color'
2001
+ }
2002
+ },
1546
2003
  testaro: {
1547
2004
  linkUl: {
1548
2005
  quality: 1,
1549
- what: 'Non-underlined inline links'
2006
+ what: 'Non-underlined adjacent links'
1550
2007
  }
1551
2008
  }
1552
2009
  }
@@ -1606,6 +2063,34 @@ const groups = {
1606
2063
  }
1607
2064
  }
1608
2065
  },
2066
+ tabIndexPositive: {
2067
+ weight: 1,
2068
+ packages: {
2069
+ axe: {
2070
+ tabindex: {
2071
+ quality: 1,
2072
+ what: 'Positive tabIndex risks creating a confusing focus order'
2073
+ }
2074
+ },
2075
+ wave: {
2076
+ 'a:tabindex': {
2077
+ quality: 1,
2078
+ what: 'tabIndex value positive'
2079
+ }
2080
+ }
2081
+ }
2082
+ },
2083
+ tabIndexMissing: {
2084
+ weight: 4,
2085
+ packages: {
2086
+ tenon: {
2087
+ 190: {
2088
+ quality: 1,
2089
+ what: 'Interactive item is not natively actionable, but has no tabindex=0 attribute'
2090
+ }
2091
+ }
2092
+ }
2093
+ },
1609
2094
  videoCaptionMissing: {
1610
2095
  weight: 4,
1611
2096
  packages: {
@@ -1621,9 +2106,13 @@ const groups = {
1621
2106
  weight: 1,
1622
2107
  packages: {
1623
2108
  wave: {
2109
+ 'a:html5_video_audio': {
2110
+ quality: 1,
2111
+ what: 'video or audio element may have no or incorrect captions, transcript, or audio description'
2112
+ },
1624
2113
  'a:youtube_video': {
1625
2114
  quality: 1,
1626
- what: 'YouTube video may fail to be captioned'
2115
+ what: 'YouTube video may have no or incorrect captions'
1627
2116
  }
1628
2117
  }
1629
2118
  }
@@ -1669,6 +2158,12 @@ const groups = {
1669
2158
  quality: 0.5,
1670
2159
  what: 'First focusable element is not a link to the main content'
1671
2160
  }
2161
+ },
2162
+ axe: {
2163
+ 'skip-link': {
2164
+ quality: 1,
2165
+ what: 'Skip-link target is not focusable or does not exist'
2166
+ }
1672
2167
  }
1673
2168
  }
1674
2169
  },
@@ -1697,10 +2192,16 @@ const groups = {
1697
2192
  obsoleteElement: {
1698
2193
  weight: 1,
1699
2194
  packages: {
2195
+ alfa: {
2196
+ r70: {
2197
+ quality: 1,
2198
+ what: 'Element is obsolete or deprecated'
2199
+ }
2200
+ },
1700
2201
  htmlcs: {
1701
2202
  'e:AA.1_3_1.H49.Center': {
1702
2203
  quality: 1,
1703
- what: 'The center element is obsolete'
2204
+ what: 'Element is obsolete'
1704
2205
  }
1705
2206
  }
1706
2207
  }