testilo 12.3.1 → 13.2.0

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/README.md CHANGED
@@ -181,8 +181,7 @@ The `batch()` function of the `batch` module generates a batch and returns it as
181
181
  A user can invoke `batch` in this way:
182
182
 
183
183
  - Create a target list and save it as a text file (with tab-delimited items in newline-delimited lines) in the `targetLists` subdirectory of the `process.env.SPECDIR` directory. Name the file `x.tsv`, where `x` is the list ID.
184
- - In the Testilo project directory, execute this statement:
185
- - `node call batch i w`
184
+ - In the Testilo project directory, execute the statement `node call batch i w`.
186
185
 
187
186
  In this statement, replace `i` with the list ID and `w` with a description of the batch.
188
187
 
package/call.js CHANGED
@@ -58,7 +58,7 @@ const callBatch = async (listID, what) => {
58
58
  const callScript = async (scriptID, classificationID = null, ... issueIDs) => {
59
59
  // Get any issue classification.
60
60
  const issueClasses = classificationID
61
- ? require(`${functionDir}/score/${classificationID}`)
61
+ ? require(`${functionDir}/score/${classificationID}`).issueClasses
62
62
  : null;
63
63
  // Create a script.
64
64
  const scriptObj = script(scriptID, issueClasses, ... issueIDs);
@@ -117,7 +117,8 @@ const callScore = async (scorerID, selector = '') => {
117
117
  // Save it.
118
118
  await fs.writeFile(`${scoredReportDir}/${report.id}.json`, JSON.stringify(report, null, 2));
119
119
  };
120
- }
120
+ console.log(`Reports scored and saved in ${scoredReportDir}`);
121
+ }
121
122
  // Otherwise, i.e. if no raw reports are to be scored:
122
123
  else {
123
124
  // Report this.
@@ -132,17 +133,16 @@ const callDigest = async (digesterID, selector = '') => {
132
133
  if (reports.length) {
133
134
  const digesterDir = `${functionDir}/digest/${digesterID}`;
134
135
  // Get the digester.
135
- const digester = require(`${digesterDir}/index`).makeQuery;
136
- // Get the template.
137
- const template = await fs.readFile(`${digesterDir}/index.html`, 'utf8');
136
+ const {digester} = require(`${digesterDir}/index`);
138
137
  // Digest the reports.
139
- const digestedReports = digest(template, digester, reports);
138
+ const digestedReports = await digest(digester, reports);
140
139
  const digestedReportDir = `${reportDir}/digested`;
141
140
  // For each digested report:
142
141
  for (const reportID of Object.keys(digestedReports)) {
143
142
  // Save it.
144
143
  await fs.writeFile(`${digestedReportDir}/${reportID}.html`, digestedReports[reportID]);
145
144
  };
145
+ console.log(`Reports digested and saved in ${digestedReportDir}`);
146
146
  }
147
147
  // Otherwise, i.e. if no scored reports are to be digested:
148
148
  else {
package/digest.js CHANGED
@@ -2,27 +2,19 @@
2
2
  digest.js
3
3
  Creates digests from a scored reports.
4
4
  Arguments:
5
- 0. Digest template.
6
- 1. Digesting function.
7
- 2. Array of scored reports.
5
+ 0. Digesting function.
6
+ 1. Array of scored reports.
8
7
  */
9
8
 
10
9
  // ########## FUNCTIONS
11
10
 
12
- // Replaces the placeholders in content with eponymous query parameters.
13
- const replaceHolders = (content, query) => content
14
- .replace(/__([a-zA-Z]+)__/g, (ph, qp) => query[qp]);
15
11
  // Digests the scored reports and returns them, digested.
16
- exports.digest = (digestTemplate, digester, reports) => {
12
+ exports.digest = async (digester, reports) => {
17
13
  const digests = {};
18
- // Create a query.
19
- const query = {};
20
14
  // For each report:
21
15
  for (const report of reports) {
22
- // Populate the query.
23
- digester(report, query);
24
16
  // Use it to create a digest.
25
- const digestedReport = replaceHolders(digestTemplate, query);
17
+ const digestedReport = await digester(report);
26
18
  // Add the digest to the array of digests.
27
19
  digests[report.id] = digestedReport;
28
20
  console.log(`Report ${report.id} digested`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testilo",
3
- "version": "12.3.1",
3
+ "version": "13.2.0",
4
4
  "description": "Client that scores and digests Testaro reports",
5
5
  "main": "aim.js",
6
6
  "scripts": {
@@ -22,28 +22,40 @@
22
22
  <tr><th>URL</th><td>__url__</td></tr>
23
23
  <tr><th>Requester</th><td>__requester__</td></tr>
24
24
  <tr><th>Test date</th><td>__dateSlash__</td></tr>
25
- <tr><th>Score</th><td>__totalScore__</td></tr>
25
+ <tr><th>Score</th><td>__total__</td></tr>
26
26
  <tr><th>Tested by</th><td>Testaro, procedure <code>__ts__</code></td></tr>
27
27
  <tr><th>Scored by</th><td>Testilo, procedure <code>__sp__</code></td></tr>
28
28
  <tr><th>Digested by</th><td>Testilo, procedure <code>__dp__</code></td></tr>
29
29
  </table>
30
30
  </header>
31
31
  <h2>Introduction</h2>
32
- <p>This is a digest of results from a battery of accessibility Tests.</p>
33
- <p>The battery includes 1353 automated accessibility tests drawn from ten different packages: Alfa, Axe, Continuum, Equal Access, HTML CodeSniffer, Nu Html Checker, QualWeb, Tenon, Testaro, and WAVE.</p>
34
- <p>These tests were run on the web page named above and gave the page a score of __totalScore__, where 0 would be <q>perfect</q>.</p>
35
- <h2>Score summary</h2>
32
+ <p>This is a digest of results from a battery of accessibility tests.</p>
33
+ <p>The battery includes about 1350 tests drawn from ten different packages: Alfa, Axe, Continuum, Equal Access, HTML CodeSniffer, Nu Html Checker, QualWeb, Tenon, Testaro, and WAVE.</p>
34
+ <p>These tests were run on the web page named above and gave the page a score of __total__, where 0 would be <q>perfect</q>.</p>
35
+ <h2>Scores</h2>
36
36
  <table class="allBorder secondCellRight">
37
- <caption>Score components</caption>
37
+ <caption>Score summary</caption>
38
+ <thead>
39
+ <tr><th>Component</th><th>Score</th></tr>
40
+ </thead>
38
41
  <tbody class="headersLeft">
39
- __scoreRows__
42
+ <tr><th>total</th><td>__total__</td></tr>
43
+ <tr><th>issue</th><td>__issue__</td></tr>
44
+ <tr><th>tool</th><td>__tool__</td></tr>
45
+ <tr><th>prevention</th><td>__prevention__</td></tr>
46
+ <tr><th>log</th><td>__log__</td></tr>
47
+ <tr><th>latency</th><td>__latency__</td></tr>
48
+ </tbody>
49
+ </table>
50
+ <table class="allBorder secondCellRight">
51
+ <caption>Issue scores</caption>
52
+ <thead>
53
+ <tr><th>Issue</th><th>Score</th></tr>
54
+ </thead>
55
+ <tbody class="headersLeft">
56
+ __issueRows__
40
57
  </tbody>
41
58
  </table>
42
- <h2>Issue summary</h2>
43
- <h3>Special issues</h3>
44
- __specialSummary__
45
- <h3>Classified issues</h3>
46
- __issueSummary__
47
59
  <h2>Complete report</h2>
48
60
  <pre>__report__</pre>
49
61
  <footer>
@@ -1,11 +1,11 @@
1
- /*
2
- index: digester for scoring procedure tsp24.
3
- Creator of parameters for substitution into index.html.
4
- */
1
+ // index: digester for scoring procedure tsp27.
5
2
 
6
3
  // IMPORTS
7
4
 
5
+ // Issue classification
8
6
  const {issueClasses} = require('../../score/tic27');
7
+ // Function to process files.
8
+ const fs = require('fs/promises');
9
9
 
10
10
  // CONSTANTS
11
11
 
@@ -21,21 +21,15 @@ const innerJoiner = '\n ';
21
21
  const htmlEscape = textOrNumber => textOrNumber
22
22
  .toString()
23
23
  .replace(/&/g, '&amp;')
24
- .replace(/</g, '&lt;');
24
+ .replace(/</g, '&lt;')
25
+ .replace(/"/g, '&quot;');
25
26
  // Gets a row of the score-summary table.
26
27
  const getScoreRow = (componentName, score) => `<tr><th>${componentName}</th><td>${score}</td></tr>`;
27
- // Gets the start of a paragraph about a special score.
28
- const getSpecialPStart = (summary, scoreID) =>
29
- `<p><span class="componentID">${scoreID}</span>: Score ${summary[scoreID]}.`;
30
28
  // Adds parameters to a query for a digest.
31
- exports.makeQuery = (report, query) => {
32
- // Add an HTML-safe copy of the report to the query to be appended to the digest.
33
- const {acts, sources, jobData, score} = report;
29
+ const populateQuery = (report, query) => {
30
+ const {sources, jobData, score} = report;
34
31
  const {script, target, requester} = sources;
35
- const reportJSON = JSON.stringify(report, null, 2);
36
- const reportJSONSafe = htmlEscape(reportJSON);
37
- query.report = reportJSONSafe;
38
- const {scoreProcID, summary, issues} = score;
32
+ const {scoreProcID, summary, details} = score;
39
33
  const {total} = summary;
40
34
  query.ts = script;
41
35
  query.sp = scoreProcID;
@@ -46,37 +40,37 @@ exports.makeQuery = (report, query) => {
46
40
  query.org = target.what;
47
41
  query.url = target.which;
48
42
  query.requester = requester;
49
- // Add the total score to the query.
50
- if (typeof total === 'number') {
51
- query.totalScore = total;
52
- }
53
- else {
54
- console.log('ERROR: missing or invalid total score');
55
- return;
56
- }
57
- // Get rows for a score-summary table.
58
- const scoreRows = [];
43
+ // Add values for the score-summary table to the query.
44
+ ['total', 'issue', 'tool', 'prevention', 'log', 'latency'].forEach(sumItem => {
45
+ query[sumItem] = summary[sumItem];
46
+ });
47
+ const rows = {
48
+ summaryRows: [],
49
+ issueRows: []
50
+ };
59
51
  const componentIDs = ['issues', 'tools', 'preventions', 'log', 'latency'];
60
52
  ['total'].concat(componentIDs).forEach(itemID => {
61
53
  if (summary[itemID]) {
62
- scoreRows.push(getScoreRow(itemID, summary[itemID]));
54
+ rows.summaryRows.push(getScoreRow(itemID, summary[itemID]));
63
55
  }
64
56
  });
65
- // Add the issue rows to them.
66
- Object.keys(issues).forEach(issueID => {
67
- scoreRows.push(getScoreRow(issueID, issues[issueID]));
57
+ // Get rows for an issue-score table.
58
+ Object.keys(details.issue).forEach(issueID => {
59
+ rows.issueRows.push(getScoreRow(issueID, details.issue[issueID].score));
68
60
  });
69
61
  // Add the rows to the query.
70
- query.scoreRows = scoreRows.join(innerJoiner);
71
- // Add paragraphs about them for the issue summary to the query.
62
+ ['summaryRows', 'issueRows'].forEach(rowType => {
63
+ query[rowType] = rows[rowType].join(innerJoiner);
64
+ });
65
+ // Add paragraphs about the issues to the query.
72
66
  const issueSummaryItems = [];
73
- Object.keys(issues).forEach(issueID => {
67
+ Object.keys(details.issue).forEach(issueID => {
74
68
  const issueHeading = `<h4>Issue ${issueID}</h4>`;
75
69
  const wcagP = `<p>WCAG: ${issueClasses[issueID].wcag || 'N/A'}</p>`;
76
- const scoreP = `<p>Score: ${issues[issueID]}</p>`;
70
+ const scoreP = `<p>Score: ${details.issue[issueID]}</p>`;
77
71
  const issueIntroP = '<p>Issue reports in this category:</p>';
78
72
  const issueListItems = [];
79
- const issueData = issueDetails.issues[issueName];
73
+ const issueData = details.issue[issueID];
80
74
  const toolIDs = Object.keys(issueData.tools);
81
75
  toolIDs.forEach(toolID => {
82
76
  const testIDs = Object.keys(issueData.tools[toolID]);
@@ -96,4 +90,22 @@ exports.makeQuery = (report, query) => {
96
90
  issueSummaryItems.push(issueHeading, wcagP, scoreP, issueIntroP, issueList);
97
91
  });
98
92
  query.issueSummary = issueSummaryItems.join(joiner);
93
+ // Add an HTML-safe copy of the report to the query to be appended to the digest.
94
+ const reportJSON = JSON.stringify(report, null, 2);
95
+ const reportJSONSafe = htmlEscape(reportJSON);
96
+ query.report = reportJSONSafe;
97
+ };
98
+ // Returns a digested report.
99
+ exports.digester = async report => {
100
+ // Create a query to replace plateholders.
101
+ const query = {};
102
+ populateQuery(report, query);
103
+ // Get the template.
104
+ let template = await fs.readFile(`${__dirname}/index.html`, 'utf8');
105
+ // Replace its placeholders.
106
+ Object.keys(query).forEach(param => {
107
+ template = template.replace(new RegExp(`__${param}__`, 'g'), query[param]);
108
+ });
109
+ // Return the digest.
110
+ return template;
99
111
  };
@@ -2577,13 +2577,6 @@ exports.issueClasses = {
2577
2577
  quality: 1,
2578
2578
  what: 'role attribute has an invalid value'
2579
2579
  }
2580
- },
2581
- testaro: {
2582
- 'role-bad': {
2583
- variable: false,
2584
- quality: 0.5,
2585
- what: 'Nonexistent or implicit-overriding role'
2586
- }
2587
2580
  }
2588
2581
  }
2589
2582
  },
@@ -2621,10 +2614,10 @@ exports.issueClasses = {
2621
2614
  }
2622
2615
  },
2623
2616
  testaro: {
2624
- 'role-redundant': {
2617
+ 'role': {
2625
2618
  variable: false,
2626
2619
  quality: 1,
2627
- what: 'Redundant role'
2620
+ what: 'Invalid, native-replacing, or redundant role'
2628
2621
  }
2629
2622
  }
2630
2623
  }
@@ -4696,13 +4689,6 @@ exports.issueClasses = {
4696
4689
  what: 'Form control has no accessible name'
4697
4690
  }
4698
4691
  },
4699
- testaro: {
4700
- 'labClash-unlabeled': {
4701
- variable: false,
4702
- quality: 1,
4703
- what: 'Button, input, select, or textarea is unlabeled'
4704
- }
4705
- },
4706
4692
  wave: {
4707
4693
  'label_missing': {
4708
4694
  variable: false,
@@ -4967,23 +4953,10 @@ exports.issueClasses = {
4967
4953
  }
4968
4954
  },
4969
4955
  testaro: {
4970
- 'focInd-missing': {
4956
+ 'focInd': {
4971
4957
  variable: false,
4972
4958
  quality: 1,
4973
- what: 'Focused element displays no focus indicator'
4974
- }
4975
- }
4976
- }
4977
- },
4978
- focusIndicationBad: {
4979
- wcag: '2.4.7',
4980
- weight: 3,
4981
- tools: {
4982
- testaro: {
4983
- 'focInd-nonoutline': {
4984
- variable: false,
4985
- quality: 1,
4986
- what: 'Focused element displays a nostandard focus indicator'
4959
+ what: 'Focused element displays a nonstandard or no focus indicator'
4987
4960
  }
4988
4961
  }
4989
4962
  }
@@ -5438,15 +5411,10 @@ exports.issueClasses = {
5438
5411
  weight: 3,
5439
5412
  tools: {
5440
5413
  testaro: {
5441
- 'focOp-focusable-inoperable': {
5414
+ 'focOp': {
5442
5415
  variable: false,
5443
5416
  quality: 1,
5444
- what: 'Tab-focusable elements that are inoperable'
5445
- },
5446
- 'focOp-operable-nonfocusable': {
5447
- variable: false,
5448
- quality: 1,
5449
- what: 'Operable elements that cannot be Tab-focused'
5417
+ what: 'Tab-focusable elements that are inoperable or operable elements that are not focusable'
5450
5418
  }
5451
5419
  }
5452
5420
  }
@@ -5645,12 +5613,12 @@ exports.issueClasses = {
5645
5613
  }
5646
5614
  }
5647
5615
  },
5648
- hoverImpact: {
5616
+ hoverSurprise: {
5649
5617
  wcag: '1.4.13',
5650
5618
  weight: 3,
5651
5619
  tools: {
5652
5620
  testaro: {
5653
- 'hover-impactTriggers': {
5621
+ 'hover': {
5654
5622
  variable: false,
5655
5623
  quality: 1,
5656
5624
  what: 'Hovering over element has unexpected effects'
@@ -5658,71 +5626,6 @@ exports.issueClasses = {
5658
5626
  }
5659
5627
  }
5660
5628
  },
5661
- unhoverable: {
5662
- wcag: '1.4.13',
5663
- weight: 3,
5664
- tools: {
5665
- testaro: {
5666
- 'hover-unhoverables': {
5667
- variable: false,
5668
- quality: 1,
5669
- what: 'Operable element cannot be hovered over'
5670
- }
5671
- }
5672
- }
5673
- },
5674
- hoverNoCursor: {
5675
- wcag: '1.4.13',
5676
- weight: 3,
5677
- tools: {
5678
- testaro: {
5679
- 'hover-noCursors': {
5680
- variable: false,
5681
- quality: 1,
5682
- what: 'Hoverable element hides the mouse cursor'
5683
- }
5684
- }
5685
- }
5686
- },
5687
- hoverBadCursor: {
5688
- wcag: '1.4.13',
5689
- weight: 2,
5690
- tools: {
5691
- testaro: {
5692
- 'hover-badCursors': {
5693
- variable: false,
5694
- quality: 1,
5695
- what: 'Link or button makes the hovering mouse cursor nonstandard'
5696
- }
5697
- }
5698
- }
5699
- },
5700
- hoverNoIndicator: {
5701
- wcag: '1.4.13',
5702
- weight: 3,
5703
- tools: {
5704
- testaro: {
5705
- 'hover-noCursors': {
5706
- variable: false,
5707
- quality: 1,
5708
- what: 'Button shows no indication of being hovered over'
5709
- }
5710
- }
5711
- }
5712
- },
5713
- hoverBadIndicator: {
5714
- wcag: '1.4.13',
5715
- weight: 2,
5716
- tools: {
5717
- testaro: {
5718
- 'hover-noCursors': {
5719
- variable: false,
5720
- quality: 1,
5721
- what: 'List item changes when hovered over'
5722
- }
5723
- }
5724
- }
5725
- },
5726
5629
  labelClash: {
5727
5630
  wcag: '1.3.1',
5728
5631
  weight: 2,
@@ -5735,7 +5638,7 @@ exports.issueClasses = {
5735
5638
  }
5736
5639
  },
5737
5640
  testaro: {
5738
- 'labClash-mislabeled': {
5641
+ 'labClash': {
5739
5642
  variable: false,
5740
5643
  quality: 1,
5741
5644
  what: 'Incompatible label types'
@@ -63,23 +63,32 @@ exports.scorer = report => {
63
63
  scoreProcID,
64
64
  summary: {
65
65
  total: 0,
66
- issues: 0,
67
- tools: 0,
68
- preventions: 0,
66
+ issue: 0,
67
+ tool: 0,
68
+ prevention: 0,
69
69
  log: 0,
70
70
  latency: 0
71
71
  },
72
- toolTotals: [0, 0, 0, 0],
73
- issues: {},
74
- tools: {},
75
- preventions: {}
72
+ details: {
73
+ severity: {
74
+ total: [0, 0, 0, 0],
75
+ byTool: {}
76
+ },
77
+ prevention: {},
78
+ issue: {}
79
+ }
76
80
  };
77
- const {summary, toolTotals, issues, tools, preventions} = score;
81
+ const {summary, details} = score;
78
82
  // For each test act:
79
83
  testActs.forEach(act => {
80
- // If a successful standard result exists:
84
+ // If the page prevented the tool from operating:
81
85
  const {which, standardResult} = act;
82
- if (
86
+ if (standardResult.prevented) {
87
+ // Add this to the score.
88
+ details.prevention[which] = preventionWeight;
89
+ }
90
+ // Otherwise, if a successful standard result exists:
91
+ else if (
83
92
  standardResult
84
93
  && standardResult.totals
85
94
  && standardResult.totals.length === 4
@@ -87,16 +96,11 @@ exports.scorer = report => {
87
96
  ) {
88
97
  // Add the severity totals of the tool to the score.
89
98
  const {totals} = standardResult;
90
- tools[which] = totals;
91
- toolTotals.forEach((total, index) => {
92
- toolTotals[index] += totals[index];
93
- });
94
- // Update the issue totals for the tool.
95
- const issueTotals = {};
96
- let ruleID;
99
+ details.severity.byTool[which] = totals;
100
+ // Add the instance data of the tool to the score.
97
101
  standardResult.instances.forEach(instance => {
98
- ruleID = issueIndex[which][instance.ruleID];
99
- if (! ruleID) {
102
+ let {ruleID} = instance;
103
+ if (! issueIndex[which][ruleID]) {
100
104
  ruleID = issueMatcher.find(pattern => {
101
105
  const patternRE = new RegExp(pattern);
102
106
  return patternRE.test(instance.ruleID);
@@ -104,40 +108,92 @@ exports.scorer = report => {
104
108
  }
105
109
  if (ruleID) {
106
110
  const issueID = issueIndex[which][ruleID];
107
- if (! issueTotals[issueID]) {
108
- issueTotals[issueID] = 0;
111
+ if (! details.issue[issueID]) {
112
+ details.issue[issueID] = {
113
+ score: 0,
114
+ maxCount: 0,
115
+ weight: issueClasses[issueID].weight,
116
+ tools: {}
117
+ };
118
+ }
119
+ if (! details.issue[issueID].tools[which]) {
120
+ details.issue[issueID].tools[which] = {};
121
+ }
122
+ if (! details.issue[issueID].tools[which][ruleID]) {
123
+ details.issue[issueID].tools[which][ruleID] = {
124
+ quality: issueClasses[issueID].tools[which][ruleID].quality,
125
+ complaints: {
126
+ countTotal: 0,
127
+ texts: []
128
+ }
129
+ };
130
+ }
131
+ details
132
+ .issue[issueID]
133
+ .tools[which][ruleID]
134
+ .complaints
135
+ .countTotal += instance.count || 1;
136
+ if (
137
+ ! details
138
+ .issue[issueID]
139
+ .tools[which][ruleID]
140
+ .complaints
141
+ .texts
142
+ .includes(instance.what)
143
+ ) {
144
+ details.issue[issueID].tools[which][ruleID].complaints.texts.push(instance.what);
109
145
  }
110
- issueTotals[issueID] += instance.count || 1;
111
146
  }
112
147
  else {
113
148
  console.log(`ERROR: ${instance.ruleID} of ${which} not found in issueClasses`);
114
149
  }
115
150
  });
116
- // Update the issue totals in the score.
117
- Object.keys(issueTotals).forEach(issueID => {
118
- issues[issueID] = Math.max(issues[id] || 0, issueTotals[issueID]);
119
- });
120
- summary.issues = Object.values(issues).reduce((total, current) => total + current);
121
151
  }
122
- // Otherwise, i.e. if no no successful result exists:
152
+ // Otherwise, i.e. if no successful standard result exists:
123
153
  else {
124
- // Add a prevented result to the act if not already there.
125
- if (! act.result) {
126
- act.result = {};
127
- }
128
- if (! act.result.prevented) {
129
- act.result.prevented = true;
130
- };
131
- // Add the tool and the prevention score to the score.
132
- preventions[which] = preventionWeight;
133
- summary.preventions += preventionWeight;
154
+ // Add an inferred prevention to the score.
155
+ details.prevention[which] = preventionWeight;
134
156
  }
135
157
  });
136
- // Add the weighted tool total to the score.
137
- summary.tools = toolWeight * toolTotals.reduce(
138
- (total, current, index) => total + current * severityWeights[index], 0
158
+ // For each issue with any complaints:
159
+ Object.keys(details.issue).forEach(issueID => {
160
+ const issueData = details.issue[issueID];
161
+ // For each tool with any complaints for the issue:
162
+ Object.keys(issueData.tools).forEach(toolID => {
163
+ // Get the sum of the weighted counts of its issue rules.
164
+ let weightedCount = 0;
165
+ Object.values(issueData.tools[toolID]).forEach(ruleData => {
166
+ weightedCount += ruleData.quality * ruleData.complaints.countTotal;
167
+ });
168
+ // If the sum exceeds the existing maximum weighted count for the issue:
169
+ if (weightedCount > issueData.maxCount) {
170
+ // Change the maximum count for the issue to the sum.
171
+ issueData.maxCount = weightedCount;
172
+ }
173
+ });
174
+ // Get the score for the issue.
175
+ issueData.score = issueData.weight * issueData.maxCount;
176
+ });
177
+ // Add the severity detail totals to the score.
178
+ details.severity.total = Object.keys(details.severity.byTool).reduce((severityTotals, toolID) => {
179
+ details.severity.byTool[toolID].forEach((severityScore, index) => {
180
+ severityTotals[index] += severityScore;
181
+ });
182
+ return severityTotals;
183
+ }, details.severity.total);
184
+ // Add the summary issue total to the score.
185
+ summary.issue = Object
186
+ .values(details.issue)
187
+ .reduce((total, current) => total + current.score, 0);
188
+ // Add the summary tool total to the score.
189
+ summary.tool = toolWeight * details.severity.total.reduce(
190
+ (total, current, index) => total + severityWeights[index] * current, 0
191
+ );
192
+ // Add the summary prevention total to the score.
193
+ summary.prevention = Object.values(details.prevention).reduce(
194
+ (total, current) => total + current, 0
139
195
  );
140
- // Add the log score to the score.
196
+ // Add the summary log score to the score.
141
197
  const {jobData} = report;
142
198
  summary.log = Math.max(0, Math.round(
143
199
  logWeights.logCount * jobData.logCount
@@ -147,21 +203,21 @@ exports.scorer = report => {
147
203
  + logWeights.prohibitedCount * jobData.prohibitedCount +
148
204
  + logWeights.visitRejectionCount * jobData.visitRejectionCount
149
205
  ));
150
- // Add the latency score to the score.
206
+ // Add the summary latency score to the score.
151
207
  summary.latency = Math.round(
152
208
  latencyWeight * (Math.max(0, jobData.visitLatency - normalLatency))
153
209
  );
154
- // Round the scores.
210
+ // Round the unrounded scores.
155
211
  Object.keys(summary).forEach(summaryTypeName => {
156
212
  summary[summaryTypeName] = Math.round(summary[summaryTypeName]);
157
213
  });
158
- toolTotals.forEach((severityTotal, index) => {
159
- toolTotals[index] = Math.round(toolTotals[index]);
214
+ details.severity.total.forEach((severityTotal, index) => {
215
+ details.severity.total[index] = Math.round(severityTotal);
160
216
  });
161
- // Add the total score to the score.
162
- summary.total = summary.issues
163
- + summary.tools
164
- + summary.preventions
217
+ // Add the summary total score to the score.
218
+ summary.total = summary.issue
219
+ + summary.tool
220
+ + summary.prevention
165
221
  + summary.log
166
222
  + summary.latency;
167
223
  // Add the score to the report.