@jrpool/kilotest 57.3.0 → 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,6 +61,7 @@ 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;
57
67
  violationCount: number;
@@ -140,16 +150,37 @@ export const getPathID = (catalog: Catalog, catalogIndex: string, pathID?: strin
140
150
  }
141
151
  return pathID ?? '/html';
142
152
  };
143
- // Returns basics about an available report.
144
- export const getReportData = async (timeStamp: string, jobID: string): Promise<ReportData | {error: string}> => {
145
- // Get the report.
146
- const report = await getReport(timeStamp, jobID);
147
- // If this failed:
148
- if (isReportError(report)) {
149
- // Return why.
150
- 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
+ }
151
176
  }
152
- // 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.
153
184
  const data = {
154
185
  description: report.target.what,
155
186
  url: report.target.url,
@@ -159,6 +190,7 @@ export const getReportData = async (timeStamp: string, jobID: string): Promise<R
159
190
  issueCount: 0,
160
191
  engineNames: [] as string[],
161
192
  engineCount: 0,
193
+ testedEngineCount: 0,
162
194
  reporterNames: [] as string[],
163
195
  reporterCount: 0,
164
196
  violationCount: 0,
@@ -167,14 +199,9 @@ export const getReportData = async (timeStamp: string, jobID: string): Promise<R
167
199
  preventedEngineCount: 0
168
200
  };
169
201
  const issueIDSet = new Set<string>();
170
- const engineNameSet = new Set<string>();
171
202
  const reporterIDSet = new Set<string>();
172
203
  const violatorIndexSet = new Set<string>();
173
- // For each test act of the report:
174
- getTestActs(report).forEach(act => {
175
- // Ensure that the rule engine is in the temporary data.
176
- engineNameSet.add(ruleEngines[act.which!]![0]);
177
- });
204
+ const {calledIDs, preventedIDs} = getEngineIDs(report);
178
205
  // For each violating standard instance of each test act:
179
206
  getTestActInstances(report, {violationsOnly: true}).forEach(({act, instance}) => {
180
207
  const {catalogIndex, issueID} = instance;
@@ -196,9 +223,11 @@ export const getReportData = async (timeStamp: string, jobID: string): Promise<R
196
223
  // Populate the data with the act data.
197
224
  data.issueCount = issueIDSet.size;
198
225
  data.engineNames = Array
199
- .from(engineNameSet)
226
+ .from(calledIDs)
227
+ .map(id => ruleEngines[id]?.[0] || id)
200
228
  .sort((a, b) => a.localeCompare(b, 'en', {sensitivity: 'base'}));
201
- data.engineCount = engineNameSet.size;
229
+ data.engineCount = calledIDs.length;
230
+ data.testedEngineCount = calledIDs.length - preventedIDs.length;
202
231
  data.reporterNames = Array
203
232
  .from(reporterIDSet)
204
233
  .map(id => ruleEngines[id]![0])
@@ -206,7 +235,7 @@ export const getReportData = async (timeStamp: string, jobID: string): Promise<R
206
235
  data.reporterCount = data.reporterNames.length;
207
236
  data.violatorCount = violatorIndexSet.size;
208
237
  // Add the names of any prevented rule engines to the data.
209
- data.preventedEngineNames = Object.keys(report.jobData?.preventions || {})
238
+ data.preventedEngineNames = preventedIDs
210
239
  .map(engineID => ruleEngines[engineID]?.[0] || engineID)
211
240
  .sort((a, b) => a.localeCompare(b, 'en', {sensitivity: 'base'}));
212
241
  data.preventedEngineCount = data.preventedEngineNames.length;
@@ -214,14 +243,7 @@ export const getReportData = async (timeStamp: string, jobID: string): Promise<R
214
243
  return data;
215
244
  }
216
245
  // Returns page data from an available report.
217
- export const getPageData = async (timeStamp: string, jobID: string): Promise<PageData | {error: string}> => {
218
- // Get the report.
219
- const report = await getReport(timeStamp, jobID);
220
- // If this failed:
221
- if (isReportError(report)) {
222
- // Return why.
223
- return report;
224
- }
246
+ export const getPageData = (report: UsableReport): PageData => {
225
247
  const {what: description, url} = report.target;
226
248
  // Get the elapsed time in days since the report was completed, using the
227
249
  // report content rather than the file system birth time.
@@ -234,31 +256,122 @@ export const getPageData = async (timeStamp: string, jobID: string): Promise<Pag
234
256
  };
235
257
  };
236
258
  // Gets HTML strings for page data from a report.
237
- export const getPageDataStrings = async (
238
- timeStamp: string,
239
- jobID: string,
240
- pageData?: PageData | {error: string}
241
- ): Promise<PageDataStrings | {error: string}> => {
242
- // Get the page data if they were not specified.
243
- const data = pageData ?? await getPageData(timeStamp, jobID);
244
- // If the page data are invalid:
245
- if (data.error !== undefined) {
246
- // Return why.
247
- return {
248
- error: data.error
249
- };
250
- }
251
- const {daysAgo, url, description} = data;
252
- // Otherwise, i.e. if they are valid, get a description of the timestamp.
253
- 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));
254
263
  // Return the HTML strings.
255
264
  return {
256
265
  description,
257
266
  url,
258
267
  urlLink: `<a href="${url}">${url}</a>`,
259
- 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>`
260
269
  };
261
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
+ };
262
375
  // Gets the descriptions of multi-report pages.
263
376
  export const getMultiReportWhats = async (): Promise<string[]> => {
264
377
  const reportExtracts = await getReportExtracts();