testilo 12.3.0 → 13.1.1

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
 
@@ -196,6 +195,8 @@ Testilo classifies issues. The built-in issue classifications are located in the
196
195
 
197
196
  If you want Testaro to test targets for particular issues, you can name those issues and use the Testilo `script` module to create a script.
198
197
 
198
+ If you want Testaro to test targets for **all** the rules of all the available tools, without regard to any issue classification, you can use the `script` module to create a script that does not impose any issue restrictions.
199
+
199
200
  #### Invocation
200
201
 
201
202
  There are two ways to use the `script` module.
@@ -216,6 +217,13 @@ This invocation references `scriptID`, `issueClasses`, and `issueID` variables.
216
217
 
217
218
  The `script()` function of the `script` module generates a script and returns it as an object. The invoking module can further dispose of the script as needed.
218
219
 
220
+ To create a script without issue restrictions, a module can use this invocation:
221
+
222
+ ```javaScript
223
+ const {script} = require('testilo/script');
224
+ const scriptObj = script(scriptID);
225
+ ```
226
+
219
227
  ##### By a user
220
228
 
221
229
  A user can invoke `script` in this way: In the Testilo project directory, execute the statement `node call script s c i0 i1 i2 i3 …`.
@@ -229,6 +237,8 @@ The `call` module will retrieve the named classification from its directory.
229
237
  The `script` module will create a script.
230
238
  The `call` module will save the script as a JSON file in the `scripts` subdirectory of the `process.env.SPECDIR` directory.
231
239
 
240
+ To create a script without any issue restrictions, a user can execute the statement `node call script s`.
241
+
232
242
  #### Options
233
243
 
234
244
  When the `script` module creates a script for you, it does not ask you for all of the options that the script may require. Instead, it chooses options. After you invoke `script`, you can edit the script that it creates to revise options.
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);
package/digest.js CHANGED
@@ -2,31 +2,23 @@
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 = (digester, reports) => {
17
13
  const digests = {};
18
- // Create a query.
19
- const query = {};
20
14
  // For each report:
21
- for (const report of reports) {
22
- // Populate the query.
23
- digester(report, query);
15
+ reports.forEach(report => {
24
16
  // Use it to create a digest.
25
- const digestedReport = replaceHolders(digestTemplate, query);
17
+ const digestedReport = 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`);
29
- };
21
+ });
30
22
  // Return the digests.
31
23
  console.log(`Digesting complete. Report count: ${reports.length}`);
32
24
  return digests;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testilo",
3
- "version": "12.3.0",
3
+ "version": "13.1.1",
4
4
  "description": "Client that scores and digests Testaro reports",
5
5
  "main": "aim.js",
6
6
  "scripts": {
@@ -29,21 +29,29 @@
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>
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
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
35
  <h2>Score summary</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
+ __summaryRows__
43
+ </tbody>
44
+ </table>
45
+ <h2>Issue scores</h2>
46
+ <table class="allBorder secondCellRight">
47
+ <caption>Issue scores</caption>
48
+ <thead>
49
+ <tr><th>Issue</th><th>Score</th></tr>
50
+ </thead>
51
+ <tbody class="headersLeft">
52
+ __issueRows__
40
53
  </tbody>
41
54
  </table>
42
- <h2>Issue summary</h2>
43
- <h3>Special issues</h3>
44
- __specialSummary__
45
- <h3>Classified issues</h3>
46
- __issueSummary__
47
55
  <h2>Complete report</h2>
48
56
  <pre>__report__</pre>
49
57
  <footer>
@@ -1,7 +1,4 @@
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
 
@@ -21,20 +18,14 @@ const innerJoiner = '\n ';
21
18
  const htmlEscape = textOrNumber => textOrNumber
22
19
  .toString()
23
20
  .replace(/&/g, '&amp;')
24
- .replace(/</g, '&lt;');
21
+ .replace(/</g, '&lt;')
22
+ .replace(/"/g, '&quot;');
25
23
  // Gets a row of the score-summary table.
26
24
  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
25
  // 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;
26
+ const makeQuery = (report, query) => {
27
+ const {sources, jobData, score} = report;
34
28
  const {script, target, requester} = sources;
35
- const reportJSON = JSON.stringify(report, null, 2);
36
- const reportJSONSafe = htmlEscape(reportJSON);
37
- query.report = reportJSONSafe;
38
29
  const {scoreProcID, summary, issues} = score;
39
30
  const {total} = summary;
40
31
  query.ts = script;
@@ -55,20 +46,25 @@ exports.makeQuery = (report, query) => {
55
46
  return;
56
47
  }
57
48
  // Get rows for a score-summary table.
58
- const scoreRows = [];
49
+ const rows = {
50
+ summaryRows: [],
51
+ issueRows: []
52
+ };
59
53
  const componentIDs = ['issues', 'tools', 'preventions', 'log', 'latency'];
60
54
  ['total'].concat(componentIDs).forEach(itemID => {
61
55
  if (summary[itemID]) {
62
- scoreRows.push(getScoreRow(itemID, summary[itemID]));
56
+ rows.summaryRows.push(getScoreRow(itemID, summary[itemID]));
63
57
  }
64
58
  });
65
- // Add the issue rows to them.
59
+ // Get rows for an issue-score table.
66
60
  Object.keys(issues).forEach(issueID => {
67
- scoreRows.push(getScoreRow(issueID, issues[issueID]));
61
+ rows.issueRows.push(getScoreRow(issueID, issues[issueID]));
68
62
  });
69
63
  // Add the rows to the query.
70
- query.scoreRows = scoreRows.join(innerJoiner);
71
- // Add paragraphs about them for the issue summary to the query.
64
+ ['summaryRows', 'issueRows'].forEach(rowType => {
65
+ query[rowType] = rows[rowType].join(innerJoiner);
66
+ });
67
+ // Add paragraphs about the issues to the query.
72
68
  const issueSummaryItems = [];
73
69
  Object.keys(issues).forEach(issueID => {
74
70
  const issueHeading = `<h4>Issue ${issueID}</h4>`;
@@ -96,4 +92,21 @@ exports.makeQuery = (report, query) => {
96
92
  issueSummaryItems.push(issueHeading, wcagP, scoreP, issueIntroP, issueList);
97
93
  });
98
94
  query.issueSummary = issueSummaryItems.join(joiner);
95
+ // Add an HTML-safe copy of the report to the query to be appended to the digest.
96
+ const reportJSON = JSON.stringify(report, null, 2);
97
+ const reportJSONSafe = htmlEscape(reportJSON);
98
+ query.report = reportJSONSafe;
99
+ };
100
+ // Returns a digested report.
101
+ exports.digester = async report => {
102
+ // Create a query to replace plateholders.
103
+ const query = makeQuery(report, {});
104
+ // Get the template.
105
+ let template = await fs.readFile(`${__dirname}/index.html`, 'utf8');
106
+ // Replace its placeholders.
107
+ Object.keys(query).forEach(param => {
108
+ template = template.replace(new RegExp(`__${param}__`, 'g'), query[param]);
109
+ });
110
+ // Return the digest.
111
+ return template;
99
112
  };
@@ -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,18 +63,22 @@ 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
84
  // If a successful standard result exists:
@@ -87,16 +91,11 @@ exports.scorer = report => {
87
91
  ) {
88
92
  // Add the severity totals of the tool to the score.
89
93
  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;
94
+ details.severity.byTool[which] = totals;
95
+ // Add the instance data of the tool to the score.
97
96
  standardResult.instances.forEach(instance => {
98
- ruleID = issueIndex[which][instance.ruleID];
99
- if (! ruleID) {
97
+ let {ruleID} = instance;
98
+ if (! issueIndex[which][ruleID]) {
100
99
  ruleID = issueMatcher.find(pattern => {
101
100
  const patternRE = new RegExp(pattern);
102
101
  return patternRE.test(instance.ruleID);
@@ -104,22 +103,46 @@ exports.scorer = report => {
104
103
  }
105
104
  if (ruleID) {
106
105
  const issueID = issueIndex[which][ruleID];
107
- if (! issueTotals[issueID]) {
108
- issueTotals[issueID] = 0;
106
+ if (! details.issue[issueID]) {
107
+ details.issue[issueID] = {
108
+ weight: issueClasses[issueID].weight,
109
+ tools: {}
110
+ };
111
+ }
112
+ if (! details.issue[issueID].tools[which]) {
113
+ details.issue[issueID].tools[which] = {};
114
+ }
115
+ if (! details.issue[issueID].tools[which][ruleID]) {
116
+ details.issue[issueID].tools[which][ruleID] = {
117
+ quality: issueClasses[issueID].tools[which][ruleID].quality,
118
+ complaints: {
119
+ countTotal: 0,
120
+ texts: []
121
+ }
122
+ };
123
+ }
124
+ details
125
+ .issue[issueID]
126
+ .tools[which][ruleID]
127
+ .complaints
128
+ .countTotal += instance.count || 1;
129
+ if (
130
+ ! details
131
+ .issue[issueID]
132
+ .tools[which][ruleID]
133
+ .complaints
134
+ .texts
135
+ .includes(instance.what)
136
+ ) {
137
+ details.issue[issueID].tools[which][ruleID].complaints.texts.push(instance.what);
109
138
  }
110
- issueTotals[issueID] += instance.count || 1;
111
139
  }
112
140
  else {
113
141
  console.log(`ERROR: ${instance.ruleID} of ${which} not found in issueClasses`);
114
142
  }
115
143
  });
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
144
  }
122
- // Otherwise, i.e. if no no successful result exists:
145
+ // Otherwise, i.e. if no successful standard result exists:
123
146
  else {
124
147
  // Add a prevented result to the act if not already there.
125
148
  if (! act.result) {
@@ -129,15 +152,35 @@ exports.scorer = report => {
129
152
  act.result.prevented = true;
130
153
  };
131
154
  // Add the tool and the prevention score to the score.
132
- preventions[which] = preventionWeight;
133
- summary.preventions += preventionWeight;
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
+ // Add the severity detail totals to the score.
159
+ details.severity.total = Object.keys(details.severity.byTool).reduce((severityTotals, toolID) => {
160
+ details.severity.byTool[toolID].forEach((severityScore, index) => {
161
+ severityTotals[index] += severityScore;
162
+ });
163
+ return severityTotals;
164
+ }, details.severity.total);
165
+ // Add the summary issue total to the score.
166
+ Object.keys(details.issue).forEach(issueID => {
167
+ Object.keys(details.issue[issueID].tools).forEach(toolID => {
168
+ Object.keys(details.issue[issueID].tools[toolID]).forEach(ruleID => {
169
+ summary.issue += details.issue[issueID].weight
170
+ * details.issue[issueID].tools[toolID][ruleID].quality
171
+ * details.issue[issueID].tools[toolID][ruleID].complaints.countTotal;
172
+ });
173
+ });
174
+ });
175
+ // Add the summary tool total to the score.
176
+ summary.tool = toolWeight * details.severity.total.reduce(
177
+ (total, current, index) => total + severityWeights[index] * current, 0
178
+ );
179
+ // Add the summary prevention total to the score.
180
+ summary.prevention = Object.values(details.prevention).reduce(
181
+ (total, current) => total + current, 0
139
182
  );
140
- // Add the log score to the score.
183
+ // Add the summary log score to the score.
141
184
  const {jobData} = report;
142
185
  summary.log = Math.max(0, Math.round(
143
186
  logWeights.logCount * jobData.logCount
@@ -147,21 +190,21 @@ exports.scorer = report => {
147
190
  + logWeights.prohibitedCount * jobData.prohibitedCount +
148
191
  + logWeights.visitRejectionCount * jobData.visitRejectionCount
149
192
  ));
150
- // Add the latency score to the score.
193
+ // Add the summary latency score to the score.
151
194
  summary.latency = Math.round(
152
195
  latencyWeight * (Math.max(0, jobData.visitLatency - normalLatency))
153
196
  );
154
- // Round the scores.
197
+ // Round the unrounded scores.
155
198
  Object.keys(summary).forEach(summaryTypeName => {
156
199
  summary[summaryTypeName] = Math.round(summary[summaryTypeName]);
157
200
  });
158
- toolTotals.forEach((severityTotal, index) => {
159
- toolTotals[index] = Math.round(toolTotals[index]);
201
+ details.severity.total.forEach((severityTotal, index) => {
202
+ details.severity.total[index] = Math.round(severityTotal);
160
203
  });
161
- // Add the total score to the score.
162
- summary.total = summary.issues
163
- + summary.tools
164
- + summary.preventions
204
+ // Add the summary total score to the score.
205
+ summary.total = summary.issue
206
+ + summary.tool
207
+ + summary.prevention
165
208
  + summary.log
166
209
  + summary.latency;
167
210
  // Add the score to the report.