@jrpool/kilotest 57.2.3 → 58.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.
@@ -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';
@@ -103,64 +104,34 @@ const populateQuery = async (query: Record<string, any>) => {
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
- violatorCount
123
- } = reportData;
124
- // 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.
125
123
  lines.tested.push(`${margin}<details>`);
126
- const daysAgo = getAgoDays(timeStamp);
127
- const pageDataStrings = await getPageDataStrings(timeStamp, jobID, {description, url, daysAgo});
128
- const {urlLink, testInfo}: any = pageDataStrings;
129
124
  const testText = multiReportWhats.includes(description) ? ` (${testInfo.toLowerCase()})` : '';
130
125
  lines.tested.push(`${margin} <summary>${description}${testText}</summary>`);
131
- lines.tested.push(`${margin} <ul>`);
132
- // Add the URL of the target to the lines.
133
- lines.tested.push(`${margin} <li>URL: ${urlLink}</li>`);
134
- // Add facts about the report to the lines.
135
- lines.tested.push(`${margin} <li>${testInfo}</li>`);
136
- // If the page prevented any rule engine from performing its tests:
137
- if (preventedEngineCount) {
138
- // Add this to the lines.
139
- const engineCountString = getCountString(preventedEngineCount, 'rule engine', 'rule engines');
140
- lines.tested.push(
141
- `${margin} <li>Page not testable by ${engineCountString} (${preventedEngineNames.join(' + ')})</li>`,
142
- );
143
- }
144
- // Add facts about the test results to the lines.
145
- let reporterString = `${getCountString(reporterCount, 'rule engine', 'rule engines')} reported issues`;
146
- if (reporterCount) {
147
- const reporterNamesString = reporterNames.join(' + ');
148
- reporterString = `${reporterString} (${reporterNamesString})`;
149
- }
150
- const issueCountString = getCountString(issueCount, 'issue was', 'issues were');
151
- const violatorString = getCountString(violatorCount, 'violator was', 'violators were');
152
- lines.tested.push(`${margin} <li>${reporterString}</li>`);
153
- lines.tested.push(`${margin} <li>${issueCountString} reported</li>`);
154
- lines.tested.push(`${margin} <li>${violatorString} reported</li>`);
155
- lines.tested.push(`${margin} </ul>`);
156
- 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">`);
157
129
  // If any issues were reported:
158
130
  if (issueCount) {
159
- // Add a question link about the reported issues to the lines.
131
+ // Add a link to issue details to the lines.
160
132
  const href = `href="listIssues.html/${timeStamp}/${jobID}"`;
161
- const label = `aria-label="What ${issueCountString} reported for the ${description} page?"`;
162
- const questionString = issueCount === 1 ? 'was the issue' : 'were the issues';
163
- const link = `<a ${href} ${label}>What ${questionString}?</a>`;
133
+ const label = `aria-label="issue details for the ${description} page"`;
134
+ const link = `<a ${href} ${label}>Issue details</a>`;
164
135
  lines.tested.push(`${margin} <li>${link}</li>`);
165
136
  }
166
137
  let retestString: string = '';
@@ -216,3 +216,85 @@ test('listReports shows no-reports message when the database is empty', async ()
216
216
  await fs.rm(tmpDir, {recursive: true}).catch(() => {});
217
217
  }
218
218
  });
219
+
220
+ // Returns the details element of the report whose summary starts with a description and whose issue link, if required, contains a fragment.
221
+ const getReportDetails = (html: any, description: string, hrefFragment?: string) => {
222
+ const matches = html.querySelectorAll('details').filter((d: any) => {
223
+ const summary = d.querySelector('summary')?.text ?? '';
224
+ if (!summary.startsWith(description)) {
225
+ return false;
226
+ }
227
+ return hrefFragment === undefined
228
+ || d.querySelectorAll('a').some((a: any) => a.getAttribute('href')?.includes(hrefFragment));
229
+ });
230
+ assert.equal(matches.length, 1, `exactly one report matches ${description} ${hrefFragment ?? ''}`);
231
+ return matches[0];
232
+ };
233
+
234
+ // Returns the 3 rule-engine amounts and the 3 summary amounts of a details element.
235
+ const getResultFacts = (details: any) => {
236
+ const items = details.querySelectorAll('li');
237
+ const itemTexts: string[] = items.map((li: any) => li.text.trim());
238
+ const getAmount = (label: string) => {
239
+ const text = itemTexts.find(t => t.startsWith(`${label}: `));
240
+ assert.ok(text, `${label} item exists`);
241
+ return Number(text.slice(label.length + 2));
242
+ };
243
+ // The summary item is the parent of the rule-engine item and the 3 amount items.
244
+ const summaryItem = items.find((li: any) => li.text.trim().startsWith('Summary of results:'));
245
+ assert.ok(summaryItem, 'Summary of results item exists');
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]);
250
+ return {
251
+ called: getAmount('Called'),
252
+ tested: getAmount('Were able to test'),
253
+ reporters: getAmount('Reported any rule violations'),
254
+ amountLabels,
255
+ violations: getAmount('Violations'),
256
+ violators: getAmount('Violators'),
257
+ issues: getAmount('Issues')
258
+ };
259
+ };
260
+
261
+ // Expected facts, hand-computed from the fixtures.
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]
272
+ ];
273
+
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 () => {
276
+ const result: any = await answer();
277
+ const html = parse(result.answerPage);
278
+ const facts = getResultFacts(getReportDetails(html, description, fragment));
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']);
283
+ assert.equal(facts.violations, violations);
284
+ assert.equal(facts.violators, violators);
285
+ assert.equal(facts.issues, issues);
286
+ });
287
+ }
288
+
289
+ test('listReports counts a prevented rule engine as called but not able to test', async () => {
290
+ const result: any = await answer();
291
+ const html = parse(result.answerPage);
292
+ const prevented = getResultFacts(getReportDetails(html, 'Prevented Page'));
293
+ assert.equal(prevented.called - prevented.tested, 1);
294
+ const others = html.querySelectorAll('details').filter((d: any) => !d.text.includes('Prevented Page'));
295
+ assert.equal(others.length, 7);
296
+ assert.ok(others.every((d: any) => {
297
+ const facts = getResultFacts(d);
298
+ return facts.called === facts.tested;
299
+ }));
300
+ });
@@ -14,19 +14,10 @@
14
14
  <main>
15
15
  <h1><a href="/">Kilotest</a>: Violations of <q>__issue__</q> issue for __target__ page</h1>
16
16
  <h2>Basics</h2>
17
- <h3>About the __target__ page</h3>
18
- <ul>
19
- <li>URL: __urlLink__</li>
20
- <li>__testInfo__</li>
21
- </ul>
22
- <h3>About the <q>__issue__</q> issue</h3>
23
- <ul>
24
- <li>Why it matters: __why__</li>
25
- <li>Priority: __priority__</li>
26
- <li>Related WCAG standard: __wcag__</li>
27
- <li>Reported by __reporterCount__ (__reporters__)</li>
28
- <li>__violatorCount__ reported</li>
29
- </ul>
17
+ <h3>About the page</h3>
18
+ __pageFacts__
19
+ <h3>About the issue</h3>
20
+ __issueFacts__
30
21
  <h2>Violators</h2>
31
22
  <p>Here are the HTML elements reported to violate rules belonging to the <q>__issue__</q> issue.</p>
32
23
  __takeMeThere__