@jrpool/kilotest 46.1.0 → 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 +8 -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} +75 -46
  29. package/api/version.ts +8 -0
  30. package/{apiIntegration.test.js → apiIntegration.test.ts} +13 -14
  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} +462 -137
  48. package/{index.js → index.ts} +388 -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 -87
  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 -126
  133. package/util.js +0 -733
  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,45 +1,39 @@
1
1
  /*
2
- listIssues.test.js
3
- UI tests for web/listIssues/index.js using the fixture corpus.
2
+ listIssues.test.ts
3
+ UI tests for web/listIssues/index.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 {parse} = require('node-html-parser');
12
-
13
- // Monkey-patch util functions before requiring index, so that index.js
14
- // destructures the patched versions.
15
- const util = require('../../util');
16
- const realIsHidden = util.isHidden;
17
- const realGetPageDataStrings = util.getPageDataStrings;
18
- let isHiddenCallCount = 0;
19
- let forceHiddenOnCall = -1;
20
- let pageDataStringsOverride = null;
21
- util.isHidden = async (timeStamp, jobID) => {
22
- isHiddenCallCount++;
23
- if (isHiddenCallCount === forceHiddenOnCall) {
24
- return true;
8
+ import {test, before, after, mock} from 'node:test';
9
+ import assert from 'node:assert/strict';
10
+ import path from 'node:path';
11
+ import {parse} from 'node-html-parser';
12
+
13
+ // Mock the util functions called by index.ts before requiring it, so that
14
+ // it imports the mocked versions. Other exports delegate to the real module.
15
+ import * as realUtil from '../../util.ts';
16
+ let pageDataStringsOverride: any = null;
17
+ mock.module('../../util.ts', {
18
+ exports: {
19
+ ...realUtil,
20
+ getPageDataStrings: async (...args: any[]) => {
21
+ if (pageDataStringsOverride !== null) {
22
+ return pageDataStringsOverride;
23
+ }
24
+ return (realUtil.getPageDataStrings as any)(...args);
25
+ }
25
26
  }
26
- return realIsHidden(timeStamp, jobID);
27
- };
28
- util.getPageDataStrings = async (...args) => {
29
- if (pageDataStringsOverride !== null) {
30
- return pageDataStringsOverride;
31
- }
32
- return realGetPageDataStrings(...args);
33
- };
27
+ } as any);
34
28
 
35
- const {answer} = require('./index');
29
+ const {answer} = await import('./index.ts') as any;
36
30
 
37
31
  // SETUP AND TEARDOWN
38
32
 
39
33
  const savedDBDir = process.env.DB_DIR;
40
34
 
41
- before(() => {
42
- process.env.DB_DIR = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
35
+ before(async () => {
36
+ process.env.DB_DIR = (await import('../../test/dbFixture.ts')).fixtureDBDir;
43
37
  });
44
38
 
45
39
  after(() => {
@@ -72,13 +66,8 @@ test('listIssues includes links to listViolators for issues in the mixed report'
72
66
  const violatorLinks = html.querySelectorAll('a[href*="listViolators.html"]');
73
67
  assert.ok(violatorLinks.length >= 2);
74
68
  const hrefs = violatorLinks.map(a => a.getAttribute('href'));
75
- assert.ok(hrefs.some(href => href.includes('linkNoText')));
76
- assert.ok(hrefs.some(href => href.includes('allCaps')));
77
- });
78
-
79
- test('listIssues returns an error status for a hidden report', async () => {
80
- const result = await answer('260101T0007/hid');
81
- assert.equal(result.status, 'error');
69
+ assert.ok(hrefs.some(href => href?.includes('linkNoText')));
70
+ assert.ok(hrefs.some(href => href?.includes('allCaps')));
82
71
  });
83
72
 
84
73
  test('listIssues returns an error status for a nonexistent report', async () => {
@@ -102,18 +91,6 @@ test('listIssues for the empty report shows no issue links', async () => {
102
91
  assert.equal(violatorLinks.length, 0);
103
92
  });
104
93
 
105
- test('listIssues returns an error when a report becomes hidden during processing', async () => {
106
- isHiddenCallCount = 0;
107
- forceHiddenOnCall = 2;
108
- try {
109
- const result = await answer('260101T0000/mix');
110
- assert.equal(result.status, 'error');
111
- assert.equal(result.message, 'Report is not available');
112
- }
113
- finally {
114
- forceHiddenOnCall = -1;
115
- }
116
- });
117
94
 
118
95
  test('listIssues includes prevention notices for the prevented report', async () => {
119
96
  const result = await answer('260101T0006/prv');
@@ -122,6 +99,23 @@ test('listIssues includes prevention notices for the prevented report', async ()
122
99
  assert.ok(result.answerPage.includes('page timed out'));
123
100
  });
124
101
 
102
+ test('listIssues falls back to the engine ID for an unknown prevented engine', async () => {
103
+ const fs = await import('node:fs/promises');
104
+ const prvJSON = await fs.readFile(path.join(realUtil.reportsPath(), '260101T0006-prv.json'), 'utf8');
105
+ const report = JSON.parse(prvJSON);
106
+ report.jobData.preventions.unknownEngine = 'mystery failure';
107
+ const reportPath = path.join(realUtil.reportsPath(), '260103T0000-unk.json');
108
+ await fs.writeFile(reportPath, JSON.stringify(report));
109
+ try {
110
+ const result = await answer('260103T0000/unk');
111
+ assert.equal(result.status, 'ok');
112
+ assert.ok(result.answerPage.includes('unknownEngine (unknown sponsor)'));
113
+ }
114
+ finally {
115
+ await fs.unlink(reportPath);
116
+ }
117
+ });
118
+
125
119
  test('listIssues returns an error when getPageDataStrings fails after getData succeeds', async () => {
126
120
  pageDataStringsOverride = {error: 'Page data strings error'};
127
121
  try {
@@ -159,8 +153,8 @@ test('listIssues shows plural violator count for an issue with multiple violator
159
153
 
160
154
  test('listIssues handles acts with no standardResult instances', async () => {
161
155
  // Create a temporary report with an act that has no standardResult.
162
- const fs = require('node:fs/promises');
163
- const dbDir = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
156
+ const fs = await import('node:fs/promises');
157
+ const dbDir = (await import('../../test/dbFixture.ts')).fixtureDBDir;
164
158
  const reportPath = path.join(dbDir, 'reports', '260101T0004-nsi.json');
165
159
  const report = {
166
160
  id: '260101T0004-nsi',
@@ -13,11 +13,11 @@
13
13
  <body>
14
14
  <main>
15
15
  <h1>Pages tested by <a href="/">Kilotest</a></h1>
16
- <h2>Recommended for testing</h2>
16
+ <h2>Requested for testing</h2>
17
17
  <ol>
18
- __recs__
18
+ __requests__
19
19
  </ol>
20
- <p>__noRecs__</p>
20
+ <p>__noRequests__</p>
21
21
  <p>Should Kilotest <a href="requestTestForm.html">test a page</a> that is not listed below?</p>
22
22
  <h2>Queued for testing</h2>
23
23
  <ol>
@@ -1,66 +1,66 @@
1
1
  /*
2
- index.js
2
+ index.ts
3
3
  Lists all available reports.
4
4
  */
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {
8
+ import {
9
9
  getAgoDays,
10
10
  getCountString,
11
11
  getJobNames,
12
12
  getMultiReportWhats,
13
13
  getObject,
14
14
  getPageDataStrings,
15
- getRecs,
15
+ getTestRequests,
16
16
  getReportData,
17
17
  getReportExtracts,
18
- isRecommendable,
18
+ getRequestability,
19
19
  jobsPath,
20
- objectSort
21
- } = require('../../util');
22
- const fs = require('fs/promises');
23
- const path = require('path');
20
+ objectSort,
21
+ populateTemplate
22
+ } from '../../util.ts';
23
+ import path from 'node:path';
24
24
 
25
25
  // FUNCTIONS
26
26
 
27
27
  // Adds parameters to a query for the answer page.
28
- const populateQuery = async query => {
28
+ const populateQuery = async (query: Record<string, any>) => {
29
29
  const margin = ' '.repeat(8);
30
30
  // Initialize the classes of lines.
31
- const lines = {
32
- recs: [],
31
+ const lines: {requests: string[], queue: string[], claimed: string[], tested: string[]} = {
32
+ requests: [],
33
33
  queue: [],
34
34
  claimed: [],
35
35
  tested: []
36
36
  };
37
- // Get the recommendations.
38
- const recs = await getRecs();
39
- // For each recommended URL:
40
- Object.keys(recs).forEach(url => {
41
- // For each of its recommendations:
42
- recs[url].forEach(rec => {
43
- const {what, why} = rec;
37
+ // Get the test requests.
38
+ const testRequests = await getTestRequests() as Record<string, {description: string, why: string}[]>;
39
+ // For each requested URL:
40
+ Object.keys(testRequests).forEach(url => {
41
+ // For each of its requests:
42
+ testRequests[url]!.forEach((req: any) => {
43
+ const {description, why} = req;
44
44
  // Add a line.
45
- lines.recs.push(`${margin}<li><code>${url}</code> (${what}): ${why}</li>`);
45
+ lines.requests.push(`${margin}<li><code>${url}</code> (${description}): ${why}</li>`);
46
46
  });
47
47
  });
48
48
  // Sort the lines in alphabetical order by URL and secondarily by proposed name.
49
- lines.recs.sort();
49
+ lines.requests.sort();
50
50
  // Add the lines to the query.
51
- query.recs = lines.recs.join('\n');
52
- // Add a no-recommendations message, if applicable, to the query.
53
- query.noRecs = lines.recs.length
54
- ? 'Kilotest managers can <a href="enqueueForm.html">approve or reject a recommendation</a>.'
55
- : 'No recommendations await approval now.';
51
+ query.requests = lines.requests.join('\n');
52
+ // Add a no-requests message, if applicable, to the query.
53
+ query.noRequests = lines.requests.length
54
+ ? 'Kilotest managers can <a href="enqueueForm.html">approve or reject a request</a>.'
55
+ : 'No requests await approval now.';
56
56
  // Get the file names of all queued and claimed jobs.
57
57
  const jobFileNames = await getJobNames();
58
58
  // For each job category:
59
- for (const category of ['queue', 'claimed']) {
59
+ for (const category of ['queue', 'claimed'] as const) {
60
60
  // For each job in the category:
61
61
  for (const fileName of jobFileNames[category]) {
62
62
  // Get the job.
63
- const job = await getObject(path.join(jobsPath(), category, fileName));
63
+ const job = await getObject(path.join(jobsPath(), category, fileName)) as {target: {url: string, what: string}};
64
64
  // Add a line.
65
65
  lines[category].push(`${margin}<li><code>${job.target.url}</code> (${job.target.what})</li>`);
66
66
  }
@@ -81,14 +81,21 @@ const populateQuery = async query => {
81
81
  const multiReportWhats = await getMultiReportWhats();
82
82
  // Sort them primarily by page description and secondarily by completion time.
83
83
  let sortedExtracts = objectSort(reportExtracts, 'reportTime', 'alpha');
84
- sortedExtracts = objectSort(sortedExtracts, 'what', 'alpha');
84
+ sortedExtracts = objectSort(sortedExtracts, 'description', 'alpha');
85
85
  // For each report:
86
86
  for (const extract of sortedExtracts) {
87
- const {jobID, timeStamp, url, what} = extract;
87
+ const {jobID, timeStamp, url, description} = extract;
88
88
  // Get data about it.
89
89
  const reportData = await getReportData(timeStamp, jobID);
90
+ // If this failed:
91
+ if (reportData.error !== undefined) {
92
+ console.error(reportData.error);
93
+ // Populate the query with the reason.
94
+ query.error = reportData.error;
95
+ // Stop populating the query.
96
+ return;
97
+ }
90
98
  const {
91
- error,
92
99
  issueCount,
93
100
  preventedEngineCount,
94
101
  preventedEngineNames,
@@ -96,21 +103,13 @@ const populateQuery = async query => {
96
103
  reporterCount,
97
104
  violatorCount
98
105
  } = reportData;
99
- // If this failed:
100
- if (error) {
101
- console.error(error);
102
- // Populate the query with the reason.
103
- query.error = error;
104
- // Stop populating the query.
105
- return;
106
- }
107
106
  // Otherwise, i.e. if it succeeded, add lines about the report.
108
107
  lines.tested.push(`${margin}<details>`);
109
108
  const daysAgo = getAgoDays(timeStamp);
110
- const pageDataStrings = await getPageDataStrings(timeStamp, jobID, {what, url, daysAgo});
111
- const {urlLink, testInfo} = pageDataStrings;
112
- const testText = multiReportWhats.includes(what) ? ` (${testInfo.toLowerCase()})` : '';
113
- lines.tested.push(`${margin} <summary>${what}${testText}</summary>`);
109
+ const pageDataStrings = await getPageDataStrings(timeStamp, jobID, {description, url, daysAgo});
110
+ const {urlLink, testInfo}: any = pageDataStrings;
111
+ const testText = multiReportWhats.includes(description) ? ` (${testInfo.toLowerCase()})` : '';
112
+ lines.tested.push(`${margin} <summary>${description}${testText}</summary>`);
114
113
  lines.tested.push(`${margin} <ul>`);
115
114
  // Add the URL of the target to the lines.
116
115
  lines.tested.push(`${margin} <li>URL: ${urlLink}</li>`);
@@ -125,7 +124,7 @@ const populateQuery = async query => {
125
124
  );
126
125
  }
127
126
  // Add facts about the test results to the lines.
128
- let reporterString = `${getCountString(reporterCount, 'rule engine', 'rule engines')} reported issues`;
127
+ let reporterString = `${getCountString(reporterCount, 'rule engine', 'rule engines')} reported issues`;
129
128
  if (reporterCount) {
130
129
  const reporterNamesString = reporterNames.join(' + ');
131
130
  reporterString = `${reporterString} (${reporterNamesString})`;
@@ -141,14 +140,14 @@ const populateQuery = async query => {
141
140
  if (issueCount) {
142
141
  // Add a question link about the reported issues to the lines.
143
142
  const href = `href="listIssues.html/${timeStamp}/${jobID}"`;
144
- const label = `aria-label="What ${issueCountString} reported for the ${what} page?"`;
143
+ const label = `aria-label="What ${issueCountString} reported for the ${description} page?"`;
145
144
  const questionString = issueCount === 1 ? 'was the issue' : 'were the issues';
146
145
  const link = `<a ${href} ${label}>What ${questionString}?</a>`;
147
146
  lines.tested.push(`${margin} <li>${link}</li>`);
148
147
  }
149
148
  // Add the status of, and if necessary a question link about, retesting to the lines.
150
- const status = await isRecommendable(url);
151
- let retestString;
149
+ const status = await getRequestability(url);
150
+ let retestString: string;
152
151
  if (status === 'claimed') {
153
152
  retestString = 'Currently being retested';
154
153
  }
@@ -167,8 +166,8 @@ const populateQuery = async query => {
167
166
  query.testedPages = lines.tested.join('\n');
168
167
  };
169
168
  // Returns a page answering the targets question.
170
- exports.answer = async () => {
171
- const query = {};
169
+ export const answer = async () => {
170
+ const query: Record<string, any> = {};
172
171
  // Create a query to replace placeholders.
173
172
  await populateQuery(query);
174
173
  // If the query reports an error:
@@ -180,11 +179,7 @@ exports.answer = async () => {
180
179
  };
181
180
  }
182
181
  // Otherwise, i.e. if it does not report an error, get the template.
183
- let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
184
- // Replace its placeholders.
185
- Object.keys(query).forEach(param => {
186
- answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
187
- });
182
+ const answerPage = await populateTemplate(import.meta.dirname, query);
188
183
  // Return the populated page.
189
184
  return {
190
185
  status: 'ok',
@@ -1,23 +1,23 @@
1
1
  /*
2
- listReports.test.js
3
- UI tests for web/listReports/index.js using the fixture corpus.
2
+ listReports.test.ts
3
+ UI tests for web/listReports/index.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 fs = require('node:fs/promises');
12
- const {parse} = require('node-html-parser');
13
- const {answer} = require('./index');
8
+ import {test, before, after} from 'node:test';
9
+ import assert from 'node:assert/strict';
10
+ import path from 'node:path';
11
+ import fs from 'node:fs/promises';
12
+ import {parse} from 'node-html-parser';
13
+ import {answer} from './index.ts';
14
14
 
15
15
  // SETUP AND TEARDOWN
16
16
 
17
17
  const savedDBDir = process.env.DB_DIR;
18
18
 
19
19
  before(async () => {
20
- process.env.DB_DIR = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
20
+ process.env.DB_DIR = (await import('../../test/dbFixture.ts')).fixtureDBDir;
21
21
  // Ensure job subdirectories exist (they are empty and not tracked by Git).
22
22
  for (const sub of ['queue', 'claimed', 'failed']) {
23
23
  await fs.mkdir(path.join(process.env.DB_DIR, 'jobs', sub), {recursive: true});
@@ -36,22 +36,22 @@ after(() => {
36
36
  // TESTS
37
37
 
38
38
  test('listReports returns an ok status with valid HTML', async () => {
39
- const result = await answer();
39
+ const result: any = await answer();
40
40
  assert.equal(result.status, 'ok');
41
41
  assert.ok(result.answerPage);
42
42
  const html = parse(result.answerPage);
43
- assert.equal(html.querySelector('title').text, 'Pages tested | Kilotest');
43
+ assert.equal(html.querySelector('title')?.text, 'Pages tested | Kilotest');
44
44
  });
45
45
 
46
46
  test('listReports includes the 8 non-hidden fixture reports as details elements', async () => {
47
- const result = await answer();
47
+ const result: any = await answer();
48
48
  const html = parse(result.answerPage);
49
49
  const details = html.querySelectorAll('details');
50
50
  assert.equal(details.length, 8);
51
51
  });
52
52
 
53
53
  test('listReports includes the page descriptions in summary elements', async () => {
54
- const result = await answer();
54
+ const result: any = await answer();
55
55
  const html = parse(result.answerPage);
56
56
  const summaries = html.querySelectorAll('details > summary').map(s => s.text);
57
57
  assert.ok(summaries.some(s => s.startsWith('Mixed Outcomes Page')));
@@ -62,55 +62,55 @@ test('listReports includes the page descriptions in summary elements', async ()
62
62
  });
63
63
 
64
64
  test('listReports does not include the hidden report', async () => {
65
- const result = await answer();
65
+ const result: any = await answer();
66
66
  assert.ok(!result.answerPage.includes('Hidden Page'));
67
67
  });
68
68
 
69
69
  test('listReports includes links to listIssues for reports with issues', async () => {
70
- const result = await answer();
70
+ const result: any = await answer();
71
71
  const html = parse(result.answerPage);
72
72
  const issueLinks = html.querySelectorAll('a[href*="listIssues.html"]');
73
73
  assert.ok(issueLinks.length > 0);
74
74
  const hrefs = issueLinks.map(a => a.getAttribute('href'));
75
- assert.ok(hrefs.some(href => href.includes('260101T0000/mix')));
75
+ assert.ok(hrefs.some(href => href?.includes('260101T0000/mix')));
76
76
  });
77
77
 
78
78
  test('listReports includes the page URLs in the report details', async () => {
79
- const result = await answer();
79
+ const result: any = await answer();
80
80
  assert.ok(result.answerPage.includes('https://example.com/mixed'));
81
81
  assert.ok(result.answerPage.includes('https://example.com/canttell'));
82
82
  });
83
83
 
84
84
  test('listReports includes a link to request testing a new page', async () => {
85
- const result = await answer();
85
+ const result: any = await answer();
86
86
  const html = parse(result.answerPage);
87
87
  const testLink = html.querySelector('a[href="requestTestForm.html"]');
88
88
  assert.ok(testLink);
89
89
  });
90
90
 
91
- test('listReports shows recommendations when recs.json has entries', {timeout: 500}, async () => {
92
- const dbDir = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
93
- const recsPath = path.join(dbDir, 'jobs', 'recs.json');
94
- const originalRecs = await fs.readFile(recsPath, 'utf8');
91
+ test('listReports shows requests when testRequests.json has entries', {timeout: 500}, async () => {
92
+ const dbDir = (await import('../../test/dbFixture.ts')).fixtureDBDir;
93
+ const testRequestsPath = path.join(dbDir, 'jobs', 'testRequests.json');
94
+ const originalTestRequests = await fs.readFile(testRequestsPath, 'utf8');
95
95
  try {
96
- const testRecs = {
96
+ const testRequests = {
97
97
  'https://example.com/mixed': [
98
- {what: 'Mixed Outcomes Page', why: 'Needs retesting for accessibility'}
98
+ {description: 'Mixed Outcomes Page', why: 'Needs retesting for accessibility'}
99
99
  ]
100
100
  };
101
- await fs.writeFile(recsPath, JSON.stringify(testRecs, null, 2));
102
- const result = await answer();
101
+ await fs.writeFile(testRequestsPath, JSON.stringify(testRequests, null, 2));
102
+ const result: any = await answer();
103
103
  assert.equal(result.status, 'ok');
104
104
  assert.ok(result.answerPage.includes('https://example.com/mixed'));
105
105
  assert.ok(result.answerPage.includes('Needs retesting for accessibility'));
106
106
  }
107
107
  finally {
108
- await fs.writeFile(recsPath, originalRecs);
108
+ await fs.writeFile(testRequestsPath, originalTestRequests);
109
109
  }
110
110
  });
111
111
 
112
112
  test('listReports shows queued and claimed jobs when they exist', async () => {
113
- const dbDir = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
113
+ const dbDir = (await import('../../test/dbFixture.ts')).fixtureDBDir;
114
114
  const queueDir = path.join(dbDir, 'jobs', 'queue');
115
115
  const claimedDir = path.join(dbDir, 'jobs', 'claimed');
116
116
  const queueFile = path.join(queueDir, 'queuedJob.json');
@@ -124,7 +124,7 @@ test('listReports shows queued and claimed jobs when they exist', async () => {
124
124
  };
125
125
  await fs.writeFile(queueFile, JSON.stringify(queuedJob, null, 2));
126
126
  await fs.writeFile(claimedFile, JSON.stringify(claimedJob, null, 2));
127
- const result = await answer();
127
+ const result: any = await answer();
128
128
  assert.equal(result.status, 'ok');
129
129
  assert.ok(result.answerPage.includes('https://example.com/queued'));
130
130
  assert.ok(result.answerPage.includes('Queued Page'));
@@ -138,7 +138,7 @@ test('listReports shows queued and claimed jobs when they exist', async () => {
138
138
  });
139
139
 
140
140
  test('listReports shows claimed retest status for a report with a matching claimed job', async () => {
141
- const dbDir = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
141
+ const dbDir = (await import('../../test/dbFixture.ts')).fixtureDBDir;
142
142
  const claimedDir = path.join(dbDir, 'jobs', 'claimed');
143
143
  const claimedFile = path.join(claimedDir, 'claimedRetest.json');
144
144
  try {
@@ -146,7 +146,7 @@ test('listReports shows claimed retest status for a report with a matching claim
146
146
  target: {url: 'https://example.com/mixed', what: 'Mixed Outcomes Page'}
147
147
  };
148
148
  await fs.writeFile(claimedFile, JSON.stringify(claimedJob, null, 2));
149
- const result = await answer();
149
+ const result: any = await answer();
150
150
  assert.equal(result.status, 'ok');
151
151
  assert.ok(result.answerPage.includes('Currently being retested'));
152
152
  }
@@ -156,7 +156,7 @@ test('listReports shows claimed retest status for a report with a matching claim
156
156
  });
157
157
 
158
158
  test('listReports shows queued retest status for a report with a matching queued job', async () => {
159
- const dbDir = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
159
+ const dbDir = (await import('../../test/dbFixture.ts')).fixtureDBDir;
160
160
  const queueDir = path.join(dbDir, 'jobs', 'queue');
161
161
  const queueFile = path.join(queueDir, 'queuedRetest.json');
162
162
  try {
@@ -164,7 +164,7 @@ test('listReports shows queued retest status for a report with a matching queued
164
164
  target: {url: 'https://example.com/mixed', what: 'Mixed Outcomes Page'}
165
165
  };
166
166
  await fs.writeFile(queueFile, JSON.stringify(queuedJob, null, 2));
167
- const result = await answer();
167
+ const result: any = await answer();
168
168
  assert.equal(result.status, 'ok');
169
169
  assert.ok(result.answerPage.includes('Currently in the queue for retesting'));
170
170
  }
@@ -173,18 +173,18 @@ test('listReports shows queued retest status for a report with a matching queued
173
173
  }
174
174
  });
175
175
 
176
- test('listReports returns an error when a report file is invalid', async () => {
177
- const dbDir = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
176
+ test('listReports returns an error when a report file is invalid', {timeout: 500}, async () => {
177
+ const dbDir = (await import('../../test/dbFixture.ts')).fixtureDBDir;
178
178
  const reportsDir = path.join(dbDir, 'reports');
179
179
  const invalidReportPath = path.join(reportsDir, '260101T9999-bad.json');
180
180
  try {
181
- // Valid for getReportExtract (has target and jobData.endTime) but invalid for isValidReport (no acts or catalog).
181
+ // Valid for getReportExtract (has target and jobData.endTime) but not usable by isUsableReport (no acts or catalog).
182
182
  const invalidReport = {
183
183
  target: {what: 'Invalid Report', url: 'https://example.com/invalid'},
184
184
  jobData: {endTime: '26-01-01T00:10'}
185
185
  };
186
186
  await fs.writeFile(invalidReportPath, JSON.stringify(invalidReport, null, 2));
187
- const result = await answer();
187
+ const result: any = await answer();
188
188
  assert.equal(result.status, 'error');
189
189
  assert.ok(result.message);
190
190
  }
@@ -195,26 +195,24 @@ test('listReports returns an error when a report file is invalid', async () => {
195
195
 
196
196
  test('listReports shows no-reports message when the database is empty', async () => {
197
197
  // Create a temp DB with empty reports and jobs directories.
198
- const os = require('node:os');
198
+ const os = await import('node:os');
199
199
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'listreports-empty-'));
200
200
  await fs.mkdir(path.join(tmpDir, 'reports'), {recursive: true});
201
201
  await fs.mkdir(path.join(tmpDir, 'jobs', 'queue'), {recursive: true});
202
202
  await fs.mkdir(path.join(tmpDir, 'jobs', 'claimed'), {recursive: true});
203
203
  await fs.mkdir(path.join(tmpDir, 'jobs', 'failed'), {recursive: true});
204
- await fs.writeFile(path.join(tmpDir, 'jobs', 'recs.json'), '{}\n');
204
+ await fs.writeFile(path.join(tmpDir, 'jobs', 'testRequests.json'), '{}\n');
205
205
  const savedDBDir = process.env.DB_DIR;
206
206
  process.env.DB_DIR = tmpDir;
207
207
  try {
208
- delete require.cache[require.resolve('./index')];
209
- const {answer} = require('./index');
210
- const result = await answer();
208
+ const {answer} = await import('./index.ts');
209
+ const result: any = await answer();
211
210
  assert.equal(result.status, 'ok');
212
211
  assert.ok(result.answerPage.includes('no'));
213
212
  assert.ok(result.answerPage.includes(' a '));
214
213
  }
215
214
  finally {
216
215
  process.env.DB_DIR = savedDBDir;
217
- delete require.cache[require.resolve('./index')];
218
216
  await fs.rm(tmpDir, {recursive: true}).catch(() => {});
219
217
  }
220
218
  });