@jrpool/kilotest 57.3.0 → 58.2.4

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.
@@ -8,147 +8,132 @@
8
8
  import {
9
9
  getReport,
10
10
  getTestActInstances,
11
- htmlSafe,
12
- isUsableReport,
11
+ isReportError,
13
12
  objectSort,
14
- populateTemplate,
15
- ruleEngines
13
+ populateTemplate
16
14
  } from '../../util.ts';
15
+ import type {UsableReport} from '../../util.ts';
17
16
  import {
18
- getPageData,
17
+ getIssueFactsLines,
19
18
  getPageDataStrings,
19
+ getPageFactsLines,
20
+ getEngineIDs,
20
21
  getEngineNamesString,
21
- getWCAGLink,
22
22
  getWeightName
23
23
  } from '../util.ts';
24
+ import type {ResultsSummary} from '../util.ts';
24
25
  import {issues as issueSpecs} from 'testaro-issues';
25
26
 
26
27
  // FUNCTIONS
27
28
 
28
29
  // Returns data on the issues reported by a report.
29
- const getIssuesData = async (timeStamp: string, jobID: string) => {
30
- // Get the report.
31
- const report = await getReport(timeStamp, jobID);
32
- // If it exists and is valid:
33
- if (isUsableReport(report)) {
34
- // Initialize the temporary data.
35
- const temp = {
36
- issues: {} as Record<string, any>,
37
- reporters: new Set<string>(),
38
- violators: new Set<string>()
39
- };
40
- // Initialize the final data.
41
- const final: Record<string, any> = {
42
- reporters: [],
43
- reporterList: '',
44
- reporterCount: 0,
45
- violatorCount: 0,
46
- preventions: report.jobData.preventions,
47
- issues: {
48
- 4: [],
49
- 3: [],
50
- 2: [],
51
- 1: []
52
- },
53
- issueCount: 0
54
- };
55
- // For each violating standard instance of each test act:
56
- getTestActInstances(report, {violationsOnly: true}).forEach(({act, instance}) => {
57
- const {catalogIndex, issueID} = instance;
58
- const which = act.which!;
59
- // If it identifies a non-ignorable issue:
60
- if (issueID && issueID !== 'ignorable') {
61
- const issueClassification = issueSpecs[issueID];
62
- // If the issue has a current weighted classification:
63
- if (issueClassification && [1, 2, 3, 4].includes(issueClassification.weight)) {
64
- const {summary, wcag, weight, why} = issueClassification;
65
- // Initialize the temporary data on the issue if necessary.
66
- temp.issues[issueID] ??= {
67
- issueID,
68
- summary,
69
- wcag,
70
- why,
71
- weight,
72
- reporters: new Set(),
73
- reporterList: '',
74
- violators: new Set()
75
- };
76
- // Ensure the rule engine is in the temporary data.
77
- temp.issues[issueID].reporters.add(which);
78
- temp.reporters.add(which);
79
- // If the instance has a catalog index:
80
- if (catalogIndex) {
81
- // Ensure the violator is in the temporary data.
82
- temp.issues[issueID].violators.add(catalogIndex);
83
- temp.violators.add(String(catalogIndex));
84
- }
30
+ const getIssuesData = (report: UsableReport): ResultsSummary & Record<string, any> => {
31
+ // Initialize the temporary data.
32
+ const temp = {
33
+ issues: {} as Record<string, any>,
34
+ reporters: new Set<string>(),
35
+ violators: new Set<string>()
36
+ };
37
+ // Initialize the final data.
38
+ const final: ResultsSummary & Record<string, any> = {
39
+ reporters: [],
40
+ reporterList: '',
41
+ reporterCount: 0,
42
+ engineCount: 0,
43
+ testedEngineCount: 0,
44
+ violationCount: 0,
45
+ violatorCount: 0,
46
+ issues: {
47
+ 4: [],
48
+ 3: [],
49
+ 2: [],
50
+ 1: []
51
+ },
52
+ issueCount: 0
53
+ };
54
+ // For each violating standard instance of each test act:
55
+ getTestActInstances(report, {violationsOnly: true}).forEach(({act, instance}) => {
56
+ const {catalogIndex, issueID} = instance;
57
+ const which = act.which!;
58
+ // If it identifies a non-ignorable issue:
59
+ if (issueID && issueID !== 'ignorable') {
60
+ const issueClassification = issueSpecs[issueID];
61
+ // If the issue has a current weighted classification:
62
+ if (issueClassification && [1, 2, 3, 4].includes(issueClassification.weight)) {
63
+ const {summary, wcag, weight, why} = issueClassification;
64
+ // Initialize the temporary data on the issue if necessary.
65
+ temp.issues[issueID] ??= {
66
+ issueID,
67
+ summary,
68
+ wcag,
69
+ why,
70
+ weight,
71
+ reporters: new Set(),
72
+ reporterList: '',
73
+ violationCount: 0,
74
+ violators: new Set()
75
+ };
76
+ // Ensure the rule engine is in the temporary data.
77
+ temp.issues[issueID].reporters.add(which);
78
+ temp.reporters.add(which);
79
+ // Increment the violation counts.
80
+ temp.issues[issueID].violationCount++;
81
+ final.violationCount++;
82
+ // If the instance has a catalog index:
83
+ if (catalogIndex) {
84
+ // Ensure the violator is in the temporary data.
85
+ temp.issues[issueID].violators.add(catalogIndex);
86
+ temp.violators.add(String(catalogIndex));
85
87
  }
86
88
  }
87
- });
88
- // Finish populating the final data.
89
- final.reporterList = getEngineNamesString(temp.reporters);
90
- final.reporterCount = temp.reporters.size;
91
- final.violatorCount = temp.violators.size;
92
- Object.values(temp.issues).forEach(issue => {
93
- const {issueID, summary, wcag, why, weight} = issue;
94
- const finalIssue: Record<string, any> = {
95
- issueID,
96
- summary,
97
- wcag,
98
- why,
99
- weight
100
- };
101
- finalIssue.reporterList = getEngineNamesString(issue.reporters);
102
- finalIssue.reporterCount = issue.reporters.size;
103
- finalIssue.violatorCount = issue.violators.size;
104
- final.issues[issue.weight].push(finalIssue);
105
- });
106
- final.issueCount = Object.keys(temp.issues).length;
107
- // For each weight:
108
- [4, 3, 2, 1].forEach(weight => {
109
- // Sort its issues in the final data alphabetically by reporter names.
110
- objectSort(final.issues[weight], 'reporterList', 'alpha');
111
- // Sort the issues again in descending reporter-count order, making this the primary order.
112
- objectSort(final.issues[weight], 'reporterCount', 'numericDown');
113
- });
114
- // Return the data.
115
- return final;
116
- }
117
- // Otherwise, i.e. if it is invalid or does not exist, return this.
118
- return {error: 'Report missing or invalid.'};
119
- };
120
- // Get page and issues data from a report.
121
- const getData = async (timeStamp: string, jobID: string) => {
122
- const pageData = await getPageData(timeStamp, jobID);
123
- const issuesData: any = await getIssuesData(timeStamp, jobID);
124
- const pageError = ('error' in pageData ? pageData.error : '');
125
- const issuesError = issuesData.error || '';
126
- const errors = [pageError, issuesError].filter(Boolean).join('; ');
127
- // If the data of either type are missing or invalid:
128
- if (errors) {
129
- // Return this.
130
- return {error: errors};
131
- }
132
- // Otherwise, return the data.
133
- return {
134
- pageData,
135
- issuesData
136
- };
89
+ }
90
+ });
91
+ // Finish populating the final data.
92
+ const {calledIDs, preventedIDs} = getEngineIDs(report);
93
+ final.engineCount = calledIDs.length;
94
+ final.testedEngineCount = calledIDs.length - preventedIDs.length;
95
+ final.reporterList = getEngineNamesString(temp.reporters);
96
+ final.reporterCount = temp.reporters.size;
97
+ final.violatorCount = temp.violators.size;
98
+ Object.values(temp.issues).forEach(issue => {
99
+ const {issueID, summary, wcag, why, weight} = issue;
100
+ const finalIssue: Record<string, any> = {
101
+ issueID,
102
+ summary,
103
+ wcag,
104
+ why,
105
+ weight
106
+ };
107
+ finalIssue.reporterList = getEngineNamesString(issue.reporters);
108
+ finalIssue.reporterCount = issue.reporters.size;
109
+ finalIssue.violationCount = issue.violationCount;
110
+ finalIssue.violatorCount = issue.violators.size;
111
+ final.issues[issue.weight].push(finalIssue);
112
+ });
113
+ final.issueCount = Object.keys(temp.issues).length;
114
+ // For each weight:
115
+ [4, 3, 2, 1].forEach(weight => {
116
+ // Sort its issues in the final data alphabetically by reporter names.
117
+ objectSort(final.issues[weight], 'reporterList', 'alpha');
118
+ // Sort the issues again in descending reporter-count order, making this the primary order.
119
+ objectSort(final.issues[weight], 'reporterCount', 'numericDown');
120
+ });
121
+ // Return the data.
122
+ return final;
137
123
  };
138
124
  // Adds parameters to a query for the answer page.
139
125
  const populateQuery = async (timeStamp: string, jobID: string, query: Record<string, any>) => {
140
- // Get data on the target and its issues according to the report.
141
- const data: any = await getData(timeStamp, jobID);
142
- const {pageData, issuesData} = data;
126
+ // Get the report.
127
+ const report = await getReport(timeStamp, jobID);
143
128
  // If this failed:
144
- if (data.error) {
129
+ if (isReportError(report)) {
145
130
  // Populate the query with the reason.
146
- query.error = data.error;
131
+ query.error = report.error;
147
132
  // Stop populating the query.
148
133
  return;
149
134
  }
150
135
  // Otherwise, i.e. if it succeeded, get fact descriptions for the target.
151
- const pageInfo = await getPageDataStrings(timeStamp, jobID, pageData);
136
+ const pageInfo = getPageDataStrings(report);
152
137
  // If this failed:
153
138
  if (pageInfo.error !== undefined) {
154
139
  // Populate the query with the reason.
@@ -156,45 +141,24 @@ const populateQuery = async (timeStamp: string, jobID: string, query: Record<str
156
141
  // Stop populating the query.
157
142
  return;
158
143
  }
159
- const {testInfo, urlLink, description} = pageInfo;
160
- // Otherwise, i.e. if it succeeded, add target data to the query.
144
+ // Otherwise, i.e. if it succeeded, get data on the issues.
145
+ const issuesData = getIssuesData(report);
146
+ const {testInfo, description} = pageInfo;
147
+ // Add target data to the query.
161
148
  query.target = description;
162
- query.urlLink = urlLink;
163
149
  query.testInfo = testInfo;
164
- const {
165
- reporterList,
166
- reporterCount,
167
- violatorCount,
168
- issueCount,
169
- preventions,
170
- issues
171
- } = issuesData;
172
- // Initialize strings for the prevention notices query property.
173
- const preventionStrings: string[] = [];
174
150
  const margin = ' '.repeat(6);
175
- Object.keys(preventions).forEach(preventedEngineID => {
176
- const [engineName, engineSponsor] = ruleEngines[preventedEngineID] ?? [preventedEngineID, 'unknown sponsor'];
177
- const engineNameString = `${engineName} (${engineSponsor})`;
178
- const causeString = htmlSafe(preventions[preventedEngineID]);
179
- const preventionString = `${margin}<li>Page not testable by ${engineNameString}: ${causeString}</li>`;
180
- preventionStrings.push(preventionString);
181
- });
182
- // Add prevention notices to the query.
183
- query.preventions = preventionStrings.join('\n');
151
+ // Add the page-facts list to the query.
152
+ query.pageFacts = getPageFactsLines(pageInfo, issuesData, margin).join('\n');
153
+ const {issues} = issuesData;
184
154
  // Add report data to the query.
185
155
  query.timeStamp = timeStamp;
186
156
  query.jobID = jobID;
187
- // Add reporter information to the query.
188
- query.reporterCount = reporterCount === 1 ? '1 rule engine' : `${reporterCount} rule engines`;
189
- query.reporters = reporterList;
190
157
  // Add a summary of the issues to the query.
191
- query.issueCount = issueCount === 1 ? '1 issue was' : `${issueCount} issues were`;
192
158
  query.highestCount = issues[4].length;
193
159
  query.highCount = issues[3].length;
194
160
  query.lowCount = issues[2].length;
195
161
  query.lowestCount = issues[1].length;
196
- // Add a violator count to the query.
197
- query.violatorCount = violatorCount === 1 ? '1 violator was' : `${violatorCount} violators were`;
198
162
  // For each weight:
199
163
  [4, 3, 2, 1].forEach(weight => {
200
164
  const weightName = getWeightName(weight);
@@ -208,35 +172,15 @@ const populateQuery = async (timeStamp: string, jobID: string, query: Record<str
208
172
  const weightIssueCount = weightIssues.length;
209
173
  // Add the issue count to the query.
210
174
  query[`${weightName}Count`] = weightIssueCount;
211
- const {
212
- issueID,
213
- reporterCount,
214
- reporterList,
215
- summary,
216
- violatorCount,
217
- wcag,
218
- why
219
- } = issueData;
220
- const wcagLink = `<a href="${getWCAGLink(wcag)}">${wcag}</a>`;
175
+ const {issueID, summary} = issueData;
221
176
  // Add the start of a list item to the lines.
222
177
  detailsLines.push(`${margin} <li>`);
223
178
  // Add a heading summarizing the issue to the lines.
224
179
  detailsLines.push(`${margin} <h5>${summary}</h5>`);
225
- // Add the start of a fact list about the issue to the lines.
226
- detailsLines.push(`${margin} <ul class="pseudoTopLevel">`);
227
180
  // Add the issue facts to the lines.
228
- detailsLines.push(`${margin} <li>Why it matters: ${why}`);
229
- detailsLines.push(`${margin} <li>Related WCAG standard: ${wcagLink}`);
230
- const reporterCountString = reporterCount === 1 ? '1 rule engine' : `${reporterCount} rule engines`;
231
181
  detailsLines.push(
232
- `${margin} <li>Reported by ${reporterCountString} (${reporterList})</li>`
182
+ ...getIssueFactsLines(report, issueID, `${margin} `, 'pseudoTopLevel')
233
183
  );
234
- const violatorCountString = violatorCount === 1
235
- ? '1 violator was'
236
- : `${violatorCount} violators were`;
237
- detailsLines.push(`${margin} <li>${violatorCountString} reported</li>`);
238
- // Add the end of the fact list to the lines.
239
- detailsLines.push(`${margin} </ul>`);
240
184
  // Add the start of a link list to the lines.
241
185
  detailsLines.push(`${margin} <ul class="nav">`);
242
186
  const whereQuestionString = 'Where was the issue found?';
@@ -18,7 +18,7 @@ let pageDataStringsOverride: any = null;
18
18
  mock.module('../util.ts', {
19
19
  exports: {
20
20
  ...realWebUtil,
21
- getPageDataStrings: async (...args: any[]) => {
21
+ getPageDataStrings: (...args: any[]) => {
22
22
  if (pageDataStringsOverride !== null) {
23
23
  return pageDataStringsOverride;
24
24
  }
@@ -61,6 +61,18 @@ test('listIssues includes the page description in the HTML for the mixed report'
61
61
  assert.ok(result.answerPage.includes('Mixed Outcomes Page'));
62
62
  });
63
63
 
64
+ test('listIssues interpolates the page description into the AI prompt block quote', async () => {
65
+ const result = await answer('260101T0000/mix');
66
+ const html = parse(result.answerPage);
67
+ const blockQuote = html.querySelector('details blockquote');
68
+ assert.ok(blockQuote);
69
+ assert.ok(
70
+ blockQuote.textContent.startsWith('Kilotest reports defects on the Mixed Outcomes Page web page.')
71
+ );
72
+ // No placeholder may remain uninterpolated.
73
+ assert.ok(!blockQuote.textContent.includes('__target__'));
74
+ });
75
+
64
76
  test('listIssues includes links to listViolators for issues in the mixed report', async () => {
65
77
  const result = await answer('260101T0000/mix');
66
78
  const html = parse(result.answerPage);
@@ -93,14 +105,23 @@ test('listIssues for the empty report shows no issue links', async () => {
93
105
  });
94
106
 
95
107
 
96
- test('listIssues includes prevention notices for the prevented report', async () => {
108
+ test('listIssues counts the prevented rule engine as called but not able to test', async () => {
97
109
  const result = await answer('260101T0006/prv');
98
110
  assert.equal(result.status, 'ok');
99
- assert.ok(result.answerPage.includes('Page not testable by'));
100
- assert.ok(result.answerPage.includes('page timed out'));
111
+ assert.ok(result.answerPage.includes('<li>Called: 2</li>'));
112
+ assert.ok(result.answerPage.includes('<li>Were able to test: 1</li>'));
113
+ assert.ok(result.answerPage.includes('<li>Reported any rule violations: 1</li>'));
114
+ });
115
+
116
+ test('listIssues counts a rule engine that reported no violations as called and able to test', async () => {
117
+ const result = await answer('260101T0005/emp');
118
+ assert.equal(result.status, 'ok');
119
+ assert.ok(result.answerPage.includes('<li>Called: 1</li>'));
120
+ assert.ok(result.answerPage.includes('<li>Were able to test: 1</li>'));
121
+ assert.ok(result.answerPage.includes('<li>Reported any rule violations: 0</li>'));
101
122
  });
102
123
 
103
- test('listIssues falls back to the engine ID for an unknown prevented engine', async () => {
124
+ test('listIssues counts an unknown prevented engine as called but not able to test', async () => {
104
125
  const fs = await import('node:fs/promises');
105
126
  const prvJSON = await fs.readFile(path.join(realUtil.reportsPath(), '260101T0006-prv.json'), 'utf8');
106
127
  const report = JSON.parse(prvJSON);
@@ -110,14 +131,15 @@ test('listIssues falls back to the engine ID for an unknown prevented engine', a
110
131
  try {
111
132
  const result = await answer('260103T0000/unk');
112
133
  assert.equal(result.status, 'ok');
113
- assert.ok(result.answerPage.includes('unknownEngine (unknown sponsor)'));
134
+ assert.ok(result.answerPage.includes('<li>Called: 3</li>'));
135
+ assert.ok(result.answerPage.includes('<li>Were able to test: 1</li>'));
114
136
  }
115
137
  finally {
116
138
  await fs.unlink(reportPath);
117
139
  }
118
140
  });
119
141
 
120
- test('listIssues returns an error when getPageDataStrings fails after getData succeeds', async () => {
142
+ test('listIssues returns an error when getPageDataStrings fails after getReport succeeds', async () => {
121
143
  pageDataStringsOverride = {error: 'Page data strings error'};
122
144
  try {
123
145
  const result = await answer('260101T0000/mix');
@@ -145,11 +167,12 @@ test('listIssues returns an error when report facts are not obtained', async ()
145
167
  }
146
168
  });
147
169
 
148
- test('listIssues shows plural violator count for an issue with multiple violators', async () => {
149
- // The mul report has linkNoText with 3 violators.
170
+ test('listIssues shows violation and violator counts for an issue with multiple violators', async () => {
171
+ // The mul report has linkNoText with 4 violations by 3 violators.
150
172
  const result = await answer('260101T0008/mul');
151
173
  assert.equal(result.status, 'ok');
152
- assert.ok(result.answerPage.includes('3 violators were'));
174
+ assert.ok(result.answerPage.includes('<li>Violations: 4</li>'));
175
+ assert.ok(result.answerPage.includes('<li>Violators: 3</li>'));
153
176
  });
154
177
 
155
178
  test('listIssues handles acts with no standardResult instances', async () => {
@@ -6,19 +6,20 @@
6
6
  // IMPORTS
7
7
 
8
8
  import {
9
- getAgoDays,
10
9
  getJobNames,
11
10
  getObject,
12
- getTestRequests,
11
+ getReport,
13
12
  getReportExtracts,
13
+ getTestRequests,
14
+ isReportError,
14
15
  jobsPath,
15
16
  objectSort,
16
17
  populateTemplate
17
18
  } from '../../util.ts';
18
19
  import {
19
- getCountString,
20
20
  getMultiReportWhats,
21
21
  getPageDataStrings,
22
+ getPageFactsLines,
22
23
  getReportData
23
24
  } from '../util.ts';
24
25
  import path from 'node:path';
@@ -96,68 +97,35 @@ const populateQuery = async (query: Record<string, any>) => {
96
97
  query.some = (reportCount || jobFileNames.queue.length || jobFileNames.claimed.length)
97
98
  ? 'another'
98
99
  : 'a';
99
- const multiReportWhats = await getMultiReportWhats();
100
+ const multiReportWhats = await getMultiReportWhats(reportExtracts);
100
101
  // Sort them primarily by page description and secondarily by completion time.
101
102
  let sortedExtracts = objectSort(reportExtracts, 'reportTime', 'alpha');
102
103
  sortedExtracts = objectSort(sortedExtracts, 'description', 'alpha');
103
104
  // For each report:
104
105
  for (const extract of sortedExtracts) {
105
106
  const {jobID, timeStamp, url, description, superseded} = extract;
106
- // Get data about it.
107
- const reportData = await getReportData(timeStamp, jobID);
107
+ // Get the report.
108
+ const report = await getReport(timeStamp, jobID);
108
109
  // If this failed:
109
- if (reportData.error !== undefined) {
110
- console.error(reportData.error);
110
+ if (isReportError(report)) {
111
+ console.error(report.error);
111
112
  // Populate the query with the reason.
112
- query.error = reportData.error;
113
+ query.error = report.error;
113
114
  // Stop populating the query.
114
115
  return;
115
116
  }
116
- const {
117
- issueCount,
118
- preventedEngineCount,
119
- preventedEngineNames,
120
- reporterNames,
121
- reporterCount,
122
- violationCount,
123
- violatorCount
124
- } = reportData;
125
- // Otherwise, i.e. if it succeeded, add lines about the report.
117
+ // Otherwise, i.e. if it succeeded, get data about the report.
118
+ const reportData = getReportData(report);
119
+ const pageDataStrings = getPageDataStrings(report);
120
+ const {issueCount} = reportData;
121
+ const {testInfo} = pageDataStrings;
122
+ // Add lines about the report.
126
123
  lines.tested.push(`${margin}<details>`);
127
- const daysAgo = getAgoDays(timeStamp);
128
- const pageDataStrings = await getPageDataStrings(timeStamp, jobID, {description, url, daysAgo});
129
- const {urlLink, testInfo}: any = pageDataStrings;
130
124
  const testText = multiReportWhats.includes(description) ? ` (${testInfo.toLowerCase()})` : '';
131
125
  lines.tested.push(`${margin} <summary>${description}${testText}</summary>`);
132
- lines.tested.push(`${margin} <ul>`);
133
- // Add the URL of the target to the lines.
134
- lines.tested.push(`${margin} <li>URL: ${urlLink}</li>`);
135
- // Add facts about the report to the lines.
136
- lines.tested.push(`${margin} <li>${testInfo}</li>`);
137
- // If the page prevented any rule engine from performing its tests:
138
- if (preventedEngineCount) {
139
- // Add this to the lines.
140
- const engineCountString = getCountString(preventedEngineCount, 'rule engine', 'rule engines');
141
- lines.tested.push(
142
- `${margin} <li>Page not testable by ${engineCountString} (${preventedEngineNames.join(' + ')})</li>`,
143
- );
144
- }
145
- // Add facts about the test results to the lines.
146
- let reporterString = `${getCountString(reporterCount, 'rule engine', 'rule engines')} reported issues`;
147
- if (reporterCount) {
148
- const reporterNamesString = reporterNames.join(' + ');
149
- reporterString = `${reporterString} (${reporterNamesString})`;
150
- }
151
- lines.tested.push(`${margin} <li>${reporterString}</li>`);
152
- lines.tested.push(`${margin} <li>Summary of results:`);
153
- lines.tested.push(`${margin} <ul>`);
154
- lines.tested.push(`${margin} <li>Violations: ${violationCount}</li>`);
155
- lines.tested.push(`${margin} <li>Violators: ${violatorCount}</li>`);
156
- lines.tested.push(`${margin} <li>Issues: ${issueCount}</li>`);
157
- lines.tested.push(`${margin} </ul>`);
158
- lines.tested.push(`${margin} </li>`);
159
- lines.tested.push(`${margin} </ul>`);
160
- lines.tested.push(`${margin}<ul class="nav">`);
126
+ // Add the page facts to the lines.
127
+ lines.tested.push(...getPageFactsLines(pageDataStrings, reportData, `${margin} `));
128
+ lines.tested.push(`${margin} <ul class="nav">`);
161
129
  // If any issues were reported:
162
130
  if (issueCount) {
163
131
  // Add a link to issue details to the lines.
@@ -231,7 +231,7 @@ const getReportDetails = (html: any, description: string, hrefFragment?: string)
231
231
  return matches[0];
232
232
  };
233
233
 
234
- // Returns the rule-engine line, the prevention line, and the 3 summary amounts of a details element.
234
+ // Returns the 3 rule-engine amounts and the 3 summary amounts of a details element.
235
235
  const getResultFacts = (details: any) => {
236
236
  const items = details.querySelectorAll('li');
237
237
  const itemTexts: string[] = items.map((li: any) => li.text.trim());
@@ -240,13 +240,17 @@ const getResultFacts = (details: any) => {
240
240
  assert.ok(text, `${label} item exists`);
241
241
  return Number(text.slice(label.length + 2));
242
242
  };
243
- // The summary item is the parent of the 3 amount items.
243
+ // The summary item is the parent of the rule-engine item and the 3 amount items.
244
244
  const summaryItem = items.find((li: any) => li.text.trim().startsWith('Summary of results:'));
245
245
  assert.ok(summaryItem, 'Summary of results item exists');
246
- const amountLabels = summaryItem.querySelectorAll('ul > li').map((li: any) => li.text.trim().split(':')[0]);
246
+ const summaryList = summaryItem.querySelector('ul');
247
+ const amountLabels = summaryList.childNodes
248
+ .filter((node: any) => node.tagName === 'LI')
249
+ .map((li: any) => li.text.trim().split(':')[0]);
247
250
  return {
248
- reporterLine: itemTexts.find(t => t.endsWith('reported issues') || t.includes('reported issues (')),
249
- preventionLine: itemTexts.find(t => t.startsWith('Page not testable by')),
251
+ called: getAmount('Called'),
252
+ tested: getAmount('Were able to test'),
253
+ reporters: getAmount('Reported any rule violations'),
250
254
  amountLabels,
251
255
  violations: getAmount('Violations'),
252
256
  violators: getAmount('Violators'),
@@ -255,37 +259,42 @@ const getResultFacts = (details: any) => {
255
259
  };
256
260
 
257
261
  // Expected facts, hand-computed from the fixtures.
258
- const expectedFacts: [string, string | undefined, string, number, number, number][] = [
259
- // Description, issue-link fragment, reporter line, violations, violators, issues.
260
- ['Mixed Outcomes Page', '260101T0000/mix', '2 rule engines reported issues (Alfa + Axe)', 3, 2, 2],
261
- ['Mixed Outcomes Page', '260202T0000/new', '1 rule engine reported issues (Axe)', 1, 1, 1],
262
- ['All CantTell Page', undefined, '0 rule engines reported issues', 0, 0, 0],
263
- ['No Outcomes Page', undefined, '1 rule engine reported issues (Accessibility Checker)', 1, 1, 1],
264
- ['Empty Results Page', undefined, '0 rule engines reported issues', 0, 0, 0],
265
- ['Prevented Page', undefined, '1 rule engine reported issues (Axe)', 1, 1, 1],
266
- ['Multi Violator Page', undefined, '2 rule engines reported issues (Alfa + Axe)', 4, 3, 1],
267
- ['Branch Coverage Page', undefined, '1 rule engine reported issues (Axe)', 4, 4, 1]
262
+ const expectedFacts: [string, string | undefined, number, number, number, number, number, number][] = [
263
+ // Description, issue-link fragment, engines called, engines able to test, reporting engines, violations, violators, issues.
264
+ ['Mixed Outcomes Page', '260101T0000/mix', 2, 2, 2, 3, 2, 2],
265
+ ['Mixed Outcomes Page', '260202T0000/new', 1, 1, 1, 1, 1, 1],
266
+ ['All CantTell Page', undefined, 1, 1, 0, 0, 0, 0],
267
+ ['No Outcomes Page', undefined, 1, 1, 1, 1, 1, 1],
268
+ ['Empty Results Page', undefined, 1, 1, 0, 0, 0, 0],
269
+ ['Prevented Page', undefined, 2, 1, 1, 1, 1, 1],
270
+ ['Multi Violator Page', undefined, 2, 2, 2, 4, 3, 1],
271
+ ['Branch Coverage Page', undefined, 3, 3, 1, 4, 4, 1]
268
272
  ];
269
273
 
270
- for (const [description, fragment, reporterLine, violations, violators, issues] of expectedFacts) {
271
- test(`listReports gives correct rule-engine count and names and summary amounts for ${description}${fragment ? ` (${fragment})` : ''}`, async () => {
274
+ for (const [description, fragment, called, tested, reporters, violations, violators, issues] of expectedFacts) {
275
+ test(`listReports gives correct rule-engine and summary amounts for ${description}${fragment ? ` (${fragment})` : ''}`, async () => {
272
276
  const result: any = await answer();
273
277
  const html = parse(result.answerPage);
274
278
  const facts = getResultFacts(getReportDetails(html, description, fragment));
275
- assert.equal(facts.reporterLine, reporterLine);
276
- assert.deepEqual(facts.amountLabels, ['Violations', 'Violators', 'Issues']);
279
+ assert.equal(facts.called, called);
280
+ assert.equal(facts.tested, tested);
281
+ assert.equal(facts.reporters, reporters);
282
+ assert.deepEqual(facts.amountLabels, ['Rule engines', 'Violations', 'Violators', 'Issues']);
277
283
  assert.equal(facts.violations, violations);
278
284
  assert.equal(facts.violators, violators);
279
285
  assert.equal(facts.issues, issues);
280
286
  });
281
287
  }
282
288
 
283
- test('listReports reports the prevented rule engine only for the report with a prevention', async () => {
289
+ test('listReports counts a prevented rule engine as called but not able to test', async () => {
284
290
  const result: any = await answer();
285
291
  const html = parse(result.answerPage);
286
292
  const prevented = getResultFacts(getReportDetails(html, 'Prevented Page'));
287
- assert.equal(prevented.preventionLine, 'Page not testable by 1 rule engine (Alfa)');
293
+ assert.equal(prevented.called - prevented.tested, 1);
288
294
  const others = html.querySelectorAll('details').filter((d: any) => !d.text.includes('Prevented Page'));
289
295
  assert.equal(others.length, 7);
290
- assert.ok(others.every((d: any) => !d.text.includes('Page not testable by')));
296
+ assert.ok(others.every((d: any) => {
297
+ const facts = getResultFacts(d);
298
+ return facts.called === facts.tested;
299
+ }));
291
300
  });