@jrpool/kilotest 58.2.4 → 59.0.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.
Files changed (53) hide show
  1. package/api/listIssues.test.ts +32 -0
  2. package/api/listIssues.ts +38 -8
  3. package/api/listPages.test.ts +92 -0
  4. package/api/listPages.ts +93 -0
  5. package/api/orderNewTest.test.ts +1 -1
  6. package/api/orderNewTest.ts +4 -4
  7. package/api/requestNewTest.ts +6 -6
  8. package/api/requestRetest.ts +2 -2
  9. package/api/routes.test.ts +1 -1
  10. package/api/routes.ts +5 -5
  11. package/api/schemas.ts +24 -7
  12. package/api/util.ts +26 -45
  13. package/apiIntegration.test.ts +5 -5
  14. package/docs/SERVICE.md +16 -5
  15. package/index.html +1 -1
  16. package/index.test.ts +30 -18
  17. package/index.ts +22 -11
  18. package/lhm.plugin.json +1433 -141
  19. package/linkValidation.test.ts +2 -2
  20. package/llms-full.txt +9 -9
  21. package/llms.txt +4 -4
  22. package/mcp.test.ts +8 -8
  23. package/mcp.ts +14 -14
  24. package/openapi.yaml +84 -30
  25. package/package.json +1 -1
  26. package/routing.test.ts +5 -5
  27. package/server.json +1 -1
  28. package/sitemap.xml +1 -1
  29. package/smokeTest.ts +2 -2
  30. package/style.css +8 -8
  31. package/test/fixtures/buildFixtures.ts +1 -1
  32. package/util.test.ts +20 -11
  33. package/util.ts +16 -5
  34. package/web/enqueueForm/index.html +1 -1
  35. package/web/hideReportForm/hideReportForm.test.ts +5 -0
  36. package/web/hideReportForm/index.ts +9 -6
  37. package/web/listIssues/index.html +2 -1
  38. package/web/listIssues/index.ts +45 -3
  39. package/web/listIssues/listIssues.test.ts +76 -0
  40. package/web/{listReports → listPages}/index.html +1 -2
  41. package/web/listPages/index.ts +99 -0
  42. package/web/listPages/listPages.test.ts +196 -0
  43. package/web/metrics/metrics.test.ts +2 -2
  44. package/web/requestRetestForm/index.ts +4 -2
  45. package/web/requestRetestForm/requestRetestForm.test.ts +11 -0
  46. package/web/unhideReportForm/index.ts +9 -6
  47. package/web/unhideReportForm/unhideReportForm.test.ts +8 -5
  48. package/web/util.test.ts +85 -21
  49. package/web/util.ts +27 -31
  50. package/api/listReports.test.ts +0 -109
  51. package/api/listReports.ts +0 -87
  52. package/web/listReports/index.ts +0 -182
  53. package/web/listReports/listReports.test.ts +0 -300
@@ -6,8 +6,9 @@
6
6
  // IMPORTS
7
7
 
8
8
  import {
9
- hiddenReportsPath, isValidAuthCode, populateTemplate, readdirOrCreate, reportsPath
9
+ getReportEndTime, hiddenReportsPath, isValidAuthCode, populateTemplate, readdirOrCreate, reportsPath
10
10
  } from '../../util.ts';
11
+ import {getDateTimeString} from '../util.ts';
11
12
  import fs from 'node:fs/promises';
12
13
  import path from 'node:path';
13
14
 
@@ -64,13 +65,14 @@ export const answer = async (_: any, search: string, method: string) => {
64
65
  reportsData.push({
65
66
  description,
66
67
  timeStamp,
67
- jobID
68
+ jobID,
69
+ reportTime: getReportEndTime(report)
68
70
  });
69
71
  }
70
- // Sort the data by page name and then by time stamp.
72
+ // Sort the data by page name and then by completion time.
71
73
  reportsData.sort((a, b) => {
72
74
  if (a.description === b.description) {
73
- return a.timeStamp.localeCompare(b.timeStamp);
75
+ return a.reportTime.getTime() - b.reportTime.getTime();
74
76
  }
75
77
  return a.description.localeCompare(b.description, 'en', {sensitivity: 'base'});
76
78
  });
@@ -78,8 +80,9 @@ export const answer = async (_: any, search: string, method: string) => {
78
80
  const margin = ' '.repeat(12);
79
81
  // For each report:
80
82
  reportsData.forEach(data => {
81
- const {jobID, timeStamp, description} = data;
82
- const specString = `${description} (job <code>${jobID}</code> at ${timeStamp})`;
83
+ const {jobID, timeStamp, description, reportTime} = data;
84
+ const when = getDateTimeString(reportTime);
85
+ const specString = `${description} (job <code>${timeStamp}-${jobID}</code> completed ${when})`;
83
86
  // Add a line with a radio button to unhide it.
84
87
  lines.push(
85
88
  `${margin}<p><input type="radio" name="report" value="${timeStamp}-${jobID}"> ${specString}</p>`
@@ -100,21 +100,24 @@ test('unhideReportForm returns an error when unhiding a nonexistent report on a
100
100
  assert.ok(result.message.includes('Unhiding report'));
101
101
  });
102
102
 
103
- test('unhideReportForm sorts hidden reports with the same page name by timeStamp', async () => {
103
+ test('unhideReportForm sorts hidden reports with the same page name by completion time', async () => {
104
104
  const {hidden} = getPaths();
105
105
  // Add a second hidden report with the same what as the existing one.
106
106
  const existing = JSON.parse(await fs.readFile(hidden, 'utf8'));
107
- // Create a copy with a different timeStamp but same target.what.
107
+ // Create a copy with a later job time stamp but an earlier completion time.
108
108
  const copy = JSON.parse(JSON.stringify(existing));
109
109
  copy.id = '260101T0010-hid2';
110
+ copy.jobData.endTime = '26-01-01T00:05';
110
111
  const copyPath = path.join(hiddenReportsPath(), '260101T0010-hid2.json');
111
112
  await fs.writeFile(copyPath, JSON.stringify(copy));
112
113
  try {
113
114
  const result: any = await answer(null, '', 'GET');
114
115
  assert.equal(result.status, 'ok');
115
- // Both reports should appear in the form.
116
- assert.ok(result.answerPage.includes('260101T0007-hid'));
117
- assert.ok(result.answerPage.includes('260101T0010-hid2'));
116
+ // Both reports should appear in the form, in order of completion time.
117
+ const page: string = result.answerPage;
118
+ assert.ok(page.includes('(job <code>260101T0007-hid</code> completed 2026-01-01 at 00:10)'));
119
+ assert.ok(page.includes('(job <code>260101T0010-hid2</code> completed 2026-01-01 at 00:05)'));
120
+ assert.ok(page.indexOf('260101T0010-hid2') < page.indexOf('260101T0007-hid'));
118
121
  }
119
122
  finally {
120
123
  await fs.unlink(copyPath).catch(() => {});
package/web/util.test.ts CHANGED
@@ -9,14 +9,15 @@ import {test, before, after} from 'node:test';
9
9
  import assert from 'node:assert/strict';
10
10
  import path from 'node:path';
11
11
  import fs from 'node:fs/promises';
12
+ import {parse} from 'node-html-parser';
12
13
  import {getReport, isReportError, reportsPath} from '../util.ts';
13
14
  import {
14
15
  checkCommentLength,
15
16
  getAgoString,
16
17
  getCountString,
18
+ getDateTimeString,
17
19
  getEngineIDs,
18
20
  getEngineNamesString,
19
- getMultiReportWhats,
20
21
  getPageData,
21
22
  getPageDataStrings,
22
23
  getPageFactsLines,
@@ -114,7 +115,7 @@ test('getWeightName returns the correct name for each weight', () => {
114
115
  });
115
116
 
116
117
  test('makeBreakable inserts wbr before non-initial slashes', () => {
117
- assert.equal(makeBreakable('/api/listReports'), '/api<wbr>/listReports');
118
+ assert.equal(makeBreakable('/api/listPages'), '/api<wbr>/listPages');
118
119
  });
119
120
 
120
121
  // Returns a fixture report, failing the test if it is not usable.
@@ -144,14 +145,25 @@ test('getPageDataStrings returns HTML strings for a valid report', async () => {
144
145
  assert.equal(strings.url, 'https://example.com/mixed');
145
146
  assert.equal(strings.urlLink, '<a href="https://example.com/mixed">https://example.com/mixed</a>');
146
147
  assert.ok(strings.testInfo.includes('by job <code>260101T0000-mix</code>'));
147
- assert.ok(strings.testInfo.includes('2026-01-01 at 00:00'));
148
+ assert.ok(strings.testInfo.includes('2026-01-01 at 00:10'));
148
149
  });
149
150
 
150
- test('getPageDataStrings returns different testInfo for a different timeStamp', async () => {
151
- const strings = getPageDataStrings(await getFixtureReport('260101T0001', 'ct'));
151
+ test('getPageDataStrings dates a report by its completion time, not by its job time stamp', async () => {
152
+ const report = await getFixtureReport('260101T0001', 'ct');
153
+ report.jobData.endTime = '26-03-04T05:06';
154
+ const strings = getPageDataStrings(report);
152
155
  assert.equal(strings.description, 'All CantTell Page');
153
156
  assert.ok(strings.testInfo.includes('by job <code>260101T0001-ct</code>'));
154
- assert.ok(strings.testInfo.includes('2026-01-01 at 00:01'));
157
+ assert.ok(strings.testInfo.includes('2026-03-04 at 05:06'));
158
+ assert.ok(!strings.testInfo.includes('2026-01-01'));
159
+ });
160
+
161
+ test('getDateTimeString describes a date', () => {
162
+ assert.equal(getDateTimeString(new Date('2026-03-04T05:06:07Z')), '2026-03-04 at 05:06');
163
+ });
164
+
165
+ test('getDateTimeString describes an invalid date as unknown', () => {
166
+ assert.equal(getDateTimeString(new Date(NaN)), 'an unknown date at an unknown time');
155
167
  });
156
168
 
157
169
  test('getPageDataStrings derives the strings from the report object', async () => {
@@ -209,6 +221,73 @@ test('getPageFactsLines returns the about-page list lines for a report', async (
209
221
  assert.ok(html.includes('<li>Issues: 2</li>'));
210
222
  });
211
223
 
224
+ test('getPageFactsLines ends with a link to download the full report', async () => {
225
+ const {getReportData} = await import('./util.ts');
226
+ const report = await getFixtureReport('260101T0000', 'mix');
227
+ const lines = getPageFactsLines(getPageDataStrings(report), getReportData(report), '');
228
+ const topItems = parse(lines.join('\n')).querySelectorAll('ul:not(ul ul) > li');
229
+ const lastItem = topItems[topItems.length - 1]!;
230
+ assert.ok(topItems[topItems.length - 2]!.text.trim().startsWith('Summary of results:'));
231
+ assert.equal(lastItem.querySelector('a')?.getAttribute('href'), '/fullReport.json/260101T0000/mix');
232
+ assert.equal(lastItem.text.trim(), 'Download the full technical report (JSON)');
233
+ });
234
+
235
+ // Returns the 3 rule-engine amounts and the 3 summary amounts of the page facts of a report.
236
+ const getResultFacts = async (timeStamp: string, jobID: string) => {
237
+ const {getReportData} = await import('./util.ts');
238
+ const report = await getFixtureReport(timeStamp, jobID);
239
+ const lines = getPageFactsLines(getPageDataStrings(report), getReportData(report), '');
240
+ const items = parse(lines.join('\n')).querySelectorAll('li');
241
+ const itemTexts: string[] = items.map(li => li.text.trim());
242
+ const getAmount = (label: string) => {
243
+ const text = itemTexts.find(t => t.startsWith(`${label}: `));
244
+ assert.ok(text, `${label} item exists`);
245
+ return Number(text.slice(label.length + 2));
246
+ };
247
+ // The summary item is the parent of the rule-engine item and the 3 amount items.
248
+ const summaryItem = items.find(li => li.text.trim().startsWith('Summary of results:'));
249
+ assert.ok(summaryItem, 'Summary of results item exists');
250
+ const summaryList = summaryItem.querySelector('ul')!;
251
+ const amountLabels = summaryList.childNodes
252
+ .filter((node: any) => node.tagName === 'LI')
253
+ .map(li => li.text.trim().split(':')[0]);
254
+ return {
255
+ called: getAmount('Called'),
256
+ tested: getAmount('Were able to test'),
257
+ reporters: getAmount('Reported any rule violations'),
258
+ amountLabels,
259
+ violations: getAmount('Violations'),
260
+ violators: getAmount('Violators'),
261
+ issues: getAmount('Issues')
262
+ };
263
+ };
264
+
265
+ // Expected facts, hand-computed from the fixtures.
266
+ const expectedFacts: [string, string, number, number, number, number, number, number][] = [
267
+ // Time stamp, job ID, engines called, engines able to test, reporting engines, violations, violators, issues.
268
+ ['260101T0000', 'mix', 2, 2, 2, 3, 2, 2],
269
+ ['260202T0000', 'new', 1, 1, 1, 1, 1, 1],
270
+ ['260101T0001', 'ct', 1, 1, 0, 0, 0, 0],
271
+ ['260101T0002', 'no', 1, 1, 1, 1, 1, 1],
272
+ ['260101T0005', 'emp', 1, 1, 0, 0, 0, 0],
273
+ ['260101T0006', 'prv', 2, 1, 1, 1, 1, 1],
274
+ ['260101T0008', 'mul', 2, 2, 2, 4, 3, 1],
275
+ ['260101T0009', 'brd', 3, 3, 1, 4, 4, 1]
276
+ ];
277
+
278
+ for (const [timeStamp, jobID, called, tested, reporters, violations, violators, issues] of expectedFacts) {
279
+ test(`getPageFactsLines gives correct rule-engine and summary amounts for ${timeStamp}-${jobID}`, async () => {
280
+ const facts = await getResultFacts(timeStamp, jobID);
281
+ assert.equal(facts.called, called);
282
+ assert.equal(facts.tested, tested);
283
+ assert.equal(facts.reporters, reporters);
284
+ assert.deepEqual(facts.amountLabels, ['Rule engines', 'Violations', 'Violators', 'Issues']);
285
+ assert.equal(facts.violations, violations);
286
+ assert.equal(facts.violators, violators);
287
+ assert.equal(facts.issues, issues);
288
+ });
289
+ }
290
+
212
291
  test('getIssueFactsLines returns the about-issue list lines for a report', async () => {
213
292
  const {getIssueFactsLines} = await import('./util.ts');
214
293
  const report = await getFixtureReport('260101T0000', 'mix');
@@ -288,21 +367,6 @@ test('getEngineNamesString falls back to the ID for an unknown engine', () => {
288
367
  assert.equal(getEngineNamesString(new Set(['unknownEngine'])), 'unknownEngine');
289
368
  });
290
369
 
291
- test('getMultiReportWhats returns descriptions that have multiple reports', async () => {
292
- const whats = await getMultiReportWhats();
293
- assert.ok(whats.includes('Mixed Outcomes Page'));
294
- });
295
-
296
- test('getMultiReportWhats uses extracts provided by the caller', async () => {
297
- const extract = {timeStamp: 't', jobID: 'j', url: 'https://example.com/', reportTime: ''};
298
- const whats = await getMultiReportWhats([
299
- {...extract, description: 'B'},
300
- {...extract, description: 'A'},
301
- {...extract, description: 'B'}
302
- ]);
303
- assert.deepEqual(whats, ['B']);
304
- });
305
-
306
370
  // Returns a minimal report for testing rule-engine identification.
307
371
  const makeEngineReport = (whiches: string[], preventions: Record<string, string>) => ({
308
372
  acts: whiches.map(which => ({type: 'test', which})),
package/web/util.ts CHANGED
@@ -12,8 +12,7 @@ import {
12
12
  alphaCompare,
13
13
  getAgoDays,
14
14
  getDateString,
15
- getDateTime,
16
- getReportExtracts,
15
+ getReportEndTime,
17
16
  getTestActInstances,
18
17
  getTestActs,
19
18
  htmlSafe,
@@ -21,7 +20,7 @@ import {
21
20
  } from '../util.ts';
22
21
  import {issues as issueSpecs} from 'testaro-issues';
23
22
  import type {Catalog} from 'testaro';
24
- import type {ReportExtract, UsableReport} from '../util.ts';
23
+ import type {UsableReport} from '../util.ts';
25
24
  /* c8 ignore stop */
26
25
  import wcagMap from '../wcagMap.json' with {type: 'json'};
27
26
 
@@ -40,6 +39,7 @@ export type PageDataStrings = {
40
39
  url: string;
41
40
  urlLink: string;
42
41
  testInfo: string;
42
+ fullReportLink: string;
43
43
  error?: never;
44
44
  };
45
45
  // Counts summarizing the results of an available report.
@@ -56,8 +56,6 @@ export type ReportData = {
56
56
  description: string;
57
57
  url: string;
58
58
  jobName: unknown;
59
- creationDate: Date | null;
60
- daysAgo: number | null;
61
59
  issueCount: number;
62
60
  engineNames: string[];
63
61
  engineCount: number;
@@ -95,9 +93,9 @@ export const checkCommentLength = (
95
93
  const fragmentEncode = (string: string) => {
96
94
  return encodeURIComponent(string).replace(/-/g, '%2D');
97
95
  };
98
- // Returns a string describing the time in days since a time stamp.
99
- export const getAgoString = (timeStamp: string): string => {
100
- const agoDays = getAgoDays(timeStamp);
96
+ // Returns a string describing the time in days since a time stamp or date.
97
+ export const getAgoString = (timeArg: string | Date): string => {
98
+ const agoDays = getAgoDays(timeArg);
101
99
  if (agoDays === null) {
102
100
  return 'an unknown number of days';
103
101
  }
@@ -111,8 +109,20 @@ const getTimeString = (timeStamp: string) => {
111
109
  // Return the time string if valid, or null if not.
112
110
  return (!isNaN(Date.parse(`2000-01-01T${timeString}Z`))) ? timeString : null;
113
111
  };
114
- // Returns a date-and-time string.
115
- export const getDateTimeString = (timeStamp: string): string => {
112
+ // Returns a date-and-time string from a time stamp or date.
113
+ export const getDateTimeString = (timeArg: string | Date): string => {
114
+ let timeStamp: string;
115
+ // If the argument is a date:
116
+ if (timeArg instanceof Date) {
117
+ // Convert it to a time stamp, or to an invalid one if the date is invalid.
118
+ timeStamp = isNaN(timeArg.getTime())
119
+ ? ''
120
+ : timeArg.toISOString().slice(2, 16).replace(/[-:]/g, '');
121
+ }
122
+ // Otherwise, i.e. if it is a time stamp:
123
+ else {
124
+ timeStamp = timeArg;
125
+ }
116
126
  const dateString = getDateString(timeStamp) || 'an unknown date';
117
127
  const timeString = getTimeString(timeStamp) || 'an unknown time';
118
128
  const dateTimeString = `${dateString} at ${timeString}`;
@@ -178,15 +188,11 @@ export const getEngineIDs = (report: any) => {
178
188
  };
179
189
  // Returns basics about an available report.
180
190
  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
191
  // Initialize the data.
184
192
  const data = {
185
193
  description: report.target.what,
186
194
  url: report.target.url,
187
195
  jobName: report.id,
188
- creationDate: getDateTime(timeStamp),
189
- daysAgo: getAgoDays(timeStamp),
190
196
  issueCount: 0,
191
197
  engineNames: [] as string[],
192
198
  engineCount: 0,
@@ -245,9 +251,8 @@ export const getReportData = (report: UsableReport): ReportData => {
245
251
  // Returns page data from an available report.
246
252
  export const getPageData = (report: UsableReport): PageData => {
247
253
  const {what: description, url} = report.target;
248
- // Get the elapsed time in days since the report was completed, using the
249
- // report content rather than the file system birth time.
250
- const daysAgo = getAgoDays(new Date(`20${report.jobData.endTime}Z`));
254
+ // Get the elapsed time in days since the report was completed.
255
+ const daysAgo = getAgoDays(getReportEndTime(report));
251
256
  // Return the data.
252
257
  return {
253
258
  description,
@@ -258,14 +263,15 @@ export const getPageData = (report: UsableReport): PageData => {
258
263
  // Gets HTML strings for page data from a report.
259
264
  export const getPageDataStrings = (report: UsableReport): PageDataStrings => {
260
265
  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));
266
+ // Get a description of the completion time of the report.
267
+ const when = getDateTimeString(getReportEndTime(report));
263
268
  // Return the HTML strings.
264
269
  return {
265
270
  description,
266
271
  url,
267
272
  urlLink: `<a href="${url}">${url}</a>`,
268
- testInfo: `Tested ${daysAgo === 1 ? '1 day' : `${daysAgo} days`} ago on ${when} by job <code>${report.id}</code>`
273
+ testInfo: `Tested ${daysAgo === 1 ? '1 day' : `${daysAgo} days`} ago on ${when} by job <code>${report.id}</code>`,
274
+ fullReportLink: `<a href="/fullReport.json/${report.id.replace('-', '/')}">Download the full technical report</a> (JSON)`
269
275
  };
270
276
  };
271
277
  // Returns the lines of a list of facts about the page of an available report.
@@ -291,6 +297,7 @@ export const getPageFactsLines = (
291
297
  `${margin} <li>Issues: ${summary.issueCount}</li>`,
292
298
  `${margin} </ul>`,
293
299
  `${margin} </li>`,
300
+ `${margin} <li>${strings.fullReportLink}</li>`,
294
301
  `${margin}</ul>`
295
302
  ];
296
303
  // Returns the lines of a list of facts about an issue in an available report.
@@ -372,14 +379,3 @@ export const getViolatorFactsLines = (
372
379
  lines.push(`${margin}</ul>`);
373
380
  return lines;
374
381
  };
375
- // Gets the descriptions of multi-report pages, from extracts already obtained, if provided.
376
- export const getMultiReportWhats = async (
377
- reportExtracts?: ReportExtract[]
378
- ): Promise<string[]> => {
379
- reportExtracts ??= await getReportExtracts();
380
- const sortedDescriptions = reportExtracts.map(extract => extract.description).sort();
381
- const multiReportDescriptions = sortedDescriptions.filter(
382
- (description, index) => description !== sortedDescriptions[index - 1] && description === sortedDescriptions[index + 1]
383
- );
384
- return multiReportDescriptions;
385
- };
@@ -1,109 +0,0 @@
1
- /*
2
- listReports.test.ts
3
- Tests for api/listReports.ts using the fixture corpus.
4
- */
5
-
6
- // IMPORTS
7
-
8
- import {test, before, after} from 'node:test';
9
- import assert from 'node:assert/strict';
10
- import {response} from './listReports.ts';
11
-
12
- // CONSTANTS
13
-
14
- // Fixture report identifiers in the reports/ directory (excluding the hidden one).
15
- const fixtureIds = [
16
- ['260101T0000', 'mix'],
17
- ['260101T0001', 'ct'],
18
- ['260101T0002', 'no'],
19
- ['260101T0005', 'emp'],
20
- ['260101T0006', 'prv'],
21
- ['260101T0008', 'mul'],
22
- ['260101T0009', 'brd'],
23
- ['260202T0000', 'new']
24
- ];
25
-
26
- // SETUP AND TEARDOWN
27
-
28
- const savedDBDir = process.env.DB_DIR;
29
-
30
- before(async () => {
31
- process.env.DB_DIR = (await import('../test/dbFixture.ts')).fixtureDBDir;
32
- });
33
-
34
- after(() => {
35
- if (savedDBDir !== undefined) {
36
- process.env.DB_DIR = savedDBDir;
37
- }
38
- else {
39
- delete process.env.DB_DIR;
40
- }
41
- });
42
-
43
- // TESTS
44
-
45
- test('listReports returns basics about all 8 non-hidden reports', async () => {
46
- const body = await response();
47
- const reportsBasics = body['response content']['basics about all available reports'];
48
- assert.equal(reportsBasics.length, 8);
49
- const ids = reportsBasics.map((b: any) => b.identifier).sort();
50
- const expectedIds = fixtureIds.map(([ts, jid]) => `${ts}-${jid}`).sort();
51
- assert.deepEqual(ids, expectedIds);
52
- });
53
-
54
- test('listReports does not include the hidden report', async () => {
55
- const body = await response();
56
- const reportsBasics = body['response content']['basics about all available reports'];
57
- const ids = reportsBasics.map((b: any) => b.identifier);
58
- assert.ok(!ids.includes('260101T0007-hid'), 'hidden report must not appear in listReports');
59
- });
60
-
61
- test('listReports sorts reports by page description and then by completion time', async () => {
62
- const body = await response();
63
- const reportsBasics = body['response content']['basics about all available reports'];
64
- const descriptions = reportsBasics.map((b: any) => b['tested web page'].description);
65
- // All CantTell Page, Empty Results Page, Mixed Outcomes Page, Mixed Outcomes Page, No Outcomes Page, Prevented Page.
66
- assert.deepEqual(
67
- descriptions,
68
- [
69
- 'All CantTell Page',
70
- 'Branch Coverage Page',
71
- 'Empty Results Page',
72
- 'Mixed Outcomes Page',
73
- 'Mixed Outcomes Page',
74
- 'Multi Violator Page',
75
- 'No Outcomes Page',
76
- 'Prevented Page'
77
- ]
78
- );
79
- // The two Mixed Outcomes Page reports should be sorted by completion time (older first).
80
- const mixedReports = reportsBasics.filter((b: any) => b['tested web page'].description === 'Mixed Outcomes Page');
81
- assert.equal(mixedReports[0]!.identifier, '260101T0000-mix');
82
- assert.equal(mixedReports[1]!.identifier, '260202T0000-new');
83
- });
84
-
85
- test('listReports marks the superseded report correctly', async () => {
86
- const body = await response();
87
- const reportsBasics = body['response content']['basics about all available reports'];
88
- const mix = reportsBasics.find((b: any) => b.identifier === '260101T0000-mix');
89
- const newer = reportsBasics.find((b: any) => b.identifier === '260202T0000-new');
90
- assert.ok(mix);
91
- assert.ok(newer);
92
- assert.equal(mix['whether a later report about the same page exists'], true);
93
- assert.equal(newer['whether a later report about the same page exists'], false);
94
- });
95
-
96
- test('listReports includes request-test instructions', async () => {
97
- const body = await response();
98
- const content = body['response content'];
99
- assert.ok(content['how to request that a page with no report be tested']);
100
- assert.ok(content['how a web user can request that the page be tested']);
101
- assert.equal(content['how to request that a page with no report be tested'].method, 'POST');
102
- });
103
-
104
- test('listReports includes tool name and metadata', async () => {
105
- const body = await response();
106
- assert.equal(body['tool name'], 'listReports');
107
- assert.ok(body['response metadata'].identifier);
108
- assert.ok(body['response metadata']['date and time']);
109
- });
@@ -1,87 +0,0 @@
1
- /*
2
- listReports.ts
3
- Returns basics about all the available reports.
4
- */
5
-
6
- // IMPORTS
7
-
8
- import {z} from 'zod';
9
- import {getReportBasics, getResponseMetadata, getThisHost, getToolsFacts} from './util.ts';
10
- import {getReportExtracts} from '../util.ts';
11
- import {listReportsResponseSchema} from './schemas.ts';
12
-
13
- // TYPES
14
-
15
- // The response content defined by the response schema.
16
- type ResponseContent = z.infer<typeof listReportsResponseSchema>['response content'];
17
-
18
- // FUNCTIONS
19
-
20
- // Returns the response body.
21
- export const response = async () => {
22
- const thisHost = getThisHost();
23
- // Initialize an array of basics about the reports.
24
- const reportsBasics: ResponseContent['basics about all available reports'] = [];
25
- // Get extracts of all available reports.
26
- const reportExtracts = await getReportExtracts();
27
- // For each report:
28
- for (const extract of reportExtracts) {
29
- const {jobID, timeStamp} = extract;
30
- // Get the basics about it.
31
- const reportBasics = await getReportBasics(timeStamp, jobID, extract);
32
- // Add the basics, with instructions for getting details, to the array.
33
- reportsBasics.push({
34
- ...reportBasics,
35
- 'how to get details about the report': {
36
- method: 'GET',
37
- URL: `${thisHost}/api/listIssues/${timeStamp}/${jobID}`
38
- },
39
- 'web users can get details about the report at': `${thisHost}/listIssues.html/${timeStamp}/${jobID}`
40
- });
41
- }
42
- // Sort the array by page description and secondarily by completion recency.
43
- reportsBasics.sort((a, b) => {
44
- if (a['tested web page'].description !== b['tested web page'].description) {
45
- return a['tested web page']
46
- .description
47
- .localeCompare(b['tested web page'].description, 'en', {sensitivity: 'base'});
48
- }
49
- return a['completion date and time'].localeCompare(b['completion date and time']);
50
- });
51
- // Create the response content.
52
- const responseContent: ResponseContent = {
53
- 'basics about all available reports': reportsBasics,
54
- 'how to request that a page with no report be tested': {
55
- method: 'POST',
56
- URL: `${thisHost}/api/requestNewTest`,
57
- 'request body': {
58
- description: '10- to 100-character description of the page conforming to the naming convention used in this list of reports',
59
- URL: '12- to 300-character URL of the page, including the https:// scheme and any query',
60
- reason: '20- to 100-character reason why the page should be tested'
61
- },
62
- 'how to check whether the request has been fulfilled': 'use this listReports tool to determine whether a report about the page has become available (typical wait time: 1 hour to 1 day)'
63
- },
64
- 'how a web user can request that the page be tested': {
65
- URL: `${thisHost}/requestNewTestForm.html`
66
- }
67
- };
68
- // Create a response body.
69
- const body = {
70
- 'tool collection': getToolsFacts(),
71
- 'tool name': 'listReports',
72
- 'this request': {
73
- description: 'Provide basics about all available reports.',
74
- method: 'GET',
75
- URL: `${thisHost}/api/listReports`,
76
- 'closest ancestor request': null
77
- },
78
- 'URLs of similar requests for web users': {
79
- 'this request': `${thisHost}/listReports.html`,
80
- 'closest ancestor request': null
81
- },
82
- 'response metadata': getResponseMetadata(),
83
- 'response content': responseContent
84
- };
85
- // Return it.
86
- return body;
87
- };