testilo 11.0.6 → 11.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testilo",
3
- "version": "11.0.6",
3
+ "version": "11.0.7",
4
4
  "description": "Client that scores and digests Testaro reports",
5
5
  "main": "aim.js",
6
6
  "scripts": {
@@ -17,7 +17,7 @@
17
17
  - what: description of the test.
18
18
  */
19
19
 
20
- exports.issues = {
20
+ exports.issueClasses = {
21
21
  ignorable: {
22
22
  wcag: '',
23
23
  weight: 0,
@@ -2586,7 +2586,7 @@ exports.issues = {
2586
2586
  }
2587
2587
  },
2588
2588
  testaro: {
2589
- role: {
2589
+ 'role-bad': {
2590
2590
  variable: false,
2591
2591
  quality: 1,
2592
2592
  what: 'Nonexistent or implicit-overriding role'
@@ -2626,6 +2626,13 @@ exports.issues = {
2626
2626
  quality: 1,
2627
2627
  what: 'explicit role is redundant for a text-type input element without a list attribute'
2628
2628
  }
2629
+ },
2630
+ testaro: {
2631
+ 'role-redundant': {
2632
+ variable: false,
2633
+ quality: 1,
2634
+ what: 'Redundant role'
2635
+ }
2629
2636
  }
2630
2637
  }
2631
2638
  },
@@ -2799,20 +2806,25 @@ exports.issues = {
2799
2806
  }
2800
2807
  },
2801
2808
  continuum: {
2809
+ 1039: {
2810
+ variable: false,
2811
+ quality: 1,
2812
+ what: 'Element with a checkbox role has no aria-checked attribute'
2813
+ },
2802
2814
  1040: {
2803
2815
  variable: false,
2804
2816
  quality: 1,
2805
- what: 'element with a combobox role has no aria-controls or no aria-expanded attribute'
2817
+ what: 'Element with a combobox role has no aria-controls or no aria-expanded attribute'
2806
2818
  },
2807
2819
  1042: {
2808
2820
  variable: false,
2809
2821
  quality: 1,
2810
- what: 'element with an option role has no aria-selected attribute'
2822
+ what: 'Element with an option role has no aria-selected attribute'
2811
2823
  },
2812
2824
  1043: {
2813
2825
  variable: false,
2814
2826
  quality: 1,
2815
- what: 'element with a radio role has no aria-checked attribute'
2827
+ what: 'Element with a radio role has no aria-checked attribute'
2816
2828
  }
2817
2829
  },
2818
2830
  nuVal: {
@@ -3220,6 +3232,11 @@ exports.issues = {
3220
3232
  variable: false,
3221
3233
  quality: 1,
3222
3234
  what: 'Contrast ratio of text with background does not meet WCAG 2.1 AA'
3235
+ },
3236
+ text_contrast_sufficient: {
3237
+ variable: false,
3238
+ quality: 1,
3239
+ what: 'Text has a contrast with its background less than the WCAG AA minimum for its size and weight'
3223
3240
  }
3224
3241
  },
3225
3242
  qualWeb: {
@@ -3307,6 +3324,11 @@ exports.issues = {
3307
3324
  quality: 1,
3308
3325
  what: 'Inline background color may lack a complementary foreground color'
3309
3326
  },
3327
+ 'AAA.1_4_6.G18.BgImage': {
3328
+ variable: false,
3329
+ quality: 1,
3330
+ what: 'Contrast between the text and the background image may be less than 4.5:1'
3331
+ },
3310
3332
  'AAA.1_4_3_F24.F24.FGColour': {
3311
3333
  variable: false,
3312
3334
  quality: 1,
@@ -4115,6 +4137,13 @@ exports.issues = {
4115
4137
  wcag: '1.3.1',
4116
4138
  weight: 1,
4117
4139
  tools: {
4140
+ qualWeb: {
4141
+ 'QW-BP23': {
4142
+ variable: false,
4143
+ quality: 1,
4144
+ what: 'List item is used nonsemantically'
4145
+ }
4146
+ },
4118
4147
  wave: {
4119
4148
  'list_possible': {
4120
4149
  variable: false,
@@ -4681,6 +4710,13 @@ exports.issues = {
4681
4710
  what: 'Form control has no accessible name'
4682
4711
  }
4683
4712
  },
4713
+ testaro: {
4714
+ 'labClash-unlabeled': {
4715
+ variable: false,
4716
+ quality: 1,
4717
+ what: 'Button, input, select, or textarea is unlabeled'
4718
+ }
4719
+ },
4684
4720
  wave: {
4685
4721
  'label_missing': {
4686
4722
  variable: false,
@@ -4742,6 +4778,11 @@ exports.issues = {
4742
4778
  }
4743
4779
  },
4744
4780
  ibm: {
4781
+ label_name_visible: {
4782
+ variable: false,
4783
+ quality: 1,
4784
+ what: 'Accessible name does not match or contain the visible label text'
4785
+ },
4745
4786
  WCAG21_Label_Accessible: {
4746
4787
  variable: false,
4747
4788
  quality: 1,
@@ -4940,10 +4981,23 @@ exports.issues = {
4940
4981
  }
4941
4982
  },
4942
4983
  testaro: {
4943
- focInd: {
4984
+ 'focInd-missing': {
4944
4985
  variable: false,
4945
4986
  quality: 1,
4946
- what: 'Focused element displays no or a nostandard focus indicator'
4987
+ what: 'Focused element displays no focus indicator'
4988
+ }
4989
+ }
4990
+ }
4991
+ },
4992
+ focusIndicationBad: {
4993
+ wcag: '2.4.7',
4994
+ weight: 3,
4995
+ tools: {
4996
+ testaro: {
4997
+ 'focInd-nonoutline': {
4998
+ variable: false,
4999
+ quality: 1,
5000
+ what: 'Focused element displays a nostandard focus indicator'
4947
5001
  }
4948
5002
  }
4949
5003
  }
@@ -5605,6 +5659,84 @@ exports.issues = {
5605
5659
  }
5606
5660
  }
5607
5661
  },
5662
+ hoverImpact: {
5663
+ wcag: '1.4.13',
5664
+ weight: 3,
5665
+ tools: {
5666
+ testaro: {
5667
+ 'hover-impactTriggers': {
5668
+ variable: false,
5669
+ quality: 1,
5670
+ what: 'Hovering over element has unexpected effects'
5671
+ }
5672
+ }
5673
+ }
5674
+ },
5675
+ unhoverable: {
5676
+ wcag: '1.4.13',
5677
+ weight: 3,
5678
+ tools: {
5679
+ testaro: {
5680
+ 'hover-unhoverables': {
5681
+ variable: false,
5682
+ quality: 1,
5683
+ what: 'Operable element cannot be hovered over'
5684
+ }
5685
+ }
5686
+ }
5687
+ },
5688
+ hoverNoCursor: {
5689
+ wcag: '1.4.13',
5690
+ weight: 3,
5691
+ tools: {
5692
+ testaro: {
5693
+ 'hover-noCursors': {
5694
+ variable: false,
5695
+ quality: 1,
5696
+ what: 'Hoverable element hides the mouse cursor'
5697
+ }
5698
+ }
5699
+ }
5700
+ },
5701
+ hoverBadCursor: {
5702
+ wcag: '1.4.13',
5703
+ weight: 2,
5704
+ tools: {
5705
+ testaro: {
5706
+ 'hover-badCursors': {
5707
+ variable: false,
5708
+ quality: 1,
5709
+ what: 'Link or button makes the hovering mouse cursor nonstandard'
5710
+ }
5711
+ }
5712
+ }
5713
+ },
5714
+ hoverNoIndicator: {
5715
+ wcag: '1.4.13',
5716
+ weight: 3,
5717
+ tools: {
5718
+ testaro: {
5719
+ 'hover-noCursors': {
5720
+ variable: false,
5721
+ quality: 1,
5722
+ what: 'Button shows no indication of being hovered over'
5723
+ }
5724
+ }
5725
+ }
5726
+ },
5727
+ hoverBadIndicator: {
5728
+ wcag: '1.4.13',
5729
+ weight: 2,
5730
+ tools: {
5731
+ testaro: {
5732
+ 'hover-noCursors': {
5733
+ variable: false,
5734
+ quality: 1,
5735
+ what: 'List item changes when hovered over'
5736
+ }
5737
+ }
5738
+ }
5739
+ },
5608
5740
  labelClash: {
5609
5741
  wcag: '1.3.1',
5610
5742
  weight: 2,
@@ -5617,7 +5749,7 @@ exports.issues = {
5617
5749
  }
5618
5750
  },
5619
5751
  testaro: {
5620
- labClash: {
5752
+ 'labClash-mislabeled': {
5621
5753
  variable: false,
5622
5754
  quality: 1,
5623
5755
  what: 'Incompatible label types'
@@ -5956,6 +6088,19 @@ exports.issues = {
5956
6088
  }
5957
6089
  }
5958
6090
  },
6091
+ filterStyle: {
6092
+ wcag: '4.1',
6093
+ weight: 1,
6094
+ tools: {
6095
+ testaro: {
6096
+ filterStyle: {
6097
+ variable: false,
6098
+ quality: 1,
6099
+ what: 'Element styles include filter'
6100
+ }
6101
+ }
6102
+ }
6103
+ },
5959
6104
  zIndexNotZero: {
5960
6105
  wcag: '1.4',
5961
6106
  weight: 1,
@@ -6917,6 +7062,19 @@ exports.issues = {
6917
7062
  },
6918
7063
  }
6919
7064
  },
7065
+ slashParseRisk: {
7066
+ wcag: '4.1',
7067
+ weight: 1,
7068
+ tools: {
7069
+ nuVal: {
7070
+ 'Trailing slash on void elements has no effect and interacts badly with unquoted attribute values.': {
7071
+ variable: false,
7072
+ quality: 1,
7073
+ what: 'Void element has a useless trailing slash.'
7074
+ }
7075
+ }
7076
+ }
7077
+ },
6920
7078
  encodingBad: {
6921
7079
  wcag: '3.1.3',
6922
7080
  weight: 4,
@@ -0,0 +1,180 @@
1
+ /*
2
+ tsp27
3
+ Testilo score proc 27
4
+
5
+ Computes target score data and adds them to a ts26 report.
6
+ */
7
+
8
+ // IMPORTS
9
+
10
+ const {issueClasses} = require('./tic27');
11
+
12
+ // CONSTANTS
13
+
14
+ // ID of this proc.
15
+ const scoreProcID = 'tsp27';
16
+ // Configuration disclosures.
17
+ const severityWeights = [1, 2, 3, 4];
18
+ const toolWeight = 0.1;
19
+ const logWeights = {
20
+ logCount: 0.1,
21
+ logSize: 0.002,
22
+ errorLogCount: 0.2,
23
+ errorLogSize: 0.004,
24
+ prohibitedCount: 3,
25
+ visitRejectionCount: 2
26
+ };
27
+ // How much each second of excess latency adds to the score.
28
+ const latencyWeight = 1;
29
+ // Normal latency (1.5 second per visit).
30
+ const normalLatency = 20;
31
+ // How much each prevention adds to the score.
32
+ const preventionWeight = 200;
33
+ // Indexes of issues.
34
+ const issueIndex = {};
35
+ const issueMatcher = [];
36
+ Object.keys(issueClasses).forEach(issueClassName => {
37
+ Object.keys(issueClasses[issueClassName].tools).forEach(toolName => {
38
+ Object.keys(issueClasses[issueClassName].tools[toolName]).forEach(issueID => {
39
+ if (! issueIndex[toolName]) {
40
+ issueIndex[toolName] = {};
41
+ }
42
+ issueIndex[toolName][issueID] = issueClassName;
43
+ if (issueClasses[issueClassName].tools[toolName][issueID].variable) {
44
+ issueMatcher.push(issueID);
45
+ }
46
+ })
47
+ });
48
+ });
49
+
50
+ // FUNCTIONS
51
+
52
+ // Scores a report.
53
+ exports.scorer = report => {
54
+ console.log(report.id);
55
+ // If there are any acts in the report:
56
+ const {acts} = report;
57
+ if (Array.isArray(acts) && acts.length) {
58
+ // If any of them are test acts:
59
+ const testActs = acts.filter(act => act.type === 'test');
60
+ if (testActs.length) {
61
+ // Initialize the score data.
62
+ const score = {
63
+ scoreProcID,
64
+ summary: {
65
+ total: 0,
66
+ issues: 0,
67
+ tools: 0,
68
+ preventions: 0,
69
+ log: 0,
70
+ latency: 0
71
+ },
72
+ toolTotals: [0, 0, 0, 0],
73
+ issues: {},
74
+ tools: {},
75
+ preventions: {}
76
+ };
77
+ const {summary, toolTotals, issues, tools, preventions} = score;
78
+ // For each test act:
79
+ testActs.forEach(act => {
80
+ // If a standard result with valid totals and instances exists:
81
+ const {which, standardResult} = act;
82
+ if (
83
+ standardResult
84
+ && standardResult.totals
85
+ && standardResult.totals.length === 4
86
+ && standardResult.instances
87
+ && standardResult.instances.length
88
+ ) {
89
+ // Add the tool totals to the score.
90
+ const {totals} = standardResult;
91
+ tools[which] = totals;
92
+ toolTotals.forEach((total, index) => {
93
+ toolTotals[index] += toolWeight * totals[index];
94
+ summary.tools += toolWeight * totals[index] * severityWeights[index];
95
+ });
96
+ // Update the issue totals for the tool.
97
+ const issueTotals = {};
98
+ standardResult.instances.forEach(instance => {
99
+ const issueID = issueIndex[which][instance.issueID];
100
+ let indexIssueID;
101
+ if (issueID) {
102
+ indexIssueID = issueID;
103
+ }
104
+ else {
105
+ indexIssueID = issueMatcher.find(pattern => {
106
+ const patternRE = new RegExp(pattern);
107
+ return patternRE.test(instance.issueID);
108
+ });
109
+ }
110
+ if (indexIssueID) {
111
+ if (! issueTotals[indexIssueID]) {
112
+ issueTotals[indexIssueID] = 0;
113
+ }
114
+ issueTotals[indexIssueID] += instance.count || 1;
115
+ }
116
+ else {
117
+ console.log(`ERROR: ${instance.issueID} not found in issueClasses`);
118
+ }
119
+ });
120
+ // Update the issue totals in the score.
121
+ Object.keys(issueTotals).forEach(id => {
122
+ issues[id] = Math.max(issues[id] || 0, issueTotals[id]);
123
+ });
124
+ summary.issues = Object.values(issues).reduce((total, current) => total + current);
125
+ }
126
+ // Otherwise, i.e. if no result with totals exists:
127
+ else {
128
+ // Add a prevented result to the act if not already there.
129
+ if (! act.result) {
130
+ act.result = {};
131
+ }
132
+ if (! act.result.prevented) {
133
+ act.result.prevented = true;
134
+ };
135
+ // Add the tool and the prevention score to the score.
136
+ score.preventions[which] = preventionWeight;
137
+ summary.preventions += preventionWeight;
138
+ }
139
+ });
140
+ // Add the log score to the score.
141
+ const {jobData} = report;
142
+ summary.log = Math.max(0, Math.round(
143
+ logWeights.logCount * jobData.logCount
144
+ + logWeights.logSize * jobData.logSize +
145
+ + logWeights.errorLogCount * jobData.errorLogCount
146
+ + logWeights.errorLogSize * jobData.errorLogSize
147
+ + logWeights.prohibitedCount * jobData.prohibitedCount +
148
+ + logWeights.visitRejectionCount * jobData.visitRejectionCount
149
+ ));
150
+ summary.latency = Math.round(
151
+ latencyWeight * (Math.max(0, jobData.visitLatency - normalLatency))
152
+ );
153
+ // Round the scores.
154
+ Object.keys(summary).forEach(summaryTypeName => {
155
+ summary[summaryTypeName] = Math.round(summary[summaryTypeName]);
156
+ });
157
+ toolTotals.forEach((severityTotal, index) => {
158
+ toolTotals[index] = Math.round(toolTotals[index]);
159
+ });
160
+ // Add the total score to the score.
161
+ summary.total = summary.issues
162
+ + summary.tools
163
+ + summary.preventions
164
+ + summary.log
165
+ + summary.latency;
166
+ // Add the score to the report.
167
+ report.score = score;
168
+ }
169
+ // Otherwise, i.e. if none of them is a test act:
170
+ else {
171
+ // Report this.
172
+ console.log('ERROR: No test acts');
173
+ }
174
+ }
175
+ // Otherwise, i.e. if there are no acts in the report:
176
+ else {
177
+ // Report this.
178
+ console.log('ERROR: No acts');
179
+ }
180
+ };
@@ -0,0 +1,97 @@
1
+ {
2
+ "id": "ts26",
3
+ "what": "tests of 10 tools",
4
+ "strict": true,
5
+ "timeLimit": 600,
6
+ "acts": [
7
+ {
8
+ "type": "placeholder",
9
+ "which": "main",
10
+ "launch": "webkit"
11
+ },
12
+ {
13
+ "type": "tenonRequest",
14
+ "id": "a",
15
+ "withNewContent": true,
16
+ "what": "Tenon API version 2 test request, with URL"
17
+ },
18
+ {
19
+ "type": "test",
20
+ "which": "testaro",
21
+ "what": "Testaro motion test requiring webkit",
22
+ "withItems": true,
23
+ "rules": ["y", "motion"],
24
+ "args": {
25
+ "motion": [2500, 2500, 5]
26
+ }
27
+ },
28
+ {
29
+ "type": "placeholder",
30
+ "which": "main",
31
+ "launch": "chromium"
32
+ },
33
+ {
34
+ "type": "test",
35
+ "which": "testaro",
36
+ "what": "Testaro rules except motion",
37
+ "withItems": true,
38
+ "rules": ["n", "motion"],
39
+ "args": {
40
+ "focInd": [false, 250],
41
+ "hover": [20]
42
+ }
43
+ },
44
+ {
45
+ "type": "test",
46
+ "which": "alfa",
47
+ "what": "Siteimprove alfa"
48
+ },
49
+ {
50
+ "type": "test",
51
+ "which": "axe",
52
+ "detailLevel": 2,
53
+ "rules": [],
54
+ "what": "Axe core, all rules"
55
+ },
56
+ {
57
+ "type": "test",
58
+ "which": "continuum",
59
+ "what": "Continuum Community Edition"
60
+ },
61
+ {
62
+ "type": "test",
63
+ "which": "htmlcs",
64
+ "what": "HTML CodeSniffer"
65
+ },
66
+ {
67
+ "type": "test",
68
+ "which": "ibm",
69
+ "withNewContent": false,
70
+ "withItems": true,
71
+ "what": "IBM Accessibility Checker"
72
+ },
73
+ {
74
+ "type": "test",
75
+ "which": "nuVal",
76
+ "what": "Nu Html Checker"
77
+ },
78
+ {
79
+ "type": "test",
80
+ "which": "qualWeb",
81
+ "withNewContent": false,
82
+ "what": "QualWeb"
83
+ },
84
+ {
85
+ "type": "test",
86
+ "which": "wave",
87
+ "reportType": 4,
88
+ "what": "WAVE, report type 4"
89
+ },
90
+ {
91
+ "type": "test",
92
+ "which": "tenon",
93
+ "id": "a",
94
+ "what": "Tenon API version 2 result retrieval"
95
+ }
96
+ ]
97
+ }
@@ -1,740 +0,0 @@
1
- /*
2
- tsp24
3
- Testilo score proc 24
4
-
5
- Computes scores from Testilo script ts23 or ts24 and adds them to a report.
6
- */
7
-
8
- // IMPORTS
9
-
10
- const {issues} = require('./tic24');
11
-
12
- // CONSTANTS
13
-
14
- // ID of this proc.
15
- const scoreProcID = 'tsp24';
16
- // Configuration disclosures.
17
- const logWeights = {
18
- logCount: 0.5,
19
- logSize: 0.01,
20
- errorLogCount: 1,
21
- errorLogSize: 0.02,
22
- prohibitedCount: 15,
23
- visitTimeoutCount: 10,
24
- visitRejectionCount: 10,
25
- visitLatency: 1
26
- };
27
- // Normal latency (1 second per visit).
28
- const normalLatency = 13;
29
- // How much each unclassified failure adds to the score.
30
- const soloWeight = 2;
31
- // How much classified issues add to the score.
32
- const issueWeights = {
33
- // Added per issue.
34
- absolute: 2,
35
- // Added per instance reported by the tool with the largest count.
36
- largest: 1,
37
- // Added per instance reported by each other tool.
38
- smaller: 0.4
39
- };
40
- // How much each prevention adds to the score.
41
- const preventionWeights = {
42
- testaro: 50,
43
- other: 100
44
- };
45
- // Non-Testaro tools.
46
- const otherTools = [
47
- 'alfa',
48
- 'axe',
49
- 'continuum',
50
- 'htmlcs',
51
- 'ibm',
52
- 'nuVal',
53
- 'qualWeb',
54
- 'tenon',
55
- 'wave'
56
- ];
57
-
58
- // VARIABLES
59
-
60
- let toolDetails = {};
61
- let issueDetails = {};
62
- let summary = {};
63
- let preventionScores = {};
64
-
65
- // FUNCTIONS
66
-
67
- // Initialize the variables.
68
- const init = () => {
69
- toolDetails = {};
70
- issueDetails = {
71
- issues: {},
72
- solos: {}
73
- };
74
- summary = {
75
- total: 0,
76
- log: 0,
77
- preventions: 0,
78
- solos: 0,
79
- issues: []
80
- };
81
- preventionScores = {};
82
- };
83
-
84
- // Adds a score to the tool details.
85
- const addDetail = (actWhich, testID, addition = 1) => {
86
- if (addition) {
87
- if (!toolDetails[actWhich]) {
88
- toolDetails[actWhich] = {};
89
- }
90
- if (!toolDetails[actWhich][testID]) {
91
- toolDetails[actWhich][testID] = 0;
92
- }
93
- toolDetails[actWhich][testID] += Math.round(addition);
94
- }
95
- };
96
- // Scores a report.
97
- exports.scorer = async report => {
98
- // Initialize the variables.
99
- init();
100
- // If there are any acts in the report:
101
- const {acts} = report;
102
- if (Array.isArray(acts)) {
103
- // If any of them are test acts:
104
- const testActs = acts.filter(act => act.type === 'test');
105
- if (testActs.length) {
106
- // For each test act:
107
- testActs.forEach(test => {
108
- // Add a prevented result if no result exists.
109
- if (! test.result) {
110
- test.result = {
111
- prevented: true
112
- };
113
- }
114
- const {which} = test;
115
- // Add scores to the tool details.
116
- if (which === 'alfa') {
117
- const issues = test.result && test.result.items;
118
- if (issues && Array.isArray(issues)) {
119
- issues.forEach(issue => {
120
- const {verdict, rule} = issue;
121
- if (verdict && rule) {
122
- const {ruleID} = rule;
123
- if (ruleID) {
124
- // Add 4 per failure, 1 per warning (cantTell).
125
- addDetail(which, ruleID, verdict === 'failed' ? 4 : 1);
126
- }
127
- }
128
- });
129
- }
130
- }
131
- else if (which === 'axe') {
132
- const impactScores = {
133
- minor: 1,
134
- moderate: 2,
135
- serious: 3,
136
- critical: 4
137
- };
138
- const tests = test.result && test.result.details;
139
- if (tests) {
140
- const warnings = tests.incomplete;
141
- const {violations} = tests;
142
- [[warnings, 0.25], [violations, 1]].forEach(issueSeverity => {
143
- if (issueSeverity[0] && Array.isArray(issueSeverity[0])) {
144
- issueSeverity[0].forEach(issueType => {
145
- const {id, nodes} = issueType;
146
- if (id && nodes && Array.isArray(nodes)) {
147
- nodes.forEach(node => {
148
- const {impact} = node;
149
- if (impact) {
150
- // Add the impact score for a violation or 25% of it for a warning.
151
- addDetail(which, id, issueSeverity[1] * impactScores[impact]);
152
- }
153
- });
154
- }
155
- });
156
- }
157
- });
158
- }
159
- }
160
- else if (which === 'continuum') {
161
- const issues = test.result;
162
- if (issues && Array.isArray(issues)) {
163
- issues.forEach(issue => {
164
- // Add 4 per violation.
165
- addDetail(which, issue.engineTestId, 4);
166
- });
167
- }
168
- }
169
- else if (which === 'htmlcs') {
170
- const issues = test.result;
171
- if (issues) {
172
- ['Error', 'Warning'].forEach(issueSeverityName => {
173
- const severityData = issues[issueSeverityName];
174
- if (severityData) {
175
- const issueTypes = Object.keys(severityData);
176
- issueTypes.forEach(issueTypeName => {
177
- const issueArrays = Object.values(severityData[issueTypeName]);
178
- const issueCount = issueArrays.reduce((count, array) => count + array.length, 0);
179
- const severityCode = issueSeverityName[0].toLowerCase();
180
- const code = `${severityCode}:${issueTypeName}`;
181
- // Add 4 per error, 1 per warning.
182
- const weight = severityCode === 'e' ? 4 : 1;
183
- addDetail(which, code, weight * issueCount);
184
- });
185
- }
186
- });
187
- }
188
- }
189
- else if (which === 'ibm') {
190
- const {result} = test;
191
- const {content, url} = result;
192
- if (content && url) {
193
- let preferredMode = 'content';
194
- if (
195
- content.error ||
196
- (content.totals &&
197
- content.totals.violation &&
198
- url.totals &&
199
- url.totals.violation &&
200
- url.totals.violation > content.totals.violation)
201
- ) {
202
- preferredMode = 'url';
203
- }
204
- const {items} = result[preferredMode];
205
- if (items && Array.isArray(items)) {
206
- items.forEach(issue => {
207
- const {ruleId, level} = issue;
208
- if (ruleId && level) {
209
- // Add 4 per violation, 1 per warning (recommendation).
210
- addDetail(which, ruleId, level === 'violation' ? 4 : 1);
211
- }
212
- });
213
- }
214
- }
215
- }
216
- else if (which === 'nuVal') {
217
- const issues = test.result && test.result.messages;
218
- if (issues) {
219
- issues.forEach(issue => {
220
- // Add 4 per error, 1 per warning.
221
- const weight = issue.type === 'error' ? 4 : 1;
222
- addDetail(which, issue.message, weight);
223
- });
224
- }
225
- }
226
- else if (which === 'qualWeb') {
227
- // For each section of the results:
228
- const modules = test.result && test.result.modules;
229
- if (modules) {
230
- ['act-rules', 'wcag-techniques', 'best-practices'].forEach(type => {
231
- // For each test in the section:
232
- const {assertions} = modules[type];
233
- if (assertions) {
234
- const issueIDs = Object.keys(assertions);
235
- issueIDs.forEach(issueID => {
236
- // For each error or warning from the test:
237
- const {results} = assertions[issueID];
238
- results.forEach(result => {
239
- // Add 4 per error, 1 per warning, per element.
240
- let weight = 0;
241
- const {verdict} = result;
242
- if (verdict === 'error') {
243
- weight = 4;
244
- }
245
- else if (verdict === 'warning') {
246
- weight = 1;
247
- }
248
- if (weight) {
249
- addDetail(which, issueID, result.elements.length * weight);
250
- }
251
- });
252
- });
253
- }
254
- });
255
- }
256
- }
257
- else if (which === 'tenon') {
258
- const issues =
259
- test.result && test.result.data && test.result.data.resultSet;
260
- if (issues && Array.isArray(issues)) {
261
- issues.forEach(issue => {
262
- const {tID, priority, certainty} = issue;
263
- if (tID && priority && certainty) {
264
- // Add 4 per issue if certainty and priority 100, less if less.
265
- addDetail(which, tID, certainty * priority / 2500);
266
- }
267
- });
268
- }
269
- }
270
- else if (which === 'wave') {
271
- const severityScores = {
272
- error: 4,
273
- contrast: 3,
274
- alert: 1
275
- };
276
- const issueSeverities = test.result && test.result.categories;
277
- if (issueSeverities) {
278
- ['error', 'contrast', 'alert'].forEach(issueSeverity => {
279
- const {items} = issueSeverities[issueSeverity];
280
- if (items) {
281
- const testIDs = Object.keys(items);
282
- if (testIDs.length) {
283
- testIDs.forEach(testID => {
284
- const {count} = items[testID];
285
- if (count) {
286
- // Add 4 per error, 3 per contrast error, 1 per warning (alert).
287
- addDetail(
288
- which, `${issueSeverity[0]}:${testID}`, count * severityScores[issueSeverity]
289
- );
290
- }
291
- });
292
- }
293
- }
294
- });
295
- }
296
- }
297
- else if (which === 'allHidden') {
298
- const {result} = test;
299
- if (
300
- result
301
- && ['hidden', 'reallyHidden', 'visHidden', 'ariaHidden'].every(
302
- key => result[key]
303
- && ['document', 'body', 'main'].every(
304
- element => typeof result[key][element] === 'boolean'
305
- )
306
- )
307
- ) {
308
- // Get a score for the test.
309
- const score = 8 * result.hidden.document
310
- + 8 * result.hidden.body
311
- + 6 * result.hidden.main
312
- + 10 * result.reallyHidden.document
313
- + 10 * result.reallyHidden.body
314
- + 8 * result.reallyHidden.main
315
- + 8 * result.visHidden.document
316
- + 8 * result.visHidden.body
317
- + 6 * result.visHidden.main
318
- + 10 * result.ariaHidden.document
319
- + 10 * result.ariaHidden.body
320
- + 8 * result.ariaHidden.main;
321
- // Add the score.
322
- addDetail('testaro', which, score);
323
- }
324
- }
325
- else if (which === 'autocomplete') {
326
- const count = test.result && test.result.total;
327
- if (typeof count === 'number') {
328
- // Add 4 per autocomplete violation.
329
- addDetail('testaro', which, 4 * count);
330
- }
331
- }
332
- else if (which === 'bulk') {
333
- const count = test.result && test.result.visibleElements;
334
- if (typeof count === 'number') {
335
- // Add 1 per 300 visible elements beyond 300.
336
- addDetail('testaro', which, Math.max(0, count / 300 - 1));
337
- }
338
- }
339
- else if (which === 'docType') {
340
- // If document has no or invalid doctype:
341
- const hasType = test.result && test.result.docHasType;
342
- if (typeof hasType === 'boolean' && ! hasType) {
343
- // Add 10.
344
- addDetail('testaro', which, 10);
345
- }
346
- }
347
- else if (which === 'dupAtt') {
348
- const count = test.result && test.result.total;
349
- if (typeof count === 'number') {
350
- // Add 2 per element with duplicate attributes.
351
- addDetail('testaro', which, 2 * count);
352
- }
353
- }
354
- else if (which === 'embAc') {
355
- const issueCounts = test.result && test.result.totals;
356
- if (issueCounts) {
357
- const counts = Object.values(issueCounts);
358
- const total = counts.reduce((sum, current) => sum + current);
359
- // Add 3 per embedded element.
360
- addDetail('testaro', which, 3 * total);
361
- }
362
- }
363
- else if (which === 'filter') {
364
- const totals = test.result && test.result.totals;
365
- if (totals) {
366
- // Add 2 per filter-styled element, 1 per filter-impacted element.
367
- addDetail('testaro', which, 2 * totals.elements + totals.impact);
368
- }
369
- }
370
- else if (which === 'focAll') {
371
- const discrepancy = test.result && test.result.discrepancy;
372
- if (discrepancy) {
373
- // Add 2 per discrepancy.
374
- addDetail('testaro', which, 2 * Math.abs(discrepancy));
375
- }
376
- }
377
- else if (which === 'focInd') {
378
- const issueTypes =
379
- test.result && test.result.totals && test.result.totals.types;
380
- if (issueTypes) {
381
- const missingCount = issueTypes.indicatorMissing
382
- && issueTypes.indicatorMissing.total
383
- || 0;
384
- const badCount = issueTypes.nonOutlinePresent
385
- && issueTypes.nonOutlinePresent.total
386
- || 0;
387
- // Add 3 per missing, 1 per non-outline focus indicator.
388
- addDetail('testaro', which, badCount + 3 * missingCount);
389
- }
390
- }
391
- else if (which === 'focOp') {
392
- const issueTypes =
393
- test.result && test.result.totals && test.result.totals.types;
394
- if (issueTypes) {
395
- const noOpCount = issueTypes.onlyFocusable && issueTypes.onlyFocusable.total || 0;
396
- const noFocCount = issueTypes.onlyOperable && issueTypes.onlyOperable.total || 0;
397
- // Add 2 per unfocusable, 0.5 per inoperable element.
398
- addDetail('testaro', which, 2 * noFocCount + 0.5 * noOpCount);
399
- }
400
- }
401
- else if (which === 'focVis') {
402
- const count = test.result && test.result.total;
403
- if (count) {
404
- // Add 1 per link outside the viewport.
405
- addDetail('testaro', which, count);
406
- }
407
- }
408
- else if (which === 'hover') {
409
- const issues = test.result && test.result.totals;
410
- if (issues) {
411
- const {
412
- impactTriggers,
413
- additions,
414
- removals,
415
- opacityChanges,
416
- opacityImpact,
417
- unhoverables,
418
- noCursors,
419
- badCursors,
420
- noIndicators,
421
- badIndicators
422
- } = issues;
423
- // Add score with weights on hover-impact types.
424
- const score = 2 * impactTriggers
425
- + 0.3 * additions
426
- + removals
427
- + 0.2 * opacityChanges
428
- + 0.1 * opacityImpact
429
- + unhoverables
430
- + 3 * noCursors
431
- + 2 * badCursors
432
- + noIndicators
433
- + badIndicators;
434
- if (score) {
435
- addDetail('testaro', which, score);
436
- }
437
- }
438
- }
439
- else if (which === 'labClash') {
440
- const mislabeledCount = test.result
441
- && test.result.totals
442
- && test.result.totals.mislabeled
443
- || 0;
444
- // Add 1 per element with conflicting labels (ignoring unlabeled elements).
445
- addDetail('testaro', which, mislabeledCount);
446
- }
447
- else if (which === 'linkTo') {
448
- const count = test.result && test.result.total;
449
- if (count) {
450
- // Add 2 per link with no destination.
451
- addDetail('testaro', which, count);
452
- }
453
- }
454
- else if (which === 'linkUl') {
455
- const totals = test.result && test.result.totals && test.result.totals.adjacent;
456
- if (totals) {
457
- const nonUl = totals.total - totals.underlined || 0;
458
- // Add 2 per non-underlined adjacent link.
459
- addDetail('testaro', which, 2 * nonUl);
460
- }
461
- }
462
- else if (which === 'menuNav') {
463
- const issueCount = test.result
464
- && test.result.totals
465
- && test.result.totals.navigations
466
- && test.result.totals.navigations.all
467
- && test.result.totals.navigations.all.incorrect
468
- || 0;
469
- // Add 2 per defect.
470
- addDetail('testaro', which, 2 * issueCount);
471
- }
472
- else if (which === 'miniText') {
473
- const items = test.result && test.result.items;
474
- if (items && items.length) {
475
- // Add 1 per 100 characters of small-text.
476
- const totalLength = items.reduce((total, item) => total + item.length, 0);
477
- addDetail('testaro', which, Math.floor(totalLength / 100));
478
- }
479
- }
480
- else if (which === 'motion') {
481
- const data = test.result;
482
- if (data) {
483
- const {
484
- meanLocalRatio,
485
- maxLocalRatio,
486
- globalRatio,
487
- meanPixelChange,
488
- maxPixelChange,
489
- changeFrequency
490
- } = data;
491
- const score = 2 * (meanLocalRatio - 1)
492
- + maxLocalRatio - 1
493
- + globalRatio - 1
494
- + meanPixelChange / 10000
495
- + maxPixelChange / 25000
496
- + 3 * changeFrequency
497
- || 0;
498
- addDetail('testaro', which, score);
499
- }
500
- }
501
- else if (which === 'nonTable') {
502
- const total = test.result && test.result.total;
503
- if (total) {
504
- // Add 2 per pseudotable.
505
- addDetail('testaro', which, 2 * total);
506
- }
507
- }
508
- else if (which === 'radioSet') {
509
- const totals = test.result && test.result.totals;
510
- if (totals) {
511
- const {total, inSet} = totals;
512
- const score = total - inSet || 0;
513
- // Add 1 per misissueed radio button.
514
- addDetail('testaro', which, score);
515
- }
516
- }
517
- else if (which === 'role') {
518
- const badCount = test.result && test.result.badRoleElements || 0;
519
- const redundantCount = test.result && test.result.redundantRoleElements || 0;
520
- // Add 2 per bad role and 1 per redundant role.
521
- addDetail('testaro', which, 2 * badCount + redundantCount);
522
- }
523
- else if (which === 'styleDiff') {
524
- const totals = test.result && test.result.totals;
525
- if (totals) {
526
- let score = 0;
527
- // For each element type that has any style diversity:
528
- Object.values(totals).forEach(typeData => {
529
- const {total, subtotals} = typeData;
530
- if (subtotals) {
531
- const styleCount = subtotals.length;
532
- const plurality = subtotals[0];
533
- const minorities = total - plurality;
534
- // Add 1 per style, 0.2 per element with any nonplurality style.
535
- score += styleCount + 0.2 * minorities;
536
- }
537
- });
538
- addDetail('testaro', which, score);
539
- }
540
- }
541
- else if (which === 'tabNav') {
542
- const issueCount = test.result
543
- && test.result.totals
544
- && test.result.totals.navigations
545
- && test.result.totals.navigations.all
546
- && test.result.totals.navigations.all.incorrect
547
- || 0;
548
- // Add 2 per defect.
549
- addDetail('testaro', which, 2 * issueCount);
550
- }
551
- else if (which === 'titledEl') {
552
- const total = test.result && test.result.total;
553
- if (total) {
554
- const score = 4 * total;
555
- // Add 4 per mistitled element.
556
- addDetail('testaro', which, score);
557
- }
558
- }
559
- else if (which === 'zIndex') {
560
- const issueCount = test.result && test.result.totals && test.result.totals.total || 0;
561
- // Add 1 per non-auto zIndex.
562
- addDetail('testaro', which, issueCount);
563
- }
564
- });
565
- // Get the prevention scores and add them to the summary.
566
- const actsPrevented = testActs.filter(test => test.result.prevented);
567
- actsPrevented.forEach(act => {
568
- if (otherTools.includes(act.which)) {
569
- preventionScores[act.which] = preventionWeights.other;
570
- }
571
- else {
572
- preventionScores[`testaro-${act.which}`] = preventionWeights.testaro;
573
- }
574
- });
575
- const preventionScore = Object.values(preventionScores).reduce(
576
- (sum, current) => sum + current,
577
- 0
578
- );
579
- const roundedPreventionScore = Math.round(preventionScore);
580
- summary.preventions = roundedPreventionScore;
581
- summary.total += roundedPreventionScore;
582
- // Initialize a table of the issues to which tests belong.
583
- const toolIssues = {
584
- testaro: {},
585
- alfa: {},
586
- axe: {},
587
- continuum: {},
588
- htmlcs: {},
589
- ibm: {},
590
- nuVal: {},
591
- qualWeb: {},
592
- tenon: {},
593
- wave: {}
594
- };
595
- // Initialize a table of the regular expressions of variably named tests of tools.
596
- const testMatchers = {};
597
- Object.keys(issues).forEach(issueName => {
598
- Object.keys(issues[issueName].tools).forEach(toolName => {
599
- Object.keys(issues[issueName].tools[toolName]).forEach(testID => {
600
- // Update the issue table.
601
- toolIssues[toolName][testID] = issueName;
602
- // If the test is variably named:
603
- if (issues[issueName].tools[toolName][testID].variable) {
604
- // Add its regular expression, as multiline, to the variably-named-test table.
605
- if (! testMatchers[toolName]) {
606
- testMatchers[toolName] = [];
607
- }
608
- testMatchers[toolName].push(new RegExp(testID, 's'));
609
- }
610
- });
611
- });
612
- });
613
- // For each tool with any scores:
614
- Object.keys(toolDetails).forEach(toolName => {
615
- const matchers = testMatchers[toolName];
616
- // For each test with any scores in the tool:
617
- Object.keys(toolDetails[toolName]).forEach(testMessage => {
618
- // Initialize the test ID as the reported test message.
619
- let testID = testMessage;
620
- // Get the issue of the test, if it has a fixed name and is in a issue.
621
- let issueName = toolIssues[toolName][testMessage];
622
- // If the test has a variable name or is a solo test:
623
- if (! issueName) {
624
- // Determine whether the tool has variably named tests and the test is among them.
625
- testRegExp = matchers && matchers.find(matcher => matcher.test(testMessage));
626
- // If so:
627
- if (testRegExp) {
628
- // Make the matching regular expression the test ID.
629
- testID = testRegExp.source;
630
- // Get the issue of the test.
631
- issueName = toolIssues[toolName][testID];
632
- }
633
- }
634
- // If the test is in a issue:
635
- if (issueName) {
636
- // Initialize its score as its score in the tool details.
637
- if (! issueDetails.issues[issueName]) {
638
- issueDetails.issues[issueName] = {
639
- wcag: issues[issueName].wcag,
640
- tools: {}
641
- };
642
- }
643
- if (! issueDetails.issues[issueName].tools[toolName]) {
644
- issueDetails.issues[issueName].tools[toolName] = {};
645
- }
646
- let weightedScore = toolDetails[toolName][testMessage];
647
- // Weight that by the issue weight and normalize it to a 1–4 scale per instance.
648
- weightedScore *= issues[issueName].weight / 4;
649
- // Adjust the score for the quality of the test.
650
- weightedScore *= issues[issueName].tools[toolName][testID].quality;
651
- // Round the score, but not to less than 1.
652
- const roundedScore = Math.max(Math.round(weightedScore), 1);
653
- // Add the rounded score and the test description to the issue details.
654
- issueDetails.issues[issueName].tools[toolName][testID] = {
655
- score: roundedScore,
656
- what: issues[issueName].tools[toolName][testID].what
657
- };
658
- }
659
- // Otherwise, i.e. if the test is solo:
660
- else {
661
- if (! issueDetails.solos[toolName]) {
662
- issueDetails.solos[toolName] = {};
663
- }
664
- const roundedScore = Math.round(toolDetails[toolName][testID]);
665
- issueDetails.solos[toolName][testID] = roundedScore;
666
- }
667
- });
668
- });
669
- // Determine the issue scores and add them to the summary.
670
- const issueNames = Object.keys(issueDetails.issues);
671
- const {absolute, largest, smaller} = issueWeights;
672
- // For each issue with any scores:
673
- issueNames.forEach(issueName => {
674
- const scores = [];
675
- // For each tool with any scores in the issue:
676
- const issueToolData = Object.values(issueDetails.issues[issueName].tools);
677
- issueToolData.forEach(toolObj => {
678
- // Get the sum of the scores of the tests of the tool in the issue.
679
- const scoreSum = Object.values(toolObj).reduce(
680
- (sum, current) => sum + current.score,
681
- 0
682
- );
683
- // Add the sum to the list of tool scores in the issue.
684
- scores.push(scoreSum);
685
- });
686
- // Sort the scores in descending order.
687
- scores.sort((a, b) => b - a);
688
- // Compute the sum of the absolute score and the weighted largest and other scores.
689
- const issueScore = absolute
690
- + largest * scores[0]
691
- + smaller * scores.slice(1).reduce((sum, current) => sum + current, 0);
692
- const roundedIssueScore = Math.round(issueScore);
693
- summary.issues.push({
694
- issueName,
695
- score: roundedIssueScore
696
- });
697
- summary.total += roundedIssueScore;
698
- });
699
- summary.issues.sort((a, b) => b.score - a.score);
700
- // Determine the solo score and add it to the summary.
701
- const soloToolNames = Object.keys(issueDetails.solos);
702
- soloToolNames.forEach(toolName => {
703
- const testIDs = Object.keys(issueDetails.solos[toolName]);
704
- testIDs.forEach(testID => {
705
- const score = soloWeight * issueDetails.solos[toolName][testID];
706
- summary.solos += score;
707
- summary.total += score;
708
- });
709
- });
710
- summary.solos = Math.round(summary.solos);
711
- summary.total = Math.round(summary.total);
712
- }
713
- }
714
- // Get the log score.
715
- const {jobData} = report;
716
- const logScore = logWeights.logCount * jobData.logCount
717
- + logWeights.logSize * jobData.logSize +
718
- + logWeights.errorLogCount * jobData.errorLogCount
719
- + logWeights.errorLogSize * jobData.errorLogSize
720
- + logWeights.prohibitedCount * jobData.prohibitedCount +
721
- + logWeights.visitTimeoutCount * jobData.visitTimeoutCount +
722
- + logWeights.visitRejectionCount * jobData.visitRejectionCount
723
- + logWeights.visitLatency * (jobData.visitLatency - normalLatency);
724
- const roundedLogScore = Math.max(0, Math.round(logScore));
725
- summary.log = roundedLogScore;
726
- summary.total += roundedLogScore;
727
- // Add the score facts to the report.
728
- report.score = {
729
- scoreProcID,
730
- logWeights,
731
- soloWeight,
732
- issueWeights,
733
- preventionWeights,
734
- toolDetails,
735
- issueDetails,
736
- preventionScores,
737
- summary
738
- };
739
- };
740
- exports.issues = issues;