testilo 3.8.2 → 3.8.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.
@@ -0,0 +1,3711 @@
1
+ /*
2
+ sp14a
3
+ Testilo score proc 14a
4
+
5
+ Computes scores from Testaro script tp14 and adds them to a report.
6
+ Usage examples:
7
+ node score sp14a 35k1r
8
+ node score sp14a
9
+
10
+ This proc applies specified weights to the component scores before summing them. An issue reported
11
+ by a test is given a score. That score is determined by:
12
+ Whether the issue is reported as an error or a warning.
13
+ How important the issue is, if the test package is “pre-weighted” (axe, tenon, and testaro)
14
+ Whether the test belongs to a group or is a “solo” test.
15
+ How heavily the group is weighted, if the test package is not pre-weighted and the test belongs
16
+ to a group
17
+
18
+ The scores of solo tests are added together, multiplied by the soloWeight multiplier, and
19
+ contributed to the total score.
20
+
21
+ The scores of grouped tests are aggregated into a group score before being contributed to the
22
+ total score. The group score is the sum of (1) an absolute score, assigned because the group has
23
+ at least one test with a non-zero score, (2) the largest score among the tests of the group
24
+ multiplied by a multiplier, and (3) the sum of the scores from the other tests of the group
25
+ multiplied by a smaller multiplier. These three amounts are given by the groupWeights object.
26
+
27
+ Browser logging produces a log score, and the prevention of tests produces a prevention score.
28
+ They, too, are added to the total score.
29
+
30
+ Each grouped test has a “quality” property, typically set to 1. The value of this property can be
31
+ modified when the test is found to be higher or lower in quality than usual.
32
+ */
33
+
34
+ // CONSTANTS
35
+
36
+ const scoreProcID = 'sp14a';
37
+ // Define the configuration disclosures.
38
+ const logWeights = {
39
+ logCount: 0.5,
40
+ logSize: 0.01,
41
+ errorLogCount: 1,
42
+ errorLogSize: 0.02,
43
+ prohibitedCount: 15,
44
+ visitTimeoutCount: 10,
45
+ visitRejectionCount: 10
46
+ };
47
+ const soloWeight = 2;
48
+ const groupWeights = {
49
+ absolute: 2,
50
+ largest: 1,
51
+ smaller: 0.4
52
+ };
53
+ const preventionWeights = {
54
+ testaro: 50,
55
+ other: 100
56
+ };
57
+ const otherPackages = ['alfa', 'axe', 'continuum', 'htmlcs', 'ibm', 'tenon', 'wave'];
58
+ const preWeightedPackages = ['axe', 'tenon', 'testaro'];
59
+ // Define the test groups.
60
+ const groups = {
61
+ duplicateID: {
62
+ weight: 3,
63
+ packages: {
64
+ alfa: {
65
+ r3: {
66
+ quality: 1,
67
+ what: 'Element id attribute value is not unique'
68
+ }
69
+ },
70
+ axe: {
71
+ 'duplicate-id': {
72
+ quality: 1,
73
+ what: 'id attribute value is not unique'
74
+ },
75
+ 'duplicate-id-active': {
76
+ quality: 1,
77
+ what: 'id attribute value of the active element is not unique'
78
+ },
79
+ 'duplicate-id-aria': {
80
+ quality: 1,
81
+ what: 'id attribute used in ARIA or in a label has a value that is not unique'
82
+ }
83
+ },
84
+ continuum: {
85
+ 94: {
86
+ quality: 1,
87
+ what: 'Elements contains an id attribute set to a value that is not unique in the DOM'
88
+ }
89
+ },
90
+ htmlcs: {
91
+ 'e:AA.4_1_1.F77': {
92
+ quality: 1,
93
+ what: 'Duplicate id attribute value'
94
+ }
95
+ },
96
+ ibm: {
97
+ RPT_Elem_UniqueId: {
98
+ quality: 1,
99
+ what: 'Element id attribute value is not unique within the document'
100
+ }
101
+ }
102
+ }
103
+ },
104
+ regionNoText: {
105
+ weight: 4,
106
+ packages: {
107
+ alfa: {
108
+ r40: {
109
+ quality: 1,
110
+ what: 'Region has no accessible name'
111
+ }
112
+ },
113
+ continuum: {
114
+ 1010: {
115
+ quality: 1,
116
+ what: 'Element with a region role has no mechanism that allows an accessible name to be calculated'
117
+ }
118
+ }
119
+ }
120
+ },
121
+ formFieldNoText: {
122
+ weight: 4,
123
+ packages: {
124
+ alfa: {
125
+ r8: {
126
+ quality: 1,
127
+ what: 'Form field has no accessible name'
128
+ }
129
+ }
130
+ }
131
+ },
132
+ inputNoText: {
133
+ weight: 4,
134
+ packages: {
135
+ axe: {
136
+ 'aria-input-field-name': {
137
+ quality: 1,
138
+ what: 'ARIA input field has no accessible name'
139
+ }
140
+ },
141
+ continuum: {
142
+ 118: {
143
+ quality: 1,
144
+ what: 'Text input element has no mechanism that allows an accessible name to be calculated'
145
+ },
146
+ 370: {
147
+ quality: 1,
148
+ what: 'Search input element has no mechanism that allows an accessible name to be calculated'
149
+ },
150
+ 509: {
151
+ quality: 1,
152
+ what: 'element with a textbox role has no mechanism that allows an accessible name to be calculated'
153
+ }
154
+ },
155
+ htmlcs: {
156
+ 'e:AA.4_1_2.H91.InputText.Name': {
157
+ quality: 1,
158
+ what: 'Text input has no accessible name'
159
+ },
160
+ 'e:AA.4_1_2.H91.InputEmail.Name': {
161
+ quality: 1,
162
+ what: 'Email input has no accessible name'
163
+ },
164
+ 'e:AA.4_1_2.H91.InputFile.Name': {
165
+ quality: 1,
166
+ what: 'File input element has no accessible name'
167
+ },
168
+ 'e:AA.4_1_2.H91.InputTel.Name': {
169
+ quality: 1,
170
+ what: 'Telephone input has no accessible name'
171
+ },
172
+ 'e:AA.4_1_2.H91.InputNumber.Name': {
173
+ quality: 1,
174
+ what: 'Number input has no accessible name'
175
+ },
176
+ 'e:AA.4_1_2.H91.InputSearch.Name': {
177
+ quality: 1,
178
+ what: 'Search input has no accessible name'
179
+ },
180
+ 'e:AA.4_1_2.H91.InputCheckbox.Name': {
181
+ quality: 1,
182
+ what: 'Checkbox input has no accessible name'
183
+ }
184
+ }
185
+ }
186
+ },
187
+ inputOnlyPlaceholder: {
188
+ weight: 3,
189
+ packages: {
190
+ continuum: {
191
+ 863: {
192
+ quality: 1,
193
+ what: 'input has an accessible name that depends on a placeholder'
194
+ }
195
+ }
196
+ }
197
+ },
198
+ imageInputNoText: {
199
+ weight: 4,
200
+ packages: {
201
+ alfa: {
202
+ r28: {
203
+ quality: 1,
204
+ what: 'Image input element has no accessible name'
205
+ }
206
+ },
207
+ axe: {
208
+ 'input-image-alt': {
209
+ quality: 1,
210
+ what: 'Image button has no text alternative'
211
+ }
212
+ },
213
+ htmlcs: {
214
+ 'e:H36': {
215
+ quality: 1,
216
+ what: 'Image submit button has no alt attribute'
217
+ }
218
+ },
219
+ ibm: {
220
+ 'v:WCAG20_Input_ExplicitLabelImage': {
221
+ quality: 1,
222
+ what: 'Input element of type image has no text alternative'
223
+ }
224
+ },
225
+ wave: {
226
+ 'e:alt_input_missing': {
227
+ quality: 1,
228
+ what: 'Image button missing alternative text'
229
+ }
230
+ }
231
+ }
232
+ },
233
+ imageNoText: {
234
+ weight: 4,
235
+ packages: {
236
+ alfa: {
237
+ r2: {
238
+ quality: 1,
239
+ what: 'Image has no accessible name'
240
+ }
241
+ },
242
+ axe: {
243
+ 'image-alt': {
244
+ quality: 1,
245
+ what: 'Image has no text alternative'
246
+ },
247
+ 'role-img-alt': {
248
+ quality: 1,
249
+ what: 'Element with role img has no text alternative'
250
+ }
251
+ },
252
+ continuum: {
253
+ 87: {
254
+ quality: 1,
255
+ what: 'element with an image, graphics-symbol, or graphics-document role has no mechanism to calculate an accessible name'
256
+ },
257
+ 89: {
258
+ quality: 1,
259
+ what: 'img element has no mechanism that allows an accessible name to be calculated'
260
+ }
261
+ },
262
+ htmlcs: {
263
+ 'e:AA.1_1_1.H37': {
264
+ quality: 1,
265
+ what: 'img element has no alt attribute'
266
+ }
267
+ },
268
+ ibm: {
269
+ 'v:WCAG20_Img_HasAlt': {
270
+ quality: 1,
271
+ what: 'Image has no alt attribute conveying its meaning, or alt="" if decorative'
272
+ }
273
+ },
274
+ wave: {
275
+ 'e:alt_missing': {
276
+ quality: 1,
277
+ what: 'Text alternative is missing'
278
+ },
279
+ 'e:alt_spacer_missing': {
280
+ quality: 1,
281
+ what: 'Spacer image has no text alternative'
282
+ }
283
+ }
284
+ }
285
+ },
286
+ imageTextBad: {
287
+ weight: 3,
288
+ packages: {
289
+ alfa: {
290
+ 'r39': {
291
+ quality: 1,
292
+ what: 'Image text alternative is the filename instead'
293
+ }
294
+ }
295
+ }
296
+ },
297
+ inputAlt: {
298
+ weight: 4,
299
+ packages: {
300
+ continuum: {
301
+ 15: {
302
+ quality: 1,
303
+ what: 'input element has an alt attribute'
304
+ }
305
+ }
306
+ }
307
+ },
308
+ imagesSameAlt: {
309
+ weight: 1,
310
+ packages: {
311
+ wave: {
312
+ 'a:alt_duplicate': {
313
+ quality: 1,
314
+ what: 'Two images near each other have the same text alternative'
315
+ }
316
+ }
317
+ }
318
+ },
319
+ imageTextLong: {
320
+ weight: 2,
321
+ packages: {
322
+ wave: {
323
+ 'a:alt_long': {
324
+ quality: 1,
325
+ what: 'Long text alternative'
326
+ }
327
+ }
328
+ }
329
+ },
330
+ imageTextRisk: {
331
+ weight: 1,
332
+ packages: {
333
+ continuum: {
334
+ 234: {
335
+ quality: 1,
336
+ what: 'img element has a suspicious calculated accessible name value'
337
+ },
338
+ },
339
+ wave: {
340
+ 'a:alt_suspicious': {
341
+ quality: 1,
342
+ what: 'Image text alternative is suspicious'
343
+ }
344
+ }
345
+ }
346
+ },
347
+ decorativeImageRisk: {
348
+ weight: 1,
349
+ packages: {
350
+ htmlcs: {
351
+ 'w:AA.1_1_1.H67.2': {
352
+ quality: 1,
353
+ what: 'Image marked as decorative may be informative'
354
+ }
355
+ }
356
+ }
357
+ },
358
+ decorativeElementExposed: {
359
+ weight: 1,
360
+ packages: {
361
+ alfa: {
362
+ r67: {
363
+ quality: 1,
364
+ what: 'Image marked as decorative is in the accessibility tree or has no none/presentation role'
365
+ },
366
+ r86: {
367
+ quality: 1,
368
+ what: 'Element marked as decorative is in the accessibility tree or has no none/presentation role'
369
+ }
370
+ }
371
+ }
372
+ },
373
+ pageLanguage: {
374
+ weight: 4,
375
+ packages: {
376
+ alfa: {
377
+ r4: {
378
+ quality: 1,
379
+ what: 'Lang attribute missing, empty, or only whitespace'
380
+ }
381
+ },
382
+ axe: {
383
+ 'html-has-lang': {
384
+ quality: 1,
385
+ what: 'html element has no lang attribute'
386
+ }
387
+ },
388
+ continuum: {
389
+ 101: {
390
+ quality: 1,
391
+ what: 'root html element has no lang attribute'
392
+ }
393
+ },
394
+ htmlcs: {
395
+ 'e:AA.3_1_1.H57.2': {
396
+ quality: 1,
397
+ what: 'html element has no lang or xml:lang attribute'
398
+ }
399
+ },
400
+ ibm: {
401
+ WCAG20_Html_HasLang: {
402
+ quality: 1,
403
+ what: 'Page detected as HTML, but has no lang attribute'
404
+ }
405
+ },
406
+ wave: {
407
+ 'e:language_missing': {
408
+ quality: 1,
409
+ what: 'Language missing or invalid'
410
+ }
411
+ }
412
+ }
413
+ },
414
+ pageLanguageBad: {
415
+ weight: 4,
416
+ packages: {
417
+ alfa: {
418
+ r5: {
419
+ quality: 1,
420
+ what: 'lang attribute has no valid primary language tag'
421
+ }
422
+ },
423
+ axe: {
424
+ 'html-lang-valid': {
425
+ quality: 1,
426
+ what: 'html element has no valid value for the lang attribute'
427
+ }
428
+ },
429
+ htmlcs: {
430
+ 'e:AA.3_1_1.H57.3.Lang': {
431
+ quality: 1,
432
+ what: 'Language specified in the lang attribute of the document does not appear to be well-formed'
433
+ }
434
+ },
435
+ ibm: {
436
+ 'v:WCAG20_Elem_Lang_Valid': {
437
+ quality: 1,
438
+ what: 'lang attribute does not include a valid primary language'
439
+ }
440
+ }
441
+ }
442
+ },
443
+ elementLanguageBad: {
444
+ weight: 4,
445
+ packages: {
446
+ htmlcs: {
447
+ 'e:AA.3_1_2.H58.1.Lang': {
448
+ quality: 1,
449
+ what: 'Language specified in the lang attribute of the element does not appear to be well-formed'
450
+ }
451
+ }
452
+ }
453
+ },
454
+ languageChange: {
455
+ weight: 3,
456
+ packages: {
457
+ alfa: {
458
+ r7: {
459
+ quality: 1,
460
+ what: 'lang attribute has no valid primary language subtag'
461
+ }
462
+ },
463
+ axe: {
464
+ 'valid-lang': {
465
+ quality: 1,
466
+ what: 'lang attribute has no valid value'
467
+ }
468
+ },
469
+ htmlcs: {
470
+ 'e:WCAG2AAA.Principle3.Guideline3_1.3_1_2.H58': {
471
+ quality: 1,
472
+ what: 'Change in language is not marked'
473
+ }
474
+ }
475
+ }
476
+ },
477
+ dialogNoText: {
478
+ weight: 4,
479
+ packages: {
480
+ axe: {
481
+ 'aria-dialog-name': {
482
+ quality: 1,
483
+ what: 'ARIA dialog or alertdialog node has no accessible name'
484
+ }
485
+ },
486
+ continuum: {
487
+ 736: {
488
+ quality: 1,
489
+ what: 'Element with a dialog role has no mechanism that allows an accessible name to be calculated.'
490
+ }
491
+ }
492
+ }
493
+ },
494
+ objectNoText: {
495
+ weight: 4,
496
+ packages: {
497
+ alfa: {
498
+ r63: {
499
+ quality: 1,
500
+ what: 'Object element has no accessible name'
501
+ }
502
+ },
503
+ axe: {
504
+ 'object-alt': {
505
+ quality: 1,
506
+ what: 'Object element has no text alternative'
507
+ }
508
+ },
509
+ continuum: {
510
+ 249: {
511
+ quality: 1,
512
+ what: 'object element has no mechanism that allows an accessible name to be calculated'
513
+ }
514
+ },
515
+ htmlcs: {
516
+ 'e:ARIA6+H53': {
517
+ quality: 1,
518
+ what: 'Object element contains no text alternative'
519
+ }
520
+ },
521
+ ibm: {
522
+ 'v:WCAG20_Object_HasText': {
523
+ quality: 1,
524
+ what: 'Object element has no text alternative'
525
+ }
526
+ },
527
+ wave: {
528
+ 'a:plugin': {
529
+ quality: 1,
530
+ what: 'An unidentified plugin is present'
531
+ }
532
+ }
533
+ }
534
+ },
535
+ videoNoText: {
536
+ weight: 4,
537
+ packages: {
538
+ continuum: {
539
+ 252: {
540
+ quality: 1,
541
+ what: 'video element has no mechanism that allows an accessible name to be calculated'
542
+ }
543
+ }
544
+ }
545
+ },
546
+ imageMapNoText: {
547
+ weight: 4,
548
+ packages: {
549
+ wave: {
550
+ 'e:alt_map_missing': {
551
+ quality: 1,
552
+ what: 'Image that has hot spots has no alt attribute'
553
+ }
554
+ }
555
+ }
556
+ },
557
+ imageMapAreaNoText: {
558
+ weight: 4,
559
+ packages: {
560
+ axe: {
561
+ 'area-alt': {
562
+ quality: 1,
563
+ what: 'Active area element has no text alternative'
564
+ }
565
+ },
566
+ htmlcs: {
567
+ 'e:AA.1_1_1.H24': {
568
+ quality: 1,
569
+ what: 'Area element in an image map missing an alt attribute'
570
+ }
571
+ },
572
+ ibm: {
573
+ 'v:HAAC_Img_UsemapAlt': {
574
+ quality: 1,
575
+ what: 'Image map or child area has no text alternative'
576
+ },
577
+ 'v:WCAG20_Area_HasAlt': {
578
+ quality: 1,
579
+ what: 'Area element in an image map has no text alternative'
580
+ }
581
+ },
582
+ wave: {
583
+ 'e:alt_area_missing': {
584
+ quality: 1,
585
+ what: 'Image map area missing alternative text'
586
+ }
587
+ }
588
+ }
589
+ },
590
+ objectBlurKeyboardRisk: {
591
+ weight: 1,
592
+ packages: {
593
+ htmlcs: {
594
+ 'w:AA.2_1_2.F10': {
595
+ quality: 1,
596
+ what: 'Applet or plugin may fail to enable moving the focus away with the keyboard'
597
+ }
598
+ }
599
+ }
600
+ },
601
+ keyboardAccess: {
602
+ weight: 4,
603
+ packages: {
604
+ tenon: {
605
+ 180: {
606
+ quality: 1,
607
+ what: 'Element is interactive but has a negative tabindex value'
608
+ }
609
+ }
610
+ }
611
+ },
612
+ eventKeyboardRisk: {
613
+ weight: 1,
614
+ packages: {
615
+ htmlcs: {
616
+ 'w:AA.2_1_1.G90': {
617
+ quality: 1,
618
+ what: 'Event handler functionality may not be available by keyboard'
619
+ },
620
+ 'w:AA.2_1_1.SCR20.MouseOut': {
621
+ quality: 1,
622
+ what: 'Mousing-out functionality may not be available by keyboard'
623
+ },
624
+ 'w:AA.2_1_1.SCR20.MouseOver': {
625
+ quality: 1,
626
+ what: 'Mousing-over functionality may not be available by keyboard'
627
+ },
628
+ 'w:AA.2_1_1.SCR20.MouseDown': {
629
+ quality: 1,
630
+ what: 'Mousing-down functionality may not be available by keyboard'
631
+ }
632
+ },
633
+ wave: {
634
+ 'a:event_handler': {
635
+ quality: 1,
636
+ what: 'Device-dependent event handler'
637
+ }
638
+ }
639
+ }
640
+ },
641
+ internalLinkBroken: {
642
+ weight: 4,
643
+ packages: {
644
+ htmlcs: {
645
+ 'e:AA.2_4_1.G1,G123,G124.NoSuchID': {
646
+ quality: 1,
647
+ what: 'Internal link references a nonexistent destination'
648
+ }
649
+ },
650
+ wave: {
651
+ 'a:label_orphaned': {
652
+ quality: 1,
653
+ what: 'Orphaned form label'
654
+ },
655
+ 'a:link_internal_broken': {
656
+ quality: 1,
657
+ what: 'Broken same-page link'
658
+ }
659
+ }
660
+ }
661
+ },
662
+ labelForWrongRisk: {
663
+ weight: 1,
664
+ packages: {
665
+ htmlcs: {
666
+ 'w:AA.1_3_1.H44.NotFormControl': {
667
+ quality: 1,
668
+ what: 'Label for attribute may reference the wrong element, because it is not a form control'
669
+ }
670
+ }
671
+ }
672
+ },
673
+ activeDescendantBadID: {
674
+ weight: 4,
675
+ packages: {
676
+ continuum: {
677
+ 290: {
678
+ quality: 1,
679
+ what: 'aria-activedescendant attribute is set to an invalid or duplicate id'
680
+ }
681
+ }
682
+ }
683
+ },
684
+ controlleeBadID: {
685
+ weight: 4,
686
+ packages: {
687
+ continuum: {
688
+ 85: {
689
+ quality: 1,
690
+ what: 'aria-controls attribute references an invalid or duplicate ID'
691
+ }
692
+ }
693
+ }
694
+ },
695
+ descriptionBadID: {
696
+ weight: 4,
697
+ packages: {
698
+ continuum: {
699
+ 83: {
700
+ quality: 1,
701
+ what: 'aria-describedby attribute references an invalid or duplicate ID'
702
+ }
703
+ }
704
+ }
705
+ },
706
+ labelBadID: {
707
+ weight: 4,
708
+ packages: {
709
+ continuum: {
710
+ 95: {
711
+ quality: 1,
712
+ what: 'element has an aria-labelledby value that includes an invalid or duplicate id'
713
+ }
714
+ },
715
+ htmlcs: {
716
+ 'w:AA.1_3_1.H44.NonExistentFragment': {
717
+ quality: 1,
718
+ what: 'Label for attribute references a nonexistent element'
719
+ },
720
+ 'w:AA.1_3_1.ARIA16,ARIA9': {
721
+ quality: 1,
722
+ what: 'aria-labelledby attribute references a nonexistent element'
723
+ },
724
+ 'w:AA.4_1_2.ARIA16,ARIA9': {
725
+ quality: 1,
726
+ what: 'aria-labelledby attribute references a nonexistent element'
727
+ }
728
+ },
729
+ wave: {
730
+ 'a:label_orphaned': {
731
+ quality: 1,
732
+ what: 'Orphaned form label'
733
+ }
734
+ }
735
+ }
736
+ },
737
+ ownerConflict: {
738
+ weight: 4,
739
+ packages: {
740
+ continuum: {
741
+ 360: {
742
+ quality: 1,
743
+ what: 'Element and another element have aria-owns attributes with identical id values'
744
+ }
745
+ }
746
+ }
747
+ },
748
+ linkNoText: {
749
+ weight: 4,
750
+ packages: {
751
+ alfa: {
752
+ r11: {
753
+ quality: 1,
754
+ what: 'Link has no accessible name'
755
+ }
756
+ },
757
+ axe: {
758
+ 'link-name': {
759
+ quality: 1,
760
+ what: 'Link has no discernible text'
761
+ }
762
+ },
763
+ continuum: {
764
+ 237: {
765
+ quality: 1,
766
+ what: 'a element has no mechanism that allows an accessible name value to be calculated'
767
+ }
768
+ },
769
+ htmlcs: {
770
+ 'e:AA.1_1_1.H30.2': {
771
+ quality: 1,
772
+ what: 'img element is the only link content but has no text alternative'
773
+ },
774
+ 'w:AA.4_1_2.H91.A.Empty': {
775
+ quality: 1,
776
+ what: 'Link element has an id attribute but no href attribute or text'
777
+ },
778
+ 'e:AA.4_1_2.H91.A.EmptyNoId': {
779
+ quality: 1,
780
+ what: 'Link has no name or id attribute or value'
781
+ },
782
+ 'w:AA.4_1_2.H91.A.EmptyWithName': {
783
+ quality: 1,
784
+ what: 'Link has a name attribute but no href attribute or text'
785
+ },
786
+ 'e:AA.4_1_2.H91.A.NoContent': {
787
+ quality: 1,
788
+ what: 'Link has an href attribute but no text'
789
+ }
790
+ },
791
+ ibm: {
792
+ 'v:WCAG20_A_HasText': {
793
+ quality: 1,
794
+ what: 'Hyperlink has no text description'
795
+ }
796
+ },
797
+ tenon: {
798
+ 57: {
799
+ quality: 1,
800
+ what: 'Link has no text inside it'
801
+ },
802
+ 91: {
803
+ quality: 1,
804
+ what: 'Link has a background image but no text inside it'
805
+ }
806
+ },
807
+ wave: {
808
+ 'e:link_empty': {
809
+ quality: 1,
810
+ what: 'Link contains no text'
811
+ },
812
+ 'e:alt_link_missing': {
813
+ quality: 1,
814
+ what: 'Linked image has no text alternative'
815
+ },
816
+ }
817
+ }
818
+ },
819
+ linkBrokenRisk: {
820
+ weight: 2,
821
+ packages: {
822
+ htmlcs: {
823
+ 'w:AA.4_1_2.H91.A.Placeholder': {
824
+ quality: 1,
825
+ what: 'Link has text but no href, id, or name attribute'
826
+ }
827
+ }
828
+ }
829
+ },
830
+ acronymNoTitle: {
831
+ weight: 4,
832
+ packages: {
833
+ tenon: {
834
+ 117: {
835
+ quality: 1,
836
+ what: 'acronym element has no useful title value (and is deprecated; use abbr)'
837
+ }
838
+ }
839
+ }
840
+ },
841
+ abbreviationNoTitle: {
842
+ weight: 4,
843
+ packages: {
844
+ tenon: {
845
+ 233: {
846
+ quality: 1,
847
+ what: 'abbr element is first for its abbreviation but has no useful title value'
848
+ }
849
+ }
850
+ }
851
+ },
852
+ pdfLink: {
853
+ weight: 1,
854
+ packages: {
855
+ wave: {
856
+ 'a:link_pdf': {
857
+ quality: 1,
858
+ what: 'Link to PDF document'
859
+ }
860
+ }
861
+ }
862
+ },
863
+ destinationLink: {
864
+ weight: 2,
865
+ packages: {
866
+ htmlcs: {
867
+ 'w:AA.4_1_2.H91.A.NoHref': {
868
+ quality: 1,
869
+ what: 'Link is misused as a link destination'
870
+ }
871
+ }
872
+ }
873
+ },
874
+ textAreaNoText: {
875
+ weight: 4,
876
+ packages: {
877
+ htmlcs: {
878
+ 'e:AA.4_1_2.H91.Textarea.Name': {
879
+ quality: 1,
880
+ what: 'textarea element has no accessible name'
881
+ }
882
+ }
883
+ }
884
+ },
885
+ linkTextsSame: {
886
+ weight: 2,
887
+ packages: {
888
+ htmlcs: {
889
+ 'e:AA.1_1_1.H2.EG3': {
890
+ quality: 1,
891
+ what: 'alt value of the link img element duplicates the text of a link beside it'
892
+ }
893
+ },
894
+ tenon: {
895
+ 98: {
896
+ quality: 1,
897
+ what: 'Links have the same text but different destinations'
898
+ }
899
+ }
900
+ }
901
+ },
902
+ nextLinkDestinationSame: {
903
+ weight: 2,
904
+ packages: {
905
+ tenon: {
906
+ 184: {
907
+ quality: 1,
908
+ what: 'Adjacent links point to the same destination'
909
+ }
910
+ }
911
+ }
912
+ },
913
+ linkDestinationsSame: {
914
+ weight: 2,
915
+ packages: {
916
+ tenon: {
917
+ 132: {
918
+ quality: 1,
919
+ what: 'area element has the same href as another but a different alt'
920
+ }
921
+ }
922
+ }
923
+ },
924
+ linkConfusionRisk: {
925
+ weight: 1,
926
+ packages: {
927
+ axe: {
928
+ 'identical-links-same-purpose': {
929
+ quality: 1,
930
+ what: 'Links with the same accessible name may serve dissimilar purposes'
931
+ }
932
+ }
933
+ }
934
+ },
935
+ linkPair: {
936
+ weight: 2,
937
+ packages: {
938
+ wave: {
939
+ 'a:link_redundant': {
940
+ quality: 1,
941
+ what: 'Adjacent links go to the same URL'
942
+ }
943
+ }
944
+ }
945
+ },
946
+ formNewWindow: {
947
+ weight: 2,
948
+ packages: {
949
+ tenon: {
950
+ 214: {
951
+ quality: 1,
952
+ what: 'Form submission opens a new window'
953
+ }
954
+ }
955
+ }
956
+ },
957
+ linkForcesNewWindow: {
958
+ weight: 3,
959
+ packages: {
960
+ tenon: {
961
+ 218: {
962
+ quality: 1,
963
+ what: 'Link opens in a new window without user control'
964
+ }
965
+ }
966
+ }
967
+ },
968
+ linkWindowSurpriseRisk: {
969
+ weight: 1,
970
+ packages: {
971
+ htmlcs: {
972
+ 'w:WCAG2AAA.Principle3.Guideline3_2.3_2_5.H83.3': {
973
+ quality: 1,
974
+ what: 'Link may open in a new window without notice'
975
+ }
976
+ }
977
+ }
978
+ },
979
+ selectNavSurpriseRisk: {
980
+ weight: 1,
981
+ packages: {
982
+ wave: {
983
+ 'a:javascript_jumpmenu': {
984
+ quality: 1,
985
+ what: 'selection change may navigate to another page without notice'
986
+ }
987
+ }
988
+ }
989
+ },
990
+ buttonNoText: {
991
+ weight: 4,
992
+ packages: {
993
+ alfa: {
994
+ r12: {
995
+ quality: 1,
996
+ what: 'Button has no accessible name'
997
+ }
998
+ },
999
+ axe: {
1000
+ 'aria-command-name': {
1001
+ quality: 1,
1002
+ what: 'ARIA command has no accessible name'
1003
+ },
1004
+ 'button-name': {
1005
+ quality: 1,
1006
+ what: 'Button has no discernible text'
1007
+ },
1008
+ 'input-button-name': {
1009
+ quality: 1,
1010
+ what: 'Input button has no discernible text'
1011
+ }
1012
+ },
1013
+ continuum: {
1014
+ 224: {
1015
+ quality: 1,
1016
+ what: 'button element has no mechanism that allows an accessible name to be calculated'
1017
+ }
1018
+ },
1019
+ htmlcs: {
1020
+ 'e:AA.4_1_2.H91.A.Name': {
1021
+ quality: 1,
1022
+ what: 'Link with button role has no accessible name'
1023
+ },
1024
+ 'e:AA.4_1_2.H91.Div.Name': {
1025
+ quality: 1,
1026
+ what: 'div element with button role has no accessible name'
1027
+ },
1028
+ 'e:AA.4_1_2.H91.Button.Name': {
1029
+ quality: 1,
1030
+ what: 'Button element has no accessible name'
1031
+ },
1032
+ 'e:AA.4_1_2.H91.Img.Name': {
1033
+ quality: 1,
1034
+ what: 'img element with button role has no accessible name'
1035
+ },
1036
+ 'e:AA.4_1_2.H91.InputButton.Name': {
1037
+ quality: 1,
1038
+ what: 'Button input element has no accessible name'
1039
+ },
1040
+ 'e:AA.4_1_2.H91.Span.Name': {
1041
+ quality: 1,
1042
+ what: 'Element with button role has no accessible name'
1043
+ }
1044
+ },
1045
+ wave: {
1046
+ 'e:button_empty': {
1047
+ quality: 1,
1048
+ what: 'Button is empty or has no value text'
1049
+ }
1050
+ }
1051
+ }
1052
+ },
1053
+ parentMissing: {
1054
+ weight: 4,
1055
+ packages: {
1056
+ alfa: {
1057
+ r42: {
1058
+ quality: 1,
1059
+ what: 'Element is not owned by an element of its required context role'
1060
+ }
1061
+ },
1062
+ axe: {
1063
+ 'aria-required-parent': {
1064
+ quality: 1,
1065
+ what: 'ARIA role is not contained by a required parent'
1066
+ }
1067
+ }
1068
+ }
1069
+ },
1070
+ svgImageNoText: {
1071
+ weight: 4,
1072
+ packages: {
1073
+ alfa: {
1074
+ r43: {
1075
+ quality: 1,
1076
+ what: 'SVG image element has no accessible name'
1077
+ }
1078
+ },
1079
+ axe: {
1080
+ 'svg-img-alt': {
1081
+ quality: 1,
1082
+ what: 'svg element with an img role has no text alternative'
1083
+ }
1084
+ },
1085
+ continuum: {
1086
+ 123: {
1087
+ quality: 1,
1088
+ what: 'svg element has no mechanism that allows an accessible name to be calculated'
1089
+ }
1090
+ }
1091
+ }
1092
+ },
1093
+ cssBansRotate: {
1094
+ weight: 4,
1095
+ packages: {
1096
+ axe: {
1097
+ 'css-orientation-lock': {
1098
+ quality: 1,
1099
+ what: 'CSS media query locks display orientation'
1100
+ }
1101
+ }
1102
+ }
1103
+ },
1104
+ textRotated: {
1105
+ weight: 2,
1106
+ packages: {
1107
+ tenon: {
1108
+ 271: {
1109
+ quality: 1,
1110
+ what: 'Text is needlessly rotated 60+ degrees or more, hurting comprehension'
1111
+ }
1112
+ }
1113
+ }
1114
+ },
1115
+ metaBansZoom: {
1116
+ weight: 4,
1117
+ packages: {
1118
+ alfa: {
1119
+ r47: {
1120
+ quality: 1,
1121
+ what: 'Meta element restricts zooming'
1122
+ }
1123
+ },
1124
+ axe: {
1125
+ 'meta-viewport': {
1126
+ quality: 1,
1127
+ what: 'Zooming and scaling are disabled'
1128
+ },
1129
+ 'meta-viewport-large': {
1130
+ quality: 1,
1131
+ what: 'User cannot zoom and scale the text up to 500%'
1132
+ }
1133
+ },
1134
+ continuum: {
1135
+ 55: {
1136
+ quality: 1,
1137
+ what: 'meta element in the head stops a user from scaling the viewport size'
1138
+ },
1139
+ 59: {
1140
+ quality: 1,
1141
+ what: 'meta element in the head sets the viewport maximum-scale to less than 2'
1142
+ }
1143
+ }
1144
+ }
1145
+ },
1146
+ childMissing: {
1147
+ weight: 4,
1148
+ packages: {
1149
+ alfa: {
1150
+ r68: {
1151
+ quality: 1,
1152
+ what: 'Element does not own an element required by its semantic role'
1153
+ }
1154
+ },
1155
+ axe: {
1156
+ 'aria-required-children': {
1157
+ quality: 1,
1158
+ what: 'ARIA role does not contain a required child'
1159
+ }
1160
+ }
1161
+ }
1162
+ },
1163
+ presentationChild: {
1164
+ weight: 4,
1165
+ packages: {
1166
+ htmlcs: {
1167
+ 'e:AA.1_3_1.F92,ARIA4': {
1168
+ quality: 1,
1169
+ what: 'Element has presentation role but semantic child'
1170
+ }
1171
+ }
1172
+ }
1173
+ },
1174
+ fontSizeAbsolute: {
1175
+ weight: 2,
1176
+ packages: {
1177
+ alfa: {
1178
+ r74: {
1179
+ quality: 1,
1180
+ what: 'Paragraph text has an absolute font size'
1181
+ }
1182
+ }
1183
+ }
1184
+ },
1185
+ fontSmall: {
1186
+ weight: 3,
1187
+ packages: {
1188
+ alfa: {
1189
+ r75: {
1190
+ quality: 1,
1191
+ what: 'Font size is smaller than 9 pixels'
1192
+ }
1193
+ },
1194
+ tenon: {
1195
+ 134: {
1196
+ quality: 1,
1197
+ what: 'Text is very small'
1198
+ }
1199
+ },
1200
+ wave: {
1201
+ 'a:text_small': {
1202
+ quality: 1,
1203
+ what: 'Text is very small'
1204
+ }
1205
+ }
1206
+ }
1207
+ },
1208
+ leadingFrozen: {
1209
+ weight: 4,
1210
+ packages: {
1211
+ alfa: {
1212
+ r93: {
1213
+ quality: 1,
1214
+ what: 'Style attribute with !important prevents adjusting line height'
1215
+ }
1216
+ },
1217
+ axe: {
1218
+ 'avoid-inline-spacing': {
1219
+ quality: 1,
1220
+ what: 'Inline text spacing is not adjustable with a custom stylesheet'
1221
+ }
1222
+ }
1223
+ }
1224
+ },
1225
+ leadingAbsolute: {
1226
+ weight: 2,
1227
+ packages: {
1228
+ alfa: {
1229
+ r80: {
1230
+ quality: 1,
1231
+ what: 'Paragraph text has an absolute line height'
1232
+ }
1233
+ }
1234
+ }
1235
+ },
1236
+ noLeading: {
1237
+ weight: 3,
1238
+ packages: {
1239
+ alfa: {
1240
+ r73: {
1241
+ quality: 1,
1242
+ what: 'Paragraph of text has insufficient line height'
1243
+ }
1244
+ }
1245
+ }
1246
+ },
1247
+ leadingClipsText: {
1248
+ weight: 4,
1249
+ packages: {
1250
+ tenon: {
1251
+ 144: {
1252
+ quality: 1,
1253
+ what: 'Line height is insufficent to properly display the computed font size'
1254
+ }
1255
+ }
1256
+ }
1257
+ },
1258
+ overflowHidden: {
1259
+ weight: 4,
1260
+ packages: {
1261
+ alfa: {
1262
+ r83: {
1263
+ quality: 1,
1264
+ what: 'Overflow is hidden or clipped if the text is enlarged'
1265
+ }
1266
+ }
1267
+ }
1268
+ },
1269
+ iframeTitleBad: {
1270
+ weight: 4,
1271
+ packages: {
1272
+ alfa: {
1273
+ r13: {
1274
+ quality: 1,
1275
+ what: 'iframe has no accessible name'
1276
+ }
1277
+ },
1278
+ axe: {
1279
+ 'frame-title': {
1280
+ quality: 1,
1281
+ what: 'Frame has no accessible name'
1282
+ },
1283
+ 'frame-title-unique': {
1284
+ quality: 1,
1285
+ what: 'Frame title attribute is not unique'
1286
+ }
1287
+ },
1288
+ continuum: {
1289
+ 228: {
1290
+ quality: 1,
1291
+ what: 'iframe has no mechanism that allows an accessible name to be calculated'
1292
+ }
1293
+ },
1294
+ htmlcs: {
1295
+ 'e:AA.2_4_1.H64.1': {
1296
+ quality: 1,
1297
+ what: 'iframe element has no non-empty title attribute'
1298
+ }
1299
+ },
1300
+ ibm: {
1301
+ 'v:WCAG20_Frame_HasTitle': {
1302
+ quality: 1,
1303
+ what: 'Inline frame has an empty or nonunique title attribute'
1304
+ }
1305
+ }
1306
+ }
1307
+ },
1308
+ roleBad: {
1309
+ weight: 3,
1310
+ packages: {
1311
+ alfa: {
1312
+ r21: {
1313
+ quality: 1,
1314
+ what: 'Element does not have a valid role'
1315
+ }
1316
+ },
1317
+ axe: {
1318
+ 'aria-roles': {
1319
+ quality: 1,
1320
+ what: 'ARIA role has an invalid value'
1321
+ },
1322
+ 'aria-allowed-role': {
1323
+ quality: 1,
1324
+ what: 'ARIA role is not appropriate for the element'
1325
+ }
1326
+ },
1327
+ continuum: {
1328
+ 37: {
1329
+ quality: 1,
1330
+ what: 'a element has a role attribute that is not allowed'
1331
+ },
1332
+ 44: {
1333
+ quality: 1,
1334
+ what: 'hr element has a role attribute'
1335
+ },
1336
+ 176: {
1337
+ quality: 1,
1338
+ what: 'label element has a role attribute'
1339
+ },
1340
+ 319: {
1341
+ quality: 1,
1342
+ what: 'ol element has a role attribute that is not allowed'
1343
+ },
1344
+ 325: {
1345
+ quality: 1,
1346
+ what: 'ul element has a role attribute that is not allowed'
1347
+ },
1348
+ 412: {
1349
+ quality: 1,
1350
+ what: 'element has a role attribute set to an invalid ARIA role value'
1351
+ }
1352
+ },
1353
+ ibm: {
1354
+ 'v:aria_semantics_role': {
1355
+ quality: 1,
1356
+ what: 'ARIA role is not valid for the element to which it is assigned'
1357
+ }
1358
+ },
1359
+ testaro: {
1360
+ role: {
1361
+ quality: 1,
1362
+ what: 'Nonexistent or implicit-overriding role'
1363
+ }
1364
+ }
1365
+ }
1366
+ },
1367
+ roleMissingAttribute: {
1368
+ weight: 4,
1369
+ packages: {
1370
+ axe: {
1371
+ 'aria-required-attr': {
1372
+ quality: 1,
1373
+ what: 'Required ARIA attribute is not provided'
1374
+ }
1375
+ },
1376
+ ibm: {
1377
+ 'v:Rpt_Aria_RequiredProperties': {
1378
+ quality: 1,
1379
+ what: 'ARIA role on an element does not have a required attribute'
1380
+ }
1381
+ }
1382
+ }
1383
+ },
1384
+ ariaMissing: {
1385
+ weight: 4,
1386
+ packages: {
1387
+ alfa: {
1388
+ r16: {
1389
+ quality: 1,
1390
+ what: 'Element does not have all required states and properties'
1391
+ }
1392
+ },
1393
+ continuum: {
1394
+ 1040: {
1395
+ quality: 1,
1396
+ what: 'element with a combobox role has no aria-controls or no aria-expanded attribute'
1397
+ },
1398
+ 1042: {
1399
+ quality: 1,
1400
+ what: 'element with an option role has no aria-selected attribute'
1401
+ }
1402
+ },
1403
+ wave: {
1404
+ 'e:aria_reference_broken': {
1405
+ quality: 1,
1406
+ what: 'Broken ARIA reference'
1407
+ }
1408
+ }
1409
+ }
1410
+ },
1411
+ ariaBadAttribute: {
1412
+ weight: 4,
1413
+ packages: {
1414
+ alfa: {
1415
+ r18: {
1416
+ quality: 1,
1417
+ what: 'ARIA state or property is not allowed for the element on which it is specified'
1418
+ },
1419
+ r19: {
1420
+ quality: 1,
1421
+ what: 'ARIA state or property has an invalid value'
1422
+ },
1423
+ r20: {
1424
+ quality: 1,
1425
+ what: 'ARIA attribute is not defined'
1426
+ }
1427
+ },
1428
+ axe: {
1429
+ 'aria-valid-attr': {
1430
+ quality: 1,
1431
+ what: 'ARIA attribute has an invalid name'
1432
+ },
1433
+ 'aria-valid-attr-value': {
1434
+ quality: 1,
1435
+ what: 'ARIA attribute has an invalid value'
1436
+ },
1437
+ 'aria-allowed-attr': {
1438
+ quality: 1,
1439
+ what: 'ARIA attribute is invalid for the role of its element'
1440
+ }
1441
+ },
1442
+ continuum: {
1443
+ 38: {
1444
+ quality: 1,
1445
+ what: 'element has an aria-pressed attribute, which is not allowed'
1446
+ },
1447
+ 257: {
1448
+ quality: 1,
1449
+ what: 'element has an aria-checked attribute, which is not allowed'
1450
+ },
1451
+ 260: {
1452
+ quality: 1,
1453
+ what: 'element has an aria-level attribute, which is not allowed'
1454
+ },
1455
+ 264: {
1456
+ quality: 1,
1457
+ what: 'element has an aria-selected attribute, which is not allowed'
1458
+ },
1459
+ 281: {
1460
+ quality: 1,
1461
+ what: 'element has an aria-expanded attribute, which is not allowed'
1462
+ },
1463
+ 282: {
1464
+ quality: 1,
1465
+ what: 'element has an aria-autocomplete attribute, which is not allowed'
1466
+ },
1467
+ 283: {
1468
+ quality: 1,
1469
+ what: 'element has an aria-activedescendant attribute, which is not allowed'
1470
+ },
1471
+ 1066: {
1472
+ quality: 1,
1473
+ what: 'element has an ARIA attribute which is not valid'
1474
+ }
1475
+ },
1476
+ ibm: {
1477
+ 'v:Rpt_Aria_ValidProperty': {
1478
+ quality: 1,
1479
+ what: 'ARIA attribute is invalid for the role'
1480
+ }
1481
+ }
1482
+ }
1483
+ },
1484
+ ariaReferenceBad: {
1485
+ weight: 4,
1486
+ packages: {
1487
+ ibm: {
1488
+ 'v:Rpt_Aria_ValidIdRef': {
1489
+ quality: 1,
1490
+ what: 'ARIA property does not reference the non-empty unique id of a visible element'
1491
+ }
1492
+ },
1493
+ wave: {
1494
+ 'e:aria_reference_broken': {
1495
+ quality: 1,
1496
+ what: 'Broken ARIA reference'
1497
+ }
1498
+ }
1499
+ }
1500
+ },
1501
+ ariaRoleDescriptionBad: {
1502
+ weight: 3,
1503
+ packages: {
1504
+ axe: {
1505
+ 'aria-roledescription': {
1506
+ quality: 1,
1507
+ what: 'aria-roledescription is on an element with no semantic role'
1508
+ }
1509
+ }
1510
+ }
1511
+ },
1512
+ autocompleteBad: {
1513
+ weight: 3,
1514
+ packages: {
1515
+ alfa: {
1516
+ r10: {
1517
+ quality: 1,
1518
+ what: 'Autocomplete attribute has no valid value'
1519
+ }
1520
+ },
1521
+ axe: {
1522
+ 'autocomplete-valid': {
1523
+ quality: 1,
1524
+ what: 'Autocomplete attribute is used incorrectly'
1525
+ }
1526
+ },
1527
+ htmlcs: {
1528
+ 'e:AA.1_3_5.H98': {
1529
+ quality: 1,
1530
+ what: 'Autocomplete attribute and the input type are mismatched'
1531
+ }
1532
+ },
1533
+ ibm: {
1534
+ 'v:WCAG21_Input_Autocomplete': {
1535
+ quality: 1,
1536
+ what: 'Autocomplete attribute token is not appropriate for the input form field'
1537
+ }
1538
+ }
1539
+ }
1540
+ },
1541
+ autocompleteRisk: {
1542
+ weight: 1,
1543
+ packages: {
1544
+ htmlcs: {
1545
+ 'w:AA.1_3_5.H98': {
1546
+ quality: 1,
1547
+ what: 'Element contains a potentially faulty value in its autocomplete attribute'
1548
+ }
1549
+ }
1550
+ }
1551
+ },
1552
+ contrastAA: {
1553
+ weight: 3,
1554
+ packages: {
1555
+ alfa: {
1556
+ r69: {
1557
+ quality: 1,
1558
+ what: 'Text outside widget has subminimum contrast'
1559
+ }
1560
+ },
1561
+ axe: {
1562
+ 'color-contrast': {
1563
+ quality: 1,
1564
+ what: 'Element has insufficient color contrast'
1565
+ }
1566
+ },
1567
+ htmlcs: {
1568
+ 'e:AA.1_4_3.G145.Fail': {
1569
+ quality: 1,
1570
+ what: 'Contrast between the text and its background is less than 3:1.'
1571
+ },
1572
+ 'e:AA.1_4_3.G18.Fail': {
1573
+ quality: 1,
1574
+ what: 'Contrast between the text and its background is less than 4.5:1'
1575
+ }
1576
+ },
1577
+ ibm: {
1578
+ 'v:IBMA_Color_Contrast_WCAG2AA': {
1579
+ quality: 1,
1580
+ what: 'Contrast ratio of text with background does not meet WCAG 2.1 AA'
1581
+ }
1582
+ },
1583
+ wave: {
1584
+ 'c:contrast': {
1585
+ quality: 1,
1586
+ what: 'Very low contrast'
1587
+ }
1588
+ }
1589
+ }
1590
+ },
1591
+ contrastAAA: {
1592
+ weight: 1,
1593
+ packages: {
1594
+ alfa: {
1595
+ r66: {
1596
+ quality: 1,
1597
+ what: 'Text contrast less than AAA requires'
1598
+ }
1599
+ },
1600
+ axe: {
1601
+ 'color-contrast-enhanced': {
1602
+ quality: 1,
1603
+ what: 'Element has insufficient color contrast (Level AAA)'
1604
+ }
1605
+ },
1606
+ htmlcs: {
1607
+ 'e:WCAG2AAA.Principle1.Guideline1_4.1_4_3.G18': {
1608
+ quality: 1,
1609
+ what: 'Insufficient contrast'
1610
+ }
1611
+ },
1612
+ tenon: {
1613
+ 95: {
1614
+ quality: 1,
1615
+ what: 'Element has insufficient color contrast (Level AAA)'
1616
+ }
1617
+ }
1618
+ }
1619
+ },
1620
+ contrastRisk: {
1621
+ weight: 1,
1622
+ packages: {
1623
+ htmlcs: {
1624
+ 'w:AA.1_4_3_F24.F24.BGColour': {
1625
+ quality: 1,
1626
+ what: 'Inline background color may lack a complementary foreground color'
1627
+ },
1628
+ 'w:AA.1_4_3_F24.F24.FGColour': {
1629
+ quality: 1,
1630
+ what: 'Inline foreground color may lack a complementary background color'
1631
+ },
1632
+ 'w:AA.1_4_3.G18.Abs': {
1633
+ quality: 1,
1634
+ what: 'Contrast between the absolutely positioned text and its background may be inadequate'
1635
+ },
1636
+ 'w:AA.1_4_3.G18.Alpha': {
1637
+ quality: 1,
1638
+ what: 'Contrast between the text and its background may be less than 4.5:1, given the transparency'
1639
+ },
1640
+ 'w:AA.1_4_3.G145.Abs': {
1641
+ quality: 1,
1642
+ what: 'Contrast between the absolutely positioned large text and its background may be less than 3:1'
1643
+ },
1644
+ 'w:AA.1_4_3.G145.Alpha': {
1645
+ quality: 1,
1646
+ what: 'Contrast between the text and its background may be less than 3:1, given the transparency'
1647
+ },
1648
+ 'w:AA.1_4_3.G145.BgImage': {
1649
+ quality: 1,
1650
+ what: 'Contrast between the text and its background image may be less than 3:1'
1651
+ },
1652
+ 'w:AA.1_4_3.G18.BgImage': {
1653
+ quality: 1,
1654
+ what: 'Contrast between the text and its background image may be less than 4.5:1'
1655
+ }
1656
+ }
1657
+ }
1658
+ },
1659
+ headingEmpty: {
1660
+ weight: 3,
1661
+ packages: {
1662
+ alfa: {
1663
+ r64: {
1664
+ quality: 1,
1665
+ what: 'Heading has no non-empty accessible name'
1666
+ }
1667
+ },
1668
+ axe: {
1669
+ 'empty-heading': {
1670
+ quality: 1,
1671
+ what: 'Heading empty'
1672
+ }
1673
+ },
1674
+ htmlcs: {
1675
+ 'e:AA.1_3_1.H42.2': {
1676
+ quality: 1,
1677
+ what: 'Heading empty'
1678
+ }
1679
+ },
1680
+ ibm: {
1681
+ 'v:RPT_Header_HasContent': {
1682
+ quality: 1,
1683
+ what: 'Heading element provides no descriptive text'
1684
+ }
1685
+ },
1686
+ wave: {
1687
+ 'e:heading_empty': {
1688
+ quality: 1,
1689
+ what: 'Empty heading'
1690
+ }
1691
+ }
1692
+ }
1693
+ },
1694
+ headingOfNothing: {
1695
+ weight: 2,
1696
+ packages: {
1697
+ alfa: {
1698
+ r78: {
1699
+ quality: 1,
1700
+ what: 'No content between two headings of the same level'
1701
+ }
1702
+ }
1703
+ }
1704
+ },
1705
+ imageTextRedundant: {
1706
+ weight: 1,
1707
+ packages: {
1708
+ axe: {
1709
+ 'image-redundant-alt': {
1710
+ quality: 1,
1711
+ what: 'Text of a button or link is repeated in the image alternative'
1712
+ }
1713
+ },
1714
+ ibm: {
1715
+ 'v:WCAG20_Img_LinkTextNotRedundant': {
1716
+ quality: 1,
1717
+ what: 'Text alternative for the image in a link repeats text of the same or an adjacent link'
1718
+ }
1719
+ },
1720
+ tenon: {
1721
+ 138: {
1722
+ quality: 1,
1723
+ what: 'Image link alternative text repeats text in the link'
1724
+ }
1725
+ },
1726
+ wave: {
1727
+ 'a:alt_redundant': {
1728
+ quality: 1,
1729
+ what: 'Redundant text alternative'
1730
+ }
1731
+ }
1732
+ }
1733
+ },
1734
+ decorativeTitle: {
1735
+ weight: 1,
1736
+ packages: {
1737
+ htmlcs: {
1738
+ 'e:AA.1_1_1.H67.1': {
1739
+ quality: 1,
1740
+ what: 'img element has an empty alt attribute but has a nonempty title attribute'
1741
+ }
1742
+ },
1743
+ wave: {
1744
+ 'a:image_title': {
1745
+ quality: 1,
1746
+ what: 'Image has a title attribute value but no alt value'
1747
+ }
1748
+ }
1749
+ }
1750
+ },
1751
+ titleRedundant: {
1752
+ weight: 1,
1753
+ packages: {
1754
+ tenon: {
1755
+ 79: {
1756
+ quality: 1,
1757
+ what: 'Link has a title attribute that is the same as the text inside the link'
1758
+ }
1759
+ },
1760
+ wave: {
1761
+ 'a:title_redundant': {
1762
+ quality: 1,
1763
+ what: 'Title attribute text is the same as text or alternative text'
1764
+ }
1765
+ }
1766
+ }
1767
+ },
1768
+ titleEmpty: {
1769
+ weight: 1,
1770
+ packages: {
1771
+ htmlcs: {
1772
+ 'w:AA.1_3_1.H65': {
1773
+ quality: 0.5,
1774
+ what: 'Value of the title attribute of the form control is empty or only whitespace'
1775
+ },
1776
+ 'w:AA.4_1_2.H65': {
1777
+ quality: 0.5,
1778
+ what: 'Value of the title attribute of the form control is empty or only whitespace'
1779
+ }
1780
+ }
1781
+ }
1782
+ },
1783
+ pageTitle: {
1784
+ weight: 3,
1785
+ packages: {
1786
+ alfa: {
1787
+ r1: {
1788
+ quality: 1,
1789
+ what: 'Document has no valid title element'
1790
+ }
1791
+ },
1792
+ axe: {
1793
+ 'document-title': {
1794
+ quality: 1,
1795
+ what: 'Document contains no title element'
1796
+ }
1797
+ },
1798
+ continuum: {
1799
+ 884: {
1800
+ quality: 1,
1801
+ what: 'DOM contains no document title element'
1802
+ }
1803
+ },
1804
+ htmlcs: {
1805
+ 'e:AA.2_4_2.H25.1.NoTitleEl': {
1806
+ quality: 1,
1807
+ what: 'Document head element contains no non-empty title element'
1808
+ }
1809
+ },
1810
+ wave: {
1811
+ 'e:title_invalid': {
1812
+ quality: 1,
1813
+ what: 'Missing or uninformative page title'
1814
+ }
1815
+ }
1816
+ }
1817
+ },
1818
+ headingStructure: {
1819
+ weight: 2,
1820
+ packages: {
1821
+ alfa: {
1822
+ r53: {
1823
+ quality: 1,
1824
+ what: 'Heading skips one or more levels'
1825
+ }
1826
+ },
1827
+ axe: {
1828
+ 'heading-order': {
1829
+ quality: 1,
1830
+ what: 'Heading levels do not increase by only one'
1831
+ }
1832
+ },
1833
+ htmlcs: {
1834
+ 'w:AA.1_3_1_A.G141': {
1835
+ quality: 1,
1836
+ what: 'Heading level is incorrect'
1837
+ }
1838
+ },
1839
+ tenon: {
1840
+ 155: {
1841
+ quality: 1,
1842
+ what: 'Headings are not structured in a hierarchical manner'
1843
+ }
1844
+ },
1845
+ wave: {
1846
+ 'a:heading_skipped': {
1847
+ quality: 1,
1848
+ what: 'Skipped heading level'
1849
+ }
1850
+ }
1851
+ }
1852
+ },
1853
+ headingLevelless: {
1854
+ weight: 1,
1855
+ packages: {
1856
+ continuum: {
1857
+ 71: {
1858
+ quality: 1,
1859
+ what: 'element with a heading role has no aria-level attribute'
1860
+ }
1861
+ }
1862
+ }
1863
+ },
1864
+ noHeading: {
1865
+ weight: 3,
1866
+ packages: {
1867
+ alfa: {
1868
+ r59: {
1869
+ quality: 1,
1870
+ what: 'Document has no headings'
1871
+ }
1872
+ },
1873
+ wave: {
1874
+ 'a:heading_missing': {
1875
+ quality: 1,
1876
+ what: 'Page has no headings'
1877
+ }
1878
+ }
1879
+ }
1880
+ },
1881
+ h1Missing: {
1882
+ weight: 2,
1883
+ packages: {
1884
+ alfa: {
1885
+ r61: {
1886
+ quality: 1,
1887
+ what: 'First heading is not h1'
1888
+ }
1889
+ },
1890
+ axe: {
1891
+ 'page-has-heading-one': {
1892
+ quality: 1,
1893
+ what: 'Page contains no level-one heading'
1894
+ }
1895
+ },
1896
+ wave: {
1897
+ 'a:h1_missing': {
1898
+ quality: 1,
1899
+ what: 'Missing first level heading'
1900
+ }
1901
+ }
1902
+ }
1903
+ },
1904
+ justification: {
1905
+ weight: 1,
1906
+ packages: {
1907
+ alfa: {
1908
+ r71: {
1909
+ quality: 1,
1910
+ what: 'Paragraph text is fully justified'
1911
+ }
1912
+ },
1913
+ tenon: {
1914
+ 36: {
1915
+ quality: 1,
1916
+ what: 'Text is fully justified'
1917
+ }
1918
+ },
1919
+ wave: {
1920
+ 'a:text_justified': {
1921
+ quality: 1,
1922
+ what: 'Text is justified'
1923
+ }
1924
+ }
1925
+ }
1926
+ },
1927
+ nonSemanticText: {
1928
+ weight: 2,
1929
+ packages: {
1930
+ htmlcs: {
1931
+ 'w:AA.1_3_1.H49.AlignAttr': {
1932
+ quality: 1,
1933
+ what: 'Special text is aligned nonsemantically'
1934
+ },
1935
+ 'w:AA.1_3_1.H49.B': {
1936
+ quality: 1,
1937
+ what: 'Special text is bolded nonsemantically'
1938
+ },
1939
+ 'w:AA.1_3_1.H49.I': {
1940
+ quality: 1,
1941
+ what: 'Special text is italicized nonsemantically'
1942
+ },
1943
+ 'w:AA.1_3_1.H49.Big': {
1944
+ quality: 1,
1945
+ what: 'Special text is enlarged nonsemantically'
1946
+ },
1947
+ 'w:AA.1_3_1.H49.Small': {
1948
+ quality: 1,
1949
+ what: 'Special text is made small nonsemantically'
1950
+ },
1951
+ 'w:AA.1_3_1.H49.U': {
1952
+ quality: 1,
1953
+ what: 'Special text is underlined nonsemantically'
1954
+ },
1955
+ 'w:AA.1_3_1.H49.Center': {
1956
+ quality: 1,
1957
+ what: 'Special text is centered nonsemantically'
1958
+ },
1959
+ 'w:AA.1_3_1.H49.Font': {
1960
+ quality: 1,
1961
+ what: 'Special text is designated nonsemantically with a (deprecated) font element'
1962
+ }
1963
+ }
1964
+ }
1965
+ },
1966
+ pseudoParagraphRisk: {
1967
+ weight: 1,
1968
+ packages: {
1969
+ tenon: {
1970
+ 242: {
1971
+ quality: 1,
1972
+ what: 'Multiple consecutive br elements may simulate paragraphs'
1973
+ }
1974
+ }
1975
+ }
1976
+ },
1977
+ pseudoHeadingRisk: {
1978
+ weight: 1,
1979
+ packages: {
1980
+ axe: {
1981
+ 'p-as-heading': {
1982
+ quality: 1,
1983
+ what: 'Styled p element may be misused as a heading'
1984
+ }
1985
+ },
1986
+ htmlcs: {
1987
+ 'w:AA.1_3_1.H42': {
1988
+ quality: 1,
1989
+ what: 'Heading coding is not used but the element may be intended as a heading'
1990
+ }
1991
+ },
1992
+ wave: {
1993
+ 'a:heading_possible': {
1994
+ quality: 1,
1995
+ what: 'Possible heading'
1996
+ }
1997
+ }
1998
+ }
1999
+ },
2000
+ pseudoLinkRisk: {
2001
+ weight: 1,
2002
+ packages: {
2003
+ tenon: {
2004
+ 129: {
2005
+ quality: 1,
2006
+ what: 'CSS underline on text that is not a link'
2007
+ }
2008
+ },
2009
+ wave: {
2010
+ 'a:underline': {
2011
+ quality: 1,
2012
+ what: 'CSS underline on text that is not a link'
2013
+ }
2014
+ }
2015
+ }
2016
+ },
2017
+ listChild: {
2018
+ weight: 4,
2019
+ packages: {
2020
+ axe: {
2021
+ list: {
2022
+ quality: 1,
2023
+ what: 'List element ul or ol has a child element other than li, script, and template'
2024
+ },
2025
+ 'definition-list': {
2026
+ quality: 1,
2027
+ what: 'List element dl has a child element other than properly ordered dt and dt group, script, template, and div'
2028
+ }
2029
+ },
2030
+ continuum: {
2031
+ 246: {
2032
+ quality: 1,
2033
+ what: 'ul element does not contain only li, script, template, or listitem-role elements as direct child elements'
2034
+ }
2035
+ }
2036
+ }
2037
+ },
2038
+ listItemOrphan: {
2039
+ weight: 4,
2040
+ packages: {
2041
+ axe: {
2042
+ listitem: {
2043
+ quality: 1,
2044
+ what: 'li element is not contained by a ul or ol element'
2045
+ }
2046
+ },
2047
+ continuum: {
2048
+ 99: {
2049
+ quality: 1,
2050
+ what: 'li element has no ul, ol, or list-role parent'
2051
+ }
2052
+ }
2053
+ }
2054
+ },
2055
+ pseudoOrderedListRisk: {
2056
+ weight: 1,
2057
+ packages: {
2058
+ htmlcs: {
2059
+ 'w:AA.1_3_1.H48.2': {
2060
+ quality: 1,
2061
+ what: 'Ordered list may fail to be coded as such'
2062
+ }
2063
+ }
2064
+ }
2065
+ },
2066
+ pseudoNavListRisk: {
2067
+ weight: 1,
2068
+ packages: {
2069
+ htmlcs: {
2070
+ 'w:AA.1_3_1.H48': {
2071
+ quality: 1,
2072
+ what: 'Navigation links are not coded as a list'
2073
+ }
2074
+ }
2075
+ }
2076
+ },
2077
+ selectNoText: {
2078
+ weight: 3,
2079
+ packages: {
2080
+ axe: {
2081
+ 'select-name': {
2082
+ quality: 1,
2083
+ what: 'select element has no accessible name'
2084
+ }
2085
+ },
2086
+ continuum: {
2087
+ 114: {
2088
+ quality: 1,
2089
+ what: 'select element has no mechanism that allows an accessible name to be calculated'
2090
+ }
2091
+ },
2092
+ htmlcs: {
2093
+ 'e:AA.4_1_2.H91.Select.Name': {
2094
+ quality: 1,
2095
+ what: 'Select element has no accessible name'
2096
+ },
2097
+ 'w:AA.4_1_2.H91.Select.Value': {
2098
+ quality: 1,
2099
+ what: 'Select element value has no accessible name'
2100
+ }
2101
+ },
2102
+ wave: {
2103
+ 'a:select_missing_label': {
2104
+ quality: 1,
2105
+ what: 'Select missing label'
2106
+ }
2107
+ }
2108
+ }
2109
+ },
2110
+ selectFlatRisk: {
2111
+ weight: 1,
2112
+ packages: {
2113
+ htmlcs: {
2114
+ 'w:AA.1_3_1.H85.2': {
2115
+ quality: 1,
2116
+ what: 'Selection list may contain groups of related options that are not grouped with optgroup'
2117
+ }
2118
+ }
2119
+ }
2120
+ },
2121
+ accessKeyDuplicate: {
2122
+ weight: 3,
2123
+ packages: {
2124
+ axe: {
2125
+ accesskeys: {
2126
+ quality: 1,
2127
+ what: 'accesskey attribute value is not unique'
2128
+ }
2129
+ },
2130
+ ibm: {
2131
+ 'v:WCAG20_Elem_UniqueAccessKey': {
2132
+ quality: 1,
2133
+ what: 'Accesskey attribute value on an element is not unique for the page'
2134
+ }
2135
+ },
2136
+ tenon: {
2137
+ 101: {
2138
+ quality: 1,
2139
+ what: 'Duplicate accesskey value'
2140
+ }
2141
+ },
2142
+ wave: {
2143
+ 'a:accesskey': {
2144
+ quality: 1,
2145
+ what: 'Accesskey'
2146
+ }
2147
+ }
2148
+ }
2149
+ },
2150
+ fieldSetMissing: {
2151
+ weight: 2,
2152
+ packages: {
2153
+ ibm: {
2154
+ 'v:WCAG20_Input_RadioChkInFieldSet': {
2155
+ quality: 1,
2156
+ what: 'Input is in a different group than another with the name'
2157
+ }
2158
+ },
2159
+ testaro: {
2160
+ radioSet: {
2161
+ quality: 1,
2162
+ what: 'No or invalid grouping of radio buttons in fieldsets'
2163
+ }
2164
+ },
2165
+ wave: {
2166
+ 'a:fieldset_missing': {
2167
+ quality: 1,
2168
+ what: 'Missing fieldset'
2169
+ }
2170
+ }
2171
+ }
2172
+ },
2173
+ fieldSetRisk: {
2174
+ weight: 1,
2175
+ packages: {
2176
+ htmlcs: {
2177
+ 'w:AA.1_3_1.H71.SameName': {
2178
+ quality: 1,
2179
+ what: 'Radio buttons or check boxes may require a group description via a fieldset element'
2180
+ }
2181
+ }
2182
+ }
2183
+ },
2184
+ legendMissing: {
2185
+ weight: 2,
2186
+ packages: {
2187
+ htmlcs: {
2188
+ 'e:AA.1_3_1.H71.NoLegend': {
2189
+ quality: 1,
2190
+ what: 'Fieldset has no legend element'
2191
+ }
2192
+ },
2193
+ wave: {
2194
+ 'a:legend_missing': {
2195
+ quality: 1,
2196
+ what: 'Fieldset has no legend element'
2197
+ }
2198
+ }
2199
+ }
2200
+ },
2201
+ groupName: {
2202
+ weight: 3,
2203
+ packages: {
2204
+ alfa: {
2205
+ r60: {
2206
+ quality: 1,
2207
+ what: 'Form-control group has no accessible name'
2208
+ }
2209
+ },
2210
+ htmlcs: {
2211
+ 'e:AA.4_1_2.H91.Fieldset.Name': {
2212
+ quality: 1,
2213
+ what: 'Fieldset has no accessible name'
2214
+ }
2215
+ }
2216
+ }
2217
+ },
2218
+ layoutTable: {
2219
+ weight: 2,
2220
+ packages: {
2221
+ wave: {
2222
+ 'a:table_layout': {
2223
+ quality: 1,
2224
+ what: 'Table element is misused to arrange content'
2225
+ }
2226
+ }
2227
+ }
2228
+ },
2229
+ tableCaption: {
2230
+ weight: 1,
2231
+ packages: {
2232
+ axe: {
2233
+ 'table-fake-caption': {
2234
+ quality: 1,
2235
+ what: 'Data or header cells are used for a table caption instead of a caption element'
2236
+ }
2237
+ },
2238
+ htmlcs: {
2239
+ 'w:AA.1_3_1.H39.3.NoCaption': {
2240
+ quality: 1,
2241
+ what: 'Table has no caption element'
2242
+ }
2243
+ }
2244
+ }
2245
+ },
2246
+ cellHeadersNotInferrable: {
2247
+ weight: 4,
2248
+ packages: {
2249
+ htmlcs: {
2250
+ 'e:AA.1_3_1.H43.HeadersRequired': {
2251
+ quality: 1,
2252
+ what: 'Complex table requires headers attributes of cells'
2253
+ }
2254
+ }
2255
+ }
2256
+ },
2257
+ cellHeadersAmbiguityRisk: {
2258
+ weight: 3,
2259
+ packages: {
2260
+ htmlcs: {
2261
+ 'w:AA.1_3_1.H43.ScopeAmbiguous': {
2262
+ quality: 1,
2263
+ what: 'Complex table requires headers attributes of cells instead of header scopes'
2264
+ }
2265
+ }
2266
+ }
2267
+ },
2268
+ tableHeaderless: {
2269
+ weight: 3,
2270
+ packages: {
2271
+ continuum: {
2272
+ 387: {
2273
+ quality: 1,
2274
+ what: 'table element contains no th element or element with a rowheader or columnheader role'
2275
+ }
2276
+ }
2277
+ }
2278
+ },
2279
+ tableCellHeaderless: {
2280
+ weight: 3,
2281
+ packages: {
2282
+ alfa: {
2283
+ r77: {
2284
+ quality: 1,
2285
+ what: 'Table cell has no header'
2286
+ }
2287
+ },
2288
+ axe: {
2289
+ 'td-has-header': {
2290
+ quality: 1,
2291
+ what: 'Cell in table larger than 3 by 3 has no header'
2292
+ }
2293
+ }
2294
+ }
2295
+ },
2296
+ tableHeaderCelless: {
2297
+ weight: 4,
2298
+ packages: {
2299
+ alfa: {
2300
+ r46: {
2301
+ quality: 1,
2302
+ what: 'Header cell is not assigned to any cell'
2303
+ }
2304
+ },
2305
+ axe: {
2306
+ 'th-has-data-cells': {
2307
+ quality: 1,
2308
+ what: 'Table header refers to no cell'
2309
+ }
2310
+ }
2311
+ }
2312
+ },
2313
+ TableHeaderScopeRisk: {
2314
+ weight: 1,
2315
+ packages: {
2316
+ htmlcs: {
2317
+ 'e:AA.1_3_1.H63.1': {
2318
+ quality: 1,
2319
+ what: 'Not all th elements in the table have a scope attribute, so an inferred scope may be incorrect'
2320
+ }
2321
+ }
2322
+ }
2323
+ },
2324
+ tableHeaderEmpty: {
2325
+ weight: 2,
2326
+ packages: {
2327
+ wave: {
2328
+ 'e:th_empty': {
2329
+ quality: 1,
2330
+ what: 'th (table header) contains no text'
2331
+ }
2332
+ }
2333
+ }
2334
+ },
2335
+ controlLabel: {
2336
+ weight: 4,
2337
+ packages: {
2338
+ axe: {
2339
+ label: {
2340
+ quality: 1,
2341
+ what: 'Form element has no label'
2342
+ }
2343
+ },
2344
+ htmlcs: {
2345
+ 'e:AA.1_3_1.F68': {
2346
+ quality: 1,
2347
+ what: 'Form control has no label'
2348
+ }
2349
+ },
2350
+ wave: {
2351
+ 'e:label_missing': {
2352
+ quality: 1,
2353
+ what: 'Missing form label'
2354
+ }
2355
+ }
2356
+ }
2357
+ },
2358
+ controlLabelInvisible: {
2359
+ weight: 4,
2360
+ packages: {
2361
+ axe: {
2362
+ 'label-title-only': {
2363
+ quality: 1,
2364
+ what: 'Form element has no visible label'
2365
+ }
2366
+ }
2367
+ }
2368
+ },
2369
+ titleAsLabel: {
2370
+ weight: 3,
2371
+ packages: {
2372
+ wave: {
2373
+ 'a:label_title': {
2374
+ quality: 1,
2375
+ what: 'Form control has a title but no label'
2376
+ }
2377
+ }
2378
+ }
2379
+ },
2380
+ visibleLabelNotName: {
2381
+ weight: 3,
2382
+ packages: {
2383
+ alfa: {
2384
+ r14: {
2385
+ quality: 1,
2386
+ what: 'Visible label is not in the accessible name'
2387
+ }
2388
+ },
2389
+ axe: {
2390
+ 'label-content-name-mismatch': {
2391
+ quality: 1,
2392
+ what: 'Element visible text is not part of its accessible name'
2393
+ }
2394
+ },
2395
+ htmlcs: {
2396
+ 'w:AA.2_5_3.F96': {
2397
+ quality: 1,
2398
+ what: 'Visible label is not in the accessible name'
2399
+ }
2400
+ }
2401
+ }
2402
+ },
2403
+ targetSize: {
2404
+ weight: 2,
2405
+ packages: {
2406
+ tenon: {
2407
+ 152: {
2408
+ quality: 1,
2409
+ what: 'Actionable element is smaller than the minimum required size'
2410
+ }
2411
+ }
2412
+ }
2413
+ },
2414
+ visibleBulk: {
2415
+ weight: 1,
2416
+ packages: {
2417
+ testaro: {
2418
+ bulk: {
2419
+ quality: 1,
2420
+ what: 'Page contains many visible elements'
2421
+ }
2422
+ }
2423
+ }
2424
+ },
2425
+ activeEmbedding: {
2426
+ weight: 3,
2427
+ packages: {
2428
+ axe: {
2429
+ 'nested-interactive': {
2430
+ quality: 1,
2431
+ what: 'Interactive controls are nested'
2432
+ }
2433
+ },
2434
+ continuum: {
2435
+ 22: {
2436
+ quality: 1,
2437
+ what: 'Link contains an input, keygen, select, textarea, or button'
2438
+ }
2439
+ },
2440
+ testaro: {
2441
+ embAc: {
2442
+ quality: 1,
2443
+ what: 'Active element is embedded in a link or button'
2444
+ }
2445
+ }
2446
+ }
2447
+ },
2448
+ tabFocusability: {
2449
+ weight: 3,
2450
+ packages: {
2451
+ testaro: {
2452
+ focAll: {
2453
+ quality: 1,
2454
+ what: 'Discrepancy between elements that should be and that are Tab-focusable'
2455
+ }
2456
+ }
2457
+ }
2458
+ },
2459
+ focusIndication: {
2460
+ weight: 4,
2461
+ packages: {
2462
+ alfa: {
2463
+ r65: {
2464
+ quality: 1,
2465
+ what: 'Element in sequential focus order has no visible focus'
2466
+ }
2467
+ },
2468
+ testaro: {
2469
+ focInd: {
2470
+ quality: 1,
2471
+ what: 'Focused element displaying no or nostandard focus indicator'
2472
+ }
2473
+ }
2474
+ }
2475
+ },
2476
+ allCaps: {
2477
+ weight: 1,
2478
+ packages: {
2479
+ alfa: {
2480
+ r72: {
2481
+ quality: 1,
2482
+ what: 'Paragraph text is uppercased'
2483
+ }
2484
+ },
2485
+ tenon: {
2486
+ 153: {
2487
+ quality: 1,
2488
+ what: 'Long string of text is in all caps'
2489
+ }
2490
+ }
2491
+ }
2492
+ },
2493
+ allItalics: {
2494
+ weight: 1,
2495
+ packages: {
2496
+ alfa: {
2497
+ r85: {
2498
+ quality: 1,
2499
+ what: 'Text of the paragraph is all italic'
2500
+ }
2501
+ },
2502
+ tenon: {
2503
+ 154: {
2504
+ quality: 1,
2505
+ what: 'Long string of text is italic'
2506
+ }
2507
+ }
2508
+ }
2509
+ },
2510
+ noLandmarks: {
2511
+ weight: 2,
2512
+ packages: {
2513
+ wave: {
2514
+ 'a:region_missing': {
2515
+ quality: 1,
2516
+ what: 'Page has no regions or ARIA landmarks'
2517
+ }
2518
+ }
2519
+ }
2520
+ },
2521
+ contentBeyondLandmarks: {
2522
+ weight: 2,
2523
+ packages: {
2524
+ alfa: {
2525
+ r57: {
2526
+ quality: 1,
2527
+ what: 'Perceivable text content is not included in any landmark'
2528
+ }
2529
+ },
2530
+ axe: {
2531
+ region: {
2532
+ quality: 1,
2533
+ what: 'Some page content is not contained by landmarks'
2534
+ }
2535
+ }
2536
+ }
2537
+ },
2538
+ footerTopLandmark: {
2539
+ weight: 1,
2540
+ packages: {
2541
+ axe: {
2542
+ 'landmark-contentinfo-is-top-level': {
2543
+ quality: 1,
2544
+ what: 'contentinfo landmark (footer) is contained in another landmark'
2545
+ }
2546
+ }
2547
+ }
2548
+ },
2549
+ asideNotTop: {
2550
+ weight: 2,
2551
+ packages: {
2552
+ axe: {
2553
+ 'landmark-complementary-is-top-level': {
2554
+ quality: 1,
2555
+ what: 'complementary landmark (aside) is contained in another landmark'
2556
+ }
2557
+ }
2558
+ }
2559
+ },
2560
+ mainNotTop: {
2561
+ weight: 2,
2562
+ packages: {
2563
+ axe: {
2564
+ 'landmark-main-is-top-level': {
2565
+ quality: 1,
2566
+ what: 'main landmark is contained in another landmark'
2567
+ }
2568
+ }
2569
+ }
2570
+ },
2571
+ mainNot1: {
2572
+ weight: 2,
2573
+ packages: {
2574
+ axe: {
2575
+ 'landmark-one-main': {
2576
+ quality: 1,
2577
+ what: 'page has no main landmark'
2578
+ },
2579
+ 'landmark-no-duplicate-main': {
2580
+ quality: 1,
2581
+ what: 'page has more than 1 main landmark'
2582
+ }
2583
+ },
2584
+ continuum: {
2585
+ 809: {
2586
+ quality: 1,
2587
+ what: 'More than 1 main element is located in the body element'
2588
+ }
2589
+ }
2590
+ }
2591
+ },
2592
+ banners: {
2593
+ weight: 2,
2594
+ packages: {
2595
+ axe: {
2596
+ 'landmark-no-duplicate-banner': {
2597
+ quality: 1,
2598
+ what: 'page has more than 1 banner landmark'
2599
+ }
2600
+ }
2601
+ }
2602
+ },
2603
+ bannerNotTop: {
2604
+ weight: 2,
2605
+ packages: {
2606
+ axe: {
2607
+ 'landmark-banner-is-top-level': {
2608
+ quality: 1,
2609
+ what: 'banner landmark is contained in another landmark'
2610
+ }
2611
+ }
2612
+ }
2613
+ },
2614
+ footerMultiple: {
2615
+ weight: 2,
2616
+ packages: {
2617
+ axe: {
2618
+ 'landmark-no-duplicate-contentinfo': {
2619
+ quality: 1,
2620
+ what: 'page has more than 1 contentinfo landmark (footer)'
2621
+ }
2622
+ }
2623
+ }
2624
+ },
2625
+ landmarkConfusion: {
2626
+ weight: 3,
2627
+ packages: {
2628
+ axe: {
2629
+ 'landmark-unique': {
2630
+ quality: 1,
2631
+ what: 'Landmark has a role and an accessible name that are identical to another'
2632
+ }
2633
+ }
2634
+ }
2635
+ },
2636
+ asideConfusion: {
2637
+ weight: 3,
2638
+ packages: {
2639
+ continuum: {
2640
+ 527: {
2641
+ quality: 1,
2642
+ what: 'aside element has an accessible name that is non-unique among the aside elements'
2643
+ }
2644
+ }
2645
+ }
2646
+ },
2647
+ navConfusion: {
2648
+ weight: 3,
2649
+ packages: {
2650
+ continuum: {
2651
+ 531: {
2652
+ quality: 1,
2653
+ what: 'nav element has an accessible name that is non-unique among the nav elements'
2654
+ }
2655
+ }
2656
+ }
2657
+ },
2658
+ asideNoText: {
2659
+ weight: 3,
2660
+ packages: {
2661
+ continuum: {
2662
+ 532: {
2663
+ quality: 1,
2664
+ what: 'aside element is not the only aside element but has no accessible name'
2665
+ }
2666
+ }
2667
+ }
2668
+ },
2669
+ navNoText: {
2670
+ weight: 3,
2671
+ packages: {
2672
+ continuum: {
2673
+ 533: {
2674
+ quality: 1,
2675
+ what: 'nav element is not the only nav element but has no accessible name'
2676
+ }
2677
+ }
2678
+ }
2679
+ },
2680
+ focusableOperable: {
2681
+ weight: 3,
2682
+ packages: {
2683
+ testaro: {
2684
+ focOp: {
2685
+ quality: 1,
2686
+ what: 'Operable elements that cannot be Tab-focused and vice versa'
2687
+ }
2688
+ }
2689
+ }
2690
+ },
2691
+ focusableRole: {
2692
+ weight: 3,
2693
+ packages: {
2694
+ axe: {
2695
+ 'focus-order-semantics': {
2696
+ quality: 1,
2697
+ what: 'Focusable element has no active role'
2698
+ }
2699
+ }
2700
+ }
2701
+ },
2702
+ focusableHidden: {
2703
+ weight: 4,
2704
+ packages: {
2705
+ alfa: {
2706
+ r17: {
2707
+ quality: 1,
2708
+ what: 'Tab-focusable element is or has an ancestor that is aria-hidden'
2709
+ }
2710
+ },
2711
+ axe: {
2712
+ 'aria-hidden-focus': {
2713
+ quality: 1,
2714
+ what: 'ARIA hidden element is focusable or contains a focusable element'
2715
+ },
2716
+ 'presentation-role-conflict': {
2717
+ quality: 1,
2718
+ what: 'Element has a none/presentation role but is focusable or has a global ARIA state or property'
2719
+ }
2720
+ },
2721
+ continuum: {
2722
+ 790: {
2723
+ quality: 1,
2724
+ what: 'Element with an explicit or implicit nonnegative tabindex attribute is directly aria-hidden'
2725
+ }
2726
+ },
2727
+ tenon: {
2728
+ 189: {
2729
+ quality: 1,
2730
+ what: 'Element is typically used for interaction but has a presentation role'
2731
+ },
2732
+ 194: {
2733
+ quality: 1,
2734
+ what: 'Visible element is focusable but has a presentation role or aria-hidden=true attribute'
2735
+ }
2736
+ }
2737
+ }
2738
+ },
2739
+ focusableDescendants: {
2740
+ weight: 4,
2741
+ packages: {
2742
+ alfa: {
2743
+ r90: {
2744
+ quality: 1,
2745
+ what: 'Element has a role making its children presentational but contains a focusable element'
2746
+ }
2747
+ }
2748
+ }
2749
+ },
2750
+ labeledHidden: {
2751
+ weight: 2,
2752
+ packages: {
2753
+ htmlcs: {
2754
+ 'w:AA.1_3_1.F68.Hidden': {
2755
+ quality: 1,
2756
+ what: 'Hidden form field is needlessly labeled.'
2757
+ },
2758
+ 'w:AA.1_3_1.F68.HiddenAttr': {
2759
+ quality: 1,
2760
+ what: 'Form field with a hidden attribute is needlessly labeled.'
2761
+ }
2762
+ }
2763
+ }
2764
+ },
2765
+ hiddenContentRisk: {
2766
+ weight: 1,
2767
+ packages: {
2768
+ axe: {
2769
+ 'hidden-content': {
2770
+ quality: 1,
2771
+ what: 'Some content is hidden and therefore may not be testable for accessibility'
2772
+ }
2773
+ }
2774
+ }
2775
+ },
2776
+ frameContentRisk: {
2777
+ weight: 1,
2778
+ packages: {
2779
+ axe: {
2780
+ 'frame-tested': {
2781
+ quality: 0.2,
2782
+ what: 'Some content is in an iframe and therefore may not be testable for accessibility'
2783
+ }
2784
+ }
2785
+ }
2786
+ },
2787
+ hoverSurprise: {
2788
+ weight: 1,
2789
+ packages: {
2790
+ testaro: {
2791
+ hover: {
2792
+ quality: 1,
2793
+ what: 'Content changes caused by hovering'
2794
+ }
2795
+ }
2796
+ }
2797
+ },
2798
+ labelClash: {
2799
+ weight: 2,
2800
+ packages: {
2801
+ testaro: {
2802
+ labClash: {
2803
+ quality: 1,
2804
+ what: 'Incompatible label types'
2805
+ }
2806
+ },
2807
+ wave: {
2808
+ 'e:label_multiple': {
2809
+ quality: 1,
2810
+ what: 'Form control has more than one label associated with it'
2811
+ }
2812
+ }
2813
+ }
2814
+ },
2815
+ labelEmpty: {
2816
+ weight: 3,
2817
+ packages: {
2818
+ htmlcs: {
2819
+ 'w:AA.1_3_1.ARIA6': {
2820
+ quality: 1,
2821
+ what: 'Value of the aria-label attribute of the form control is empty or only whitespace'
2822
+ },
2823
+ 'w:AA.4_1_2.ARIA6': {
2824
+ quality: 1,
2825
+ what: 'Value of the aria-label attribute of the form control is empty or only whitespace'
2826
+ }
2827
+ },
2828
+ wave: {
2829
+ 'e:label_empty': {
2830
+ quality: 1,
2831
+ what: 'Empty form label'
2832
+ }
2833
+ }
2834
+ }
2835
+ },
2836
+ linkComprehensionRisk: {
2837
+ weight: 1,
2838
+ packages: {
2839
+ wave: {
2840
+ 'a:link_suspicious': {
2841
+ quality: 1,
2842
+ what: 'Suspicious link text'
2843
+ }
2844
+ }
2845
+ }
2846
+ },
2847
+ nonWebLink: {
2848
+ weight: 1,
2849
+ packages: {
2850
+ continuum: {
2851
+ 141: {
2852
+ quality: 1,
2853
+ what: 'a element has an href attribute set to an image file reference'
2854
+ }
2855
+ },
2856
+ wave: {
2857
+ 'a:link_excel': {
2858
+ quality: 1,
2859
+ what: 'Link to Microsoft Excel workbook'
2860
+ },
2861
+ 'a:link_word': {
2862
+ quality: 1,
2863
+ what: 'Link to Microsoft Word document'
2864
+ }
2865
+ }
2866
+ }
2867
+ },
2868
+ linkVague: {
2869
+ weight: 3,
2870
+ packages: {
2871
+ tenon: {
2872
+ 73: {
2873
+ quality: 1,
2874
+ what: 'Link text is too generic to communicate the purpose or destination'
2875
+ }
2876
+ }
2877
+ }
2878
+ },
2879
+ linkIndication: {
2880
+ weight: 2,
2881
+ packages: {
2882
+ alfa: {
2883
+ r62: {
2884
+ quality: 1,
2885
+ what: 'Inline link is not distinct from the surrounding text except by color'
2886
+ }
2887
+ },
2888
+ axe: {
2889
+ 'link-in-text-block': {
2890
+ quality: 1,
2891
+ what: 'Link is not distinct from surrounding text without reliance on color'
2892
+ }
2893
+ },
2894
+ testaro: {
2895
+ linkUl: {
2896
+ quality: 1,
2897
+ what: 'Non-underlined adjacent links'
2898
+ }
2899
+ }
2900
+ }
2901
+ },
2902
+ menuNavigation: {
2903
+ weight: 2,
2904
+ packages: {
2905
+ testaro: {
2906
+ menuNav: {
2907
+ quality: 1,
2908
+ what: 'Nonstandard keyboard navigation among focusable menu items'
2909
+ }
2910
+ }
2911
+ }
2912
+ },
2913
+ menuItemless: {
2914
+ weight: 4,
2915
+ packages: {
2916
+ wave: {
2917
+ 'e:aria_menu_broken': {
2918
+ quality: 1,
2919
+ what: 'ARIA menu does not contain required menu items'
2920
+ }
2921
+ }
2922
+ }
2923
+ },
2924
+ tabNavigation: {
2925
+ weight: 2,
2926
+ packages: {
2927
+ testaro: {
2928
+ tabNav: {
2929
+ quality: 1,
2930
+ what: 'Nonstandard keyboard navigation among tabs'
2931
+ }
2932
+ }
2933
+ }
2934
+ },
2935
+ spontaneousMotion: {
2936
+ weight: 2,
2937
+ packages: {
2938
+ testaro: {
2939
+ motion: {
2940
+ quality: 1,
2941
+ what: 'Change of visible content not requested by user'
2942
+ }
2943
+ }
2944
+ }
2945
+ },
2946
+ autoplay: {
2947
+ weight: 2,
2948
+ packages: {
2949
+ axe: {
2950
+ 'no-autoplay-audio': {
2951
+ quality: 1,
2952
+ what: 'video or audio element plays automatically'
2953
+ }
2954
+ }
2955
+ }
2956
+ },
2957
+ inconsistentStyles: {
2958
+ weight: 1,
2959
+ packages: {
2960
+ testaro: {
2961
+ styleDiff: {
2962
+ quality: 1,
2963
+ what: 'Heading, link, and button style inconsistencies'
2964
+ }
2965
+ }
2966
+ }
2967
+ },
2968
+ zIndexNotZero: {
2969
+ weight: 1,
2970
+ packages: {
2971
+ testaro: {
2972
+ zIndex: {
2973
+ quality: 1,
2974
+ what: 'Layering with nondefault z-index values'
2975
+ }
2976
+ }
2977
+ }
2978
+ },
2979
+ tabIndexPositive: {
2980
+ weight: 1,
2981
+ packages: {
2982
+ axe: {
2983
+ tabindex: {
2984
+ quality: 1,
2985
+ what: 'Positive tabIndex risks creating a confusing focus order'
2986
+ }
2987
+ },
2988
+ wave: {
2989
+ 'a:tabindex': {
2990
+ quality: 1,
2991
+ what: 'tabIndex value positive'
2992
+ }
2993
+ }
2994
+ }
2995
+ },
2996
+ tabIndexMissing: {
2997
+ weight: 4,
2998
+ packages: {
2999
+ continuum: {
3000
+ 337: {
3001
+ quality: 1,
3002
+ what: 'Enabled element with a button role has no nonpositive tabindex attribute'
3003
+ },
3004
+ 356: {
3005
+ quality: 1,
3006
+ what: 'Enabled element with a textbox role has no nonpositive tabindex attribute'
3007
+ }
3008
+ },
3009
+ tenon: {
3010
+ 190: {
3011
+ quality: 1,
3012
+ what: 'Interactive item is not natively actionable, but has no tabindex=0 attribute'
3013
+ }
3014
+ }
3015
+ }
3016
+ },
3017
+ trackNoLabel: {
3018
+ weight: 4,
3019
+ packages: {
3020
+ continuum: {
3021
+ 40: {
3022
+ quality: 1,
3023
+ what: 'captions track element has no label attribute set to a text value'
3024
+ },
3025
+ 368: {
3026
+ quality: 1,
3027
+ what: 'subtitle track element has no label attribute set to a text value'
3028
+ }
3029
+ }
3030
+ }
3031
+ },
3032
+ audioCaptionMissing: {
3033
+ weight: 4,
3034
+ packages: {
3035
+ axe: {
3036
+ 'audio-caption': {
3037
+ quality: 1,
3038
+ what: 'audio element has no captions track'
3039
+ }
3040
+ }
3041
+ }
3042
+ },
3043
+ videoCaptionMissing: {
3044
+ weight: 4,
3045
+ packages: {
3046
+ axe: {
3047
+ 'video-caption': {
3048
+ quality: 1,
3049
+ what: 'video element has no captions'
3050
+ }
3051
+ }
3052
+ }
3053
+ },
3054
+ videoCaptionRisk: {
3055
+ weight: 1,
3056
+ packages: {
3057
+ wave: {
3058
+ 'a:html5_video_audio': {
3059
+ quality: 1,
3060
+ what: 'video or audio element may have no or incorrect captions, transcript, or audio description'
3061
+ },
3062
+ 'a:audio_video': {
3063
+ quality: 1,
3064
+ what: 'audio or video file or link may have no or incorrect captions, transcript, or audio description'
3065
+ },
3066
+ 'a:youtube_video': {
3067
+ quality: 1,
3068
+ what: 'YouTube video may have no or incorrect captions'
3069
+ }
3070
+ }
3071
+ }
3072
+ },
3073
+ notKeyboardScrollable: {
3074
+ weight: 4,
3075
+ packages: {
3076
+ alfa: {
3077
+ r84: {
3078
+ quality: 1,
3079
+ what: 'Element is scrollable but not by keyboard'
3080
+ }
3081
+ },
3082
+ axe: {
3083
+ 'scrollable-region-focusable': {
3084
+ quality: 1,
3085
+ what: 'Element is scrollable but has no keyboard access'
3086
+ }
3087
+ }
3088
+ }
3089
+ },
3090
+ horizontalScrolling: {
3091
+ weight: 3,
3092
+ packages: {
3093
+ tenon: {
3094
+ 28: {
3095
+ quality: 1,
3096
+ what: 'Layout or sizing of the page causes horizontal scrolling'
3097
+ }
3098
+ }
3099
+ }
3100
+ },
3101
+ scrollRisk: {
3102
+ weight: 1,
3103
+ packages: {
3104
+ htmlcs: {
3105
+ 'w:AA.1_4_10.C32,C31,C33,C38,SCR34,G206': {
3106
+ quality: 1,
3107
+ what: 'Fixed-position element may force bidirectional scrolling'
3108
+ }
3109
+ }
3110
+ }
3111
+ },
3112
+ skipRepeatedContent: {
3113
+ weight: 3,
3114
+ packages: {
3115
+ alfa: {
3116
+ 'r87': {
3117
+ quality: 0.5,
3118
+ what: 'First focusable element is not a link to the main content'
3119
+ }
3120
+ },
3121
+ axe: {
3122
+ 'bypass': {
3123
+ quality: 1,
3124
+ what: 'Page has no means to bypass repeated blocks'
3125
+ },
3126
+ 'skip-link': {
3127
+ quality: 1,
3128
+ what: 'Skip-link target is not focusable or does not exist'
3129
+ }
3130
+ },
3131
+ wave: {
3132
+ 'e:link_skip_broken': {
3133
+ quality: 1,
3134
+ what: 'Skip-navigation link has no target or is not keyboard accessible'
3135
+ }
3136
+ }
3137
+ }
3138
+ },
3139
+ submitButton: {
3140
+ weight: 3,
3141
+ packages: {
3142
+ htmlcs: {
3143
+ 'e:AA.3_2_2.H32.2': {
3144
+ quality: 1,
3145
+ what: 'Form has no submit button'
3146
+ }
3147
+ }
3148
+ }
3149
+ },
3150
+ fragmentaryNoticeRisk: {
3151
+ weight: 2,
3152
+ packages: {
3153
+ alfa: {
3154
+ r54: {
3155
+ quality: 1,
3156
+ what: 'Assertive region is not atomic'
3157
+ }
3158
+ }
3159
+ }
3160
+ },
3161
+ noScriptRisk: {
3162
+ weight: 1,
3163
+ packages: {
3164
+ wave: {
3165
+ 'a:noscript': {
3166
+ quality: 1,
3167
+ what: 'noscript element may fail to contain an accessible equivalent or alternative'
3168
+ }
3169
+ }
3170
+ }
3171
+ },
3172
+ obsoleteElement: {
3173
+ weight: 3,
3174
+ packages: {
3175
+ alfa: {
3176
+ r70: {
3177
+ quality: 1,
3178
+ what: 'Element is obsolete or deprecated'
3179
+ }
3180
+ },
3181
+ htmlcs: {
3182
+ 'e:AA.1_3_1.H49.Center': {
3183
+ quality: 1,
3184
+ what: 'center element is obsolete'
3185
+ },
3186
+ 'e:AA.1_3_1.H49.Font': {
3187
+ quality: 1,
3188
+ what: 'font element is obsolete'
3189
+ }
3190
+ }
3191
+ }
3192
+ },
3193
+ obsoleteAttribute: {
3194
+ weight: 3,
3195
+ packages: {
3196
+ htmlcs: {
3197
+ 'e:AA.1_3_1.H49.AlignAttr': {
3198
+ quality: 1,
3199
+ what: 'The align attribute is obsolete'
3200
+ }
3201
+ },
3202
+ wave: {
3203
+ 'a:longdesc': {
3204
+ quality: 1,
3205
+ what: 'The longdesc attribute is obsolete'
3206
+ }
3207
+ }
3208
+ }
3209
+ }
3210
+ };
3211
+
3212
+ // VARIABLES
3213
+
3214
+ let packageDetails = {};
3215
+ let groupDetails = {};
3216
+ let summary = {};
3217
+ let preventionScores = {};
3218
+
3219
+ // FUNCTIONS
3220
+
3221
+ // Initialize the variables.
3222
+ const init = () => {
3223
+ packageDetails = {};
3224
+ groupDetails = {
3225
+ groups: {},
3226
+ solos: {}
3227
+ };
3228
+ summary = {
3229
+ total: 0,
3230
+ log: 0,
3231
+ preventions: 0,
3232
+ solos: 0,
3233
+ groups: []
3234
+ };
3235
+ preventionScores = {};
3236
+ };
3237
+
3238
+ // Adds a score to the package details.
3239
+ const addDetail = (actWhich, testID, addition = 1) => {
3240
+ if (addition) {
3241
+ if (!packageDetails[actWhich]) {
3242
+ packageDetails[actWhich] = {};
3243
+ }
3244
+ if (!packageDetails[actWhich][testID]) {
3245
+ packageDetails[actWhich][testID] = 0;
3246
+ }
3247
+ packageDetails[actWhich][testID] += Math.round(addition);
3248
+ }
3249
+ };
3250
+ // Scores a report.
3251
+ exports.scorer = async report => {
3252
+ // Initialize the variables.
3253
+ init();
3254
+ // If there are any acts in the report:
3255
+ const {acts} = report;
3256
+ if (Array.isArray(acts)) {
3257
+ // If any of them are test acts:
3258
+ const testActs = acts.filter(act => act.type === 'test');
3259
+ if (testActs.length) {
3260
+ // For each test act:
3261
+ testActs.forEach(test => {
3262
+ const {which} = test;
3263
+ // Add scores to the package details.
3264
+ if (which === 'alfa') {
3265
+ const issues = test.result && test.result.items;
3266
+ if (issues && Array.isArray(issues)) {
3267
+ issues.forEach(issue => {
3268
+ const {verdict, rule} = issue;
3269
+ if (verdict && rule) {
3270
+ const {ruleID} = rule;
3271
+ if (ruleID) {
3272
+ // Add 4 per failure, 1 per warning (“cantTell”).
3273
+ addDetail(which, ruleID, verdict === 'failed' ? 4 : 1);
3274
+ }
3275
+ }
3276
+ });
3277
+ }
3278
+ }
3279
+ else if (which === 'axe') {
3280
+ const impactScores = {
3281
+ minor: 1,
3282
+ moderate: 2,
3283
+ serious: 3,
3284
+ critical: 4
3285
+ };
3286
+ const tests = test.result && test.result.details;
3287
+ if (tests) {
3288
+ const warnings = tests.incomplete;
3289
+ const {violations} = tests;
3290
+ [[warnings, 0.25], [violations, 1]].forEach(issueClass => {
3291
+ if (issueClass[0] && Array.isArray(issueClass[0])) {
3292
+ issueClass[0].forEach(issueType => {
3293
+ const {id, nodes} = issueType;
3294
+ if (id && nodes && Array.isArray(nodes)) {
3295
+ nodes.forEach(node => {
3296
+ const {impact} = node;
3297
+ if (impact) {
3298
+ // Add the impact score for a violation or 25% of it for a warning.
3299
+ addDetail(which, id, issueClass[1] * impactScores[impact]);
3300
+ }
3301
+ });
3302
+ }
3303
+ });
3304
+ }
3305
+ });
3306
+ }
3307
+ }
3308
+ else if (which === 'continuum') {
3309
+ const issues = test.result;
3310
+ if (issues && Array.isArray(issues)) {
3311
+ issues.forEach(issue => {
3312
+ // Add 4 per violation.
3313
+ addDetail(which, issue.engineTestId, 4);
3314
+ });
3315
+ }
3316
+ }
3317
+ else if (which === 'htmlcs') {
3318
+ const issues = test.result;
3319
+ if (issues) {
3320
+ ['Error', 'Warning'].forEach(issueClassName => {
3321
+ const classData = issues[issueClassName];
3322
+ if (classData) {
3323
+ const issueTypes = Object.keys(classData);
3324
+ issueTypes.forEach(issueTypeName => {
3325
+ const issueArrays = Object.values(classData[issueTypeName]);
3326
+ const issueCount = issueArrays.reduce((count, array) => count + array.length, 0);
3327
+ const classCode = issueClassName[0].toLowerCase();
3328
+ const code = `${classCode}:${issueTypeName}`;
3329
+ // Add 4 per error, 1 per warning.
3330
+ const weight = classCode === 'e' ? 4 : 1;
3331
+ addDetail(which, code, weight * issueCount);
3332
+ });
3333
+ }
3334
+ });
3335
+ }
3336
+ }
3337
+ else if (which === 'ibm') {
3338
+ const {result} = test;
3339
+ const {content, url} = result;
3340
+ if (content && url) {
3341
+ let preferredMode = 'content';
3342
+ if (
3343
+ content.error ||
3344
+ (content.totals &&
3345
+ content.totals.violation &&
3346
+ url.totals &&
3347
+ url.totals.violation &&
3348
+ url.totals.violation > content.totals.violation)
3349
+ ) {
3350
+ preferredMode = 'url';
3351
+ }
3352
+ const {items} = result[preferredMode];
3353
+ if (items && Array.isArray(items)) {
3354
+ items.forEach(issue => {
3355
+ const {ruleID, level} = issue;
3356
+ if (ruleID && level) {
3357
+ // Add 4 per violation, 1 per warning (“recommendation”).
3358
+ addDetail(which, ruleID, level === 'violation' ? 4 : 1);
3359
+ }
3360
+ });
3361
+ }
3362
+ }
3363
+ }
3364
+ else if (which === 'tenon') {
3365
+ const issues =
3366
+ test.result && test.result.data && test.result.data.resultSet;
3367
+ if (issues && Array.isArray(issues)) {
3368
+ issues.forEach(issue => {
3369
+ const {tID, priority, certainty} = issue;
3370
+ if (tID && priority && certainty) {
3371
+ // Add 4 per issue if certainty and priority 100, less if less.
3372
+ addDetail(which, tID, certainty * priority / 2500);
3373
+ }
3374
+ });
3375
+ }
3376
+ }
3377
+ else if (which === 'wave') {
3378
+ const classScores = {
3379
+ error: 4,
3380
+ contrast: 3,
3381
+ alert: 1
3382
+ };
3383
+ const issueClasses = test.result && test.result.categories;
3384
+ if (issueClasses) {
3385
+ ['error', 'contrast', 'alert'].forEach(issueClass => {
3386
+ const {items} = issueClasses[issueClass];
3387
+ if (items) {
3388
+ const testIDs = Object.keys(items);
3389
+ if (testIDs.length) {
3390
+ testIDs.forEach(testID => {
3391
+ const {count} = items[testID];
3392
+ if (count) {
3393
+ // Add 4 per error, 3 per contrast error, 1 per warning (“alert”).
3394
+ addDetail(
3395
+ which, `${issueClass[0]}:${testID}`, count * classScores[issueClass]
3396
+ );
3397
+ }
3398
+ });
3399
+ }
3400
+ }
3401
+ });
3402
+ }
3403
+ }
3404
+ else if (which === 'bulk') {
3405
+ const count = test.result && test.result.visibleElements;
3406
+ if (typeof count === 'number') {
3407
+ // Add 1 per 300 visible elements beyond 300.
3408
+ addDetail('testaro', which, Math.max(0, count / 300 - 1));
3409
+ }
3410
+ }
3411
+ else if (which === 'embAc') {
3412
+ const issueCounts = test.result && test.result.totals;
3413
+ if (issueCounts) {
3414
+ const counts = Object.values(issueCounts);
3415
+ const total = counts.reduce((sum, current) => sum + current);
3416
+ // Add 3 per embedded element.
3417
+ addDetail('testaro', which, 3 * total);
3418
+ }
3419
+ }
3420
+ else if (which === 'focAll') {
3421
+ const discrepancy = test.result && test.result.discrepancy;
3422
+ if (discrepancy) {
3423
+ addDetail('testaro', which, 2 * Math.abs(discrepancy));
3424
+ }
3425
+ }
3426
+ else if (which === 'focInd') {
3427
+ const issueTypes =
3428
+ test.result && test.result.totals && test.result.totals.types;
3429
+ if (issueTypes) {
3430
+ const missingCount = issueTypes.indicatorMissing
3431
+ && issueTypes.indicatorMissing.total
3432
+ || 0;
3433
+ const badCount = issueTypes.nonOutlinePresent
3434
+ && issueTypes.nonOutlinePresent.total
3435
+ || 0;
3436
+ // Add 3 per missing, 1 per non-outline focus indicator.
3437
+ addDetail('testaro', which, badCount + 3 * missingCount);
3438
+ }
3439
+ }
3440
+ else if (which === 'focOp') {
3441
+ const issueTypes =
3442
+ test.result && test.result.totals && test.result.totals.types;
3443
+ if (issueTypes) {
3444
+ const noOpCount = issueTypes.onlyFocusable && issueTypes.onlyFocusable.total || 0;
3445
+ const noFocCount = issueTypes.onlyOperable && issueTypes.onlyOperable.total || 0;
3446
+ // Add 2 per unfocusable, 0.5 per inoperable element.
3447
+ addDetail('testaro', which, 2 * noFocCount + 0.5 * noOpCount);
3448
+ }
3449
+ }
3450
+ else if (which === 'hover') {
3451
+ const issues = test.result && test.result.totals;
3452
+ if (issues) {
3453
+ const {
3454
+ impactTriggers,
3455
+ additions,
3456
+ removals,
3457
+ opacityChanges,
3458
+ opacityImpact,
3459
+ unhoverables
3460
+ } = issues;
3461
+ // Add score with weights on hover-impact types.
3462
+ const score = 2 * impactTriggers
3463
+ + 0.3 * additions
3464
+ + removals
3465
+ + 0.2 * opacityChanges
3466
+ + 0.1 * opacityImpact
3467
+ + unhoverables;
3468
+ if (score) {
3469
+ addDetail('testaro', which, score);
3470
+ }
3471
+ }
3472
+ }
3473
+ else if (which === 'labClash') {
3474
+ const mislabeledCount = test.result
3475
+ && test.result.totals
3476
+ && test.result.totals.mislabeled
3477
+ || 0;
3478
+ // Add 1 per element with conflicting labels (ignoring unlabeled elements).
3479
+ addDetail('testaro', which, mislabeledCount);
3480
+ }
3481
+ else if (which === 'linkUl') {
3482
+ const totals = test.result && test.result.totals && test.result.totals.adjacent;
3483
+ if (totals) {
3484
+ const nonUl = totals.total - totals.underlined || 0;
3485
+ // Add 2 per non-underlined adjacent link.
3486
+ addDetail('testaro', which, 2 * nonUl);
3487
+ }
3488
+ }
3489
+ else if (which === 'menuNav') {
3490
+ const issueCount = test.result
3491
+ && test.result.totals
3492
+ && test.result.totals.navigations
3493
+ && test.result.totals.navigations.all
3494
+ && test.result.totals.navigations.all.incorrect
3495
+ || 0;
3496
+ // Add 2 per defect.
3497
+ addDetail('testaro', which, 2 * issueCount);
3498
+ }
3499
+ else if (which === 'motion') {
3500
+ const data = test.result;
3501
+ if (data) {
3502
+ const {
3503
+ meanLocalRatio,
3504
+ maxLocalRatio,
3505
+ globalRatio,
3506
+ meanPixelChange,
3507
+ maxPixelChange,
3508
+ changeFrequency
3509
+ } = data;
3510
+ const score = 2 * (meanLocalRatio - 1)
3511
+ + (maxLocalRatio - 1)
3512
+ + globalRatio - 1
3513
+ + meanPixelChange / 10000
3514
+ + maxPixelChange / 25000
3515
+ + 3 * changeFrequency
3516
+ || 0;
3517
+ addDetail('testaro', which, score);
3518
+ }
3519
+ }
3520
+ else if (which === 'radioSet') {
3521
+ const totals = test.result && test.result.totals;
3522
+ if (totals) {
3523
+ const {total, inSet} = totals;
3524
+ const score = total - inSet || 0;
3525
+ // Add 1 per misgrouped radio button.
3526
+ addDetail('testaro', which, score);
3527
+ }
3528
+ }
3529
+ else if (which === 'role') {
3530
+ const badCount = test.result && test.result.badRoleElements || 0;
3531
+ const redundantCount = test.result && test.result.redundantRoleElements || 0;
3532
+ // Add 2 per bad role and 1 per redundant role.
3533
+ addDetail('testaro', which, 2 * badCount + redundantCount);
3534
+ }
3535
+ else if (which === 'styleDiff') {
3536
+ const totals = test.result && test.result.totals;
3537
+ if (totals) {
3538
+ let score = 0;
3539
+ // For each element type that has any style diversity:
3540
+ Object.values(totals).forEach(typeData => {
3541
+ const {total, subtotals} = typeData;
3542
+ if (subtotals) {
3543
+ const styleCount = subtotals.length;
3544
+ const plurality = subtotals[0];
3545
+ const minorities = total - plurality;
3546
+ // Add 1 per style, 0.2 per element with any nonplurality style.
3547
+ score += styleCount + 0.2 * minorities;
3548
+ }
3549
+ });
3550
+ addDetail('testaro', which, score);
3551
+ }
3552
+ }
3553
+ else if (which === 'tabNav') {
3554
+ const issueCount = test.result
3555
+ && test.result.totals
3556
+ && test.result.totals.navigations
3557
+ && test.result.totals.navigations.all
3558
+ && test.result.totals.navigations.all.incorrect
3559
+ || 0;
3560
+ // Add 2 per defect.
3561
+ addDetail('testaro', which, 2 * issueCount);
3562
+ }
3563
+ else if (which === 'zIndex') {
3564
+ const issueCount = test.result && test.result.totals && test.result.totals.total || 0;
3565
+ // Add 1 per non-auto zIndex.
3566
+ addDetail('testaro', which, issueCount);
3567
+ }
3568
+ });
3569
+ // Get the prevention scores and add them to the summary.
3570
+ const actsPrevented = testActs.filter(test => test.result.prevented);
3571
+ actsPrevented.forEach(act => {
3572
+ if (otherPackages.includes(act.which)) {
3573
+ preventionScores[act.which] = preventionWeights.other;
3574
+ }
3575
+ else {
3576
+ preventionScores[`testaro-${act.which}`] = preventionWeights.testaro;
3577
+ }
3578
+ });
3579
+ const preventionScore = Object.values(preventionScores).reduce(
3580
+ (sum, current) => sum + current,
3581
+ 0
3582
+ );
3583
+ const roundedScore = Math.round(preventionScore);
3584
+ summary.preventions = roundedScore;
3585
+ summary.total += roundedScore;
3586
+ // Reorganize the group data.
3587
+ const testGroups = {
3588
+ testaro: {},
3589
+ alfa: {},
3590
+ axe: {},
3591
+ continuum: {},
3592
+ htmlcs: {},
3593
+ ibm: {},
3594
+ tenon: {},
3595
+ wave: {}
3596
+ };
3597
+ Object.keys(groups).forEach(groupName => {
3598
+ Object.keys(groups[groupName].packages).forEach(packageName => {
3599
+ Object.keys(groups[groupName].packages[packageName]).forEach(testID => {
3600
+ testGroups[packageName][testID] = groupName;
3601
+ });
3602
+ });
3603
+ });
3604
+ // Populate the group details with group and solo test scores.
3605
+ // For each package with any scores:
3606
+ Object.keys(packageDetails).forEach(packageName => {
3607
+ // For each test with any scores in the package:
3608
+ Object.keys(packageDetails[packageName]).forEach(testID => {
3609
+ // If the test is in a group:
3610
+ const groupName = testGroups[packageName][testID];
3611
+ if (groupName) {
3612
+ // Determine the preweighted or group-weighted score.
3613
+ if (! groupDetails.groups[groupName]) {
3614
+ groupDetails.groups[groupName] = {};
3615
+ }
3616
+ if (! groupDetails.groups[groupName][packageName]) {
3617
+ groupDetails.groups[groupName][packageName] = {};
3618
+ }
3619
+ let weightedScore = packageDetails[packageName][testID];
3620
+ if (!preWeightedPackages.includes(groupName)) {
3621
+ weightedScore *= groups[groupName].weight / 4;
3622
+ }
3623
+ // Adjust the score for the quality of the test.
3624
+ weightedScore *= groups[groupName].packages[packageName][testID].quality;
3625
+ // Round the score, but not to less than 1.
3626
+ const roundedScore = Math.max(Math.round(weightedScore), 1);
3627
+ // Add the rounded score and the test description to the group details.
3628
+ groupDetails.groups[groupName][packageName][testID] = {
3629
+ score: roundedScore,
3630
+ what: groups[groupName].packages[packageName][testID].what
3631
+ };
3632
+ }
3633
+ // Otherwise, i.e. if the test is solo:
3634
+ else {
3635
+ if (! groupDetails.solos[packageName]) {
3636
+ groupDetails.solos[packageName] = {};
3637
+ }
3638
+ const roundedScore = Math.round(packageDetails[packageName][testID]);
3639
+ groupDetails.solos[packageName][testID] = roundedScore;
3640
+ }
3641
+ });
3642
+ });
3643
+ // Determine the group scores and add them to the summary.
3644
+ const groupNames = Object.keys(groupDetails.groups);
3645
+ const {absolute, largest, smaller} = groupWeights;
3646
+ // For each group with any scores:
3647
+ groupNames.forEach(groupName => {
3648
+ const scores = [];
3649
+ // For each package with any scores in the group:
3650
+ const groupPackageData = Object.values(groupDetails.groups[groupName]);
3651
+ groupPackageData.forEach(packageObj => {
3652
+ // Get the sum of the scores of the tests of the package in the group.
3653
+ const scoreSum = Object.values(packageObj).reduce(
3654
+ (sum, current) => sum + current.score,
3655
+ 0
3656
+ );
3657
+ // Add the sum to the list of package scores in the group.
3658
+ scores.push(scoreSum);
3659
+ });
3660
+ // Sort the scores in descending order.
3661
+ scores.sort((a, b) => b - a);
3662
+ // Compute the sum of the absolute score and the weighted largest and other scores.
3663
+ const groupScore = absolute
3664
+ + largest * scores[0]
3665
+ + smaller * scores.slice(1).reduce((sum, current) => sum + current, 0);
3666
+ const roundedGroupScore = Math.round(groupScore);
3667
+ summary.groups.push({
3668
+ groupName,
3669
+ score: roundedGroupScore
3670
+ });
3671
+ summary.total += roundedGroupScore;
3672
+ });
3673
+ summary.groups.sort((a, b) => b.score - a.score);
3674
+ // Determine the solo score and add it to the summary.
3675
+ const soloPackageNames = Object.keys(groupDetails.solos);
3676
+ soloPackageNames.forEach(packageName => {
3677
+ const testIDs = Object.keys(groupDetails.solos[packageName]);
3678
+ testIDs.forEach(testID => {
3679
+ const score = soloWeight * groupDetails.solos[packageName][testID];
3680
+ summary.solos += score;
3681
+ summary.total += score;
3682
+ });
3683
+ });
3684
+ summary.solos = Math.round(summary.solos);
3685
+ summary.total = Math.round(summary.total);
3686
+ }
3687
+ }
3688
+ // Get the log score.
3689
+ const logScore = logWeights.logCount * report.logCount
3690
+ + logWeights.logSize * report.logSize +
3691
+ + logWeights.errorLogCount * report.errorLogCount
3692
+ + logWeights.errorLogSize * report.errorLogSize
3693
+ + logWeights.prohibitedCount * report.prohibitedCount +
3694
+ + logWeights.visitTimeoutCount * report.visitTimeoutCount +
3695
+ + logWeights.visitRejectionCount * report.visitRejectionCount;
3696
+ const roundedLogScore = Math.round(logScore);
3697
+ summary.log = roundedLogScore;
3698
+ summary.total += roundedLogScore;
3699
+ // Add the score facts to the report.
3700
+ report.score = {
3701
+ scoreProcID,
3702
+ logWeights,
3703
+ soloWeight,
3704
+ groupWeights,
3705
+ preventionWeights,
3706
+ packageDetails,
3707
+ groupDetails,
3708
+ preventionScores,
3709
+ summary
3710
+ };
3711
+ };