@jrpool/kilotest 46.1.2 → 48.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 (153) 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.ts +144 -0
  21. package/api/requestRetest.ts +119 -0
  22. package/api/requestTest.test.ts +153 -0
  23. package/api/requestTest.ts +123 -0
  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} +35 -14
  28. package/api/{util.js → util.ts} +54 -51
  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 +360 -0
  35. package/docs/SERVICE.md +170 -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} +397 -152
  48. package/{index.js → index.ts} +386 -285
  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.ts +1129 -0
  69. package/util.ts +1017 -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} +74 -58
  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 +71 -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} +57 -39
  103. package/web/requestRetest/index.html +7 -7
  104. package/web/requestRetest/index.ts +62 -0
  105. package/web/requestRetest/requestRetest.test.ts +130 -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.ts +52 -0
  110. package/web/requestTest/{requestTest.test.js → requestTest.test.ts} +39 -24
  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} +18 -22
  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/requestRetest.js +0 -96
  128. package/api/requestRetest.test.js +0 -80
  129. package/api/requestTest.js +0 -106
  130. package/api/requestTest.test.js +0 -92
  131. package/api/version.js +0 -8
  132. package/indexFallback.test.js +0 -33
  133. package/indexWorkers.test.js +0 -79
  134. package/jsconfig.json +0 -8
  135. package/pm2.config.js +0 -18
  136. package/smokeTest.js +0 -128
  137. package/util.js +0 -744
  138. package/util.test.js +0 -968
  139. package/web/enqueue/index.js +0 -67
  140. package/web/enqueueForm/index.js +0 -49
  141. package/web/manage/index.js +0 -22
  142. package/web/pruneReportsForm/index.js +0 -123
  143. package/web/reannotate/index.js +0 -58
  144. package/web/renewWCAGForm/index.js +0 -22
  145. package/web/requestRetest/index.js +0 -31
  146. package/web/requestRetest/requestRetest.test.js +0 -119
  147. package/web/requestTest/index.js +0 -24
  148. package/web/requestTestForm/index.js +0 -21
  149. package/web/rewindReportsForm/index.js +0 -121
  150. package/web/tutorial/index.js +0 -63
  151. package/web/tutorial/tutorial.test.js +0 -97
  152. /package/test/fixtures/db/jobs/{recs.json → testRequests.json} +0 -0
  153. /package/web/{tutorial → tutorialWeb}/images/newsletter-form.png +0 -0
@@ -1,25 +1,25 @@
1
1
  /*
2
- ai0BalanceForm.test.js
3
- Unit tests for web/ai0BalanceForm/index.js.
2
+ ai0BalanceForm.test.ts
3
+ Unit tests for web/ai0BalanceForm/index.ts.
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('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 'fs/promises';
12
+ import {parse} from 'node-html-parser';
13
+ import {answer} from './index.ts';
14
14
 
15
15
  // CONSTANTS
16
16
 
17
- const balancePath = path.join(__dirname, '..', '..', 'ai0Balance.json');
17
+ const balancePath = path.join(import.meta.dirname, '..', '..', 'ai0Balance.json');
18
18
 
19
19
  // SETUP AND TEARDOWN
20
20
 
21
21
  const savedAuthCode = process.env.AUTH_CODE;
22
- let savedBalance;
22
+ let savedBalance: any;
23
23
 
24
24
  before(async () => {
25
25
  process.env.AUTH_CODE = 'test-auth-code';
@@ -45,7 +45,7 @@ after(async () => {
45
45
  // TESTS
46
46
 
47
47
  test('ai0BalanceForm displays the current balance when no newBalance is submitted', async () => {
48
- const result = await answer(null, '');
48
+ const result: any = await answer(null, '');
49
49
  assert.equal(result.status, 'ok');
50
50
  assert.ok(result.answerPage);
51
51
  const html = parse(result.answerPage);
@@ -53,13 +53,13 @@ test('ai0BalanceForm displays the current balance when no newBalance is submitte
53
53
  });
54
54
 
55
55
  test('ai0BalanceForm returns an error for an invalid auth code', async () => {
56
- const result = await answer(null, 'authCode=wrong&newBalance=5.00');
56
+ const result: any = await answer(null, 'authCode=wrong&newBalance=5.00');
57
57
  assert.equal(result.status, 'error');
58
58
  assert.equal(result.message, 'Invalid authorization code');
59
59
  });
60
60
 
61
61
  test('ai0BalanceForm records a valid new balance with valid auth code', async () => {
62
- const result = await answer(null, 'authCode=test-auth-code&newBalance=2.50');
62
+ const result: any = await answer(null, 'authCode=test-auth-code&newBalance=2.50');
63
63
  assert.equal(result.status, 'ok');
64
64
  assert.ok(result.answerPage.includes('$2.5 is the'));
65
65
  // Verify the file was written.
@@ -69,7 +69,7 @@ test('ai0BalanceForm records a valid new balance with valid auth code', async ()
69
69
 
70
70
  test('ai0BalanceForm does not record an invalid balance', async () => {
71
71
  // 150 is out of range (>= 100).
72
- const result = await answer(null, 'authCode=test-auth-code&newBalance=150');
72
+ const result: any = await answer(null, 'authCode=test-auth-code&newBalance=150');
73
73
  assert.equal(result.status, 'ok');
74
74
  // The oldBalance should not show the new value.
75
75
  assert.ok(!result.answerPage.includes('$150 is the'));
@@ -78,7 +78,7 @@ test('ai0BalanceForm does not record an invalid balance', async () => {
78
78
  test('ai0BalanceForm shows no-balance message when balance file is missing', async () => {
79
79
  await fs.unlink(balancePath).catch(() => {});
80
80
  try {
81
- const result = await answer(null, '');
81
+ const result: any = await answer(null, '');
82
82
  assert.equal(result.status, 'ok');
83
83
  assert.ok(result.answerPage.includes('There is no'));
84
84
  }
@@ -1,26 +1,22 @@
1
1
  /*
2
- index.js
3
- Serves a form for deleting superseded reports.
2
+ index.ts
3
+ Serves a form for recording the AI service 0 balance.
4
4
  */
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const fs = require('fs/promises');
9
- const path = require('path');
10
- const {getJSON} = require('../../util');
11
-
12
- // CONSTANTS
13
-
14
- const balancePath = path.join(__dirname, '../../ai0Balance.json');
8
+ import fs from 'node:fs/promises';
9
+ import {balancePath} from '../../balances.ts';
10
+ import {getJSON, populateTemplate} from '../../util.ts';
15
11
 
16
12
  // FUNCTIONS
17
13
 
18
14
  // Returns a form for recording the AI service 0 balance.
19
- exports.answer = async (_, search) => {
15
+ export const answer = async (_: any, search: string) => {
20
16
  const searchParams = new URLSearchParams(search);
21
17
  const authCode = searchParams?.get('authCode');
22
18
  const newBalanceString = searchParams?.get('newBalance');
23
- let oldBalance;
19
+ let oldBalance: any;
24
20
  // If the form displayed itself:
25
21
  if (newBalanceString) {
26
22
  // If the authorization code is valid:
@@ -64,13 +60,9 @@ exports.answer = async (_, search) => {
64
60
  oldBalance = 'There is no';
65
61
  }
66
62
  }
67
- const query = {oldBalance};
68
- // Get the order form template.
69
- let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
70
- // Replace its placeholders.
71
- Object.keys(query).forEach(param => {
72
- answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
73
- });
63
+ const query: Record<string, any> = {oldBalance};
64
+ // Get the populated order form template.
65
+ const answerPage = await populateTemplate(import.meta.dirname, query);
74
66
  // Return the populated page.
75
67
  return {
76
68
  status: 'ok',
@@ -0,0 +1,56 @@
1
+ /*
2
+ index.ts
3
+ Implements a test request approval.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ import {
9
+ deleteTestRequests, getJSON, getNowStamp, getRandomString, isURL, jobsPath, populateTemplate
10
+ } from '../../util.ts';
11
+ import fs from 'node:fs/promises';
12
+ import path from 'node:path';
13
+
14
+ // FUNCTIONS
15
+
16
+ // Implements a test request approval and returns a revised request page.
17
+ export const answer = async (url: string, description: string, authCode: string) => {
18
+ // If the arguments are valid:
19
+ if (isURL(url) && description && authCode === process.env.AUTH_CODE) {
20
+ // Get the job template.
21
+ const jobTemplateJSON = await fs.readFile(path.join(import.meta.dirname, '..', '..', 'job.json'), 'utf8');
22
+ const job = JSON.parse(jobTemplateJSON);
23
+ const nowStamp = getNowStamp();
24
+ // Populate the template with job properties.
25
+ const jobIDSuffix = getRandomString(3);
26
+ const jobName = `${nowStamp}-${jobIDSuffix}`;
27
+ job.id = jobName;
28
+ job.creationTimeStamp = nowStamp;
29
+ job.executionTimeStamp = nowStamp;
30
+ job.target.what = description;
31
+ job.target.url = url;
32
+ const query: Record<string, string> = {
33
+ target: description,
34
+ jobName
35
+ };
36
+ // Save the job in the queue.
37
+ await fs.writeFile(
38
+ path.join(jobsPath(), 'queue', `${jobName}.json`), getJSON(job)
39
+ );
40
+ console.log(`Retest queued for ${description} as job ${jobName}`);
41
+ // Delete the test requests for the target.
42
+ await deleteTestRequests(url);
43
+ // Get the populated answer template.
44
+ const answerPage = await populateTemplate(import.meta.dirname, query);
45
+ // Return the populated page.
46
+ return {
47
+ status: 'ok',
48
+ answerPage
49
+ };
50
+ }
51
+ // Otherwise, i.e. if the authorization code is invalid, return an error page.
52
+ return {
53
+ status: 'error',
54
+ message: 'Invalid authorization code'
55
+ };
56
+ };
@@ -6,21 +6,21 @@
6
6
  <meta name="publisher" content="Jonathan Robert Pool">
7
7
  <meta name="creator" content="Jonathan Robert Pool">
8
8
  <meta name="keywords" content="report,accessibility,a11y">
9
- <title>Recommendation approval or rejection | Kilotest</title>
9
+ <title>Request approval or rejection | Kilotest</title>
10
10
  <link rel="icon" href="/favicon.ico">
11
11
  <link rel="stylesheet" href="/style.css">
12
12
  </head>
13
13
  <body>
14
14
  <main>
15
- <h1><a href="/">Kilotest</a>: Recommendation approval or rejection</h1>
16
- <p>To approve a test or retest recommendation, check its recommended page name.</p>
17
- <p>To approve testing a page that is not yet recommended, first <a href="listReports.html">recommend it</a>.</p>
18
- <p>To reject all recommendations for a page, check the radio button of its URL.</p>
19
- <form action="/recAction.html" method="post">
15
+ <h1><a href="/">Kilotest</a>: Request approval or rejection</h1>
16
+ <p>To approve a test or retest request, check its requested page name.</p>
17
+ <p>To approve testing a page that has not yet been requested, first <a href="listReports.html">request it</a>.</p>
18
+ <p>To reject all requests for a page, check the radio button of its URL.</p>
19
+ <form action="/requestAction.html" method="post">
20
20
  <fieldset>
21
- <legend>Recommendations</legend>
22
- __recs__
23
- <p>__noRecs__</p>
21
+ <legend>Requests</legend>
22
+ __requests__
23
+ <p>__noRequests__</p>
24
24
  </fieldset>
25
25
  <p><label>
26
26
  Authorization code:
@@ -0,0 +1,43 @@
1
+ /*
2
+ index.ts
3
+ Serves a form for approving or rejecting a test request.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ import {getTestRequests, populateTemplate} from '../../util.ts';
9
+
10
+ // FUNCTIONS
11
+
12
+ // Returns a test order form.
13
+ export const answer = async () => {
14
+ const testRequests = await getTestRequests() as Record<string, {description: string}[]>;
15
+ const urls = Object.keys(testRequests);
16
+ const margin = ' '.repeat(12);
17
+ const lines: string[] = [];
18
+ // For each page with any requests:
19
+ urls.forEach(url => {
20
+ // Add a radio button and its URL to the lines.
21
+ lines.push(`${margin}<h2><input type="radio" name="target" value="${url}" required> ${url}</h2>`);
22
+ // Get the requested target names for the page.
23
+ const targetNames = new Set(testRequests[url]!.map((req: any) => req.description));
24
+ // For each requested target name:
25
+ targetNames.forEach(description => {
26
+ const radio = `<input type="radio" name="target" value="${url}\t${description}" required>`;
27
+ // Add a radio button and the requested page name to the lines.
28
+ lines.push(`${margin} <p>${radio} ${description}</p>`);
29
+ });
30
+ });
31
+ const query: Record<string, string> = {
32
+ requests: lines.join('\n'),
33
+ noRequests: urls.length ? '' : 'No requests exist now.',
34
+ disabled: urls.length ? '' : ' disabled'
35
+ };
36
+ // Get the populated order form template.
37
+ const answerPage = await populateTemplate(import.meta.dirname, query);
38
+ // Return the populated page.
39
+ return {
40
+ status: 'ok',
41
+ answerPage
42
+ };
43
+ };
@@ -1,23 +1,23 @@
1
1
  /*
2
- expungeReportsForm.test.js
3
- Unit tests for web/expungeReportsForm/index.js.
2
+ expungeReportsForm.test.ts
3
+ Unit tests for web/expungeReportsForm/index.ts.
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('fs/promises');
12
- const {parse} = require('node-html-parser');
13
- const {answer} = require('./index');
14
- const {reportsPath} = require('../../util');
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 'fs/promises';
12
+ import {parse} from 'node-html-parser';
13
+ import {answer} from './index.ts';
14
+ import {reportsPath} from '../../util.ts';
15
15
 
16
16
  // SETUP AND TEARDOWN
17
17
 
18
18
  const savedAuthCode = process.env.AUTH_CODE;
19
19
  const savedDBDir = process.env.DB_DIR;
20
- const fixtureDBDir = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
20
+ import {fixtureDBDir} from '../../test/dbFixture.ts';
21
21
 
22
22
  before(() => {
23
23
  process.env.AUTH_CODE = 'test-auth-code';
@@ -36,7 +36,7 @@ after(async () => {
36
36
  // TESTS
37
37
 
38
38
  test('expungeReportsForm displays a list of reports when no submission', async () => {
39
- const result = await answer(null, '');
39
+ const result: any = await answer(null, '');
40
40
  assert.equal(result.status, 'ok');
41
41
  assert.ok(result.answerPage);
42
42
  const html = parse(result.answerPage);
@@ -44,13 +44,13 @@ test('expungeReportsForm displays a list of reports when no submission', async (
44
44
  });
45
45
 
46
46
  test('expungeReportsForm returns an error for an invalid auth code', async () => {
47
- const result = await answer(null, 'authCode=wrong&report=260101T0001-ct');
47
+ const result: any = await answer(null, 'authCode=wrong&report=260101T0001-ct');
48
48
  assert.equal(result.status, 'error');
49
49
  assert.equal(result.message, 'Invalid authorization code');
50
50
  });
51
51
 
52
52
  test('expungeReportsForm returns an error when deleting a nonexistent report', async () => {
53
- const result = await answer(null, 'authCode=test-auth-code&report=999999T9999-nope');
53
+ const result: any = await answer(null, 'authCode=test-auth-code&report=999999T9999-nope');
54
54
  assert.equal(result.status, 'error');
55
55
  assert.ok(result.message.includes('Deleting sole reports'));
56
56
  });
@@ -60,7 +60,7 @@ test('expungeReportsForm deletes a sole report with valid auth code', async () =
60
60
  const reportPath = path.join(reportsPath(), '260101T0001-ct.json');
61
61
  const backup = await fs.readFile(reportPath, 'utf8');
62
62
  try {
63
- const result = await answer(null, 'authCode=test-auth-code&report=260101T0001-ct');
63
+ const result: any = await answer(null, 'authCode=test-auth-code&report=260101T0001-ct');
64
64
  assert.equal(result.status, 'ok');
65
65
  await fs.access(reportPath).then(
66
66
  () => {throw new Error('Report should have been deleted');},
@@ -77,7 +77,7 @@ test('expungeReportsForm returns an error when a report file is corrupt', async
77
77
  const backup = await fs.readFile(reportPath, 'utf8');
78
78
  try {
79
79
  await fs.writeFile(reportPath, 'not valid json');
80
- const result = await answer(null, '');
80
+ const result: any = await answer(null, '');
81
81
  assert.equal(result.status, 'error');
82
82
  }
83
83
  finally {
@@ -87,7 +87,7 @@ test('expungeReportsForm returns an error when a report file is corrupt', async
87
87
 
88
88
  test('expungeReportsForm shows no-deletable message when every URL has at least 2 reports', async () => {
89
89
  // Create a temp DB with two reports sharing the same URL.
90
- const os = require('node:os');
90
+ const os = await import('node:os');
91
91
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'expunge-test-'));
92
92
  const tmpReports = path.join(tmpDir, 'reports');
93
93
  await fs.mkdir(tmpReports);
@@ -99,16 +99,14 @@ test('expungeReportsForm shows no-deletable message when every URL has at least
99
99
  const savedDBDir = process.env.DB_DIR;
100
100
  process.env.DB_DIR = tmpDir;
101
101
  try {
102
- delete require.cache[require.resolve('./index')];
103
- const {answer} = require('./index');
104
- const result = await answer(null, '');
102
+ const {answer} = await import('./index.ts');
103
+ const result: any = await answer(null, '');
105
104
  assert.equal(result.status, 'ok');
106
105
  assert.ok(result.answerPage.includes('no reports to delete'));
107
106
  assert.ok(result.answerPage.includes('disabled'));
108
107
  }
109
108
  finally {
110
109
  process.env.DB_DIR = savedDBDir;
111
- delete require.cache[require.resolve('./index')];
112
110
  await fs.rm(tmpDir, {recursive: true}).catch(() => {});
113
111
  }
114
112
  });
@@ -0,0 +1,18 @@
1
+ /*
2
+ index.ts
3
+ Serves a form for deleting sole reports.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ import {reportDeletionForm} from '../reportDeletion.ts';
9
+
10
+ // FUNCTIONS
11
+
12
+ // Returns a form for deleting sole reports.
13
+ export const answer = async (_: any, search: string) => reportDeletionForm(import.meta.dirname, search, {
14
+ intro: 'Choose the sole reports to delete.',
15
+ emptyIntro: 'Each target has at least 2 reports, so there are no reports to delete.',
16
+ failurePrefix: 'Deleting sole reports failed',
17
+ isDeletable: (specs, index) => specs[index - 1]?.url !== specs[index]?.url && specs[index + 1]?.url !== specs[index]?.url
18
+ });
@@ -1,23 +1,23 @@
1
1
  /*
2
- hideReportForm.test.js
3
- Unit tests for web/hideReportForm/index.js.
2
+ hideReportForm.test.ts
3
+ Unit tests for web/hideReportForm/index.ts.
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('fs/promises');
12
- const {parse} = require('node-html-parser');
13
- const {answer} = require('./index');
14
- const {reportsPath, hiddenReportsPath} = require('../../util');
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 'fs/promises';
12
+ import {parse} from 'node-html-parser';
13
+ import {answer} from './index.ts';
14
+ import {reportsPath, hiddenReportsPath} from '../../util.ts';
15
15
 
16
16
  // SETUP AND TEARDOWN
17
17
 
18
18
  const savedAuthCode = process.env.AUTH_CODE;
19
19
  const savedDBDir = process.env.DB_DIR;
20
- const fixtureDBDir = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
20
+ import {fixtureDBDir} from '../../test/dbFixture.ts';
21
21
 
22
22
  before(() => {
23
23
  process.env.AUTH_CODE = 'test-auth-code';
@@ -45,7 +45,7 @@ after(async () => {
45
45
  // TESTS
46
46
 
47
47
  test('hideReportForm displays a list of reports when no submission', async () => {
48
- const result = await answer(null, '');
48
+ const result: any = await answer(null, '');
49
49
  assert.equal(result.status, 'ok');
50
50
  assert.ok(result.answerPage);
51
51
  const html = parse(result.answerPage);
@@ -54,7 +54,7 @@ test('hideReportForm displays a list of reports when no submission', async () =>
54
54
  });
55
55
 
56
56
  test('hideReportForm returns an error for an invalid auth code', async () => {
57
- const result = await answer(null, 'authCode=wrong&report=260101T0009-brd');
57
+ const result: any = await answer(null, 'authCode=wrong&report=260101T0009-brd');
58
58
  assert.equal(result.status, 'error');
59
59
  assert.equal(result.message, 'Invalid authorization code');
60
60
  });
@@ -69,14 +69,14 @@ test('hideReportForm hides a report with valid auth code', async () => {
69
69
  catch {
70
70
  // Report may already be in reports.
71
71
  }
72
- const result = await answer(null, 'authCode=test-auth-code&report=260101T0009-brd');
72
+ const result: any = await answer(null, 'authCode=test-auth-code&report=260101T0009-brd');
73
73
  assert.equal(result.status, 'ok');
74
74
  // The report should now be in the hidden directory.
75
75
  await fs.access(hiddenPath);
76
76
  });
77
77
 
78
78
  test('hideReportForm returns an error when hiding a nonexistent report', async () => {
79
- const result = await answer(null, 'authCode=test-auth-code&report=999999T9999-nope');
79
+ const result: any = await answer(null, 'authCode=test-auth-code&report=999999T9999-nope');
80
80
  assert.equal(result.status, 'error');
81
81
  assert.ok(result.message.includes('Hiding report'));
82
82
  });
@@ -1,18 +1,18 @@
1
1
  /*
2
- index.js
2
+ index.ts
3
3
  Serves a form for hiding a report.
4
4
  */
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {getReportExtracts, hiddenReportsPath, reportsPath} = require('../../util');
9
- const fs = require('fs/promises');
10
- const path = require('path');
8
+ import {getReportExtracts, hiddenReportsPath, populateTemplate, reportsPath} from '../../util.ts';
9
+ import fs from 'node:fs/promises';
10
+ import path from 'node:path';
11
11
 
12
12
  // FUNCTIONS
13
13
 
14
14
  // Returns a form for hiding a report.
15
- exports.answer = async (_, search) => {
15
+ export const answer = async (_: any, search: string) => {
16
16
  const searchParams = new URLSearchParams(search);
17
17
  const authCode = searchParams?.get('authCode');
18
18
  const jobName = searchParams?.get('report');
@@ -26,7 +26,7 @@ exports.answer = async (_, search) => {
26
26
  await fs.rename(path.join(reportsPath(), fileName), path.join(hiddenReportsPath(), fileName));
27
27
  }
28
28
  // If this failed:
29
- catch (error) {
29
+ catch (error: any) {
30
30
  // Return why.
31
31
  return {
32
32
  status: 'error',
@@ -44,46 +44,42 @@ exports.answer = async (_, search) => {
44
44
  }
45
45
  }
46
46
  // Initialize an array of report specifications.
47
- const reportSpecs = [];
47
+ const reportSpecs: any[] = [];
48
48
  // Get data on all available reports.
49
49
  const reportExtracts = await getReportExtracts();
50
50
  // For each report:
51
51
  for (const reportExtract of reportExtracts) {
52
- const {jobID, timeStamp, what} = reportExtract;
52
+ const {jobID, timeStamp, description} = reportExtract;
53
53
  // Add the report to the array.
54
54
  reportSpecs.push({
55
- what,
55
+ description,
56
56
  timeStamp,
57
57
  jobID
58
58
  });
59
59
  }
60
60
  // Sort the data by page name and then by time stamp.
61
61
  reportSpecs.sort((a, b) => {
62
- if (a.what === b.what) {
62
+ if (a.description === b.description) {
63
63
  return a.timeStamp.localeCompare(b.timeStamp);
64
64
  }
65
- return a.what.localeCompare(b.what);
65
+ return a.description.localeCompare(b.description);
66
66
  });
67
- const lines = [];
67
+ const lines: string[] = [];
68
68
  const margin = ' '.repeat(12);
69
69
  // For each available report:
70
70
  reportSpecs.forEach(spec => {
71
- const {jobID, timeStamp, what} = spec;
72
- const specString = `${what} (job <code>${jobID}</code> at ${timeStamp})`;
71
+ const {jobID, timeStamp, description} = spec;
72
+ const specString = `${description} (job <code>${jobID}</code> at ${timeStamp})`;
73
73
  // Add a line with a radio button to hide it.
74
74
  lines.push(
75
75
  `${margin}<p><input type="radio" name="report" value="${timeStamp}-${jobID}"> ${specString}</p>`
76
76
  );
77
77
  });
78
- const query = {
78
+ const query: Record<string, string> = {
79
79
  reports: lines.join('\n'),
80
80
  };
81
- // Get the hiding form template.
82
- let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
83
- // Replace its placeholders.
84
- Object.keys(query).forEach(param => {
85
- answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
86
- });
81
+ // Get the populated hiding form template.
82
+ const answerPage = await populateTemplate(import.meta.dirname, query);
87
83
  // Return the populated page.
88
84
  return {
89
85
  status: 'ok',