testilo 3.6.2 → 3.6.3

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.6.3",
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
+ <div id="summary">
24
+ <p>Tested by <a href="https://www.npmjs.com/package/testaro">Testaro</a> with procedure <code>tp12</code></p>
25
+ <p>Scored by <a href="https://www.npmjs.com/package/testilo">Testilo</a> with procedure <code>sp12a</code></p>
26
+ <p>Digested by Testilo with procedure <code>dp12a</code></p>
27
+ <p>Compared by Testilo with procedure <code>cp12a</code></p>
28
+ </div>
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
+ };
@@ -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'
88
+ }
89
+ }
90
+ }
91
+ },
92
+ formFieldNoText: {
93
+ weight: 4,
94
+ packages: {
95
+ alfa: {
96
+ r8: {
97
+ quality: 1,
98
+ what: 'Form field has no accessible name'
84
99
  }
85
100
  }
86
101
  }
87
102
  },
88
- textInputNoText: {
103
+ inputNoText: {
89
104
  weight: 4,
90
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: {
@@ -177,6 +206,17 @@ const groups = {
177
206
  }
178
207
  }
179
208
  },
209
+ imageTextLong: {
210
+ weight: 2,
211
+ packages: {
212
+ wave: {
213
+ 'a:alt_long': {
214
+ quality: 1,
215
+ what: 'Long text alternative'
216
+ }
217
+ }
218
+ }
219
+ },
180
220
  imageTextRisk: {
181
221
  weight: 1,
182
222
  packages: {
@@ -199,6 +239,32 @@ const groups = {
199
239
  }
200
240
  }
201
241
  },
242
+ presentationConflict: {
243
+ weight: 4,
244
+ packages: {
245
+ axe: {
246
+ 'presentation-role-conflict': {
247
+ quality: 1,
248
+ what: 'Element has a none/presentation role but is focusable or has a global ARIA state or property'
249
+ }
250
+ }
251
+ }
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,7 +503,7 @@ 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: {
@@ -446,7 +516,11 @@ const groups = {
446
516
  'e:link_empty': {
447
517
  quality: 1,
448
518
  what: 'Link contains no text'
449
- }
519
+ },
520
+ 'e:alt_link_missing': {
521
+ quality: 1,
522
+ what: 'Linked image has no text alternative'
523
+ },
450
524
  }
451
525
  }
452
526
  },
@@ -461,6 +535,17 @@ const groups = {
461
535
  }
462
536
  }
463
537
  },
538
+ pdfLink: {
539
+ weight: 1,
540
+ packages: {
541
+ wave: {
542
+ 'a:link_pdf': {
543
+ quality: 1,
544
+ what: 'Link to PDF document'
545
+ }
546
+ }
547
+ }
548
+ },
464
549
  destinationLink: {
465
550
  weight: 2,
466
551
  packages: {
@@ -494,6 +579,17 @@ const groups = {
494
579
  }
495
580
  }
496
581
  },
582
+ linkDestinationsSame: {
583
+ weight: 2,
584
+ packages: {
585
+ tenon: {
586
+ 184: {
587
+ quality: 1,
588
+ what: 'Adjacent links point to the same destination'
589
+ }
590
+ }
591
+ }
592
+ },
497
593
  linkConfusionRisk: {
498
594
  weight: 1,
499
595
  packages: {
@@ -516,6 +612,17 @@ const groups = {
516
612
  }
517
613
  }
518
614
  },
615
+ formNewWindow: {
616
+ weight: 2,
617
+ packages: {
618
+ tenon: {
619
+ 214: {
620
+ quality: 1,
621
+ what: 'Form submission opens a new window'
622
+ }
623
+ }
624
+ }
625
+ },
519
626
  linkForcesNewWindow: {
520
627
  weight: 3,
521
628
  packages: {
@@ -527,7 +634,7 @@ const groups = {
527
634
  }
528
635
  }
529
636
  },
530
- newWindowSurpriseRisk: {
637
+ linkWindowSurpriseRisk: {
531
638
  weight: 1,
532
639
  packages: {
533
640
  htmlcs: {
@@ -550,10 +657,18 @@ const groups = {
550
657
  axe: {
551
658
  'aria-command-name': {
552
659
  quality: 1,
553
- what: 'ARIA commands must have an accessible name'
660
+ what: 'ARIA command has no accessible name'
661
+ },
662
+ 'button-name': {
663
+ quality: 1,
664
+ what: 'Button has no discernible text'
554
665
  }
555
666
  },
556
667
  htmlcs: {
668
+ 'e:AA.4_1_2.H91.A.Name': {
669
+ quality: 1,
670
+ what: 'Link with button role has no accessible name'
671
+ },
557
672
  'e:AA.4_1_2.H91.Button.Name': {
558
673
  quality: 1,
559
674
  what: 'Button element has no accessible name'
@@ -601,6 +716,28 @@ const groups = {
601
716
  }
602
717
  }
603
718
  },
719
+ cssBansRotate: {
720
+ weight: 4,
721
+ packages: {
722
+ axe: {
723
+ 'css-orientation-lock': {
724
+ quality: 1,
725
+ what: 'CSS media query locks display orientation'
726
+ }
727
+ }
728
+ }
729
+ },
730
+ textRotated: {
731
+ weight: 2,
732
+ packages: {
733
+ tenon: {
734
+ 271: {
735
+ quality: 1,
736
+ what: 'Text is needlessly rotated 60+ degrees or more, hurting comprehension'
737
+ }
738
+ }
739
+ }
740
+ },
604
741
  metaBansZoom: {
605
742
  weight: 4,
606
743
  packages: {
@@ -613,7 +750,11 @@ const groups = {
613
750
  axe: {
614
751
  'meta-viewport': {
615
752
  quality: 1,
616
- what: 'Zooming and scaling should not be disabled'
753
+ what: 'Zooming and scaling are disabled'
754
+ },
755
+ 'meta-viewport-large': {
756
+ quality: 1,
757
+ what: 'User cannot zoom and scale the text up to 500%'
617
758
  }
618
759
  }
619
760
  }
@@ -1006,10 +1147,16 @@ const groups = {
1006
1147
  headingEmpty: {
1007
1148
  weight: 3,
1008
1149
  packages: {
1150
+ alfa: {
1151
+ r64: {
1152
+ quality: 1,
1153
+ what: 'Heading has no non-empty accessible name'
1154
+ }
1155
+ },
1009
1156
  axe: {
1010
1157
  'empty-heading': {
1011
1158
  quality: 1,
1012
- what: 'Headings should not be empty'
1159
+ what: 'Heading empty'
1013
1160
  }
1014
1161
  },
1015
1162
  htmlcs: {
@@ -1032,6 +1179,17 @@ const groups = {
1032
1179
  }
1033
1180
  }
1034
1181
  },
1182
+ headingOfNothing: {
1183
+ weight: 2,
1184
+ packages: {
1185
+ alfa: {
1186
+ r78: {
1187
+ quality: 1,
1188
+ what: 'No content between two headings of the same level'
1189
+ }
1190
+ }
1191
+ }
1192
+ },
1035
1193
  imageTextRedundant: {
1036
1194
  weight: 1,
1037
1195
  packages: {
@@ -1044,7 +1202,19 @@ const groups = {
1044
1202
  ibm: {
1045
1203
  'v:WCAG20_Img_LinkTextNotRedundant': {
1046
1204
  quality: 1,
1047
- what: 'Text alternative for image within link should not repeat link text or adjacent link text'
1205
+ what: 'Text alternative for the image in a link repeats text of the same or an adjacent link'
1206
+ }
1207
+ },
1208
+ tenon: {
1209
+ 138: {
1210
+ quality: 1,
1211
+ what: 'Image link alternative text repeats text in the link'
1212
+ }
1213
+ },
1214
+ wave: {
1215
+ 'a:alt_redundant': {
1216
+ quality: 1,
1217
+ what: 'Redundant text alternative'
1048
1218
  }
1049
1219
  }
1050
1220
  }
@@ -1156,13 +1326,30 @@ const groups = {
1156
1326
  }
1157
1327
  }
1158
1328
  },
1329
+ pseudoParagraphRisk: {
1330
+ weight: 1,
1331
+ packages: {
1332
+ tenon: {
1333
+ 242: {
1334
+ quality: 1,
1335
+ what: 'Multiple consecutive br elements may simulate paragraphs'
1336
+ }
1337
+ }
1338
+ }
1339
+ },
1159
1340
  pseudoHeadingRisk: {
1160
1341
  weight: 1,
1161
1342
  packages: {
1343
+ axe: {
1344
+ 'p-as-heading': {
1345
+ quality: 1,
1346
+ what: 'Styled p element may be misused as a heading'
1347
+ }
1348
+ },
1162
1349
  htmlcs: {
1163
1350
  'w:AA.1_3_1.H42': {
1164
1351
  quality: 1,
1165
- what: 'Heading coding should be used if intended as a heading'
1352
+ what: 'Heading coding is not used but the element may be intended as a heading'
1166
1353
  }
1167
1354
  },
1168
1355
  wave: {
@@ -1184,6 +1371,28 @@ const groups = {
1184
1371
  }
1185
1372
  }
1186
1373
  },
1374
+ listChild: {
1375
+ weight: 4,
1376
+ packages: {
1377
+ axe: {
1378
+ 'list': {
1379
+ quality: 1,
1380
+ what: 'List element ul or ol has a child element other than li, script, and template'
1381
+ }
1382
+ }
1383
+ }
1384
+ },
1385
+ listItemOrphan: {
1386
+ weight: 4,
1387
+ packages: {
1388
+ axe: {
1389
+ listitem: {
1390
+ quality: 1,
1391
+ what: 'li element is not contained by a ul or ol element'
1392
+ }
1393
+ }
1394
+ }
1395
+ },
1187
1396
  pseudoOrderedListRisk: {
1188
1397
  weight: 1,
1189
1398
  packages: {
@@ -1212,13 +1421,17 @@ const groups = {
1212
1421
  axe: {
1213
1422
  'select-name': {
1214
1423
  quality: 1,
1215
- what: 'Select element must have an accessible name'
1424
+ what: 'Select element has no accessible name'
1216
1425
  }
1217
1426
  },
1218
1427
  htmlcs: {
1219
- 'w:H91': {
1428
+ 'e:AA.4_1_2.H91.Select.Name': {
1429
+ quality: 1,
1430
+ what: 'Select element has no accessible name'
1431
+ },
1432
+ 'w:AA.4_1_2.H91.Select.Value': {
1220
1433
  quality: 1,
1221
- what: 'Select element has no value available to an accessibility API'
1434
+ what: 'Select element value has no accessible name'
1222
1435
  }
1223
1436
  },
1224
1437
  wave: {
@@ -1288,12 +1501,24 @@ const groups = {
1288
1501
  quality: 1,
1289
1502
  what: 'Fieldset has no legend element'
1290
1503
  }
1504
+ },
1505
+ wave: {
1506
+ 'a:legend_missing': {
1507
+ quality: 1,
1508
+ what: 'Fieldset has no legend element'
1509
+ }
1291
1510
  }
1292
1511
  }
1293
1512
  },
1294
- fieldSetName: {
1513
+ groupName: {
1295
1514
  weight: 3,
1296
1515
  packages: {
1516
+ alfa: {
1517
+ r60: {
1518
+ quality: 1,
1519
+ what: 'Form-control group has no accessible name'
1520
+ }
1521
+ },
1297
1522
  htmlcs: {
1298
1523
  'e:AA.4_1_2.H91.Fieldset.Name': {
1299
1524
  quality: 1,
@@ -1333,6 +1558,18 @@ const groups = {
1333
1558
  invisibleLabel: {
1334
1559
  weight: 3,
1335
1560
  packages: {
1561
+ alfa: {
1562
+ r14: {
1563
+ quality: 1,
1564
+ what: 'Visible label is not in the accessible name'
1565
+ }
1566
+ },
1567
+ axe: {
1568
+ 'label-content-name-mismatch': {
1569
+ quality: 1,
1570
+ what: 'Element visible text is not part of its accessible name'
1571
+ }
1572
+ },
1336
1573
  htmlcs: {
1337
1574
  'w:AA.2_5_3.F96': {
1338
1575
  quality: 1,
@@ -1405,6 +1642,12 @@ const groups = {
1405
1642
  allCaps: {
1406
1643
  weight: 1,
1407
1644
  packages: {
1645
+ alfa: {
1646
+ r72: {
1647
+ quality: 1,
1648
+ what: 'Paragraph text is uppercased'
1649
+ }
1650
+ },
1408
1651
  tenon: {
1409
1652
  153: {
1410
1653
  quality: 1,
@@ -1413,13 +1656,52 @@ const groups = {
1413
1656
  }
1414
1657
  }
1415
1658
  },
1416
- textBeyondLandmarks: {
1659
+ allItalics: {
1660
+ weight: 1,
1661
+ packages: {
1662
+ tenon: {
1663
+ 154: {
1664
+ quality: 1,
1665
+ what: 'Long string of text is italic'
1666
+ }
1667
+ }
1668
+ }
1669
+ },
1670
+ contentBeyondLandmarks: {
1417
1671
  weight: 2,
1418
1672
  packages: {
1419
1673
  alfa: {
1420
1674
  r57: {
1421
1675
  quality: 1,
1422
- what: 'Perceivable text content not included in any landmark'
1676
+ what: 'Perceivable text content is not included in any landmark'
1677
+ }
1678
+ },
1679
+ axe: {
1680
+ region: {
1681
+ quality: 1,
1682
+ what: 'Some page content is not contained by landmarks'
1683
+ }
1684
+ }
1685
+ }
1686
+ },
1687
+ footerTopLandmark: {
1688
+ weight: 1,
1689
+ packages: {
1690
+ axe: {
1691
+ 'landmark-contentinfo-is-top-level': {
1692
+ quality: 1,
1693
+ what: 'contentinfo landmark (footer) is contained in another landmark'
1694
+ }
1695
+ }
1696
+ }
1697
+ },
1698
+ asideTopLandmark: {
1699
+ weight: 2,
1700
+ packages: {
1701
+ axe: {
1702
+ 'landmark-complementary-is-top-level': {
1703
+ quality: 1,
1704
+ what: 'complementary landmark (aside) is contained in another landmark'
1423
1705
  }
1424
1706
  }
1425
1707
  }
@@ -1435,10 +1717,14 @@ const groups = {
1435
1717
  }
1436
1718
  }
1437
1719
  },
1438
- multipleMain: {
1720
+ mainLandmark: {
1439
1721
  weight: 2,
1440
1722
  packages: {
1441
1723
  axe: {
1724
+ 'landmark-one-main': {
1725
+ quality: 1,
1726
+ what: 'page has no main landmark'
1727
+ },
1442
1728
  'landmark-no-duplicate-main': {
1443
1729
  quality: 1,
1444
1730
  what: 'page has more than 1 main landmark'
@@ -1446,6 +1732,28 @@ const groups = {
1446
1732
  }
1447
1733
  }
1448
1734
  },
1735
+ footerMultiple: {
1736
+ weight: 2,
1737
+ packages: {
1738
+ axe: {
1739
+ 'landmark-no-duplicate-contentinfo': {
1740
+ quality: 1,
1741
+ what: 'page has more than 1 contentinfo landmark (footer)'
1742
+ }
1743
+ }
1744
+ }
1745
+ },
1746
+ landmarkConfusion: {
1747
+ weight: 3,
1748
+ packages: {
1749
+ axe: {
1750
+ 'landmark-unique': {
1751
+ quality: 1,
1752
+ what: 'Landmark has a role and an accessible name that are identical to another'
1753
+ }
1754
+ }
1755
+ }
1756
+ },
1449
1757
  focusableOperable: {
1450
1758
  weight: 3,
1451
1759
  packages: {
@@ -1485,6 +1793,17 @@ const groups = {
1485
1793
  }
1486
1794
  }
1487
1795
  },
1796
+ labeledHidden: {
1797
+ weight: 2,
1798
+ packages: {
1799
+ htmlcs: {
1800
+ 'w:AA.1_3_1.F68.Hidden': {
1801
+ quality: 1,
1802
+ what: 'Hidden form field is needlessly labeled.'
1803
+ }
1804
+ }
1805
+ }
1806
+ },
1488
1807
  hiddenContentRisk: {
1489
1808
  weight: 1,
1490
1809
  packages: {
@@ -1540,13 +1859,47 @@ const groups = {
1540
1859
  }
1541
1860
  }
1542
1861
  },
1543
- linkUnderlines: {
1862
+ linkComprehensionRisk: {
1863
+ weight: 1,
1864
+ packages: {
1865
+ wave: {
1866
+ 'a:link_suspicious': {
1867
+ quality: 1,
1868
+ what: 'Suspicious link text'
1869
+ }
1870
+ }
1871
+ }
1872
+ },
1873
+ linkVague: {
1874
+ weight: 3,
1875
+ packages: {
1876
+ tenon: {
1877
+ 73: {
1878
+ quality: 1,
1879
+ what: 'Link text is too generic to communicate the purpose or destination'
1880
+ }
1881
+ }
1882
+ }
1883
+ },
1884
+ linkIndication: {
1544
1885
  weight: 2,
1545
1886
  packages: {
1887
+ alfa: {
1888
+ r62: {
1889
+ quality: 1,
1890
+ what: 'Inline link is not distinct from the surrounding text except by color'
1891
+ }
1892
+ },
1893
+ axe: {
1894
+ 'link-in-text-block': {
1895
+ quality: 1,
1896
+ what: 'Link is not distinct from surrounding text without reliance on color'
1897
+ }
1898
+ },
1546
1899
  testaro: {
1547
1900
  linkUl: {
1548
1901
  quality: 1,
1549
- what: 'Non-underlined inline links'
1902
+ what: 'Non-underlined adjacent links'
1550
1903
  }
1551
1904
  }
1552
1905
  }
@@ -1606,6 +1959,34 @@ const groups = {
1606
1959
  }
1607
1960
  }
1608
1961
  },
1962
+ tabIndexPositive: {
1963
+ weight: 1,
1964
+ packages: {
1965
+ axe: {
1966
+ tabindex: {
1967
+ quality: 1,
1968
+ what: 'Positive tabIndex risks creating a confusing focus order'
1969
+ }
1970
+ },
1971
+ wave: {
1972
+ 'a:tabindex': {
1973
+ quality: 1,
1974
+ what: 'tabIndex value positive'
1975
+ }
1976
+ }
1977
+ }
1978
+ },
1979
+ tabIndexMissing: {
1980
+ weight: 4,
1981
+ packages: {
1982
+ tenon: {
1983
+ 190: {
1984
+ quality: 1,
1985
+ what: 'Interactive item is not natively actionable, but has no tabindex=0 attribute'
1986
+ }
1987
+ }
1988
+ }
1989
+ },
1609
1990
  videoCaptionMissing: {
1610
1991
  weight: 4,
1611
1992
  packages: {
@@ -1697,10 +2078,16 @@ const groups = {
1697
2078
  obsoleteElement: {
1698
2079
  weight: 1,
1699
2080
  packages: {
2081
+ alfa: {
2082
+ r70: {
2083
+ quality: 1,
2084
+ what: 'Element is obsolete or deprecated'
2085
+ }
2086
+ },
1700
2087
  htmlcs: {
1701
2088
  'e:AA.1_3_1.H49.Center': {
1702
2089
  quality: 1,
1703
- what: 'The center element is obsolete'
2090
+ what: 'Element is obsolete'
1704
2091
  }
1705
2092
  }
1706
2093
  }