testilo 10.0.0 → 10.0.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,720 @@
1
+ /*
2
+ tsp22
3
+ Testilo score proc 22
4
+
5
+ Computes scores from Testilo script ts21 or ts22 and adds them to a report.
6
+ */
7
+
8
+ // IMPORTS
9
+
10
+ const {issues} = require('./tic22');
11
+
12
+ // CONSTANTS
13
+
14
+ // ID of this proc.
15
+ const scoreProcID = 'tsp22';
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
+ const {which} = test;
109
+ // Add scores to the tool details.
110
+ if (which === 'alfa') {
111
+ const issues = test.result && test.result.items;
112
+ if (issues && Array.isArray(issues)) {
113
+ issues.forEach(issue => {
114
+ const {verdict, rule} = issue;
115
+ if (verdict && rule) {
116
+ const {ruleID} = rule;
117
+ if (ruleID) {
118
+ // Add 4 per failure, 1 per warning (cantTell).
119
+ addDetail(which, ruleID, verdict === 'failed' ? 4 : 1);
120
+ }
121
+ }
122
+ });
123
+ }
124
+ }
125
+ else if (which === 'axe') {
126
+ const impactScores = {
127
+ minor: 1,
128
+ moderate: 2,
129
+ serious: 3,
130
+ critical: 4
131
+ };
132
+ const tests = test.result && test.result.details;
133
+ if (tests) {
134
+ const warnings = tests.incomplete;
135
+ const {violations} = tests;
136
+ [[warnings, 0.25], [violations, 1]].forEach(issueSeverity => {
137
+ if (issueSeverity[0] && Array.isArray(issueSeverity[0])) {
138
+ issueSeverity[0].forEach(issueType => {
139
+ const {id, nodes} = issueType;
140
+ if (id && nodes && Array.isArray(nodes)) {
141
+ nodes.forEach(node => {
142
+ const {impact} = node;
143
+ if (impact) {
144
+ // Add the impact score for a violation or 25% of it for a warning.
145
+ addDetail(which, id, issueSeverity[1] * impactScores[impact]);
146
+ }
147
+ });
148
+ }
149
+ });
150
+ }
151
+ });
152
+ }
153
+ }
154
+ else if (which === 'continuum') {
155
+ const issues = test.result;
156
+ if (issues && Array.isArray(issues)) {
157
+ issues.forEach(issue => {
158
+ // Add 4 per violation.
159
+ addDetail(which, issue.engineTestId, 4);
160
+ });
161
+ }
162
+ }
163
+ else if (which === 'htmlcs') {
164
+ const issues = test.result;
165
+ if (issues) {
166
+ ['Error', 'Warning'].forEach(issueSeverityName => {
167
+ const severityData = issues[issueSeverityName];
168
+ if (severityData) {
169
+ const issueTypes = Object.keys(severityData);
170
+ issueTypes.forEach(issueTypeName => {
171
+ const issueArrays = Object.values(severityData[issueTypeName]);
172
+ const issueCount = issueArrays.reduce((count, array) => count + array.length, 0);
173
+ const severityCode = issueSeverityName[0].toLowerCase();
174
+ const code = `${severityCode}:${issueTypeName}`;
175
+ // Add 4 per error, 1 per warning.
176
+ const weight = severityCode === 'e' ? 4 : 1;
177
+ addDetail(which, code, weight * issueCount);
178
+ });
179
+ }
180
+ });
181
+ }
182
+ }
183
+ else if (which === 'ibm') {
184
+ const {result} = test;
185
+ const {content, url} = result;
186
+ if (content && url) {
187
+ let preferredMode = 'content';
188
+ if (
189
+ content.error ||
190
+ (content.totals &&
191
+ content.totals.violation &&
192
+ url.totals &&
193
+ url.totals.violation &&
194
+ url.totals.violation > content.totals.violation)
195
+ ) {
196
+ preferredMode = 'url';
197
+ }
198
+ const {items} = result[preferredMode];
199
+ if (items && Array.isArray(items)) {
200
+ items.forEach(issue => {
201
+ const {ruleId, level} = issue;
202
+ if (ruleId && level) {
203
+ // Add 4 per violation, 1 per warning (recommendation).
204
+ addDetail(which, ruleId, level === 'violation' ? 4 : 1);
205
+ }
206
+ });
207
+ }
208
+ }
209
+ }
210
+ else if (which === 'nuVal') {
211
+ const issues = test.result && test.result.messages;
212
+ if (issues) {
213
+ issues.forEach(issue => {
214
+ // Add 4 per error, 1 per warning.
215
+ const weight = issue.type === 'error' ? 4 : 1;
216
+ addDetail(which, issue.message, weight);
217
+ });
218
+ }
219
+ }
220
+ else if (which === 'qualWeb') {
221
+ // For each section of the results:
222
+ const modules = test.result && test.result.modules;
223
+ if (modules) {
224
+ ['act-rules', 'wcag-techniques', 'best-practices'].forEach(type => {
225
+ // For each test in the section:
226
+ const {assertions} = modules[type];
227
+ if (assertions) {
228
+ const issueIDs = Object.keys(assertions);
229
+ issueIDs.forEach(issueID => {
230
+ // For each error or warning from the test:
231
+ const {results} = assertions[issueID];
232
+ results.forEach(result => {
233
+ // Add 4 per error, 1 per warning, per element.
234
+ let weight = 0;
235
+ const {verdict} = result;
236
+ if (verdict === 'error') {
237
+ weight = 4;
238
+ }
239
+ else if (verdict === 'warning') {
240
+ weight = 1;
241
+ }
242
+ if (weight) {
243
+ addDetail(which, issueID, result.elements.length * weight);
244
+ }
245
+ });
246
+ });
247
+ }
248
+ });
249
+ }
250
+ }
251
+ else if (which === 'tenon') {
252
+ const issues =
253
+ test.result && test.result.data && test.result.data.resultSet;
254
+ if (issues && Array.isArray(issues)) {
255
+ issues.forEach(issue => {
256
+ const {tID, priority, certainty} = issue;
257
+ if (tID && priority && certainty) {
258
+ // Add 4 per issue if certainty and priority 100, less if less.
259
+ addDetail(which, tID, certainty * priority / 2500);
260
+ }
261
+ });
262
+ }
263
+ }
264
+ else if (which === 'wave') {
265
+ const severityScores = {
266
+ error: 4,
267
+ contrast: 3,
268
+ alert: 1
269
+ };
270
+ const issueSeverities = test.result && test.result.categories;
271
+ if (issueSeverities) {
272
+ ['error', 'contrast', 'alert'].forEach(issueSeverity => {
273
+ const {items} = issueSeverities[issueSeverity];
274
+ if (items) {
275
+ const testIDs = Object.keys(items);
276
+ if (testIDs.length) {
277
+ testIDs.forEach(testID => {
278
+ const {count} = items[testID];
279
+ if (count) {
280
+ // Add 4 per error, 3 per contrast error, 1 per warning (alert).
281
+ addDetail(
282
+ which, `${issueSeverity[0]}:${testID}`, count * severityScores[issueSeverity]
283
+ );
284
+ }
285
+ });
286
+ }
287
+ }
288
+ });
289
+ }
290
+ }
291
+ else if (which === 'allHidden') {
292
+ const {result} = test;
293
+ if (
294
+ result
295
+ && ['hidden', 'reallyHidden', 'visHidden', 'ariaHidden'].every(
296
+ key => result[key]
297
+ && ['document', 'body', 'main'].every(
298
+ element => typeof result[key][element] === 'boolean'
299
+ )
300
+ )
301
+ ) {
302
+ // Get a score for the test.
303
+ const score = 8 * result.hidden.document
304
+ + 8 * result.hidden.body
305
+ + 6 * result.hidden.main
306
+ + 10 * result.reallyHidden.document
307
+ + 10 * result.reallyHidden.body
308
+ + 8 * result.reallyHidden.main
309
+ + 8 * result.visHidden.document
310
+ + 8 * result.visHidden.body
311
+ + 6 * result.visHidden.main
312
+ + 10 * result.ariaHidden.document
313
+ + 10 * result.ariaHidden.body
314
+ + 8 * result.ariaHidden.main;
315
+ // Add the score.
316
+ addDetail('testaro', which, score);
317
+ }
318
+ }
319
+ else if (which === 'bulk') {
320
+ const count = test.result && test.result.visibleElements;
321
+ if (typeof count === 'number') {
322
+ // Add 1 per 300 visible elements beyond 300.
323
+ addDetail('testaro', which, Math.max(0, count / 300 - 1));
324
+ }
325
+ }
326
+ else if (which === 'docType') {
327
+ // If document has no or invalid doctype:
328
+ const hasType = test.result && test.result.docHasType;
329
+ if (typeof hasType === 'boolean' && ! hasType) {
330
+ // Add 10.
331
+ addDetail('testaro', which, 10);
332
+ }
333
+ }
334
+ else if (which === 'embAc') {
335
+ const issueCounts = test.result && test.result.totals;
336
+ if (issueCounts) {
337
+ const counts = Object.values(issueCounts);
338
+ const total = counts.reduce((sum, current) => sum + current);
339
+ // Add 3 per embedded element.
340
+ addDetail('testaro', which, 3 * total);
341
+ }
342
+ }
343
+ else if (which === 'filter') {
344
+ const totals = test.result && test.result.totals;
345
+ if (totals) {
346
+ // Add 2 per filter-styled element, 1 per filter-impacted element.
347
+ addDetail('testaro', which, 2 * totals.elements + totals.impact);
348
+ }
349
+ }
350
+ else if (which === 'focAll') {
351
+ const discrepancy = test.result && test.result.discrepancy;
352
+ if (discrepancy) {
353
+ // Add 2 per discrepancy.
354
+ addDetail('testaro', which, 2 * Math.abs(discrepancy));
355
+ }
356
+ }
357
+ else if (which === 'focInd') {
358
+ const issueTypes =
359
+ test.result && test.result.totals && test.result.totals.types;
360
+ if (issueTypes) {
361
+ const missingCount = issueTypes.indicatorMissing
362
+ && issueTypes.indicatorMissing.total
363
+ || 0;
364
+ const badCount = issueTypes.nonOutlinePresent
365
+ && issueTypes.nonOutlinePresent.total
366
+ || 0;
367
+ // Add 3 per missing, 1 per non-outline focus indicator.
368
+ addDetail('testaro', which, badCount + 3 * missingCount);
369
+ }
370
+ }
371
+ else if (which === 'focOp') {
372
+ const issueTypes =
373
+ test.result && test.result.totals && test.result.totals.types;
374
+ if (issueTypes) {
375
+ const noOpCount = issueTypes.onlyFocusable && issueTypes.onlyFocusable.total || 0;
376
+ const noFocCount = issueTypes.onlyOperable && issueTypes.onlyOperable.total || 0;
377
+ // Add 2 per unfocusable, 0.5 per inoperable element.
378
+ addDetail('testaro', which, 2 * noFocCount + 0.5 * noOpCount);
379
+ }
380
+ }
381
+ else if (which === 'focVis') {
382
+ const count = test.result && test.result.total;
383
+ if (count) {
384
+ // Add 1 per link outside the viewport.
385
+ addDetail('testaro', which, count);
386
+ }
387
+ }
388
+ else if (which === 'hover') {
389
+ const issues = test.result && test.result.totals;
390
+ if (issues) {
391
+ const {
392
+ impactTriggers,
393
+ additions,
394
+ removals,
395
+ opacityChanges,
396
+ opacityImpact,
397
+ unhoverables,
398
+ noCursors,
399
+ badCursors,
400
+ noIndicators,
401
+ badIndicators
402
+ } = issues;
403
+ // Add score with weights on hover-impact types.
404
+ const score = 2 * impactTriggers
405
+ + 0.3 * additions
406
+ + removals
407
+ + 0.2 * opacityChanges
408
+ + 0.1 * opacityImpact
409
+ + unhoverables
410
+ + 3 * noCursors
411
+ + 2 * badCursors
412
+ + noIndicators
413
+ + badIndicators;
414
+ if (score) {
415
+ addDetail('testaro', which, score);
416
+ }
417
+ }
418
+ }
419
+ else if (which === 'labClash') {
420
+ const mislabeledCount = test.result
421
+ && test.result.totals
422
+ && test.result.totals.mislabeled
423
+ || 0;
424
+ // Add 1 per element with conflicting labels (ignoring unlabeled elements).
425
+ addDetail('testaro', which, mislabeledCount);
426
+ }
427
+ else if (which === 'linkTo') {
428
+ const count = test.result && test.result.total;
429
+ if (count) {
430
+ // Add 2 per link with no destination.
431
+ addDetail('testaro', which, count);
432
+ }
433
+ }
434
+ else if (which === 'linkUl') {
435
+ const totals = test.result && test.result.totals && test.result.totals.adjacent;
436
+ if (totals) {
437
+ const nonUl = totals.total - totals.underlined || 0;
438
+ // Add 2 per non-underlined adjacent link.
439
+ addDetail('testaro', which, 2 * nonUl);
440
+ }
441
+ }
442
+ else if (which === 'menuNav') {
443
+ const issueCount = test.result
444
+ && test.result.totals
445
+ && test.result.totals.navigations
446
+ && test.result.totals.navigations.all
447
+ && test.result.totals.navigations.all.incorrect
448
+ || 0;
449
+ // Add 2 per defect.
450
+ addDetail('testaro', which, 2 * issueCount);
451
+ }
452
+ else if (which === 'miniText') {
453
+ const items = test.result && test.result.items;
454
+ if (items && items.length) {
455
+ // Add 1 per 100 characters of small-text.
456
+ const totalLength = items.reduce((total, item) => total + item.length, 0);
457
+ addDetail('testaro', which, Math.floor(totalLength / 100));
458
+ }
459
+ }
460
+ else if (which === 'motion') {
461
+ const data = test.result;
462
+ if (data) {
463
+ const {
464
+ meanLocalRatio,
465
+ maxLocalRatio,
466
+ globalRatio,
467
+ meanPixelChange,
468
+ maxPixelChange,
469
+ changeFrequency
470
+ } = data;
471
+ const score = 2 * (meanLocalRatio - 1)
472
+ + (maxLocalRatio - 1)
473
+ + globalRatio - 1
474
+ + meanPixelChange / 10000
475
+ + maxPixelChange / 25000
476
+ + 3 * changeFrequency
477
+ || 0;
478
+ addDetail('testaro', which, score);
479
+ }
480
+ }
481
+ else if (which === 'nonTable') {
482
+ const total = test.result && test.result.total;
483
+ if (total) {
484
+ // Add 2 per pseudotable.
485
+ addDetail('testaro', which, 2 * total);
486
+ }
487
+ }
488
+ else if (which === 'radioSet') {
489
+ const totals = test.result && test.result.totals;
490
+ if (totals) {
491
+ const {total, inSet} = totals;
492
+ const score = total - inSet || 0;
493
+ // Add 1 per misissueed radio button.
494
+ addDetail('testaro', which, score);
495
+ }
496
+ }
497
+ else if (which === 'role') {
498
+ const badCount = test.result && test.result.badRoleElements || 0;
499
+ const redundantCount = test.result && test.result.redundantRoleElements || 0;
500
+ // Add 2 per bad role and 1 per redundant role.
501
+ addDetail('testaro', which, 2 * badCount + redundantCount);
502
+ }
503
+ else if (which === 'styleDiff') {
504
+ const totals = test.result && test.result.totals;
505
+ if (totals) {
506
+ let score = 0;
507
+ // For each element type that has any style diversity:
508
+ Object.values(totals).forEach(typeData => {
509
+ const {total, subtotals} = typeData;
510
+ if (subtotals) {
511
+ const styleCount = subtotals.length;
512
+ const plurality = subtotals[0];
513
+ const minorities = total - plurality;
514
+ // Add 1 per style, 0.2 per element with any nonplurality style.
515
+ score += styleCount + 0.2 * minorities;
516
+ }
517
+ });
518
+ addDetail('testaro', which, score);
519
+ }
520
+ }
521
+ else if (which === 'tabNav') {
522
+ const issueCount = test.result
523
+ && test.result.totals
524
+ && test.result.totals.navigations
525
+ && test.result.totals.navigations.all
526
+ && test.result.totals.navigations.all.incorrect
527
+ || 0;
528
+ // Add 2 per defect.
529
+ addDetail('testaro', which, 2 * issueCount);
530
+ }
531
+ else if (which === 'titledEl') {
532
+ const total = test.result && test.result.total;
533
+ if (total) {
534
+ const score = 4 * total;
535
+ // Add 4 per mistitled element.
536
+ addDetail('testaro', which, score);
537
+ }
538
+ }
539
+ else if (which === 'zIndex') {
540
+ const issueCount = test.result && test.result.totals && test.result.totals.total || 0;
541
+ // Add 1 per non-auto zIndex.
542
+ addDetail('testaro', which, issueCount);
543
+ }
544
+ });
545
+ // Get the prevention scores and add them to the summary.
546
+ const actsPrevented = testActs.filter(test => test.result.prevented);
547
+ actsPrevented.forEach(act => {
548
+ if (otherTools.includes(act.which)) {
549
+ preventionScores[act.which] = preventionWeights.other;
550
+ }
551
+ else {
552
+ preventionScores[`testaro-${act.which}`] = preventionWeights.testaro;
553
+ }
554
+ });
555
+ const preventionScore = Object.values(preventionScores).reduce(
556
+ (sum, current) => sum + current,
557
+ 0
558
+ );
559
+ const roundedPreventionScore = Math.round(preventionScore);
560
+ summary.preventions = roundedPreventionScore;
561
+ summary.total += roundedPreventionScore;
562
+ // Initialize a table of the issues to which tests belong.
563
+ const toolIssues = {
564
+ testaro: {},
565
+ alfa: {},
566
+ axe: {},
567
+ continuum: {},
568
+ htmlcs: {},
569
+ ibm: {},
570
+ nuVal: {},
571
+ qualWeb: {},
572
+ tenon: {},
573
+ wave: {}
574
+ };
575
+ // Initialize a table of the regular expressions of variably named tests of tools.
576
+ const testMatchers = {};
577
+ Object.keys(issues).forEach(issueName => {
578
+ Object.keys(issues[issueName].tools).forEach(toolName => {
579
+ Object.keys(issues[issueName].tools[toolName]).forEach(testID => {
580
+ // Update the issue table.
581
+ toolIssues[toolName][testID] = issueName;
582
+ // If the test is variably named:
583
+ if (issues[issueName].tools[toolName][testID].variable) {
584
+ // Add its regular expression, as multiline, to the variably-named-test table.
585
+ if (! testMatchers[toolName]) {
586
+ testMatchers[toolName] = [];
587
+ }
588
+ testMatchers[toolName].push(new RegExp(testID, 's'));
589
+ }
590
+ });
591
+ });
592
+ });
593
+ // For each tool with any scores:
594
+ Object.keys(toolDetails).forEach(toolName => {
595
+ const matchers = testMatchers[toolName];
596
+ // For each test with any scores in the tool:
597
+ Object.keys(toolDetails[toolName]).forEach(testMessage => {
598
+ // Initialize the test ID as the reported test message.
599
+ let testID = testMessage;
600
+ // Get the issue of the test, if it has a fixed name and is in a issue.
601
+ let issueName = toolIssues[toolName][testMessage];
602
+ // If the test has a variable name or is a solo test:
603
+ if (! issueName) {
604
+ // Determine whether the tool has variably named tests and the test is among them.
605
+ testRegExp = matchers && matchers.find(matcher => matcher.test(testMessage));
606
+ // If so:
607
+ if (testRegExp) {
608
+ // Make the matching regular expression the test ID.
609
+ testID = testRegExp.source;
610
+ // Get the issue of the test.
611
+ issueName = toolIssues[toolName][testID];
612
+ }
613
+ }
614
+ // If the test is in a issue:
615
+ if (issueName) {
616
+ // Initialize its score as its score in the tool details.
617
+ if (! issueDetails.issues[issueName]) {
618
+ issueDetails.issues[issueName] = {
619
+ wcag: issues[issueName].wcag,
620
+ tools: {}
621
+ };
622
+ }
623
+ if (! issueDetails.issues[issueName].tools[toolName]) {
624
+ issueDetails.issues[issueName].tools[toolName] = {};
625
+ }
626
+ let weightedScore = toolDetails[toolName][testMessage];
627
+ // Weight that by the issue weight and normalize it to a 1–4 scale per instance.
628
+ weightedScore *= issues[issueName].weight / 4;
629
+ // Adjust the score for the quality of the test.
630
+ weightedScore *= issues[issueName].tools[toolName][testID].quality;
631
+ // Round the score, but not to less than 1.
632
+ const roundedScore = Math.max(Math.round(weightedScore), 1);
633
+ // Add the rounded score and the test description to the issue details.
634
+ issueDetails.issues[issueName].tools[toolName][testID] = {
635
+ score: roundedScore,
636
+ what: issues[issueName].tools[toolName][testID].what
637
+ };
638
+ }
639
+ // Otherwise, i.e. if the test is solo:
640
+ else {
641
+ if (! issueDetails.solos[toolName]) {
642
+ issueDetails.solos[toolName] = {};
643
+ }
644
+ const roundedScore = Math.round(toolDetails[toolName][testID]);
645
+ issueDetails.solos[toolName][testID] = roundedScore;
646
+ }
647
+ });
648
+ });
649
+ // Determine the issue scores and add them to the summary.
650
+ const issueNames = Object.keys(issueDetails.issues);
651
+ const {absolute, largest, smaller} = issueWeights;
652
+ // For each issue with any scores:
653
+ issueNames.forEach(issueName => {
654
+ const scores = [];
655
+ // For each tool with any scores in the issue:
656
+ const issueToolData = Object.values(issueDetails.issues[issueName].tools);
657
+ issueToolData.forEach(toolObj => {
658
+ // Get the sum of the scores of the tests of the tool in the issue.
659
+ const scoreSum = Object.values(toolObj).reduce(
660
+ (sum, current) => sum + current.score,
661
+ 0
662
+ );
663
+ // Add the sum to the list of tool scores in the issue.
664
+ scores.push(scoreSum);
665
+ });
666
+ // Sort the scores in descending order.
667
+ scores.sort((a, b) => b - a);
668
+ // Compute the sum of the absolute score and the weighted largest and other scores.
669
+ const issueScore = absolute
670
+ + largest * scores[0]
671
+ + smaller * scores.slice(1).reduce((sum, current) => sum + current, 0);
672
+ const roundedIssueScore = Math.round(issueScore);
673
+ summary.issues.push({
674
+ issueName,
675
+ score: roundedIssueScore
676
+ });
677
+ summary.total += roundedIssueScore;
678
+ });
679
+ summary.issues.sort((a, b) => b.score - a.score);
680
+ // Determine the solo score and add it to the summary.
681
+ const soloToolNames = Object.keys(issueDetails.solos);
682
+ soloToolNames.forEach(toolName => {
683
+ const testIDs = Object.keys(issueDetails.solos[toolName]);
684
+ testIDs.forEach(testID => {
685
+ const score = soloWeight * issueDetails.solos[toolName][testID];
686
+ summary.solos += score;
687
+ summary.total += score;
688
+ });
689
+ });
690
+ summary.solos = Math.round(summary.solos);
691
+ summary.total = Math.round(summary.total);
692
+ }
693
+ }
694
+ // Get the log score.
695
+ const {jobData} = report;
696
+ const logScore = logWeights.logCount * jobData.logCount
697
+ + logWeights.logSize * jobData.logSize +
698
+ + logWeights.errorLogCount * jobData.errorLogCount
699
+ + logWeights.errorLogSize * jobData.errorLogSize
700
+ + logWeights.prohibitedCount * jobData.prohibitedCount +
701
+ + logWeights.visitTimeoutCount * jobData.visitTimeoutCount +
702
+ + logWeights.visitRejectionCount * jobData.visitRejectionCount
703
+ + logWeights.visitLatency * (jobData.visitLatency - normalLatency);
704
+ const roundedLogScore = Math.max(0, Math.round(logScore));
705
+ summary.log = roundedLogScore;
706
+ summary.total += roundedLogScore;
707
+ // Add the score facts to the report.
708
+ report.score = {
709
+ scoreProcID,
710
+ logWeights,
711
+ soloWeight,
712
+ issueWeights,
713
+ preventionWeights,
714
+ toolDetails,
715
+ issueDetails,
716
+ preventionScores,
717
+ summary
718
+ };
719
+ };
720
+ exports.issues = issues;