@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.
package/web/util.ts CHANGED
@@ -13,15 +13,15 @@ import {
13
13
  getAgoDays,
14
14
  getDateString,
15
15
  getDateTime,
16
- getReport,
17
16
  getReportExtracts,
18
17
  getTestActInstances,
19
18
  getTestActs,
20
- isReportError,
19
+ htmlSafe,
21
20
  ruleEngines
22
21
  } from '../util.ts';
23
22
  import {issues as issueSpecs} from 'testaro-issues';
24
23
  import type {Catalog} from 'testaro';
24
+ import type {UsableReport} from '../util.ts';
25
25
  /* c8 ignore stop */
26
26
  import wcagMap from '../wcagMap.json' with {type: 'json'};
27
27
 
@@ -42,6 +42,15 @@ export type PageDataStrings = {
42
42
  testInfo: string;
43
43
  error?: never;
44
44
  };
45
+ // Counts summarizing the results of an available report.
46
+ export type ResultsSummary = {
47
+ engineCount: number;
48
+ testedEngineCount: number;
49
+ reporterCount: number;
50
+ violationCount: number;
51
+ violatorCount: number;
52
+ issueCount: number;
53
+ };
45
54
  // Basics about an available report.
46
55
  export type ReportData = {
47
56
  description: string;
@@ -52,8 +61,10 @@ export type ReportData = {
52
61
  issueCount: number;
53
62
  engineNames: string[];
54
63
  engineCount: number;
64
+ testedEngineCount: number;
55
65
  reporterNames: string[];
56
66
  reporterCount: number;
67
+ violationCount: number;
57
68
  violatorCount: number;
58
69
  preventedEngineNames: string[];
59
70
  preventedEngineCount: number;
@@ -139,16 +150,37 @@ export const getPathID = (catalog: Catalog, catalogIndex: string, pathID?: strin
139
150
  }
140
151
  return pathID ?? '/html';
141
152
  };
142
- // Returns basics about an available report.
143
- export const getReportData = async (timeStamp: string, jobID: string): Promise<ReportData | {error: string}> => {
144
- // Get the report.
145
- const report = await getReport(timeStamp, jobID);
146
- // If this failed:
147
- if (isReportError(report)) {
148
- // Return why.
149
- return {error: report.error};
153
+ // Identifies the rule engines called and the rule engines prevented from testing, by a report.
154
+ // Prevented engines have test acts. The nuVal and nuVnu engines are 2 implementations of one
155
+ // engine, so they count as one. A nuVal prevention is ignored if nuVnu then ran successfully.
156
+ export const getEngineIDs = (report: any) => {
157
+ const preventions = report.jobData?.preventions ?? {};
158
+ const calledIDs = new Set<string>([
159
+ ...getTestActs(report).map((act: any) => act.which as string),
160
+ ...Object.keys(preventions)
161
+ ]);
162
+ const preventedIDs = new Set<string>(Object.keys(preventions));
163
+ // If both implementations were called:
164
+ if (calledIDs.has('nuVal') && calledIDs.has('nuVnu')) {
165
+ // Count them as one engine.
166
+ calledIDs.delete('nuVnu');
167
+ // If nuVnu was not prevented from testing:
168
+ if (!preventedIDs.has('nuVnu')) {
169
+ // Ignore any nuVal prevention.
170
+ preventedIDs.delete('nuVal');
171
+ }
172
+ // Otherwise, i.e. if nuVnu was prevented, count both preventions as one.
173
+ else {
174
+ preventedIDs.delete('nuVnu');
175
+ }
150
176
  }
151
- // Otherwise, i.e. if it succeeded, initialize the data.
177
+ return {calledIDs: Array.from(calledIDs), preventedIDs: Array.from(preventedIDs)};
178
+ };
179
+ // Returns basics about an available report.
180
+ export const getReportData = (report: UsableReport): ReportData => {
181
+ // Identify the report by the time stamp at the start of its job name.
182
+ const timeStamp = report.id.slice(0, 11);
183
+ // Initialize the data.
152
184
  const data = {
153
185
  description: report.target.what,
154
186
  url: report.target.url,
@@ -158,21 +190,18 @@ export const getReportData = async (timeStamp: string, jobID: string): Promise<R
158
190
  issueCount: 0,
159
191
  engineNames: [] as string[],
160
192
  engineCount: 0,
193
+ testedEngineCount: 0,
161
194
  reporterNames: [] as string[],
162
195
  reporterCount: 0,
196
+ violationCount: 0,
163
197
  violatorCount: 0,
164
198
  preventedEngineNames: [] as string[],
165
199
  preventedEngineCount: 0
166
200
  };
167
201
  const issueIDSet = new Set<string>();
168
- const engineNameSet = new Set<string>();
169
202
  const reporterIDSet = new Set<string>();
170
203
  const violatorIndexSet = new Set<string>();
171
- // For each test act of the report:
172
- getTestActs(report).forEach(act => {
173
- // Ensure that the rule engine is in the temporary data.
174
- engineNameSet.add(ruleEngines[act.which!]![0]);
175
- });
204
+ const {calledIDs, preventedIDs} = getEngineIDs(report);
176
205
  // For each violating standard instance of each test act:
177
206
  getTestActInstances(report, {violationsOnly: true}).forEach(({act, instance}) => {
178
207
  const {catalogIndex, issueID} = instance;
@@ -182,6 +211,8 @@ export const getReportData = async (timeStamp: string, jobID: string): Promise<R
182
211
  reporterIDSet.add(act.which!);
183
212
  // Ensure that the issue is in the temporary data.
184
213
  issueIDSet.add(issueID);
214
+ // Increment the violation count.
215
+ data.violationCount++;
185
216
  // If the violator has a catalog index:
186
217
  if (catalogIndex) {
187
218
  // Ensure that the violator is in the temporary data.
@@ -192,9 +223,11 @@ export const getReportData = async (timeStamp: string, jobID: string): Promise<R
192
223
  // Populate the data with the act data.
193
224
  data.issueCount = issueIDSet.size;
194
225
  data.engineNames = Array
195
- .from(engineNameSet)
226
+ .from(calledIDs)
227
+ .map(id => ruleEngines[id]?.[0] || id)
196
228
  .sort((a, b) => a.localeCompare(b, 'en', {sensitivity: 'base'}));
197
- data.engineCount = engineNameSet.size;
229
+ data.engineCount = calledIDs.length;
230
+ data.testedEngineCount = calledIDs.length - preventedIDs.length;
198
231
  data.reporterNames = Array
199
232
  .from(reporterIDSet)
200
233
  .map(id => ruleEngines[id]![0])
@@ -202,7 +235,7 @@ export const getReportData = async (timeStamp: string, jobID: string): Promise<R
202
235
  data.reporterCount = data.reporterNames.length;
203
236
  data.violatorCount = violatorIndexSet.size;
204
237
  // Add the names of any prevented rule engines to the data.
205
- data.preventedEngineNames = Object.keys(report.jobData?.preventions || {})
238
+ data.preventedEngineNames = preventedIDs
206
239
  .map(engineID => ruleEngines[engineID]?.[0] || engineID)
207
240
  .sort((a, b) => a.localeCompare(b, 'en', {sensitivity: 'base'}));
208
241
  data.preventedEngineCount = data.preventedEngineNames.length;
@@ -210,14 +243,7 @@ export const getReportData = async (timeStamp: string, jobID: string): Promise<R
210
243
  return data;
211
244
  }
212
245
  // Returns page data from an available report.
213
- export const getPageData = async (timeStamp: string, jobID: string): Promise<PageData | {error: string}> => {
214
- // Get the report.
215
- const report = await getReport(timeStamp, jobID);
216
- // If this failed:
217
- if (isReportError(report)) {
218
- // Return why.
219
- return report;
220
- }
246
+ export const getPageData = (report: UsableReport): PageData => {
221
247
  const {what: description, url} = report.target;
222
248
  // Get the elapsed time in days since the report was completed, using the
223
249
  // report content rather than the file system birth time.
@@ -230,31 +256,122 @@ export const getPageData = async (timeStamp: string, jobID: string): Promise<Pag
230
256
  };
231
257
  };
232
258
  // Gets HTML strings for page data from a report.
233
- export const getPageDataStrings = async (
234
- timeStamp: string,
235
- jobID: string,
236
- pageData?: PageData | {error: string}
237
- ): Promise<PageDataStrings | {error: string}> => {
238
- // Get the page data if they were not specified.
239
- const data = pageData ?? await getPageData(timeStamp, jobID);
240
- // If the page data are invalid:
241
- if (data.error !== undefined) {
242
- // Return why.
243
- return {
244
- error: data.error
245
- };
246
- }
247
- const {daysAgo, url, description} = data;
248
- // Otherwise, i.e. if they are valid, get a description of the timestamp.
249
- const when = getDateTimeString(timeStamp);
259
+ export const getPageDataStrings = (report: UsableReport): PageDataStrings => {
260
+ const {daysAgo, url, description} = getPageData(report);
261
+ // Get a description of the time stamp at the start of the job name.
262
+ const when = getDateTimeString(report.id.slice(0, 11));
250
263
  // Return the HTML strings.
251
264
  return {
252
265
  description,
253
266
  url,
254
267
  urlLink: `<a href="${url}">${url}</a>`,
255
- testInfo: `Tested ${daysAgo === 1 ? '1 day' : `${daysAgo} days`} ago by job <code>${jobID}</code> on ${when}`
268
+ testInfo: `Tested ${daysAgo === 1 ? '1 day' : `${daysAgo} days`} ago on ${when} by job <code>${report.id}</code>`
256
269
  };
257
270
  };
271
+ // Returns the lines of a list of facts about the page of an available report.
272
+ export const getPageFactsLines = (
273
+ strings: PageDataStrings,
274
+ summary: ResultsSummary,
275
+ margin: string
276
+ ): string[] => [
277
+ `${margin}<ul>`,
278
+ `${margin} <li>URL: ${strings.urlLink}</li>`,
279
+ `${margin} <li>${strings.testInfo}</li>`,
280
+ `${margin} <li>Summary of results:`,
281
+ `${margin} <ul>`,
282
+ `${margin} <li>Rule engines:`,
283
+ `${margin} <ul>`,
284
+ `${margin} <li>Called: ${summary.engineCount}</li>`,
285
+ `${margin} <li>Were able to test: ${summary.testedEngineCount}</li>`,
286
+ `${margin} <li>Reported any rule violations: ${summary.reporterCount}</li>`,
287
+ `${margin} </ul>`,
288
+ `${margin} </li>`,
289
+ `${margin} <li>Violations: ${summary.violationCount}</li>`,
290
+ `${margin} <li>Violators: ${summary.violatorCount}</li>`,
291
+ `${margin} <li>Issues: ${summary.issueCount}</li>`,
292
+ `${margin} </ul>`,
293
+ `${margin} </li>`,
294
+ `${margin}</ul>`
295
+ ];
296
+ // Returns the lines of a list of facts about an issue in an available report.
297
+ export const getIssueFactsLines = (
298
+ report: UsableReport,
299
+ issueID: string,
300
+ margin: string,
301
+ listClass?: string
302
+ ): string[] => {
303
+ // Get the issue classification.
304
+ const {wcag, weight, why} = issueSpecs[issueID]!;
305
+ // Initialize the issue counts.
306
+ const reporters = new Set<string>();
307
+ const violatorIndexes = new Set<string>();
308
+ let violationCount = 0;
309
+ // For each violating standard instance of the issue:
310
+ getTestActInstances(report, {violationsOnly: true, issueID}).forEach(({act, instance}) => {
311
+ // Increment the counts.
312
+ violationCount++;
313
+ reporters.add(act.which!);
314
+ violatorIndexes.add(String(instance.catalogIndex || '0'));
315
+ });
316
+ const reporterString = getCountString(reporters.size, 'rule engine', 'rule engines');
317
+ // Return the lines.
318
+ return [
319
+ `${margin}<ul${listClass ? ` class="${listClass}"` : ''}>`,
320
+ `${margin} <li>Why it matters: ${why}</li>`,
321
+ `${margin} <li>Priority: ${getWeightName(weight)}</li>`,
322
+ `${margin} <li>Related WCAG standard: <a href="${getWCAGLink(wcag)}">${wcag}</a></li>`,
323
+ `${margin} <li>Reported by ${reporterString} (${getEngineNamesString(reporters)})</li>`,
324
+ `${margin} <li>Violations: ${violationCount}</li>`,
325
+ `${margin} <li>Violators: ${violatorIndexes.size}</li>`,
326
+ `${margin}</ul>`
327
+ ];
328
+ };
329
+ // Returns the lines of a list of facts about a violator of an issue in an available report.
330
+ export const getViolatorFactsLines = (
331
+ report: UsableReport,
332
+ issueID: string,
333
+ catalogIndex: string,
334
+ pathID: string | null,
335
+ margin: string,
336
+ listClass?: string
337
+ ): string[] => {
338
+ const {catalog} = report;
339
+ const catalogItem = catalog[catalogIndex];
340
+ const boxID = catalogItem?.boxID;
341
+ const startTag = catalogItem?.startTag;
342
+ const tagName = catalogItem?.tagName;
343
+ const text = catalogItem?.text;
344
+ // Identify the rule engines that reported the violator for the issue.
345
+ const reporters = new Set<string>();
346
+ getTestActInstances(report, {violationsOnly: true, issueID, catalogIndex}).forEach(({act}) => {
347
+ reporters.add(act.which!);
348
+ });
349
+ const lines: string[] = [
350
+ `${margin}<ul${listClass ? ` class="${listClass}"` : ''}>`,
351
+ `${margin} <li>Tag name: <code>${tagName || 'HTML'}</code></li>`
352
+ ];
353
+ // Text is not applicable for container elements.
354
+ if (text && !['HTML', 'BODY', 'HEAD', 'SCRIPT', 'STYLE', 'NOSCRIPT'].includes(tagName as string)) {
355
+ const textString = text.split('\n').join(' … ');
356
+ lines.push(`${margin} <li>Text: <q>${htmlSafe(textString)}</q></li>`);
357
+ }
358
+ else {
359
+ lines.push(`${margin} <li>Text: [not applicable]</li>`);
360
+ }
361
+ lines.push(`${margin} <li>Start tag: <code>${htmlSafe(startTag ?? '') || '[not obtained]'}</code></li>`);
362
+ lines.push(`${margin} <li>XPath: <code>${makeBreakable(pathID || '[not obtained]')}</code></li>`);
363
+ // If the catalog item has a bounding box, add its dimensions.
364
+ if (boxID) {
365
+ const dims = boxID.split(':');
366
+ lines.push(`${margin} <li>Bounding box: x = ${dims[0]}, y = ${dims[1]}, width = ${dims[2]}, height = ${dims[3]}</li>`);
367
+ }
368
+ else {
369
+ lines.push(`${margin} <li>Bounding box: [not obtained]</li>`);
370
+ }
371
+ lines.push(`${margin} <li>Reported by ${getEngineNamesString(reporters)}</li>`);
372
+ lines.push(`${margin}</ul>`);
373
+ return lines;
374
+ };
258
375
  // Gets the descriptions of multi-report pages.
259
376
  export const getMultiReportWhats = async (): Promise<string[]> => {
260
377
  const reportExtracts = await getReportExtracts();