@jrpool/kilotest 46.0.8 → 46.0.14

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 (60) hide show
  1. package/.devin/config.local.json +4 -1
  2. package/.github/workflows/{smoke-test.yml → ci.yml} +8 -2
  3. package/README.md +14 -8
  4. package/alerts.test.js +137 -0
  5. package/api/listDiagnoses.test.js +44 -0
  6. package/api/listIssues.js +1 -1
  7. package/api/listIssues.test.js +10 -0
  8. package/api/listReports.test.js +6 -2
  9. package/api/listViolators.test.js +35 -0
  10. package/api/requestTest.test.js +7 -0
  11. package/api/routes.test.js +63 -0
  12. package/api/util.js +9 -11
  13. package/api/util.test.js +68 -2
  14. package/apiIntegration.test.js +13 -3
  15. package/docs/AI-TOOLS.md +41 -158
  16. package/docs/{Smoke Test.json → CI.json} +4 -3
  17. package/index.html +3 -3
  18. package/index.js +64 -77
  19. package/index.test.js +1152 -0
  20. package/indexBalances.test.js +188 -0
  21. package/indexFallback.test.js +33 -0
  22. package/indexServe.test.js +59 -0
  23. package/indexWorkers.test.js +79 -0
  24. package/lhm.plugin.json +3218 -0
  25. package/llms-full.txt +6 -7
  26. package/llms.txt +1 -1
  27. package/mcp.js +2 -2
  28. package/mcp.test.js +281 -0
  29. package/openapi.yaml +1 -1
  30. package/package.json +10 -5
  31. package/routing.test.js +13 -3
  32. package/scripts/generate-openapi.js +1 -1
  33. package/test/fixtures/buildFixtures.js +83 -0
  34. package/test/fixtures/db/reports/260101T0008-mul.json +137 -0
  35. package/test/fixtures/db/reports/260101T0009-brd.json +147 -0
  36. package/test/setup.cjs +14 -0
  37. package/util.js +16 -6
  38. package/util.test.js +886 -3
  39. package/web/ai0BalanceForm/ai0BalanceForm.test.js +88 -0
  40. package/web/expungeReportsForm/expungeReportsForm.test.js +114 -0
  41. package/web/hideReportForm/hideReportForm.test.js +82 -0
  42. package/web/listDiagnoses/listDiagnoses.test.js +160 -0
  43. package/web/listIssues/listIssues.test.js +109 -0
  44. package/web/listReports/listReports.test.js +139 -3
  45. package/web/listRules/listRules.test.js +103 -0
  46. package/web/listTopIssues/listTopIssues.test.js +126 -0
  47. package/web/listViolators/index.js +1 -2
  48. package/web/listViolators/listViolators.test.js +256 -0
  49. package/web/pruneReportsForm/pruneReportsForm.test.js +86 -0
  50. package/web/reannotate/reannotate.test.js +144 -0
  51. package/web/reannotateForm/reannotateForm.test.js +134 -0
  52. package/web/renewWCAG/renewWCAG.test.js +124 -0
  53. package/web/requestRetest/requestRetest.test.js +119 -0
  54. package/web/requestRetestForm/requestRetestForm.test.js +62 -0
  55. package/web/requestTest/requestTest.test.js +115 -0
  56. package/web/requestTestForm/requestTestForm.test.js +40 -0
  57. package/web/rewindReportsForm/rewindReportsForm.test.js +86 -0
  58. package/web/tutorial/index.html +3 -3
  59. package/web/tutorial/tutorial.test.js +97 -0
  60. package/web/unhideReportForm/unhideReportForm.test.js +135 -0
@@ -0,0 +1,115 @@
1
+ /*
2
+ requestTest.test.js
3
+ Unit tests for web/requestTest/index.js, covering the success path of answer.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ const {test, before, after, beforeEach} = require('node:test');
9
+ const assert = require('node:assert/strict');
10
+ const fs = require('node:fs/promises');
11
+ const path = require('node:path');
12
+ const {parse} = require('node-html-parser');
13
+
14
+ // CONSTANTS
15
+
16
+ const fixtureDBDir = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
17
+ const recsPath = path.join(fixtureDBDir, 'jobs', 'recs.json');
18
+
19
+ // SETUP AND TEARDOWN
20
+
21
+ before(async () => {
22
+ process.env.DB_DIR = fixtureDBDir;
23
+ process.env.AUTH_CODE = 'test-auth-code';
24
+ });
25
+
26
+ beforeEach(async () => {
27
+ // Reset recs.json and clean job directories before each test.
28
+ await fs.writeFile(recsPath, '{}\n');
29
+ for (const sub of ['claimed', 'queue', 'failed']) {
30
+ const dir = path.join(fixtureDBDir, 'jobs', sub);
31
+ await fs.mkdir(dir, {recursive: true});
32
+ const files = await fs.readdir(dir).catch(() => []);
33
+ for (const file of files) {
34
+ try {
35
+ await fs.unlink(path.join(dir, file));
36
+ }
37
+ catch {
38
+ // Ignore cleanup errors for files that may not exist.
39
+ }
40
+ }
41
+ }
42
+ });
43
+
44
+ after(async () => {
45
+ // Restore recs.json and clean job directories after all tests.
46
+ await fs.writeFile(recsPath, '{}\n');
47
+ for (const sub of ['claimed', 'queue', 'failed']) {
48
+ const dir = path.join(fixtureDBDir, 'jobs', sub);
49
+ await fs.mkdir(dir, {recursive: true});
50
+ const files = await fs.readdir(dir).catch(() => []);
51
+ for (const file of files) {
52
+ try {
53
+ await fs.unlink(path.join(dir, file));
54
+ }
55
+ catch {
56
+ // Ignore cleanup errors for files that may not exist.
57
+ }
58
+ }
59
+ }
60
+ });
61
+
62
+ // TESTS
63
+
64
+ test('answer returns ok with a populated answer page for a valid test request', async () => {
65
+ // Require the module after DB_DIR is set.
66
+ const {answer} = require('./index');
67
+ const result = await answer(
68
+ 'Test Page', 'https://example.com/test-success', 'Because accessibility'
69
+ );
70
+ assert.equal(result.status, 'ok');
71
+ assert.ok(result.answerPage, 'answerPage should be present');
72
+ // Verify the placeholders were replaced.
73
+ assert.ok(result.answerPage.includes('Test Page'));
74
+ assert.ok(result.answerPage.includes('Because accessibility'));
75
+ assert.ok(!result.answerPage.includes('__target__'));
76
+ assert.ok(!result.answerPage.includes('__why__'));
77
+ // Verify the HTML is well-formed.
78
+ const doc = parse(result.answerPage);
79
+ const h1 = doc.querySelector('h1');
80
+ assert.ok(h1);
81
+ assert.ok(h1.textContent.includes('Test Page'));
82
+ });
83
+
84
+ test('answer returns an error when the URL is already queued', async () => {
85
+ const {answer} = require('./index');
86
+ // Create a queued job with the target URL.
87
+ const queueDir = path.join(fixtureDBDir, 'jobs', 'queue');
88
+ const jobPath = path.join(queueDir, 'queuedJob.json');
89
+ await fs.writeFile(jobPath, JSON.stringify({
90
+ id: 'queuedJob',
91
+ target: {what: 'Queued Page', url: 'https://example.com/queued'}
92
+ }));
93
+ try {
94
+ const result = await answer(
95
+ 'Queued Page', 'https://example.com/queued', 'Because accessibility'
96
+ );
97
+ assert.equal(result.status, 'error');
98
+ assert.ok(result.message.includes('queued'));
99
+ }
100
+ finally {
101
+ try {
102
+ await fs.unlink(jobPath);
103
+ }
104
+ catch {
105
+ // Ignore cleanup errors.
106
+ }
107
+ }
108
+ });
109
+
110
+ test('answer returns an error for an invalid recommendation', async () => {
111
+ const {answer} = require('./index');
112
+ const result = await answer('', 'not-a-url', 'why');
113
+ assert.equal(result.status, 'error');
114
+ assert.equal(result.message, 'Invalid recommendation');
115
+ });
@@ -0,0 +1,40 @@
1
+ /*
2
+ requestTestForm.test.js
3
+ Unit tests for web/requestTestForm/index.js.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ const {test} = require('node:test');
9
+ const assert = require('node:assert/strict');
10
+ const {parse} = require('node-html-parser');
11
+ const {answer} = require('./index');
12
+
13
+ // TESTS
14
+
15
+ test('requestTestForm returns an ok status with valid HTML', async () => {
16
+ const result = await answer();
17
+ assert.equal(result.status, 'ok');
18
+ assert.ok(result.answerPage);
19
+ const html = parse(result.answerPage);
20
+ assert.equal(html.querySelector('title').text, 'Test recommendation | Kilotest');
21
+ });
22
+
23
+ test('requestTestForm includes a form that posts to /requestTest.html', async () => {
24
+ const result = await answer();
25
+ const html = parse(result.answerPage);
26
+ const form = html.querySelector('form');
27
+ assert.ok(form);
28
+ assert.equal(form.getAttribute('action'), '/requestTest.html');
29
+ assert.equal(form.getAttribute('method'), 'post');
30
+ });
31
+
32
+ test('requestTestForm includes required inputs for what, url, and why', async () => {
33
+ const result = await answer();
34
+ const html = parse(result.answerPage);
35
+ const inputs = html.querySelectorAll('input');
36
+ const names = inputs.map(input => input.getAttribute('name'));
37
+ assert.ok(names.includes('what'));
38
+ assert.ok(names.includes('url'));
39
+ assert.ok(names.includes('why'));
40
+ });
@@ -0,0 +1,86 @@
1
+ /*
2
+ rewindReportsForm.test.js
3
+ Unit tests for web/rewindReportsForm/index.js.
4
+ */
5
+
6
+ // IMPORTS
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');
15
+
16
+ // SETUP AND TEARDOWN
17
+
18
+ const savedAuthCode = process.env.AUTH_CODE;
19
+ const savedDBDir = process.env.DB_DIR;
20
+ const fixtureDBDir = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
21
+
22
+ before(() => {
23
+ process.env.AUTH_CODE = 'test-auth-code';
24
+ process.env.DB_DIR = fixtureDBDir;
25
+ });
26
+
27
+ after(async () => {
28
+ if (savedAuthCode !== undefined) {
29
+ process.env.AUTH_CODE = savedAuthCode;
30
+ }
31
+ if (savedDBDir !== undefined) {
32
+ process.env.DB_DIR = savedDBDir;
33
+ }
34
+ });
35
+
36
+ // TESTS
37
+
38
+ test('rewindReportsForm displays a list of reports when no submission', async () => {
39
+ const result = await answer(null, '');
40
+ assert.equal(result.status, 'ok');
41
+ assert.ok(result.answerPage);
42
+ const html = parse(result.answerPage);
43
+ assert.ok(html.querySelector('title'));
44
+ });
45
+
46
+ test('rewindReportsForm returns an error for an invalid auth code', async () => {
47
+ const result = await answer(null, 'authCode=wrong&report=260202T0000-new');
48
+ assert.equal(result.status, 'error');
49
+ assert.equal(result.message, 'Invalid authorization code');
50
+ });
51
+
52
+ test('rewindReportsForm returns an error when deleting a nonexistent report', async () => {
53
+ const result = await answer(null, 'authCode=test-auth-code&report=999999T9999-nope');
54
+ assert.equal(result.status, 'error');
55
+ assert.ok(result.message.includes('Deleting latest superseding reports'));
56
+ });
57
+
58
+ test('rewindReportsForm deletes a latest superseding report with valid auth code', async () => {
59
+ // 260202T0000-new is the latest report for its URL, superseding 260101T0000-mix.
60
+ const reportPath = path.join(reportsPath(), '260202T0000-new.json');
61
+ const backup = await fs.readFile(reportPath, 'utf8');
62
+ try {
63
+ const result = await answer(null, 'authCode=test-auth-code&report=260202T0000-new');
64
+ assert.equal(result.status, 'ok');
65
+ await fs.access(reportPath).then(
66
+ () => {throw new Error('Report should have been deleted');},
67
+ () => {}
68
+ );
69
+ }
70
+ finally {
71
+ await fs.writeFile(reportPath, backup);
72
+ }
73
+ });
74
+
75
+ test('rewindReportsForm returns an error when a report file is corrupt', async () => {
76
+ const reportPath = path.join(reportsPath(), '260101T0001-ct.json');
77
+ const backup = await fs.readFile(reportPath, 'utf8');
78
+ try {
79
+ await fs.writeFile(reportPath, 'not valid json');
80
+ const result = await answer(null, '');
81
+ assert.equal(result.status, 'error');
82
+ }
83
+ finally {
84
+ await fs.writeFile(reportPath, backup);
85
+ }
86
+ });
@@ -334,7 +334,7 @@
334
334
  <section id="testing-page">
335
335
  <h3>Testing the page</h3>
336
336
  <h4>By rule engines</h4>
337
- <p>What do rule engines find about this issue when they test this page? For a quick answer, Kilotest subjected the page to testing by all 10 rule engines in its ensemble. Two of the rule engines reported rule violations belonging to this issue:</p>
337
+ <p>What do rule engines find about this issue when they test this page? For a quick answer, Kilotest subjected the page to testing by the 10 rule engines that were then in its ensemble (since then, 2 more were added, so now there are 12). Two of the rule engines reported rule violations belonging to this issue:</p>
338
338
  <ul>
339
339
  <li>The Testaro rule engine reported, <q>input has no autocomplete="email" attribute</q>. In other words, Testaro recognized this input as asking for the user&rsquo;s email address and therefore as requiring an <code>autocomplete="email"</code> attribute, but not having one.</li>
340
340
  <li>The HTML CodeSniffer rule engine, referencing this <code>form</code> element, reported, <q>Invalid autocomplete value: new-password. Element does not belong to Password control group.</q>. In other words, HTML CodeSniffer recognized that this form is not a password input (a form is not an input at all!) and therefore is not eligible for an <code>autocomplete="new-password"</code> attribute, but has one.</li>
@@ -346,7 +346,7 @@
346
346
  </ul>
347
347
  <p>You might think that this shows rule engines were effective in revealing the defects related to this issue, but this is not a success story. Yes, two rule engines found related defects, but the other eight rule engines did not. Inspection of those eight rule engines reveals that six of them include rules enforcing this standard in some way. So, did these six rule engines report false negatives? Did they fail to enforce their applicable rules here? Perhaps, but perhaps not. Suppose a rule engine has a rule that requires the value of any <code>autocomplete</code> attribute to be in the list of allowed values. Well, <code>new-password</code> is in the list. So that rule engine would, properly, not report a violation of <strong>that</strong> rule.</p>
348
348
  <p>We can clearly see from this example that no rule engine will find all accessibility defects, not even all defects that <strong>can</strong> be detected by rule engines. That is why Kilotest uses an <strong>ensemble</strong> of rule engines.</p>
349
- <p>Of course, running ten rule engines also creates a risk of false positives reported by ten rule engines. In this particular case, there were none. For example, no rule engine claimed to find a defect related to this issue someplace else on the page. Kilotest, like any platform that integrates multiple rule engines, faces greater risks of false positives than a single rule engine would. It combats false positives by investigating user complaints and deprecating rules whose tests are found faulty.</p>
349
+ <p>Of course, running twelve rule engines also creates a risk of false positives reported by twelve rule engines. In this particular case, there were none. For example, no rule engine claimed to find a defect related to this issue someplace else on the page. Kilotest, like any platform that integrates multiple rule engines, faces greater risks of false positives than a single rule engine would. It combats false positives by investigating user complaints and deprecating rules whose tests are found faulty.</p>
350
350
  <h4>By AI agents</h4>
351
351
  <p>An AI agent powered by a frontier model was asked to visit the page and report any accessibility defect related to <code>autocomplete</code> attributes. The agent worked for about 3 minutes and then reported a missing <code>autocomplete="email"</code> attribute in the newsletter subscription form. But the page has no newsletter subscription form! It has a form to request a guide, not to subscribe to anything. Moreover, the agent provided code to support its conclusion, and the code differed pervasively from the actual code on the page. This response was a complete fabrication. The agent confessed to it, explaining that it resorted to hallucination when its security restrictions prevented it from accessing the necessary page data.</p>
352
352
  <p>A similar AI agent returned correct diagnoses when it was fed the page as an HTML document. That worked, because the two defects relevant to this practicum are discoverable from the HTML of the rendered page. Any defects arising from stylesheets or scripts would have been missed.</p>
@@ -418,7 +418,7 @@
418
418
  <legend>Choose the best answer</legend>
419
419
  <p>In this practicum, which rule engines discovered both <code>autocomplete</code> defects?</p>
420
420
  <ol>
421
- <li><label><input type="radio" name="q4" value="a"> All 10 rule engines</label></li>
421
+ <li><label><input type="radio" name="q4" value="a"> All 12 rule engines</label></li>
422
422
  <li><label><input type="radio" name="q4" value="b"> Six of the rule engines</label></li>
423
423
  <li><label><input type="radio" name="q4" value="c"> Only Testaro and HTML CodeSniffer</label></li>
424
424
  <li><label><input type="radio" name="q4" value="d"> None of the rule engines</label></li>
@@ -0,0 +1,97 @@
1
+ /*
2
+ tutorial.test.js
3
+ Unit tests for web/tutorial/index.js, covering the answer and handleComment exports.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ const {test, before, after} = require('node:test');
9
+ const assert = require('node:assert/strict');
10
+ const fs = require('node:fs/promises');
11
+ const path = require('node:path');
12
+ const {answer, handleComment} = require('./index');
13
+
14
+ // CONSTANTS
15
+
16
+ const commentsPath = path.join(__dirname, 'comments.json');
17
+
18
+ // SETUP AND TEARDOWN
19
+
20
+ let originalComments;
21
+
22
+ before(async () => {
23
+ originalComments = await fs.readFile(commentsPath, 'utf8').catch(() => null);
24
+ });
25
+
26
+ after(async () => {
27
+ if (originalComments !== null) {
28
+ await fs.writeFile(commentsPath, originalComments);
29
+ }
30
+ else {
31
+ await fs.unlink(commentsPath).catch(() => {});
32
+ }
33
+ });
34
+
35
+ // TESTS
36
+
37
+ test('answer returns the tutorial page with status ok', async () => {
38
+ const result = await answer();
39
+ assert.equal(result.status, 'ok');
40
+ assert.ok(result.answerPage);
41
+ assert.ok(result.answerPage.length > 0);
42
+ });
43
+
44
+ test('handleComment returns an error for empty content', async () => {
45
+ const result = await handleComment('');
46
+ assert.equal(result.status, 'error');
47
+ assert.equal(result.message, 'No content provided');
48
+ });
49
+
50
+ test('handleComment returns an error for non-string content', async () => {
51
+ const result = await handleComment(null);
52
+ assert.equal(result.status, 'error');
53
+ assert.equal(result.message, 'No content provided');
54
+ });
55
+
56
+ test('handleComment returns an error for content that is empty after sanitization', async () => {
57
+ const result = await handleComment('<script></script>');
58
+ assert.equal(result.status, 'error');
59
+ assert.equal(result.message, 'Comment is empty after sanitization');
60
+ });
61
+
62
+ test('handleComment saves a sanitized comment and returns ok', async () => {
63
+ // Replace comments with an empty array for a clean test.
64
+ await fs.writeFile(commentsPath, '[]\n');
65
+ const result = await handleComment('This is a <b>test</b> comment.');
66
+ assert.equal(result.status, 'ok');
67
+ const comments = JSON.parse(await fs.readFile(commentsPath, 'utf8'));
68
+ assert.equal(comments.length, 1);
69
+ assert.equal(comments[0].content, 'This is a test comment.');
70
+ assert.ok(comments[0].timeStamp);
71
+ });
72
+
73
+ test('handleComment strips HTML tags and control characters', async () => {
74
+ await fs.writeFile(commentsPath, '[]\n');
75
+ const result = await handleComment('<img src=x onerror=alert(1)>\x00\x07Hello');
76
+ assert.equal(result.status, 'ok');
77
+ const comments = JSON.parse(await fs.readFile(commentsPath, 'utf8'));
78
+ assert.equal(comments[0].content, 'Hello');
79
+ });
80
+
81
+ test('handleComment truncates content to 500 characters', async () => {
82
+ await fs.writeFile(commentsPath, '[]\n');
83
+ const longComment = 'x'.repeat(600);
84
+ const result = await handleComment(longComment);
85
+ assert.equal(result.status, 'ok');
86
+ const comments = JSON.parse(await fs.readFile(commentsPath, 'utf8'));
87
+ assert.equal(comments[0].content.length, 500);
88
+ });
89
+
90
+ test('handleComment creates comments.json when it does not exist', async () => {
91
+ await fs.unlink(commentsPath).catch(() => {});
92
+ const result = await handleComment('Test comment for missing file');
93
+ assert.equal(result.status, 'ok');
94
+ const comments = JSON.parse(await fs.readFile(commentsPath, 'utf8'));
95
+ assert.equal(comments.length, 1);
96
+ assert.equal(comments[0].content, 'Test comment for missing file');
97
+ });
@@ -0,0 +1,135 @@
1
+ /*
2
+ unhideReportForm.test.js
3
+ Unit tests for web/unhideReportForm/index.js.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ const {test, before, after, beforeEach} = 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');
15
+
16
+ // CONSTANTS
17
+
18
+ const hiddenFile = '260101T0007-hid.json';
19
+
20
+ // SETUP AND TEARDOWN
21
+
22
+ const savedAuthCode = process.env.AUTH_CODE;
23
+ const savedDBDir = process.env.DB_DIR;
24
+ const fixtureDBDir = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
25
+
26
+ const getPaths = () => ({
27
+ hidden: path.join(hiddenReportsPath(), hiddenFile),
28
+ report: path.join(reportsPath(), hiddenFile)
29
+ });
30
+
31
+ before(() => {
32
+ process.env.AUTH_CODE = 'test-auth-code';
33
+ process.env.DB_DIR = fixtureDBDir;
34
+ });
35
+
36
+ beforeEach(async () => {
37
+ // Ensure the hidden report is in the hidden directory before each test.
38
+ const {hidden, report} = getPaths();
39
+ try {
40
+ await fs.rename(report, hidden);
41
+ }
42
+ catch {
43
+ // Report may already be hidden.
44
+ }
45
+ });
46
+
47
+ after(async () => {
48
+ // Restore the hidden report if it was moved.
49
+ const {hidden, report} = getPaths();
50
+ try {
51
+ await fs.rename(report, hidden);
52
+ }
53
+ catch {
54
+ // Report may already be hidden.
55
+ }
56
+ if (savedAuthCode !== undefined) {
57
+ process.env.AUTH_CODE = savedAuthCode;
58
+ }
59
+ if (savedDBDir !== undefined) {
60
+ process.env.DB_DIR = savedDBDir;
61
+ }
62
+ });
63
+
64
+ // TESTS
65
+
66
+ test('unhideReportForm displays a list of hidden reports when no submission', async () => {
67
+ const result = await answer(null, '');
68
+ assert.equal(result.status, 'ok');
69
+ assert.ok(result.answerPage);
70
+ const html = parse(result.answerPage);
71
+ const radios = html.querySelectorAll('input[type="radio"]');
72
+ assert.ok(radios.length > 0);
73
+ });
74
+
75
+ test('unhideReportForm returns an error for an invalid auth code', async () => {
76
+ const result = await answer(null, 'authCode=wrong&report=260101T0007-hid');
77
+ assert.equal(result.status, 'error');
78
+ assert.equal(result.message, 'Invalid authorization code');
79
+ });
80
+
81
+ test('unhideReportForm unhides a report with valid auth code', async () => {
82
+ const {report} = getPaths();
83
+ const result = await answer(null, 'authCode=test-auth-code&report=260101T0007-hid');
84
+ assert.equal(result.status, 'ok');
85
+ // The report should now be in the reports directory.
86
+ await fs.access(report);
87
+ });
88
+
89
+ test('unhideReportForm returns an error when unhiding a nonexistent report', async () => {
90
+ const result = await answer(null, 'authCode=test-auth-code&report=999999T9999-nope');
91
+ assert.equal(result.status, 'error');
92
+ assert.ok(result.message.includes('Unhiding report'));
93
+ });
94
+
95
+ test('unhideReportForm sorts hidden reports with the same page name by timeStamp', async () => {
96
+ const {hidden} = getPaths();
97
+ // Add a second hidden report with the same what as the existing one.
98
+ const existing = JSON.parse(await fs.readFile(hidden, 'utf8'));
99
+ // Create a copy with a different timeStamp but same target.what.
100
+ const copy = JSON.parse(JSON.stringify(existing));
101
+ copy.id = '260101T0010-hid2';
102
+ const copyPath = path.join(hiddenReportsPath(), '260101T0010-hid2.json');
103
+ await fs.writeFile(copyPath, JSON.stringify(copy));
104
+ try {
105
+ const result = await answer(null, '');
106
+ assert.equal(result.status, 'ok');
107
+ // Both reports should appear in the form.
108
+ assert.ok(result.answerPage.includes('260101T0007-hid'));
109
+ assert.ok(result.answerPage.includes('260101T0010-hid2'));
110
+ }
111
+ finally {
112
+ await fs.unlink(copyPath).catch(() => {});
113
+ }
114
+ });
115
+
116
+ test('unhideReportForm sorts hidden reports with different page names alphabetically', async () => {
117
+ const {hidden} = getPaths();
118
+ // Add a second hidden report with a different what.
119
+ const existing = JSON.parse(await fs.readFile(hidden, 'utf8'));
120
+ const copy = JSON.parse(JSON.stringify(existing));
121
+ copy.id = '260101T0011-hid3';
122
+ copy.target.what = 'A Different Page';
123
+ const copyPath = path.join(hiddenReportsPath(), '260101T0011-hid3.json');
124
+ await fs.writeFile(copyPath, JSON.stringify(copy));
125
+ try {
126
+ const result = await answer(null, '');
127
+ assert.equal(result.status, 'ok');
128
+ // Both reports should appear in the form.
129
+ assert.ok(result.answerPage.includes('260101T0007-hid'));
130
+ assert.ok(result.answerPage.includes('260101T0011-hid3'));
131
+ }
132
+ finally {
133
+ await fs.unlink(copyPath).catch(() => {});
134
+ }
135
+ });