testilo 3.5.0 → 3.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2207 @@
1
+ /*
2
+ sp12a
3
+ Testilo score proc 12a
4
+
5
+ Computes scores from Testaro script tp12 and adds them to a report.
6
+ Usage example: node score 35k1r sp12a
7
+
8
+ This proc applies specified weights to the component scores before summing them. An issue reported
9
+ by a test is given a score. That score is determined by:
10
+ Whether the issue is reported as an error or a warning.
11
+ How important the issue is, if the test package is “pre-weighted” (axe, tenon, and testaro)
12
+ Whether the test belongs to a group or is a “solo” test.
13
+ How heavily the group is weighted, if the test package is not pre-weighted and the test belongs
14
+ to a group
15
+
16
+ The scores of solo tests are added together, multiplied by the soloWeight multiplier, and
17
+ contributed to the total score.
18
+
19
+ The scores of grouped tests are aggregated into a group score before being contributed to the
20
+ total score. The group score is the sum of (1) an absolute score, assigned because the group has
21
+ at least one test with a non-zero score, (2) the largest score among the tests of the group
22
+ multiplied by a multiplier, and (3) the sum of the scores from the other tests of the group
23
+ multiplied by a smaller multiplier. These three amounts are given by the groupWeights object.
24
+
25
+ Browser logging produces a log score, and the prevention of tests produces a prevention score.
26
+ They, too, are added to the total score.
27
+
28
+ Each grouped test has a “quality” property, typically set to 1. The value of this property can be
29
+ modified when the test is found to be higher or lower in quality than usual.
30
+ */
31
+
32
+ // CONSTANTS
33
+
34
+ const scoreProcID = 'sp12a';
35
+ // Define the configuration disclosures.
36
+ const logWeights = {
37
+ logCount: 0.5,
38
+ logSize: 0.01,
39
+ errorLogCount: 1,
40
+ errorLogSize: 0.02,
41
+ prohibitedCount: 15,
42
+ visitTimeoutCount: 10,
43
+ visitRejectionCount: 10
44
+ };
45
+ const soloWeight = 2;
46
+ const groupWeights = {
47
+ absolute: 2,
48
+ largest: 1,
49
+ smaller: 0.4
50
+ };
51
+ const preventionWeights = {
52
+ testaro: 50,
53
+ other: 100
54
+ };
55
+ const otherPackages = ['alfa', 'axe', 'htmlcs', 'ibm', 'tenon', 'wave'];
56
+ const preWeightedPackages = ['axe', 'tenon', 'testaro'];
57
+ // Define the test groups.
58
+ const groups = {
59
+ duplicateID: {
60
+ weight: 3,
61
+ packages: {
62
+ alfa: {
63
+ r3: {
64
+ quality: 1,
65
+ what: 'Element ID is not unique'
66
+ }
67
+ },
68
+ axe: {
69
+ 'duplicate-id': {
70
+ quality: 1,
71
+ what: 'ID attribute value must be unique'
72
+ }
73
+ },
74
+ htmlcs: {
75
+ 'e:AA.4_1_1.F77': {
76
+ quality: 1,
77
+ what: 'Duplicate id attribute value'
78
+ }
79
+ },
80
+ ibm: {
81
+ RPT_Elem_UniqueId: {
82
+ quality: 1,
83
+ what: 'Element id attribute values must be unique within a document'
84
+ }
85
+ }
86
+ }
87
+ },
88
+ textInputNoText: {
89
+ weight: 4,
90
+ packages: {
91
+ htmlcs: {
92
+ 'e:AA.4_1_2.H91.InputText.Name': {
93
+ quality: 1,
94
+ what: 'Text input has no accessible name'
95
+ }
96
+ }
97
+ }
98
+ },
99
+ imageInputNoText: {
100
+ weight: 4,
101
+ packages: {
102
+ alfa: {
103
+ r28: {
104
+ quality: 1,
105
+ what: 'Image input element has no accessible name'
106
+ }
107
+ },
108
+ axe: {
109
+ 'input-image-alt': {
110
+ quality: 1,
111
+ what: 'Image buttons must have alternate text'
112
+ }
113
+ },
114
+ htmlcs: {
115
+ 'e:H36': {
116
+ quality: 1,
117
+ what: 'Image submit button has no alt attribute'
118
+ }
119
+ },
120
+ ibm: {
121
+ 'v:WCAG20_Input_ExplicitLabelImage': {
122
+ quality: 1,
123
+ what: 'Input element of type image should have a text alternative'
124
+ }
125
+ },
126
+ wave: {
127
+ 'e:alt_input_missing': {
128
+ quality: 1,
129
+ what: 'Image button missing alternative text'
130
+ }
131
+ }
132
+ }
133
+ },
134
+ imageNoText: {
135
+ weight: 4,
136
+ packages: {
137
+ alfa: {
138
+ r2: {
139
+ quality: 1,
140
+ what: 'Image has no accessible name'
141
+ }
142
+ },
143
+ axe: {
144
+ 'image-alt': {
145
+ quality: 1,
146
+ what: 'Images must have alternate text'
147
+ }
148
+ },
149
+ htmlcs: {
150
+ 'e:AA.1_1_1.H37': {
151
+ quality: 1,
152
+ what: 'img element has no alt attribute'
153
+ }
154
+ },
155
+ ibm: {
156
+ 'v:WCAG20_Img_HasAlt': {
157
+ quality: 1,
158
+ what: 'Images must have an alt attribute if they convey meaning, or alt="" if decorative'
159
+ }
160
+ },
161
+ wave: {
162
+ 'e:alt_missing': {
163
+ quality: 1,
164
+ what: 'Missing alternative text'
165
+ }
166
+ }
167
+ }
168
+ },
169
+ imageTextBad: {
170
+ weight: 3,
171
+ packages: {
172
+ alfa: {
173
+ 'r39': {
174
+ quality: 1,
175
+ what: 'Image text alternative is the filename instead'
176
+ }
177
+ }
178
+ }
179
+ },
180
+ imageTextRisk: {
181
+ weight: 1,
182
+ packages: {
183
+ wave: {
184
+ 'a:alt_suspicious': {
185
+ quality: 1,
186
+ what: 'Image alternate text is suspicious'
187
+ }
188
+ }
189
+ }
190
+ },
191
+ decorativeImageRisk: {
192
+ weight: 1,
193
+ packages: {
194
+ htmlcs: {
195
+ 'w:AA.1_1_1.H67.2': {
196
+ quality: 1,
197
+ what: 'Image marked as decorative may be informative'
198
+ }
199
+ }
200
+ }
201
+ },
202
+ pageLanguage: {
203
+ weight: 4,
204
+ packages: {
205
+ alfa: {
206
+ r4: {
207
+ quality: 1,
208
+ what: 'Lang attribute missing, empty, or only whitespace'
209
+ }
210
+ },
211
+ axe: {
212
+ 'html-has-lang': {
213
+ quality: 1,
214
+ what: 'html element must have a lang attribute'
215
+ }
216
+ },
217
+ htmlcs: {
218
+ 'e:H57': {
219
+ quality: 1,
220
+ what: 'Lang attribute of the document element'
221
+ }
222
+ },
223
+ ibm: {
224
+ WCAG20_Html_HasLang: {
225
+ quality: 1,
226
+ what: 'Page detected as HTML, but has no lang attribute'
227
+ }
228
+ },
229
+ wave: {
230
+ 'e:language_missing': {
231
+ quality: 1,
232
+ what: 'Language missing or invalid'
233
+ }
234
+ }
235
+ }
236
+ },
237
+ pageLanguageBad: {
238
+ weight: 4,
239
+ packages: {
240
+ alfa: {
241
+ r5: {
242
+ quality: 1,
243
+ what: 'lang attribute has no valid primary language tag'
244
+ }
245
+ },
246
+ axe: {
247
+ 'html-lang-valid': {
248
+ quality: 1,
249
+ what: 'html element must have a valid value for the lang attribute'
250
+ }
251
+ },
252
+ ibm: {
253
+ 'v:WCAG20_Elem_Lang_Valid': {
254
+ quality: 1,
255
+ what: 'lang attribute does not include a valid primary language'
256
+ }
257
+ }
258
+ }
259
+ },
260
+ languageChange: {
261
+ weight: 3,
262
+ packages: {
263
+ alfa: {
264
+ r7: {
265
+ quality: 1,
266
+ what: 'lang attribute has no valid primary language subtag'
267
+ }
268
+ },
269
+ axe: {
270
+ 'valid-lang': {
271
+ quality: 1,
272
+ what: 'lang attribute must have a valid value'
273
+ }
274
+ },
275
+ htmlcs: {
276
+ 'e:WCAG2AAA.Principle3.Guideline3_1.3_1_2.H58': {
277
+ quality: 1,
278
+ what: 'Change in language is not marked'
279
+ }
280
+ }
281
+ }
282
+ },
283
+ objectNoText: {
284
+ weight: 4,
285
+ packages: {
286
+ axe: {
287
+ 'object-alt': {
288
+ quality: 1,
289
+ what: 'Object elements must have alternate text'
290
+ }
291
+ },
292
+ htmlcs: {
293
+ 'e:ARIA6+H53': {
294
+ quality: 1,
295
+ what: 'Object elements must contain a text alternative'
296
+ }
297
+ },
298
+ ibm: {
299
+ 'v:WCAG20_Object_HasText': {
300
+ quality: 1,
301
+ what: 'Object elements must have a text alternative'
302
+ }
303
+ },
304
+ wave: {
305
+ 'a:plugin': {
306
+ quality: 1,
307
+ what: 'An unidentified plugin is present'
308
+ }
309
+ }
310
+ }
311
+ },
312
+ imageMapAreaNoText: {
313
+ weight: 4,
314
+ packages: {
315
+ axe: {
316
+ 'area-alt': {
317
+ quality: 1,
318
+ what: 'Active area elements must have alternate text'
319
+ }
320
+ },
321
+ htmlcs: {
322
+ 'e:AA.1_1_1.H24': {
323
+ quality: 1,
324
+ what: 'Area element in an image map missing an alt attribute'
325
+ }
326
+ },
327
+ ibm: {
328
+ 'v:HAAC_Img_UsemapAlt': {
329
+ quality: 1,
330
+ what: 'Image map or child area has no text alternative'
331
+ },
332
+ 'v:WCAG20_Area_HasAlt': {
333
+ quality: 1,
334
+ what: 'Area element in an image map has no text alternative'
335
+ }
336
+ },
337
+ wave: {
338
+ 'e:alt_area_missing': {
339
+ quality: 1,
340
+ what: 'Image map area missing alternative text'
341
+ }
342
+ }
343
+ }
344
+ },
345
+ eventKeyboard: {
346
+ weight: 4,
347
+ packages: {
348
+ htmlcs: {
349
+ 'w:G90': {
350
+ quality: 1,
351
+ what: 'Event handler functionality not available by keyboard'
352
+ }
353
+ },
354
+ wave: {
355
+ 'a:event_handler': {
356
+ quality: 1,
357
+ what: 'Device-dependent event handler'
358
+ }
359
+ }
360
+ }
361
+ },
362
+ internalLinkBroken: {
363
+ weight: 4,
364
+ packages: {
365
+ htmlcs: {
366
+ 'e:AA.2_4_1.G1,G123,G124.NoSuchID': {
367
+ quality: 1,
368
+ what: 'Internal link references a nonexistent destination'
369
+ }
370
+ },
371
+ wave: {
372
+ 'a:label_orphaned': {
373
+ quality: 1,
374
+ what: 'Orphaned form label'
375
+ }
376
+ }
377
+ }
378
+ },
379
+ labelForBadID: {
380
+ weight: 4,
381
+ packages: {
382
+ htmlcs: {
383
+ 'w:AA.1_3_1.H44.NonExistentFragment': {
384
+ quality: 1,
385
+ what: 'Label for attribute references a nonexistent element'
386
+ }
387
+ },
388
+ wave: {
389
+ 'a:label_orphaned': {
390
+ quality: 1,
391
+ what: 'Orphaned form label'
392
+ }
393
+ }
394
+ }
395
+ },
396
+ linkNoText: {
397
+ weight: 4,
398
+ packages: {
399
+ alfa: {
400
+ r11: {
401
+ quality: 1,
402
+ what: 'Link has no accessible name'
403
+ }
404
+ },
405
+ axe: {
406
+ 'link-name': {
407
+ quality: 1,
408
+ what: 'Links must have discernible text'
409
+ }
410
+ },
411
+ htmlcs: {
412
+ 'e:AA.1_1_1.H30.2': {
413
+ quality: 1,
414
+ what: 'img element is the only link content but has no text alternative'
415
+ },
416
+ 'w:AA.4_1_2.H91.A.Empty': {
417
+ quality: 1,
418
+ what: 'Link element has an id attribute but no href attribute or text'
419
+ },
420
+ 'e:AA.4_1_2.H91.A.EmptyNoId': {
421
+ quality: 1,
422
+ what: 'Link has no name or id attribute or value'
423
+ },
424
+ 'w:AA.4_1_2.H91.A.EmptyWithName': {
425
+ quality: 1,
426
+ what: 'Link has a name attribute but no href attribute or text'
427
+ },
428
+ 'e:AA.4_1_2.H91.A.NoContent': {
429
+ quality: 1,
430
+ what: 'Link has an href attribute but no text'
431
+ }
432
+ },
433
+ ibm: {
434
+ 'v:WCAG20_A_HasText': {
435
+ quality: 1,
436
+ what: 'Hyperlinks must have a text description'
437
+ }
438
+ },
439
+ tenon: {
440
+ 57: {
441
+ quality: 1,
442
+ what: 'Link has no text inside it'
443
+ }
444
+ },
445
+ wave: {
446
+ 'e:link_empty': {
447
+ quality: 1,
448
+ what: 'Link contains no text'
449
+ }
450
+ }
451
+ }
452
+ },
453
+ linkBrokenRisk: {
454
+ weight: 2,
455
+ packages: {
456
+ htmlcs: {
457
+ 'w:AA.4_1_2.H91.A.Placeholder': {
458
+ quality: 1,
459
+ what: 'Link has text but no href, id, or name attribute'
460
+ }
461
+ }
462
+ }
463
+ },
464
+ destinationLink: {
465
+ weight: 2,
466
+ packages: {
467
+ htmlcs: {
468
+ 'w:AA.4_1_2.H91.A.NoHref': {
469
+ quality: 1,
470
+ what: 'Link misused as link destination'
471
+ }
472
+ }
473
+ }
474
+ },
475
+ textAreaNoText: {
476
+ weight: 4,
477
+ packages: {
478
+ htmlcs: {
479
+ 'e:AA.4_1_2.H91.Textarea.Name': {
480
+ quality: 1,
481
+ what: 'textarea element has no accessible name'
482
+ }
483
+ }
484
+ }
485
+ },
486
+ linkTextsSame: {
487
+ weight: 2,
488
+ packages: {
489
+ tenon: {
490
+ 98: {
491
+ quality: 1,
492
+ what: 'Links have the same text but different destinations'
493
+ }
494
+ }
495
+ }
496
+ },
497
+ linkConfusionRisk: {
498
+ weight: 1,
499
+ packages: {
500
+ axe: {
501
+ 'identical-links-same-purpose': {
502
+ quality: 1,
503
+ what: 'Links with the same accessible name may serve dissimilar purposes'
504
+ }
505
+ }
506
+ }
507
+ },
508
+ linkPair: {
509
+ weight: 2,
510
+ packages: {
511
+ wave: {
512
+ 'a:link_redundant': {
513
+ quality: 1,
514
+ what: 'Adjacent links go to the same URL'
515
+ }
516
+ }
517
+ }
518
+ },
519
+ linkForcesNewWindow: {
520
+ weight: 3,
521
+ packages: {
522
+ tenon: {
523
+ 218: {
524
+ quality: 1,
525
+ what: 'Link opens in a new window without user control'
526
+ }
527
+ }
528
+ }
529
+ },
530
+ newWindowSurpriseRisk: {
531
+ weight: 1,
532
+ packages: {
533
+ htmlcs: {
534
+ 'w:WCAG2AAA.Principle3.Guideline3_2.3_2_5.H83.3': {
535
+ quality: 1,
536
+ what: 'Link may open in a new window without notice'
537
+ }
538
+ }
539
+ }
540
+ },
541
+ buttonNoText: {
542
+ weight: 4,
543
+ packages: {
544
+ alfa: {
545
+ r12: {
546
+ quality: 1,
547
+ what: 'Button has no accessible name'
548
+ }
549
+ },
550
+ axe: {
551
+ 'aria-command-name': {
552
+ quality: 1,
553
+ what: 'ARIA commands must have an accessible name'
554
+ }
555
+ },
556
+ htmlcs: {
557
+ 'e:AA.4_1_2.H91.Button.Name': {
558
+ quality: 1,
559
+ what: 'Button element has no accessible name'
560
+ }
561
+ },
562
+ wave: {
563
+ 'e:button_empty': {
564
+ quality: 1,
565
+ what: 'Button is empty or has no value text'
566
+ }
567
+ }
568
+ }
569
+ },
570
+ parentMissing: {
571
+ weight: 4,
572
+ packages: {
573
+ alfa: {
574
+ r42: {
575
+ quality: 1,
576
+ what: 'Element is not owned by an element of its required context role'
577
+ }
578
+ },
579
+ axe: {
580
+ 'aria-required-parent': {
581
+ quality: 1,
582
+ what: 'Certain ARIA roles must be contained by particular parents'
583
+ }
584
+ }
585
+ }
586
+ },
587
+ svgImageNoText: {
588
+ weight: 4,
589
+ packages: {
590
+ alfa: {
591
+ r43: {
592
+ quality: 1,
593
+ what: 'SVG image element has no accessible name'
594
+ }
595
+ },
596
+ axe: {
597
+ 'svg-img-alt': {
598
+ quality: 1,
599
+ what: 'SVG elements with an img role must have an alternative text'
600
+ }
601
+ }
602
+ }
603
+ },
604
+ metaBansZoom: {
605
+ weight: 4,
606
+ packages: {
607
+ alfa: {
608
+ r47: {
609
+ quality: 1,
610
+ what: 'Meta element restricts zooming'
611
+ }
612
+ },
613
+ axe: {
614
+ 'meta-viewport': {
615
+ quality: 1,
616
+ what: 'Zooming and scaling should not be disabled'
617
+ }
618
+ }
619
+ }
620
+ },
621
+ childMissing: {
622
+ weight: 4,
623
+ packages: {
624
+ alfa: {
625
+ r68: {
626
+ quality: 1,
627
+ what: 'Element owns no elements as required by its semantic role'
628
+ }
629
+ },
630
+ axe: {
631
+ 'aria-required-children': {
632
+ quality: 1,
633
+ what: 'Certain ARIA roles must contain particular children'
634
+ }
635
+ }
636
+ }
637
+ },
638
+ presentationChild: {
639
+ weight: 4,
640
+ packages: {
641
+ htmlcs: {
642
+ 'e:AA.1_3_1.F92,ARIA4': {
643
+ quality: 1,
644
+ what: 'Element has presentation role but semantic child'
645
+ }
646
+ }
647
+ }
648
+ },
649
+ fontSizeAbsolute: {
650
+ weight: 2,
651
+ packages: {
652
+ alfa: {
653
+ r74: {
654
+ quality: 1,
655
+ what: 'Paragraph text has absolute font size'
656
+ }
657
+ }
658
+ }
659
+ },
660
+ fontSmall: {
661
+ weight: 3,
662
+ packages: {
663
+ tenon: {
664
+ 134: {
665
+ quality: 1,
666
+ what: 'Text is very small'
667
+ }
668
+ },
669
+ wave: {
670
+ 'a:text_small': {
671
+ quality: 1,
672
+ what: 'Text is very small'
673
+ }
674
+ }
675
+ }
676
+ },
677
+ leadingFrozen: {
678
+ weight: 4,
679
+ packages: {
680
+ alfa: {
681
+ r93: {
682
+ quality: 1,
683
+ what: 'Style attribute with !important prevents adjusting line height'
684
+ }
685
+ },
686
+ axe: {
687
+ 'avoid-inline-spacing': {
688
+ quality: 1,
689
+ what: 'Inline text spacing must be adjustable with custom stylesheets'
690
+ }
691
+ }
692
+ }
693
+ },
694
+ leadingAbsolute: {
695
+ weight: 2,
696
+ packages: {
697
+ alfa: {
698
+ r80: {
699
+ quality: 1,
700
+ what: 'Paragraph text has absolute line height'
701
+ }
702
+ }
703
+ }
704
+ },
705
+ noLeading: {
706
+ weight: 3,
707
+ packages: {
708
+ alfa: {
709
+ r73: {
710
+ quality: 1,
711
+ what: 'Paragraphs of text have insufficient line height'
712
+ }
713
+ }
714
+ }
715
+ },
716
+ leadingClipsText: {
717
+ weight: 4,
718
+ packages: {
719
+ tenon: {
720
+ 144: {
721
+ quality: 1,
722
+ what: 'Line height is insufficent to properly display the computed font size'
723
+ }
724
+ }
725
+ }
726
+ },
727
+ overflowHidden: {
728
+ weight: 4,
729
+ packages: {
730
+ alfa: {
731
+ r83: {
732
+ quality: 1,
733
+ what: 'Overflow is hidden or clipped if the text is enlarged'
734
+ }
735
+ }
736
+ }
737
+ },
738
+ iframeTitleBad: {
739
+ weight: 4,
740
+ packages: {
741
+ alfa: {
742
+ r13: {
743
+ quality: 1,
744
+ what: 'iframe has no accessible name'
745
+ }
746
+ },
747
+ axe: {
748
+ 'frame-title': {
749
+ quality: 1,
750
+ what: 'Frame has no accessible name'
751
+ },
752
+ 'frame-title-unique': {
753
+ quality: 1,
754
+ what: 'Frame title attribute is not unique'
755
+ }
756
+ },
757
+ htmlcs: {
758
+ 'e:AA.2_4_1.H64.1': {
759
+ quality: 1,
760
+ what: 'iframe element requires a non-empty title attribute'
761
+ }
762
+ },
763
+ ibm: {
764
+ 'v:WCAG20_Frame_HasTitle': {
765
+ quality: 1,
766
+ what: 'Inline frame has an empty or nonunique title attribute'
767
+ }
768
+ }
769
+ }
770
+ },
771
+ roleBad: {
772
+ weight: 3,
773
+ packages: {
774
+ axe: {
775
+ 'aria-allowed-role': {
776
+ quality: 1,
777
+ what: 'ARIA role should be appropriate for the element'
778
+ }
779
+ },
780
+ ibm: {
781
+ 'v:aria_semantics_role': {
782
+ quality: 1,
783
+ what: 'ARIA roles must be valid for the element to which they are assigned'
784
+ }
785
+ },
786
+ testaro: {
787
+ role: {
788
+ quality: 1,
789
+ what: 'Nonexistent or implicit-overriding role'
790
+ }
791
+ }
792
+ }
793
+ },
794
+ roleMissingAttribute: {
795
+ weight: 4,
796
+ packages: {
797
+ axe: {
798
+ 'aria-required-attr': {
799
+ quality: 1,
800
+ what: 'Required ARIA attributes must be provided'
801
+ }
802
+ },
803
+ ibm: {
804
+ 'v:Rpt_Aria_RequiredProperties': {
805
+ quality: 1,
806
+ what: 'ARIA role on an element must have required attributes'
807
+ }
808
+ }
809
+ }
810
+ },
811
+ ariaBadAttribute: {
812
+ weight: 4,
813
+ packages: {
814
+ alfa: {
815
+ r20: {
816
+ quality: 1,
817
+ what: 'ARIA attribute is not defined'
818
+ }
819
+ },
820
+ axe: {
821
+ 'aria-valid-attr': {
822
+ quality: 1,
823
+ what: 'ARIA attribute has an invalid name'
824
+ },
825
+ 'aria-valid-attr-value': {
826
+ quality: 1,
827
+ what: 'ARIA attribute has an invalid value'
828
+ },
829
+ 'aria-allowed-attr': {
830
+ quality: 1,
831
+ what: 'ARIA attribute is invalid for the role of its element'
832
+ }
833
+ },
834
+ ibm: {
835
+ 'v:Rpt_Aria_ValidProperty': {
836
+ quality: 1,
837
+ what: 'ARIA attribute is invalid for the role'
838
+ }
839
+ }
840
+ }
841
+ },
842
+ ariaReferenceBad: {
843
+ weight: 4,
844
+ packages: {
845
+ ibm: {
846
+ 'v:Rpt_Aria_ValidIdRef': {
847
+ quality: 1,
848
+ what: 'ARIA property must reference non-empty unique id of visible element'
849
+ }
850
+ },
851
+ wave: {
852
+ 'e:aria_reference_broken': {
853
+ quality: 1,
854
+ what: 'Broken ARIA reference'
855
+ }
856
+ }
857
+ }
858
+ },
859
+ ariaRoleDescriptionBad: {
860
+ weight: 3,
861
+ packages: {
862
+ axe: {
863
+ 'aria-roledescription': {
864
+ quality: 1,
865
+ what: 'aria-roledescription is on an element with no semantic role'
866
+ }
867
+ }
868
+ }
869
+ },
870
+ autocompleteBad: {
871
+ weight: 3,
872
+ packages: {
873
+ alfa: {
874
+ r10: {
875
+ quality: 1,
876
+ what: 'Autocomplete attribute has no valid value'
877
+ }
878
+ },
879
+ axe: {
880
+ 'autocomplete-valid': {
881
+ quality: 1,
882
+ what: 'Autocomplete attribute must be used correctly'
883
+ }
884
+ },
885
+ htmlcs: {
886
+ 'e:AA.1_3_5.H98': {
887
+ quality: 1,
888
+ what: 'Autocomplete attribute and the input type are mismatched'
889
+ }
890
+ },
891
+ ibm: {
892
+ 'v:WCAG21_Input_Autocomplete': {
893
+ quality: 1,
894
+ what: 'Autocomplete attribute token(s) must be appropriate for the input form field'
895
+ }
896
+ }
897
+ }
898
+ },
899
+ contrastAA: {
900
+ weight: 3,
901
+ packages: {
902
+ alfa: {
903
+ r69: {
904
+ quality: 1,
905
+ what: 'Text outside widget has subminimum contrast'
906
+ }
907
+ },
908
+ axe: {
909
+ 'color-contrast': {
910
+ quality: 1,
911
+ what: 'Elements must have sufficient color contrast'
912
+ }
913
+ },
914
+ htmlcs: {
915
+ 'e:AA.1_4_3.G145.Fail': {
916
+ quality: 1,
917
+ what: 'Contrast between the text and its background is less than 3:1.'
918
+ },
919
+ 'e:AA.1_4_3.G18.Fail': {
920
+ quality: 1,
921
+ what: 'Contrast between the text and its background is less than 4.5:1'
922
+ }
923
+ },
924
+ ibm: {
925
+ 'v:IBMA_Color_Contrast_WCAG2AA': {
926
+ quality: 1,
927
+ what: 'Contrast ratio of text with background must meet WCAG 2.1 AA'
928
+ }
929
+ },
930
+ wave: {
931
+ 'c:contrast': {
932
+ quality: 1,
933
+ what: 'Very low contrast'
934
+ }
935
+ }
936
+ }
937
+ },
938
+ contrastAAA: {
939
+ weight: 1,
940
+ packages: {
941
+ alfa: {
942
+ r66: {
943
+ quality: 1,
944
+ what: 'Text contrast less than AAA requires'
945
+ }
946
+ },
947
+ axe: {
948
+ 'color-contrast-enhanced': {
949
+ quality: 1,
950
+ what: 'Elements must have sufficient color contrast (Level AAA)'
951
+ }
952
+ },
953
+ htmlcs: {
954
+ 'e:WCAG2AAA.Principle1.Guideline1_4.1_4_3.G18': {
955
+ quality: 1,
956
+ what: 'Insufficient contrast'
957
+ }
958
+ },
959
+ tenon: {
960
+ 95: {
961
+ quality: 1,
962
+ what: 'Element has insufficient color contrast (Level AAA)'
963
+ }
964
+ }
965
+ }
966
+ },
967
+ contrastRisk: {
968
+ weight: 1,
969
+ packages: {
970
+ htmlcs: {
971
+ 'w:AA.1_4_3_F24.F24.BGColour': {
972
+ quality: 1,
973
+ what: 'Inline background color may lack a complementary foreground color'
974
+ },
975
+ 'w:AA.1_4_3_F24.F24.FGColour': {
976
+ quality: 1,
977
+ what: 'Inline foreground color may lack a complementary background color'
978
+ },
979
+ 'w:AA.1_4_3.G18.Abs': {
980
+ quality: 1,
981
+ what: 'Contrast between the absolutely positioned text and its background may be inadequate'
982
+ },
983
+ 'w:AA.1_4_3.G18.Alpha': {
984
+ quality: 1,
985
+ what: 'Contrast between the text and its background may be less than 4.5:1, given the transparency'
986
+ },
987
+ 'w:AA.1_4_3.G145.Abs': {
988
+ quality: 1,
989
+ what: 'Contrast between the absolutely positioned large text and its background may be less than 3:1'
990
+ },
991
+ 'w:AA.1_4_3.G145.Alpha': {
992
+ quality: 1,
993
+ what: 'Contrast between the text and its background may be less than 3:1, given the transparency'
994
+ },
995
+ 'w:AA.1_4_3.G145.BgImage': {
996
+ quality: 1,
997
+ what: 'Contrast between the text and its background image may be less than 3:1'
998
+ },
999
+ 'w:AA.1_4_3.G18.BgImage': {
1000
+ quality: 1,
1001
+ what: 'Contrast between the text and its background image may be less than 4.5:1'
1002
+ }
1003
+ }
1004
+ }
1005
+ },
1006
+ headingEmpty: {
1007
+ weight: 3,
1008
+ packages: {
1009
+ axe: {
1010
+ 'empty-heading': {
1011
+ quality: 1,
1012
+ what: 'Headings should not be empty'
1013
+ }
1014
+ },
1015
+ htmlcs: {
1016
+ 'e:AA.1_3_1.H42.2': {
1017
+ quality: 1,
1018
+ what: 'Heading empty'
1019
+ }
1020
+ },
1021
+ ibm: {
1022
+ 'v:RPT_Header_HasContent': {
1023
+ quality: 1,
1024
+ what: 'Heading elements must provide descriptive text'
1025
+ }
1026
+ },
1027
+ wave: {
1028
+ 'e:heading_empty': {
1029
+ quality: 1,
1030
+ what: 'Empty heading'
1031
+ }
1032
+ }
1033
+ }
1034
+ },
1035
+ imageTextRedundant: {
1036
+ weight: 1,
1037
+ packages: {
1038
+ axe: {
1039
+ 'image-redundant-alt': {
1040
+ quality: 1,
1041
+ what: 'Text of buttons and links should not be repeated in the image alternative'
1042
+ }
1043
+ },
1044
+ ibm: {
1045
+ 'v:WCAG20_Img_LinkTextNotRedundant': {
1046
+ quality: 1,
1047
+ what: 'Text alternative for image within link should not repeat link text or adjacent link text'
1048
+ }
1049
+ }
1050
+ }
1051
+ },
1052
+ titleRedundant: {
1053
+ weight: 1,
1054
+ packages: {
1055
+ tenon: {
1056
+ 79: {
1057
+ quality: 1,
1058
+ what: 'Link has a title attribute that is the same as the text inside the link'
1059
+ }
1060
+ },
1061
+ wave: {
1062
+ 'a:title_redundant': {
1063
+ quality: 1,
1064
+ what: 'Title attribute text is the same as text or alternative text'
1065
+ }
1066
+ }
1067
+ }
1068
+ },
1069
+ titleEmpty: {
1070
+ weight: 1,
1071
+ packages: {
1072
+ htmlcs: {
1073
+ 'w:AA.1_3_1.H65': {
1074
+ quality: 0.5,
1075
+ what: 'Value of the title attribute of the form control is empty or only whitespace'
1076
+ },
1077
+ 'w:AA.4_1_2.H65': {
1078
+ quality: 0.5,
1079
+ what: 'Value of the title attribute of the form control is empty or only whitespace'
1080
+ }
1081
+ }
1082
+ }
1083
+ },
1084
+ pageTitle: {
1085
+ weight: 3,
1086
+ packages: {
1087
+ axe: {
1088
+ 'document-title': {
1089
+ quality: 1,
1090
+ what: 'Documents must contain a title element'
1091
+ }
1092
+ },
1093
+ wave: {
1094
+ 'e:title_invalid': {
1095
+ quality: 1,
1096
+ what: 'Missing or uninformative page title'
1097
+ }
1098
+ }
1099
+ }
1100
+ },
1101
+ headingStructure: {
1102
+ weight: 2,
1103
+ packages: {
1104
+ alfa: {
1105
+ r53: {
1106
+ quality: 1,
1107
+ what: 'Heading skips one or more levels'
1108
+ }
1109
+ },
1110
+ axe: {
1111
+ 'heading-order': {
1112
+ quality: 1,
1113
+ what: 'Heading levels should only increase by one'
1114
+ }
1115
+ },
1116
+ htmlcs: {
1117
+ 'w:AA.1_3_1_A.G141': {
1118
+ quality: 1,
1119
+ what: 'Heading level is incorrect'
1120
+ }
1121
+ },
1122
+ tenon: {
1123
+ 155: {
1124
+ quality: 1,
1125
+ what: 'These headings are not structured in a hierarchical manner'
1126
+ }
1127
+ },
1128
+ wave: {
1129
+ 'a:heading_skipped': {
1130
+ quality: 1,
1131
+ what: 'Skipped heading level'
1132
+ }
1133
+ }
1134
+ }
1135
+ },
1136
+ h1Missing: {
1137
+ weight: 2,
1138
+ packages: {
1139
+ alfa: {
1140
+ 'r61': {
1141
+ quality: 1,
1142
+ what: 'First heading is not h1'
1143
+ }
1144
+ },
1145
+ axe: {
1146
+ 'page-has-heading-one': {
1147
+ quality: 1,
1148
+ what: 'Page should contain a level-one heading'
1149
+ }
1150
+ },
1151
+ wave: {
1152
+ 'a:h1_missing': {
1153
+ quality: 1,
1154
+ what: 'Missing first level heading'
1155
+ }
1156
+ }
1157
+ }
1158
+ },
1159
+ pseudoHeadingRisk: {
1160
+ weight: 1,
1161
+ packages: {
1162
+ htmlcs: {
1163
+ 'w:AA.1_3_1.H42': {
1164
+ quality: 1,
1165
+ what: 'Heading coding should be used if intended as a heading'
1166
+ }
1167
+ },
1168
+ wave: {
1169
+ 'a:heading_possible': {
1170
+ quality: 1,
1171
+ what: 'Possible heading'
1172
+ }
1173
+ }
1174
+ }
1175
+ },
1176
+ pseudoLinkRisk: {
1177
+ weight: 1,
1178
+ packages: {
1179
+ tenon: {
1180
+ 129: {
1181
+ quality: 1,
1182
+ what: 'CSS underline on text that is not a link'
1183
+ }
1184
+ }
1185
+ }
1186
+ },
1187
+ pseudoOrderedListRisk: {
1188
+ weight: 1,
1189
+ packages: {
1190
+ htmlcs: {
1191
+ 'w:AA.1_3_1.H48.2': {
1192
+ quality: 1,
1193
+ what: 'Ordered list may fail to be coded as such'
1194
+ }
1195
+ }
1196
+ }
1197
+ },
1198
+ pseudoNavListRisk: {
1199
+ weight: 1,
1200
+ packages: {
1201
+ htmlcs: {
1202
+ 'w:AA.1_3_1.H48': {
1203
+ quality: 1,
1204
+ what: 'Navigation links should best be coded as a list'
1205
+ }
1206
+ }
1207
+ }
1208
+ },
1209
+ selectNoText: {
1210
+ weight: 3,
1211
+ packages: {
1212
+ axe: {
1213
+ 'select-name': {
1214
+ quality: 1,
1215
+ what: 'Select element must have an accessible name'
1216
+ }
1217
+ },
1218
+ htmlcs: {
1219
+ 'w:H91': {
1220
+ quality: 1,
1221
+ what: 'Select element has no value available to an accessibility API'
1222
+ }
1223
+ },
1224
+ wave: {
1225
+ 'a:select_missing_label': {
1226
+ quality: 1,
1227
+ what: 'Select missing label'
1228
+ }
1229
+ }
1230
+ }
1231
+ },
1232
+ selectFlatRisk: {
1233
+ weight: 1,
1234
+ packages: {
1235
+ htmlcs: {
1236
+ 'w:AA.1_3_1.H85.2': {
1237
+ quality: 1,
1238
+ what: 'If selection list contains groups of related options, they should be grouped with optgroup'
1239
+ }
1240
+ }
1241
+ }
1242
+ },
1243
+ accessKeyDuplicate: {
1244
+ weight: 3,
1245
+ packages: {
1246
+ ibm: {
1247
+ 'v:WCAG20_Elem_UniqueAccessKey': {
1248
+ quality: 1,
1249
+ what: 'Accesskey attribute values on each element must be unique for the page'
1250
+ }
1251
+ },
1252
+ wave: {
1253
+ 'a:accesskey': {
1254
+ quality: 1,
1255
+ what: 'Accesskey'
1256
+ }
1257
+ }
1258
+ }
1259
+ },
1260
+ fieldSetMissing: {
1261
+ weight: 2,
1262
+ packages: {
1263
+ ibm: {
1264
+ 'v:WCAG20_Input_RadioChkInFieldSet': {
1265
+ quality: 1,
1266
+ what: 'Input is in a different group than another with the name'
1267
+ }
1268
+ },
1269
+ testaro: {
1270
+ radioSet: {
1271
+ quality: 1,
1272
+ what: 'No or invalid grouping of radio buttons in fieldsets'
1273
+ }
1274
+ },
1275
+ wave: {
1276
+ 'a:fieldset_missing': {
1277
+ quality: 1,
1278
+ what: 'Missing fieldset'
1279
+ }
1280
+ }
1281
+ }
1282
+ },
1283
+ legendMissing: {
1284
+ weight: 2,
1285
+ packages: {
1286
+ htmlcs: {
1287
+ 'e:AA.1_3_1.H71.NoLegend': {
1288
+ quality: 1,
1289
+ what: 'Fieldset has no legend element'
1290
+ }
1291
+ }
1292
+ }
1293
+ },
1294
+ fieldSetName: {
1295
+ weight: 3,
1296
+ packages: {
1297
+ htmlcs: {
1298
+ 'e:AA.4_1_2.H91.Fieldset.Name': {
1299
+ quality: 1,
1300
+ what: 'Fieldset has no accessible name'
1301
+ }
1302
+ }
1303
+ }
1304
+ },
1305
+ tableCaption: {
1306
+ weight: 1,
1307
+ packages: {
1308
+ htmlcs: {
1309
+ 'w:AA.1_3_1.H39.3.NoCaption': {
1310
+ quality: 1,
1311
+ what: 'Table has no caption element'
1312
+ }
1313
+ }
1314
+ }
1315
+ },
1316
+ nameValue: {
1317
+ weight: 4,
1318
+ packages: {
1319
+ htmlcs: {
1320
+ 'e:AA.1_3_1.F68': {
1321
+ quality: 1,
1322
+ what: 'Form control has no label'
1323
+ }
1324
+ },
1325
+ wave: {
1326
+ 'e:label_missing': {
1327
+ quality: 1,
1328
+ what: 'Missing form label'
1329
+ }
1330
+ }
1331
+ }
1332
+ },
1333
+ invisibleLabel: {
1334
+ weight: 3,
1335
+ packages: {
1336
+ htmlcs: {
1337
+ 'w:AA.2_5_3.F96': {
1338
+ quality: 1,
1339
+ what: 'Visible label is not in the accessible name'
1340
+ }
1341
+ }
1342
+ }
1343
+ },
1344
+ targetSize: {
1345
+ weight: 2,
1346
+ packages: {
1347
+ tenon: {
1348
+ 152: {
1349
+ quality: 1,
1350
+ what: 'Actionable element is smaller than the minimum required size'
1351
+ }
1352
+ }
1353
+ }
1354
+ },
1355
+ visibleBulk: {
1356
+ weight: 1,
1357
+ packages: {
1358
+ testaro: {
1359
+ bulk: {
1360
+ quality: 1,
1361
+ what: 'Page contains many visible elements'
1362
+ }
1363
+ }
1364
+ }
1365
+ },
1366
+ activeEmbedding: {
1367
+ weight: 3,
1368
+ packages: {
1369
+ testaro: {
1370
+ embAc: {
1371
+ quality: 1,
1372
+ what: 'Active elements embedded in links or buttons'
1373
+ }
1374
+ }
1375
+ }
1376
+ },
1377
+ tabFocusability: {
1378
+ weight: 3,
1379
+ packages: {
1380
+ testaro: {
1381
+ focAll: {
1382
+ quality: 1,
1383
+ what: 'Discrepancy between elements that should be and that are Tab-focusable'
1384
+ }
1385
+ }
1386
+ }
1387
+ },
1388
+ focusIndication: {
1389
+ weight: 4,
1390
+ packages: {
1391
+ alfa: {
1392
+ r65: {
1393
+ quality: 1,
1394
+ what: 'Element in sequential focus order has no visible focus'
1395
+ }
1396
+ },
1397
+ testaro: {
1398
+ focInd: {
1399
+ quality: 1,
1400
+ what: 'Focused element displaying no or nostandard focus indicator'
1401
+ }
1402
+ }
1403
+ }
1404
+ },
1405
+ allCaps: {
1406
+ weight: 1,
1407
+ packages: {
1408
+ tenon: {
1409
+ 153: {
1410
+ quality: 1,
1411
+ what: 'Long string of text is in all caps'
1412
+ }
1413
+ }
1414
+ }
1415
+ },
1416
+ textBeyondLandmarks: {
1417
+ weight: 2,
1418
+ packages: {
1419
+ alfa: {
1420
+ r57: {
1421
+ quality: 1,
1422
+ what: 'Perceivable text content not included in any landmark'
1423
+ }
1424
+ }
1425
+ }
1426
+ },
1427
+ mainTopLandmark: {
1428
+ weight: 2,
1429
+ packages: {
1430
+ axe: {
1431
+ 'landmark-main-is-top-level': {
1432
+ quality: 1,
1433
+ what: 'main landmark is contained in another landmark'
1434
+ }
1435
+ }
1436
+ }
1437
+ },
1438
+ multipleMain: {
1439
+ weight: 2,
1440
+ packages: {
1441
+ axe: {
1442
+ 'landmark-no-duplicate-main': {
1443
+ quality: 1,
1444
+ what: 'page has more than 1 main landmark'
1445
+ }
1446
+ }
1447
+ }
1448
+ },
1449
+ focusableOperable: {
1450
+ weight: 3,
1451
+ packages: {
1452
+ testaro: {
1453
+ focOp: {
1454
+ quality: 1,
1455
+ what: 'Operable elements that cannot be Tab-focused and vice versa'
1456
+ }
1457
+ }
1458
+ }
1459
+ },
1460
+ focusableRole: {
1461
+ weight: 3,
1462
+ packages: {
1463
+ axe: {
1464
+ 'focus-order-semantics': {
1465
+ quality: 1,
1466
+ what: 'Focusable element has no active role'
1467
+ }
1468
+ }
1469
+ }
1470
+ },
1471
+ focusableHidden: {
1472
+ weight: 4,
1473
+ packages: {
1474
+ alfa: {
1475
+ r17: {
1476
+ quality: 1,
1477
+ what: 'Tab-focusable element is or has an ancestor that is aria-hidden'
1478
+ }
1479
+ },
1480
+ axe: {
1481
+ 'aria-hidden-focus': {
1482
+ quality: 1,
1483
+ what: 'ARIA hidden element is focusable or contains a focusable element'
1484
+ }
1485
+ }
1486
+ }
1487
+ },
1488
+ hiddenContentRisk: {
1489
+ weight: 1,
1490
+ packages: {
1491
+ axe: {
1492
+ 'hidden-content': {
1493
+ quality: 1,
1494
+ what: 'Some content is hidden and therefore may not be testable for accessibility'
1495
+ }
1496
+ }
1497
+ }
1498
+ },
1499
+ frameContentRisk: {
1500
+ weight: 1,
1501
+ packages: {
1502
+ axe: {
1503
+ 'frame-tested': {
1504
+ quality: 0.2,
1505
+ what: 'Some content is in an iframe and therefore may not be testable for accessibility'
1506
+ }
1507
+ }
1508
+ }
1509
+ },
1510
+ hoverSurprise: {
1511
+ weight: 1,
1512
+ packages: {
1513
+ testaro: {
1514
+ hover: {
1515
+ quality: 1,
1516
+ what: 'Content changes caused by hovering'
1517
+ }
1518
+ }
1519
+ }
1520
+ },
1521
+ labelClash: {
1522
+ weight: 2,
1523
+ packages: {
1524
+ testaro: {
1525
+ labClash: {
1526
+ quality: 1,
1527
+ what: 'Incompatible label types'
1528
+ }
1529
+ }
1530
+ }
1531
+ },
1532
+ labelEmpty: {
1533
+ weight: 3,
1534
+ packages: {
1535
+ htmlcs: {
1536
+ 'w:AA.1_3_1.ARIA6': {
1537
+ quality: 1,
1538
+ what: 'Value of the aria-label attribute of the form control is empty or only whitespace'
1539
+ }
1540
+ }
1541
+ }
1542
+ },
1543
+ linkUnderlines: {
1544
+ weight: 2,
1545
+ packages: {
1546
+ testaro: {
1547
+ linkUl: {
1548
+ quality: 1,
1549
+ what: 'Non-underlined inline links'
1550
+ }
1551
+ }
1552
+ }
1553
+ },
1554
+ menuNavigation: {
1555
+ weight: 2,
1556
+ packages: {
1557
+ testaro: {
1558
+ menuNav: {
1559
+ quality: 1,
1560
+ what: 'Nonstandard keyboard navigation among focusable menu items'
1561
+ }
1562
+ }
1563
+ }
1564
+ },
1565
+ tabNavigation: {
1566
+ weight: 2,
1567
+ packages: {
1568
+ testaro: {
1569
+ tabNav: {
1570
+ quality: 1,
1571
+ what: 'Nonstandard keyboard navigation among tabs'
1572
+ }
1573
+ }
1574
+ }
1575
+ },
1576
+ spontaneousMotion: {
1577
+ weight: 2,
1578
+ packages: {
1579
+ testaro: {
1580
+ motion: {
1581
+ quality: 1,
1582
+ what: 'Change of visible content not requested by user'
1583
+ }
1584
+ }
1585
+ }
1586
+ },
1587
+ inconsistentStyles: {
1588
+ weight: 1,
1589
+ packages: {
1590
+ testaro: {
1591
+ styleDiff: {
1592
+ quality: 1,
1593
+ what: 'Heading, link, and button style inconsistencies'
1594
+ }
1595
+ }
1596
+ }
1597
+ },
1598
+ zIndexNotZero: {
1599
+ weight: 1,
1600
+ packages: {
1601
+ testaro: {
1602
+ zIndex: {
1603
+ quality: 1,
1604
+ what: 'Layering with nondefault z-index values'
1605
+ }
1606
+ }
1607
+ }
1608
+ },
1609
+ videoCaptionMissing: {
1610
+ weight: 4,
1611
+ packages: {
1612
+ axe: {
1613
+ 'video-caption': {
1614
+ quality: 1,
1615
+ what: 'video element has no captions'
1616
+ }
1617
+ }
1618
+ }
1619
+ },
1620
+ videoCaptionRisk: {
1621
+ weight: 1,
1622
+ packages: {
1623
+ wave: {
1624
+ 'a:youtube_video': {
1625
+ quality: 1,
1626
+ what: 'YouTube video may fail to be captioned'
1627
+ }
1628
+ }
1629
+ }
1630
+ },
1631
+ notScrollable: {
1632
+ weight: 4,
1633
+ packages: {
1634
+ alfa: {
1635
+ r84: {
1636
+ quality: 1,
1637
+ what: 'Element is scrollable but not by keyboard'
1638
+ }
1639
+ }
1640
+ }
1641
+ },
1642
+ horizontalScrolling: {
1643
+ weight: 3,
1644
+ packages: {
1645
+ tenon: {
1646
+ 28: {
1647
+ quality: 1,
1648
+ what: 'Layout or sizing of the page causes horizontal scrolling'
1649
+ }
1650
+ }
1651
+ }
1652
+ },
1653
+ scrollRisk: {
1654
+ weight: 1,
1655
+ packages: {
1656
+ htmlcs: {
1657
+ 'w:AA.1_4_10.C32,C31,C33,C38,SCR34,G206': {
1658
+ quality: 1,
1659
+ what: 'Fixed-position element may force bidirectional scrolling'
1660
+ }
1661
+ }
1662
+ }
1663
+ },
1664
+ skipRepeatedContent: {
1665
+ weight: 3,
1666
+ packages: {
1667
+ alfa: {
1668
+ 'r87': {
1669
+ quality: 0.5,
1670
+ what: 'First focusable element is not a link to the main content'
1671
+ }
1672
+ }
1673
+ }
1674
+ },
1675
+ submitButton: {
1676
+ weight: 3,
1677
+ packages: {
1678
+ htmlcs: {
1679
+ 'e:AA.3_2_2.H32.2': {
1680
+ quality: 1,
1681
+ what: 'Form has no submit button'
1682
+ }
1683
+ }
1684
+ }
1685
+ },
1686
+ noScriptRisk: {
1687
+ weight: 1,
1688
+ packages: {
1689
+ wave: {
1690
+ 'a:noscript': {
1691
+ quality: 1,
1692
+ what: 'noscript element may fail to contain an accessible equivalent or alternative'
1693
+ }
1694
+ }
1695
+ }
1696
+ },
1697
+ obsoleteElement: {
1698
+ weight: 1,
1699
+ packages: {
1700
+ htmlcs: {
1701
+ 'e:AA.1_3_1.H49.Center': {
1702
+ quality: 1,
1703
+ what: 'The center element is obsolete'
1704
+ }
1705
+ }
1706
+ }
1707
+ },
1708
+ obsoleteAttribute: {
1709
+ weight: 1,
1710
+ packages: {
1711
+ htmlcs: {
1712
+ 'e:AA.1_3_1.H49.AlignAttr': {
1713
+ quality: 1,
1714
+ what: 'The align attribute is obsolete'
1715
+ }
1716
+ }
1717
+ }
1718
+ }
1719
+ };
1720
+
1721
+ // VARIABLES
1722
+
1723
+ let packageDetails = {};
1724
+ let groupDetails = {};
1725
+ let summary = {};
1726
+ let preventionScores = {};
1727
+
1728
+ // FUNCTIONS
1729
+
1730
+ // Initialize the variables.
1731
+ const init = () => {
1732
+ packageDetails = {};
1733
+ groupDetails = {
1734
+ groups: {},
1735
+ solos: {}
1736
+ };
1737
+ summary = {
1738
+ total: 0,
1739
+ log: 0,
1740
+ preventions: 0,
1741
+ solos: 0,
1742
+ groups: []
1743
+ };
1744
+ preventionScores = {};
1745
+ };
1746
+
1747
+ // Adds a score to the package details.
1748
+ const addDetail = (actWhich, testID, addition = 1) => {
1749
+ if (addition) {
1750
+ if (!packageDetails[actWhich]) {
1751
+ packageDetails[actWhich] = {};
1752
+ }
1753
+ if (!packageDetails[actWhich][testID]) {
1754
+ packageDetails[actWhich][testID] = 0;
1755
+ }
1756
+ packageDetails[actWhich][testID] += Math.round(addition);
1757
+ }
1758
+ };
1759
+ // Scores a report.
1760
+ exports.scorer = async report => {
1761
+ // Initialize the variables.
1762
+ init();
1763
+ // If there are any acts in the report:
1764
+ const {acts} = report;
1765
+ if (Array.isArray(acts)) {
1766
+ // If any of them are test acts:
1767
+ const testActs = acts.filter(act => act.type === 'test');
1768
+ if (testActs.length) {
1769
+ // For each test act:
1770
+ testActs.forEach(test => {
1771
+ const {which} = test;
1772
+ // Add scores to the package details.
1773
+ if (which === 'alfa') {
1774
+ const issues = test.result && test.result.items;
1775
+ if (issues && Array.isArray(issues)) {
1776
+ issues.forEach(issue => {
1777
+ const {verdict, rule} = issue;
1778
+ if (verdict && rule) {
1779
+ const {ruleID} = rule;
1780
+ if (ruleID) {
1781
+ // Add 4 per failure, 1 per warning (“cantTell”).
1782
+ addDetail(which, ruleID, verdict === 'failed' ? 4 : 1);
1783
+ }
1784
+ }
1785
+ });
1786
+ }
1787
+ }
1788
+ else if (which === 'axe') {
1789
+ const impactScores = {
1790
+ minor: 1,
1791
+ moderate: 2,
1792
+ serious: 3,
1793
+ critical: 4
1794
+ };
1795
+ const tests = test.result && test.result.details;
1796
+ if (tests) {
1797
+ const warnings = tests.incomplete;
1798
+ const {violations} = tests;
1799
+ [[warnings, 0.25], [violations, 1]].forEach(issueClass => {
1800
+ if (issueClass[0] && Array.isArray(issueClass[0])) {
1801
+ issueClass[0].forEach(issueType => {
1802
+ const {id, nodes} = issueType;
1803
+ if (id && nodes && Array.isArray(nodes)) {
1804
+ nodes.forEach(node => {
1805
+ const {impact} = node;
1806
+ if (impact) {
1807
+ // Add the impact score for a violation or 25% of it for a warning.
1808
+ addDetail(which, id, issueClass[1] * impactScores[impact]);
1809
+ }
1810
+ });
1811
+ }
1812
+ });
1813
+ }
1814
+ });
1815
+ }
1816
+ }
1817
+ else if (which === 'htmlcs') {
1818
+ const issues = test.result;
1819
+ if (issues) {
1820
+ ['Error', 'Warning'].forEach(issueClassName => {
1821
+ const classData = issues[issueClassName];
1822
+ if (classData) {
1823
+ const issueTypes = Object.keys(classData);
1824
+ issueTypes.forEach(issueTypeName => {
1825
+ const issueArrays = Object.values(classData[issueTypeName]);
1826
+ const issueCount = issueArrays.reduce((count, array) => count + array.length, 0);
1827
+ const classCode = issueClassName[0].toLowerCase();
1828
+ const code = `${classCode}:${issueTypeName}`;
1829
+ // Add 4 per error, 1 per warning.
1830
+ const weight = classCode === 'e' ? 4 : 1;
1831
+ addDetail(which, code, weight * issueCount);
1832
+ });
1833
+ }
1834
+ });
1835
+ }
1836
+ }
1837
+ else if (which === 'ibm') {
1838
+ const {result} = test;
1839
+ const {content, url} = result;
1840
+ if (content && url) {
1841
+ let preferredMode = 'content';
1842
+ if (
1843
+ content.error ||
1844
+ (content.totals &&
1845
+ content.totals.violation &&
1846
+ url.totals &&
1847
+ url.totals.violation &&
1848
+ url.totals.violation > content.totals.violation)
1849
+ ) {
1850
+ preferredMode = 'url';
1851
+ }
1852
+ const {items} = result[preferredMode];
1853
+ if (items && Array.isArray(items)) {
1854
+ items.forEach(issue => {
1855
+ const {ruleID, level} = issue;
1856
+ if (ruleID && level) {
1857
+ // Add 4 per violation, 1 per warning (“recommendation”).
1858
+ addDetail(which, ruleID, level === 'violation' ? 4 : 1);
1859
+ }
1860
+ });
1861
+ }
1862
+ }
1863
+ }
1864
+ else if (which === 'tenon') {
1865
+ const issues =
1866
+ test.result && test.result.data && test.result.data.resultSet;
1867
+ if (issues && Array.isArray(issues)) {
1868
+ issues.forEach(issue => {
1869
+ const {tID, priority, certainty} = issue;
1870
+ if (tID && priority && certainty) {
1871
+ // Add 4 per issue if certainty and priority 100, less if less.
1872
+ addDetail(which, tID, certainty * priority / 2500);
1873
+ }
1874
+ });
1875
+ }
1876
+ }
1877
+ else if (which === 'wave') {
1878
+ const classScores = {
1879
+ error: 4,
1880
+ contrast: 3,
1881
+ alert: 1
1882
+ };
1883
+ const issueClasses = test.result && test.result.categories;
1884
+ if (issueClasses) {
1885
+ ['error', 'contrast', 'alert'].forEach(issueClass => {
1886
+ const {items} = issueClasses[issueClass];
1887
+ if (items) {
1888
+ const testIDs = Object.keys(items);
1889
+ if (testIDs.length) {
1890
+ testIDs.forEach(testID => {
1891
+ const {count} = items[testID];
1892
+ if (count) {
1893
+ // Add 4 per error, 3 per contrast error, 1 per warning (“alert”).
1894
+ addDetail(
1895
+ which, `${issueClass[0]}:${testID}`, count * classScores[issueClass]
1896
+ );
1897
+ }
1898
+ });
1899
+ }
1900
+ }
1901
+ });
1902
+ }
1903
+ }
1904
+ else if (which === 'bulk') {
1905
+ const count = test.result && test.result.visibleElements;
1906
+ if (typeof count === 'number') {
1907
+ // Add 1 per 300 visible elements beyond 300.
1908
+ addDetail('testaro', which, Math.max(0, count / 300 - 1));
1909
+ }
1910
+ }
1911
+ else if (which === 'embAc') {
1912
+ const issueCounts = test.result && test.result.totals;
1913
+ if (issueCounts) {
1914
+ const counts = Object.values(issueCounts);
1915
+ const total = counts.reduce((sum, current) => sum + current);
1916
+ // Add 3 per embedded element.
1917
+ addDetail('testaro', which, 3 * total);
1918
+ }
1919
+ }
1920
+ else if (which === 'focAll') {
1921
+ const discrepancy = test.result && test.result.discrepancy;
1922
+ if (discrepancy) {
1923
+ addDetail('testaro', which, 2 * Math.abs(discrepancy));
1924
+ }
1925
+ }
1926
+ else if (which === 'focInd') {
1927
+ const issueTypes =
1928
+ test.result && test.result.totals && test.result.totals.types;
1929
+ if (issueTypes) {
1930
+ const missingCount = issueTypes.indicatorMissing
1931
+ && issueTypes.indicatorMissing.total
1932
+ || 0;
1933
+ const badCount = issueTypes.nonOutlinePresent
1934
+ && issueTypes.nonOutlinePresent.total
1935
+ || 0;
1936
+ // Add 3 per missing, 1 per non-outline focus indicator.
1937
+ addDetail('testaro', which, badCount + 3 * missingCount);
1938
+ }
1939
+ }
1940
+ else if (which === 'focOp') {
1941
+ const issueTypes =
1942
+ test.result && test.result.totals && test.result.totals.types;
1943
+ if (issueTypes) {
1944
+ const noOpCount = issueTypes.onlyFocusable && issueTypes.onlyFocusable.total || 0;
1945
+ const noFocCount = issueTypes.onlyOperable && issueTypes.onlyOperable.total || 0;
1946
+ // Add 2 per unfocusable, 0.5 per inoperable element.
1947
+ addDetail('testaro', which, 2 * noFocCount + 0.5 * noOpCount);
1948
+ }
1949
+ }
1950
+ else if (which === 'hover') {
1951
+ const issues = test.result && test.result.totals;
1952
+ if (issues) {
1953
+ const {
1954
+ impactTriggers,
1955
+ additions,
1956
+ removals,
1957
+ opacityChanges,
1958
+ opacityImpact,
1959
+ unhoverables
1960
+ } = issues;
1961
+ // Add score with weights on hover-impact types.
1962
+ const score = 2 * impactTriggers
1963
+ + 0.3 * additions
1964
+ + removals
1965
+ + 0.2 * opacityChanges
1966
+ + 0.1 * opacityImpact
1967
+ + unhoverables;
1968
+ if (score) {
1969
+ addDetail('testaro', which, score);
1970
+ }
1971
+ }
1972
+ }
1973
+ else if (which === 'labClash') {
1974
+ const mislabeledCount = test.result
1975
+ && test.result.totals
1976
+ && test.result.totals.mislabeled
1977
+ || 0;
1978
+ // Add 1 per element with conflicting labels (ignoring unlabeled elements).
1979
+ addDetail('testaro', which, mislabeledCount);
1980
+ }
1981
+ else if (which === 'linkUl') {
1982
+ const totals = test.result && test.result.totals && test.result.totals.adjacent;
1983
+ if (totals) {
1984
+ const nonUl = totals.total - totals.underlined || 0;
1985
+ // Add 2 per non-underlined adjacent link.
1986
+ addDetail('testaro', which, 2 * nonUl);
1987
+ }
1988
+ }
1989
+ else if (which === 'menuNav') {
1990
+ const issueCount = test.result
1991
+ && test.result.totals
1992
+ && test.result.totals.navigations
1993
+ && test.result.totals.navigations.all
1994
+ && test.result.totals.navigations.all.incorrect
1995
+ || 0;
1996
+ // Add 2 per defect.
1997
+ addDetail('testaro', which, 2 * issueCount);
1998
+ }
1999
+ else if (which === 'motion') {
2000
+ const data = test.result;
2001
+ if (data) {
2002
+ const {
2003
+ meanLocalRatio,
2004
+ maxLocalRatio,
2005
+ globalRatio,
2006
+ meanPixelChange,
2007
+ maxPixelChange,
2008
+ changeFrequency
2009
+ } = data;
2010
+ const score = 2 * (meanLocalRatio - 1)
2011
+ + (maxLocalRatio - 1)
2012
+ + globalRatio - 1
2013
+ + meanPixelChange / 10000
2014
+ + maxPixelChange / 25000
2015
+ + 3 * changeFrequency
2016
+ || 0;
2017
+ addDetail('testaro', which, score);
2018
+ }
2019
+ }
2020
+ else if (which === 'radioSet') {
2021
+ const totals = test.result && test.result.totals;
2022
+ const {total, inSet} = totals;
2023
+ const score = total - inSet || 0;
2024
+ // Add 1 per misgrouped radio button.
2025
+ addDetail('testaro', which, score);
2026
+ }
2027
+ else if (which === 'role') {
2028
+ const issueCount = test.result && test.result.badRoleElements || 0;
2029
+ // Add 1 per misassigned role.
2030
+ addDetail('testaro', which, issueCount);
2031
+ }
2032
+ else if (which === 'styleDiff') {
2033
+ const totals = test.result && test.result.totals;
2034
+ if (totals) {
2035
+ let score = 0;
2036
+ // For each element type that has any style diversity:
2037
+ Object.values(totals).forEach(typeData => {
2038
+ const {total, subtotals} = typeData;
2039
+ if (subtotals) {
2040
+ const styleCount = subtotals.length;
2041
+ const plurality = subtotals[0];
2042
+ const minorities = total - plurality;
2043
+ // Add 1 per style, 0.2 per element with any nonplurality style.
2044
+ score += styleCount + 0.2 * minorities;
2045
+ }
2046
+ });
2047
+ addDetail('testaro', which, score);
2048
+ }
2049
+ }
2050
+ else if (which === 'tabNav') {
2051
+ const issueCount = test.result
2052
+ && test.result.totals
2053
+ && test.result.totals.navigations
2054
+ && test.result.totals.navigations.all
2055
+ && test.result.totals.navigations.all.incorrect
2056
+ || 0;
2057
+ // Add 2 per defect.
2058
+ addDetail('testaro', which, 2 * issueCount);
2059
+ }
2060
+ else if (which === 'zIndex') {
2061
+ const issueCount = test.result && test.result.totals && test.result.totals.total || 0;
2062
+ // Add 1 per non-auto zIndex.
2063
+ addDetail('testaro', which, issueCount);
2064
+ }
2065
+ });
2066
+ // Get the prevention scores and add them to the summary.
2067
+ const actsPrevented = testActs.filter(test => test.result.prevented);
2068
+ actsPrevented.forEach(act => {
2069
+ if (otherPackages.includes(act.which)) {
2070
+ preventionScores[act.which] = preventionWeights.other;
2071
+ }
2072
+ else {
2073
+ preventionScores[`testaro-${act.which}`] = preventionWeights.testaro;
2074
+ }
2075
+ });
2076
+ const preventionScore = Object.values(preventionScores).reduce(
2077
+ (sum, current) => sum + current,
2078
+ 0
2079
+ );
2080
+ const roundedScore = Math.round(preventionScore);
2081
+ summary.preventions = roundedScore;
2082
+ summary.total += roundedScore;
2083
+ // Reorganize the group data.
2084
+ const testGroups = {
2085
+ testaro: {},
2086
+ htmlcs: {},
2087
+ alfa: {},
2088
+ axe: {},
2089
+ ibm: {},
2090
+ tenon: {},
2091
+ wave: {}
2092
+ };
2093
+ Object.keys(groups).forEach(groupName => {
2094
+ Object.keys(groups[groupName].packages).forEach(packageName => {
2095
+ Object.keys(groups[groupName].packages[packageName]).forEach(testID => {
2096
+ testGroups[packageName][testID] = groupName;
2097
+ });
2098
+ });
2099
+ });
2100
+ // Populate the group details with group and solo test scores.
2101
+ // For each package with any scores:
2102
+ Object.keys(packageDetails).forEach(packageName => {
2103
+ // For each test with any scores in the package:
2104
+ Object.keys(packageDetails[packageName]).forEach(testID => {
2105
+ // If the test is in a group:
2106
+ const groupName = testGroups[packageName][testID];
2107
+ if (groupName) {
2108
+ // Determine the preweighted or group-weighted score.
2109
+ if (! groupDetails.groups[groupName]) {
2110
+ groupDetails.groups[groupName] = {};
2111
+ }
2112
+ if (! groupDetails.groups[groupName][packageName]) {
2113
+ groupDetails.groups[groupName][packageName] = {};
2114
+ }
2115
+ let weightedScore = packageDetails[packageName][testID];
2116
+ if (!preWeightedPackages.includes(groupName)) {
2117
+ weightedScore *= groups[groupName].weight / 4;
2118
+ }
2119
+ // Adjust the score for the quality of the test.
2120
+ weightedScore *= groups[groupName].packages[packageName][testID].quality;
2121
+ // Round the score, but not to less than 1.
2122
+ const roundedScore = Math.max(Math.round(weightedScore), 1);
2123
+ // Add the rounded score and the test description to the group details.
2124
+ groupDetails.groups[groupName][packageName][testID] = {
2125
+ score: roundedScore,
2126
+ what: groups[groupName].packages[packageName][testID].what
2127
+ };
2128
+ }
2129
+ // Otherwise, i.e. if the test is solo:
2130
+ else {
2131
+ if (! groupDetails.solos[packageName]) {
2132
+ groupDetails.solos[packageName] = {};
2133
+ }
2134
+ const roundedScore = Math.round(packageDetails[packageName][testID]);
2135
+ groupDetails.solos[packageName][testID] = roundedScore;
2136
+ }
2137
+ });
2138
+ });
2139
+ // Determine the group scores and add them to the summary.
2140
+ const groupNames = Object.keys(groupDetails.groups);
2141
+ const {absolute, largest, smaller} = groupWeights;
2142
+ // For each group with any scores:
2143
+ groupNames.forEach(groupName => {
2144
+ const scores = [];
2145
+ // For each package with any scores in the group:
2146
+ const groupPackageData = Object.values(groupDetails.groups[groupName]);
2147
+ groupPackageData.forEach(packageObj => {
2148
+ // Get the sum of the scores of the tests of the package in the group.
2149
+ const scoreSum = Object.values(packageObj).reduce(
2150
+ (sum, current) => sum + current.score,
2151
+ 0
2152
+ );
2153
+ // Add the sum to the list of package scores in the group.
2154
+ scores.push(scoreSum);
2155
+ });
2156
+ // Sort the scores in descending order.
2157
+ scores.sort((a, b) => b - a);
2158
+ // Compute the sum of the absolute score and the weighted largest and other scores.
2159
+ const groupScore = absolute
2160
+ + largest * scores[0]
2161
+ + smaller * scores.slice(1).reduce((sum, current) => sum + current, 0);
2162
+ const roundedGroupScore = Math.round(groupScore);
2163
+ summary.groups.push({
2164
+ groupName,
2165
+ score: roundedGroupScore
2166
+ });
2167
+ summary.total += roundedGroupScore;
2168
+ });
2169
+ summary.groups.sort((a, b) => b.score - a.score);
2170
+ // Determine the solo score and add it to the summary.
2171
+ const soloPackageNames = Object.keys(groupDetails.solos);
2172
+ soloPackageNames.forEach(packageName => {
2173
+ const testIDs = Object.keys(groupDetails.solos[packageName]);
2174
+ testIDs.forEach(testID => {
2175
+ const score = soloWeight * groupDetails.solos[packageName][testID];
2176
+ summary.solos += score;
2177
+ summary.total += score;
2178
+ });
2179
+ });
2180
+ summary.solos = Math.round(summary.solos);
2181
+ summary.total = Math.round(summary.total);
2182
+ }
2183
+ }
2184
+ // Get the log score.
2185
+ const logScore = logWeights.logCount * report.logCount
2186
+ + logWeights.logSize * report.logSize +
2187
+ + logWeights.errorLogCount * report.errorLogCount
2188
+ + logWeights.errorLogSize * report.errorLogSize
2189
+ + logWeights.prohibitedCount * report.prohibitedCount +
2190
+ + logWeights.visitTimeoutCount * report.visitTimeoutCount +
2191
+ + logWeights.visitRejectionCount * report.visitRejectionCount;
2192
+ const roundedLogScore = Math.round(logScore);
2193
+ summary.log = roundedLogScore;
2194
+ summary.total += roundedLogScore;
2195
+ // Add the score facts to the report.
2196
+ report.score = {
2197
+ scoreProcID,
2198
+ logWeights,
2199
+ soloWeight,
2200
+ groupWeights,
2201
+ preventionWeights,
2202
+ packageDetails,
2203
+ groupDetails,
2204
+ preventionScores,
2205
+ summary
2206
+ };
2207
+ };