@jrpool/kilotest 46.1.2 → 47.0.1

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 (147) hide show
  1. package/.claude/settings.local.json +13 -1
  2. package/.devin/config.local.json +5 -1
  3. package/.github/workflows/ci.yml +0 -3
  4. package/.github/workflows/periodic-smoke-tests.yml +30 -0
  5. package/.github/workflows/publish-mcp.yml +4 -4
  6. package/{alerts.test.js → alerts.test.ts} +17 -29
  7. package/{alerts.js → alerts.ts} +10 -13
  8. package/api/{getReport.test.js → getReport.test.ts} +9 -10
  9. package/api/{getReport.js → getReport.ts} +23 -24
  10. package/api/{listDiagnoses.test.js → listDiagnoses.test.ts} +26 -27
  11. package/api/{listDiagnoses.js → listDiagnoses.ts} +46 -70
  12. package/api/{listIssues.test.js → listIssues.test.ts} +21 -14
  13. package/api/{listIssues.js → listIssues.ts} +75 -74
  14. package/api/{listReports.test.js → listReports.test.ts} +17 -16
  15. package/api/listReports.ts +87 -0
  16. package/api/{listViolators.test.js → listViolators.test.ts} +19 -20
  17. package/api/{listViolators.js → listViolators.ts} +45 -70
  18. package/api/requestFeature.test.ts +54 -0
  19. package/api/{requestFeature.js → requestFeature.ts} +18 -15
  20. package/api/{requestRetest.test.js → requestRetest.test.ts} +33 -28
  21. package/api/{requestRetest.js → requestRetest.ts} +32 -25
  22. package/api/{requestTest.test.js → requestTest.test.ts} +35 -30
  23. package/api/{requestTest.js → requestTest.ts} +35 -28
  24. package/api/{routes.test.js → routes.test.ts} +5 -5
  25. package/api/{routes.js → routes.ts} +6 -6
  26. package/api/{schemas.js → schemas.ts} +69 -50
  27. package/api/{util.test.js → util.test.ts} +31 -11
  28. package/api/{util.js → util.ts} +61 -36
  29. package/api/version.ts +8 -0
  30. package/{apiIntegration.test.js → apiIntegration.test.ts} +12 -13
  31. package/balances.ts +87 -0
  32. package/{checkinfo.js → checkinfo.ts} +24 -20
  33. package/docs/AI-TOOLS.md +4 -4
  34. package/docs/BACKLOG.md +356 -0
  35. package/docs/SERVICE.md +144 -17
  36. package/docs/decisions/002-coverage-tool.md +40 -0
  37. package/docs/decisions/003-adr-threshold.md +34 -0
  38. package/docs/decisions/004-docs-directory.md +23 -0
  39. package/docs/decisions/005-observability.md +28 -0
  40. package/docs/decisions/006-git-workflow.md +28 -0
  41. package/docs/decisions/007-internal-testing.md +34 -0
  42. package/docs/decisions/008-dependencies.md +28 -0
  43. package/docs/decisions/009-qai-integration.md +40 -0
  44. package/docs/decisions/QAI-HISTORICAL-CONTEXT.md +42 -0
  45. package/eslint.config.mjs +27 -0
  46. package/index.html +3 -3
  47. package/{index.test.js → index.test.ts} +380 -143
  48. package/{index.js → index.ts} +382 -283
  49. package/{indexBalances.test.js → indexBalances.test.ts} +21 -17
  50. package/{indexServe.test.js → indexServe.test.ts} +9 -9
  51. package/linkValidation.test.ts +190 -0
  52. package/{mcp.test.js → mcp.test.ts} +53 -26
  53. package/{mcp.js → mcp.ts} +29 -20
  54. package/memory/MEMORY.md +7 -0
  55. package/memory/user_maintainer_context.md +24 -0
  56. package/openapi.yaml +115 -68
  57. package/package.json +35 -15
  58. package/pm2.config.cjs +29 -0
  59. package/{routes.test.js → routes.test.ts} +8 -7
  60. package/{routing.test.js → routing.test.ts} +11 -12
  61. package/scripts/{generate-openapi.js → generate-openapi.ts} +13 -13
  62. package/serve.ts +15 -0
  63. package/smokeTest.ts +234 -0
  64. package/test/dbFixture.ts +21 -0
  65. package/test/fixtures/{buildFixtures.js → buildFixtures.ts} +14 -13
  66. package/test/{setup.cjs → setup.ts} +2 -2
  67. package/tsconfig.json +16 -0
  68. package/{util.test.js → util.test.ts} +350 -257
  69. package/util.ts +921 -0
  70. package/web/ai0BalanceForm/{ai0BalanceForm.test.js → ai0BalanceForm.test.ts} +15 -15
  71. package/web/ai0BalanceForm/{index.js → index.ts} +10 -18
  72. package/web/enqueue/index.ts +56 -0
  73. package/web/enqueueForm/index.html +9 -9
  74. package/web/enqueueForm/index.ts +43 -0
  75. package/web/expungeReportsForm/{expungeReportsForm.test.js → expungeReportsForm.test.ts} +18 -20
  76. package/web/expungeReportsForm/index.ts +18 -0
  77. package/web/hideReportForm/{hideReportForm.test.js → hideReportForm.test.ts} +14 -14
  78. package/web/hideReportForm/{index.js → index.ts} +17 -21
  79. package/web/listDiagnoses/{index.js → index.ts} +48 -58
  80. package/web/listDiagnoses/{listDiagnoses.test.js → listDiagnoses.test.ts} +39 -34
  81. package/web/listIssues/{index.js → index.ts} +67 -92
  82. package/web/listIssues/{listIssues.test.js → listIssues.test.ts} +45 -51
  83. package/web/listReports/index.html +3 -3
  84. package/web/listReports/{index.js → index.ts} +48 -53
  85. package/web/listReports/{listReports.test.js → listReports.test.ts} +41 -43
  86. package/web/listRules/{index.js → index.ts} +18 -23
  87. package/web/listRules/{listRules.test.js → listRules.test.ts} +26 -19
  88. package/web/listTopIssues/{index.js → index.ts} +36 -47
  89. package/web/listTopIssues/{listTopIssues.test.js → listTopIssues.test.ts} +26 -17
  90. package/web/listViolators/{index.js → index.ts} +51 -71
  91. package/web/listViolators/{listViolators.test.js → listViolators.test.ts} +43 -36
  92. package/web/manage/index.ts +22 -0
  93. package/web/pruneReportsForm/index.ts +18 -0
  94. package/web/pruneReportsForm/{pruneReportsForm.test.js → pruneReportsForm.test.ts} +11 -11
  95. package/web/reannotate/index.ts +70 -0
  96. package/web/reannotate/{reannotate.test.js → reannotate.test.ts} +19 -22
  97. package/web/reannotateForm/{index.js → index.ts} +41 -50
  98. package/web/reannotateForm/{reannotateForm.test.js → reannotateForm.test.ts} +13 -16
  99. package/web/renewWCAG/{index.js → index.ts} +9 -9
  100. package/web/renewWCAG/{renewWCAG.test.js → renewWCAG.test.ts} +17 -20
  101. package/web/renewWCAGForm/index.ts +22 -0
  102. package/web/{expungeReportsForm/index.js → reportDeletion.ts} +56 -38
  103. package/web/requestRetest/index.html +7 -7
  104. package/web/requestRetest/index.ts +31 -0
  105. package/web/requestRetest/requestRetest.test.ts +116 -0
  106. package/web/requestRetestForm/{index.js → index.ts} +10 -16
  107. package/web/requestRetestForm/{requestRetestForm.test.js → requestRetestForm.test.ts} +11 -12
  108. package/web/requestTest/index.html +7 -7
  109. package/web/requestTest/{index.js → index.ts} +5 -5
  110. package/web/requestTest/{requestTest.test.js → requestTest.test.ts} +21 -21
  111. package/web/requestTestForm/index.html +4 -4
  112. package/web/requestTestForm/index.ts +21 -0
  113. package/web/requestTestForm/{requestTestForm.test.js → requestTestForm.test.ts} +9 -9
  114. package/web/rewindReportsForm/index.ts +18 -0
  115. package/web/rewindReportsForm/{rewindReportsForm.test.js → rewindReportsForm.test.ts} +11 -11
  116. package/web/tutorialAI/index.html +184 -0
  117. package/web/tutorialAI/index.ts +80 -0
  118. package/web/tutorialAI/tutorialAI.test.ts +192 -0
  119. package/web/{tutorial → tutorialWeb}/index.html +9 -8
  120. package/web/tutorialWeb/index.ts +78 -0
  121. package/web/tutorialWeb/tutorialWeb.test.ts +195 -0
  122. package/web/unhideReportForm/{index.js → index.ts} +17 -21
  123. package/web/unhideReportForm/{unhideReportForm.test.js → unhideReportForm.test.ts} +16 -16
  124. package/aceconfig.js +0 -28
  125. package/api/listReports.js +0 -89
  126. package/api/requestFeature.test.js +0 -58
  127. package/api/version.js +0 -8
  128. package/indexFallback.test.js +0 -33
  129. package/indexWorkers.test.js +0 -79
  130. package/jsconfig.json +0 -8
  131. package/pm2.config.js +0 -18
  132. package/smokeTest.js +0 -128
  133. package/util.js +0 -744
  134. package/web/enqueue/index.js +0 -67
  135. package/web/enqueueForm/index.js +0 -49
  136. package/web/manage/index.js +0 -22
  137. package/web/pruneReportsForm/index.js +0 -123
  138. package/web/reannotate/index.js +0 -58
  139. package/web/renewWCAGForm/index.js +0 -22
  140. package/web/requestRetest/index.js +0 -31
  141. package/web/requestRetest/requestRetest.test.js +0 -119
  142. package/web/requestTestForm/index.js +0 -21
  143. package/web/rewindReportsForm/index.js +0 -121
  144. package/web/tutorial/index.js +0 -63
  145. package/web/tutorial/tutorial.test.js +0 -97
  146. /package/test/fixtures/db/jobs/{recs.json → testRequests.json} +0 -0
  147. /package/web/{tutorial → tutorialWeb}/images/newsletter-form.png +0 -0
@@ -1,77 +1,67 @@
1
1
  /*
2
- listDiagnoses.js
2
+ listDiagnoses.ts
3
3
  Returns details about one violator of one issue in one report and facts about all its diagnoses.
4
4
  */
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {
8
+ import {z} from 'zod';
9
+ import {
9
10
  getIssueSpec,
10
11
  getReportBasics,
11
12
  getResponseMetadata,
12
13
  getToolsFacts,
13
- thisHost
14
- } = require('./util');
15
- const {getReport} = require('../util');
14
+ getThisHost
15
+ } from './util.ts';
16
+ import {getReport, getTestActInstances, isReportError} from '../util.ts';
17
+ import {listDiagnosesResponseSchema} from './schemas.ts';
18
+
19
+ // TYPES
20
+
21
+ // The response content defined by the response schema.
22
+ type ResponseContent = z.infer<typeof listDiagnosesResponseSchema>['response content'];
23
+ // The array variant of the union allowed for the diagnoses property.
24
+ type Diagnoses = Extract<ResponseContent['diagnoses of how the element exhibited the issue'], unknown[]>;
16
25
 
17
26
  // FUNCTIONS
18
27
 
19
28
  // Returns the response body.
20
- exports.response = async args => {
29
+ export const response = async (args: string[]) => {
21
30
  const [catalogIndex = '', issueID = '', timeStamp = '', jobID = ''] = args;
22
- // Initialize the response content.
23
- const responseContent = {
24
- 'basics about the report': null,
25
- 'basics about the issue': null,
26
- 'basics about the element': null,
27
- 'details about the element': null,
28
- 'diagnoses of how the element exhibited the issue': null
29
- };
31
+ const thisHost = getThisHost();
30
32
  // Get the report.
31
33
  const report = await getReport(timeStamp, jobID);
32
- // If this failed:
33
- if (report.error) {
34
- // Add this to the response content.
35
- responseContent['basics about the report'] = report;
36
- }
37
- // Otherwise, i.e. if it succeeded:
38
- else {
39
- // Get the basics about the report (which may be only an error message).
40
- const reportBasics = await getReportBasics(timeStamp, jobID);
41
- // Add them to the response content.
42
- responseContent['basics about the report'] = reportBasics;
43
- }
44
34
  // Get the specification of the issue.
45
35
  const issueSpec = issueID ? getIssueSpec(issueID) : null;
46
- // If the issue is ignorable or not fully classified:
47
- if (!issueSpec) {
48
- // Add this to the response content.
49
- responseContent['basics about the issue'] = {
36
+ // Initialize the response content.
37
+ const responseContent: ResponseContent = {
38
+ 'basics about the report': isReportError(report) ? report : await getReportBasics(timeStamp, jobID),
39
+ 'basics about the issue': {
50
40
  'error': 'No information about the specified issue is available'
51
- };
52
- responseContent['diagnoses of how the element exhibited the issue'] = {
41
+ },
42
+ 'basics about the element': null,
43
+ 'details about the element': null,
44
+ 'diagnoses of how the element exhibited the issue': issueSpec ? null : {
53
45
  'error': 'No diagnoses of how the element exhibited the issue is available'
54
- };
55
- }
56
- // Otherwise, i.e. if it is non-ignorable and fully classified:
57
- else {
46
+ }
47
+ };
48
+ // If the issue is non-ignorable and fully classified:
49
+ if (issueSpec) {
58
50
  const {summary, wcag, weight, why} = issueSpec;
59
- // Initialize the basics about it.
60
- const issueBasics = {
51
+ // Add the basics about it to the response content.
52
+ responseContent['basics about the issue'] = {
61
53
  'identifier': issueID,
62
54
  summary,
63
55
  'impact on a user': why,
64
56
  'related WCAG standard': {
65
- 'layer': (wcag.length > 4 ? 'success criterion' : 'guideline'),
57
+ 'layer': (wcag.length > 4 ? 'success criterion' : 'guideline') as 'success criterion' | 'guideline',
66
58
  'identifier': wcag
67
59
  },
68
- priority: ['lowest', 'low', 'high', 'highest'][weight - 1]
60
+ priority: ['lowest', 'low', 'high', 'highest'][weight - 1] as 'lowest' | 'low' | 'high' | 'highest'
69
61
  };
70
- // Add them to the response content.
71
- responseContent['basics about the issue'] = issueBasics;
72
62
  }
73
- // If the report and its catalog exist:
74
- if (report.catalog) {
63
+ // If the report was retrieved successfully:
64
+ if (!isReportError(report)) {
75
65
  const catalogItem = report.catalog[catalogIndex];
76
66
  // If the violator is not in it:
77
67
  if (!catalogItem) {
@@ -101,33 +91,19 @@ exports.response = async args => {
101
91
  // If the issue is non-ignorable and fully classified:
102
92
  if (issueSpec) {
103
93
  // Initialize data about the diagnoses of the violation of the issue.
104
- const diagnoses = [];
105
- // For each act in the report:
106
- report.acts.forEach(act => {
107
- const {result, type, which} = act;
108
- const instances = result?.standardResult?.instances || [];
109
- // If the act is a test act with a specified rule engine:
110
- if (type === 'test' && which) {
111
- // For each standard instance of the act:
112
- instances.forEach(instance => {
113
- const {count, ordinalSeverity, outcome, ruleID, what} = instance;
114
- // If the instance reports a violation of the issue by the violator:
115
- if (
116
- outcome !== 'cantTell'
117
- && instance.issueID === issueID
118
- && instance.catalogIndex === catalogIndex
119
- ) {
120
- // Get the diagnosis.
121
- const diagnosis = {
122
- 'identifier of the violated rule': ruleID !== what ? ruleID : null,
123
- 'description of the violation': what,
124
- 'severity of the violation on a 0-to-3 scale': ordinalSeverity,
125
- 'count of violations of the rule by the element': count ?? 1
126
- }
127
- diagnoses.push(diagnosis);
128
- }
129
- });
94
+ const diagnoses: Diagnoses = [];
95
+ // For each instance reporting a violation of the issue by the violator:
96
+ getTestActInstances(report, {violationsOnly: true, issueID, catalogIndex})
97
+ .forEach(({instance}) => {
98
+ const {count, ordinalSeverity, ruleID, what} = instance;
99
+ // Get the diagnosis.
100
+ const diagnosis = {
101
+ 'identifier of the violated rule': ruleID !== what ? ruleID : null,
102
+ 'description of the violation': what,
103
+ 'severity of the violation on a 0-to-3 scale': ordinalSeverity,
104
+ 'count of violations of the rule by the element': count ?? 1
130
105
  }
106
+ diagnoses.push(diagnosis);
131
107
  });
132
108
  // Add the diagnoses to the response content.
133
109
  responseContent['diagnoses of how the element exhibited the issue'] = diagnoses;
@@ -1,21 +1,20 @@
1
1
  /*
2
- listIssues.test.js
3
- Tests for api/listIssues.js using the fixture corpus, with emphasis on outcome handling.
2
+ listIssues.test.ts
3
+ Tests for api/listIssues.ts using the fixture corpus, with emphasis on outcome handling.
4
4
  */
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {test, before, after} = require('node:test');
9
- const assert = require('node:assert/strict');
10
- const path = require('node:path');
11
- const {response} = require('./listIssues');
8
+ import {test, before, after} from 'node:test';
9
+ import assert from 'node:assert/strict';
10
+ import {response} from './listIssues.ts';
12
11
 
13
12
  // SETUP AND TEARDOWN
14
13
 
15
14
  const savedDBDir = process.env.DB_DIR;
16
15
 
17
- before(() => {
18
- process.env.DB_DIR = path.join(__dirname, '..', 'test', 'fixtures', 'db');
16
+ before(async () => {
17
+ process.env.DB_DIR = (await import('../test/dbFixture.ts')).fixtureDBDir;
19
18
  });
20
19
 
21
20
  after(() => {
@@ -32,33 +31,37 @@ after(() => {
32
31
  test('listIssues returns 2 issues for the mixed-outcomes report, excluding cantTell', async () => {
33
32
  const body = await response(['260101T0000', 'mix']);
34
33
  const issues = body['response content']['basics about all issues reported in the report'];
34
+ assert.ok(issues);
35
35
  assert.equal(issues.length, 2);
36
- const summaries = issues.map(i => i.summary).sort();
36
+ const summaries = issues.map((i: any) => i.summary).sort();
37
37
  assert.deepEqual(summaries, ['all-capital text', 'link not named']);
38
38
  });
39
39
 
40
40
  test('listIssues returns 0 issues for the all-cantTell report', async () => {
41
41
  const body = await response(['260101T0001', 'ct']);
42
42
  const issues = body['response content']['basics about all issues reported in the report'];
43
+ assert.ok(issues);
43
44
  assert.equal(issues.length, 0);
44
45
  });
45
46
 
46
47
  test('listIssues treats missing outcome as a violation, not cantTell', async () => {
47
48
  const body = await response(['260101T0002', 'no']);
48
49
  const issues = body['response content']['basics about all issues reported in the report'];
50
+ assert.ok(issues);
49
51
  assert.equal(issues.length, 1);
50
- assert.equal(issues[0].identifier, 'linkNoText');
52
+ assert.equal(issues[0]!.identifier, 'linkNoText');
51
53
  });
52
54
 
53
55
  test('listIssues returns 0 issues for the empty report', async () => {
54
56
  const body = await response(['260101T0005', 'emp']);
55
57
  const issues = body['response content']['basics about all issues reported in the report'];
58
+ assert.ok(issues);
56
59
  assert.equal(issues.length, 0);
57
60
  });
58
61
 
59
62
  test('listIssues reports prevented rule engines for the prevented report', async () => {
60
63
  const body = await response(['260101T0006', 'prv']);
61
- const details = body['response content']['details about the report'];
64
+ const details = body['response content']['details about the report'] as any;
62
65
  const preventions = details['test results']['rule engines that could not test the page'];
63
66
  assert.equal(preventions.length, 1);
64
67
  assert.equal(preventions[0].name, 'Alfa');
@@ -68,15 +71,18 @@ test('listIssues reports prevented rule engines for the prevented report', async
68
71
  test('listIssues includes reporter names for each issue in the mixed report', async () => {
69
72
  const body = await response(['260101T0000', 'mix']);
70
73
  const issues = body['response content']['basics about all issues reported in the report'];
74
+ assert.ok(issues);
71
75
  const linkIssue = issues.find(i => i.identifier === 'linkNoText');
76
+ assert.ok(linkIssue);
72
77
  assert.deepEqual(linkIssue['rule engines with any violations belonging to the issue'], ['Alfa', 'Axe']);
73
78
  const allCapsIssue = issues.find(i => i.identifier === 'allCaps');
79
+ assert.ok(allCapsIssue);
74
80
  assert.deepEqual(allCapsIssue['rule engines with any violations belonging to the issue'], ['Alfa']);
75
81
  });
76
82
 
77
83
  test('listIssues counts issues by priority correctly for the mixed report', async () => {
78
84
  const body = await response(['260101T0000', 'mix']);
79
- const details = body['response content']['details about the report'];
85
+ const details = body['response content']['details about the report'] as any;
80
86
  const counts = details['test results']['counts of issues by priority'];
81
87
  // linkNoText weight 4 (highest), allCaps weight 1 (lowest).
82
88
  assert.equal(counts.highest, 1);
@@ -87,13 +93,13 @@ test('listIssues counts issues by priority correctly for the mixed report', asyn
87
93
 
88
94
  test('listIssues reports the superseded status for the mixed report', async () => {
89
95
  const body = await response(['260101T0000', 'mix']);
90
- const basics = body['response content']['basics about the report'];
96
+ const basics = body['response content']['basics about the report'] as any;
91
97
  assert.equal(basics['whether a later report about the same page exists'], true);
92
98
  });
93
99
 
94
100
  test('listIssues returns an error for a nonexistent report', async () => {
95
101
  const body = await response(['999999T9999', 'xyz']);
96
- const basics = body['response content']['basics about the report'];
102
+ const basics = body['response content']['basics about the report'] as any;
97
103
  assert.ok(basics.error);
98
104
  });
99
105
 
@@ -102,6 +108,7 @@ test('listIssues handles instances with missing issueID and null instances witho
102
108
  const issues = body['response content']['basics about all issues reported in the report'];
103
109
  // The brd fixture has one instance with duplicateID (a valid issue) and one with no issueID (skipped).
104
110
  // The alfa act has no instances array, so it should be handled as empty.
111
+ assert.ok(issues);
105
112
  assert.ok(issues.length >= 1);
106
113
  const ids = issues.map(i => i.identifier);
107
114
  assert.ok(ids.includes('duplicateID'));
@@ -1,29 +1,43 @@
1
1
  /*
2
- listIssues.js
2
+ listIssues.ts
3
3
  Returns details about one report and basics about all the issues in it.
4
4
  */
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {
8
+ import {z} from 'zod';
9
+ import {
9
10
  getIssueSpec,
10
11
  getReportBasics,
11
12
  getResponseMetadata,
12
13
  getRuleEngineFacts,
13
14
  getRuleEnginesFacts,
14
15
  getToolsFacts,
15
- thisHost
16
- } = require('./util');
17
- const {getReport, getReportStats, objectSort} = require('../util');
16
+ getThisHost
17
+ } from './util.ts';
18
+ import {
19
+ getReport, getReportStats, getTestActInstances, getTestActs, isReportError, objectSort
20
+ } from '../util.ts';
21
+ import {listIssuesResponseSchema} from './schemas.ts';
22
+
23
+ // TYPES
24
+
25
+ // The response content defined by the response schema.
26
+ type ResponseContent = z.infer<typeof listIssuesResponseSchema>['response content'];
18
27
 
19
28
  // FUNCTIONS
20
29
 
21
30
  // Returns the response body.
22
- exports.response = async args => {
31
+ export const response = async (args: string[]) => {
23
32
  const [timeStamp = '', jobID = ''] = args;
33
+ const thisHost = getThisHost();
34
+ // Get the report.
35
+ const report = await getReport(timeStamp, jobID);
36
+ // Get the basics about the report (which may be only an error message).
37
+ const reportBasics = isReportError(report) ? report : await getReportBasics(timeStamp, jobID);
24
38
  // Initialize the response content.
25
- const responseContent = {
26
- 'basics about the report': null,
39
+ const responseContent: ResponseContent = {
40
+ 'basics about the report': reportBasics,
27
41
  'details about the report': null,
28
42
  'how to request that the page be retested': null,
29
43
  'how a web user can request that the page be retested': null,
@@ -31,21 +45,10 @@ exports.response = async args => {
31
45
  'how a web user can get the full report in JSON': null,
32
46
  'basics about all issues reported in the report': null
33
47
  };
34
- // Get the report.
35
- const report = await getReport(timeStamp, jobID);
36
- // If this failed:
37
- if (report.error) {
38
- // Add this to the response content.
39
- responseContent['basics about the report'] = report;
40
- }
41
- // Otherwise, i.e. if it succeeded:
42
- else {
43
- // Get the basics about the report (which may be only an error message).
44
- const reportBasics = await getReportBasics(timeStamp, jobID);
45
- // Add them to the response content.
46
- responseContent['basics about the report'] = reportBasics;
47
- // If the basics about the report were obtained:
48
- if (!reportBasics.error) {
48
+ // If the report was retrieved:
49
+ if (!isReportError(report)) {
50
+ // If its basics were computed:
51
+ if (!('error' in reportBasics)) {
49
52
  const {
50
53
  strict = null,
51
54
  standard = null,
@@ -55,54 +58,52 @@ exports.response = async args => {
55
58
  // Get details about the job definition.
56
59
  const jobDefinitionDetails = {
57
60
  'whether the job prohibited redirection': strict,
58
- 'whether the native results of rule engines are reported': ['also', 'no'].includes(standard),
59
- 'whether standardized results are reported': ['also', 'only'].includes(standard),
61
+ 'whether the native results of rule engines are reported': ['also', 'no'].includes(standard as string),
62
+ 'whether standardized results are reported': ['also', 'only'].includes(standard as string),
60
63
  'device emulated by the job': device,
61
64
  'browser type used by the job': browserID
62
65
  };
63
66
  // Initialize data about the test results.
64
- const ruleEngineIDs = new Set();
65
- const reporterIDs = new Set();
66
- const violatorIndexes = new Set();
67
- const issuesData = {};
68
- // For each act in the report:
69
- report.acts.forEach(act => {
70
- const {result, type, which} = act;
71
- // If the act is a test act with a specified rule engine:
72
- if (type === 'test' && which) {
73
- // Ensure its rule engine is in the results data.
74
- ruleEngineIDs.add(which);
75
- const instances = result?.standardResult?.instances ?? [];
76
- // For each standard instance of the act:
77
- instances.forEach(instance => {
78
- const {catalogIndex, issueID, outcome} = instance;
79
- // If the instance reports a violation:
80
- if (outcome !== 'cantTell') {
81
- // Get the specification of its issue.
82
- const issueSpec = issueID ? getIssueSpec(issueID) : null;
83
- // If the instance has a non-ignorable and fully classified issue:
84
- if (issueSpec) {
85
- const {summary, weight, why} = issueSpec;
86
- // Ensure the rule-engine ID is in the reporters data.
87
- reporterIDs.add(which);
88
- // If the instance has a catalog index:
89
- if (catalogIndex) {
90
- // Ensure the index of the violator is in the results data.
91
- violatorIndexes.add(catalogIndex);
92
- }
93
- // Ensure the data about the issue are in the results data.
94
- issuesData[issueID] ??= {
95
- id: issueID,
96
- summary,
97
- weight,
98
- why,
99
- reporterIDs: new Set()
100
- };
101
- // Ensure the reporter ID is in the data about the issue.
102
- issuesData[issueID].reporterIDs.add(which);
103
- }
104
- }
105
- });
67
+ const ruleEngineIDs: Set<string> = new Set();
68
+ const reporterIDs: Set<string> = new Set();
69
+ const violatorIndexes: Set<string> = new Set();
70
+ const issuesData: Record<string, {
71
+ id: string;
72
+ summary: string;
73
+ weight: number;
74
+ why: string;
75
+ reporterIDs: Set<string>;
76
+ }> = {};
77
+ // For each test act in the report:
78
+ getTestActs(report).forEach(act => {
79
+ // Ensure its rule engine is in the results data.
80
+ ruleEngineIDs.add(act.which!);
81
+ });
82
+ // For each violating standard instance of each test act:
83
+ getTestActInstances(report, {violationsOnly: true}).forEach(({act, instance}) => {
84
+ const {catalogIndex, issueID} = instance;
85
+ // Get the specification of its issue.
86
+ const issueSpec = issueID ? getIssueSpec(issueID) : null;
87
+ // If the instance has a non-ignorable and fully classified issue:
88
+ if (issueSpec) {
89
+ const {summary, weight, why} = issueSpec;
90
+ // Ensure the rule-engine ID is in the reporters data.
91
+ reporterIDs.add(act.which!);
92
+ // If the instance has a catalog index:
93
+ if (catalogIndex) {
94
+ // Ensure the index of the violator is in the results data.
95
+ violatorIndexes.add(String(catalogIndex));
96
+ }
97
+ // Ensure the data about the issue are in the results data.
98
+ issuesData[issueID!] ??= {
99
+ id: issueID!,
100
+ summary,
101
+ weight,
102
+ why,
103
+ reporterIDs: new Set()
104
+ };
105
+ // Ensure the reporter ID is in the data about the issue.
106
+ issuesData[issueID!]!.reporterIDs.add(act.which!);
106
107
  }
107
108
  });
108
109
  const {preventions} = report.jobData;
@@ -117,7 +118,7 @@ exports.response = async args => {
117
118
  // For each issue:
118
119
  Object.values(issuesData).forEach(issueData => {
119
120
  // Increment the count of issues with its weight.
120
- weightCounts[issueData.weight - 1]++;
121
+ weightCounts[issueData.weight - 1]!++;
121
122
  });
122
123
  // Get details about the test results.
123
124
  const resultDetails = {
@@ -126,10 +127,10 @@ exports.response = async args => {
126
127
  'names of rule engines that reported rule violations': getRuleEnginesFacts(reporterIDs)
127
128
  .map(facts => facts.name),
128
129
  'counts of issues by priority': {
129
- 'highest': weightCounts[3],
130
- 'high': weightCounts[2],
131
- 'low': weightCounts[1],
132
- 'lowest': weightCounts[0]
130
+ 'highest': weightCounts[3]!,
131
+ 'high': weightCounts[2]!,
132
+ 'low': weightCounts[1]!,
133
+ 'lowest': weightCounts[0]!
133
134
  },
134
135
  'number of elements reported as violators': violatorIndexes.size
135
136
  };
@@ -186,12 +187,12 @@ exports.response = async args => {
186
187
  return {
187
188
  identifier: id,
188
189
  summary,
189
- priority: ['lowest', 'low', 'high', 'highest'][weight - 1],
190
+ priority: ['lowest', 'low', 'high', 'highest'][weight - 1] as 'lowest' | 'low' | 'high' | 'highest',
190
191
  'impact on a user': why,
191
192
  'rule engines with any violations belonging to the issue': getRuleEnginesFacts(reporterIDs)
192
193
  .map(ruleEnginesFact => ruleEnginesFact.name),
193
194
  'how to get details about the issue': {
194
- method: 'GET',
195
+ method: 'GET' as const,
195
196
  URL: `${thisHost}/api/listViolators/${id}/${timeStamp}/${jobID}`
196
197
  },
197
198
  'web users can get details about the issue at': `${thisHost}/listViolators.html/${id}/${timeStamp}/${jobID}`
@@ -1,14 +1,13 @@
1
1
  /*
2
- listReports.test.js
3
- Tests for api/listReports.js using the fixture corpus.
2
+ listReports.test.ts
3
+ Tests for api/listReports.ts using the fixture corpus.
4
4
  */
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {test, before, after} = require('node:test');
9
- const assert = require('node:assert/strict');
10
- const path = require('node:path');
11
- const {response} = require('./listReports');
8
+ import {test, before, after} from 'node:test';
9
+ import assert from 'node:assert/strict';
10
+ import {response} from './listReports.ts';
12
11
 
13
12
  // CONSTANTS
14
13
 
@@ -28,8 +27,8 @@ const fixtureIds = [
28
27
 
29
28
  const savedDBDir = process.env.DB_DIR;
30
29
 
31
- before(() => {
32
- process.env.DB_DIR = path.join(__dirname, '..', 'test', 'fixtures', 'db');
30
+ before(async () => {
31
+ process.env.DB_DIR = (await import('../test/dbFixture.ts')).fixtureDBDir;
33
32
  });
34
33
 
35
34
  after(() => {
@@ -47,7 +46,7 @@ test('listReports returns basics about all 8 non-hidden reports', async () => {
47
46
  const body = await response();
48
47
  const reportsBasics = body['response content']['basics about all available reports'];
49
48
  assert.equal(reportsBasics.length, 8);
50
- const ids = reportsBasics.map(b => b.identifier).sort();
49
+ const ids = reportsBasics.map((b: any) => b.identifier).sort();
51
50
  const expectedIds = fixtureIds.map(([ts, jid]) => `${ts}-${jid}`).sort();
52
51
  assert.deepEqual(ids, expectedIds);
53
52
  });
@@ -55,14 +54,14 @@ test('listReports returns basics about all 8 non-hidden reports', async () => {
55
54
  test('listReports does not include the hidden report', async () => {
56
55
  const body = await response();
57
56
  const reportsBasics = body['response content']['basics about all available reports'];
58
- const ids = reportsBasics.map(b => b.identifier);
57
+ const ids = reportsBasics.map((b: any) => b.identifier);
59
58
  assert.ok(!ids.includes('260101T0007-hid'), 'hidden report must not appear in listReports');
60
59
  });
61
60
 
62
61
  test('listReports sorts reports by page description and then by completion time', async () => {
63
62
  const body = await response();
64
63
  const reportsBasics = body['response content']['basics about all available reports'];
65
- const descriptions = reportsBasics.map(b => b['tested web page'].description);
64
+ const descriptions = reportsBasics.map((b: any) => b['tested web page'].description);
66
65
  // All CantTell Page, Empty Results Page, Mixed Outcomes Page, Mixed Outcomes Page, No Outcomes Page, Prevented Page.
67
66
  assert.deepEqual(
68
67
  descriptions,
@@ -78,16 +77,18 @@ test('listReports sorts reports by page description and then by completion time'
78
77
  ]
79
78
  );
80
79
  // The two Mixed Outcomes Page reports should be sorted by completion time (older first).
81
- const mixedReports = reportsBasics.filter(b => b['tested web page'].description === 'Mixed Outcomes Page');
82
- assert.equal(mixedReports[0].identifier, '260101T0000-mix');
83
- assert.equal(mixedReports[1].identifier, '260202T0000-new');
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');
84
83
  });
85
84
 
86
85
  test('listReports marks the superseded report correctly', async () => {
87
86
  const body = await response();
88
87
  const reportsBasics = body['response content']['basics about all available reports'];
89
- const mix = reportsBasics.find(b => b.identifier === '260101T0000-mix');
90
- const newer = reportsBasics.find(b => b.identifier === '260202T0000-new');
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);
91
92
  assert.equal(mix['whether a later report about the same page exists'], true);
92
93
  assert.equal(newer['whether a later report about the same page exists'], false);
93
94
  });
@@ -0,0 +1,87 @@
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/requestTest`,
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}/requestTestForm.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
+ };