@jrpool/kilotest 58.2.4 → 59.0.2

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 (66) hide show
  1. package/api/getReport.test.ts +1 -1
  2. package/api/listIssues.test.ts +32 -0
  3. package/api/listIssues.ts +38 -8
  4. package/api/listPages.test.ts +92 -0
  5. package/api/listPages.ts +93 -0
  6. package/api/orderNewTest.test.ts +2 -2
  7. package/api/orderNewTest.ts +4 -4
  8. package/api/orderRetest.test.ts +1 -1
  9. package/api/requestNewTest.test.ts +1 -1
  10. package/api/requestNewTest.ts +6 -6
  11. package/api/requestRetest.test.ts +3 -3
  12. package/api/requestRetest.ts +2 -2
  13. package/api/routes.test.ts +1 -1
  14. package/api/routes.ts +5 -5
  15. package/api/schemas.ts +24 -7
  16. package/api/util.ts +26 -45
  17. package/apiIntegration.test.ts +5 -5
  18. package/docs/AI-TOOLS.md +1 -1
  19. package/docs/SERVICE.md +16 -5
  20. package/index.test.ts +31 -19
  21. package/index.ts +25 -13
  22. package/lhm.plugin.json +1433 -141
  23. package/linkValidation.test.ts +2 -2
  24. package/llms-full.txt +9 -9
  25. package/llms.txt +4 -4
  26. package/mcp.test.ts +9 -9
  27. package/mcp.ts +14 -14
  28. package/openapi.yaml +84 -30
  29. package/package.json +1 -1
  30. package/routing.test.ts +5 -5
  31. package/server.json +1 -1
  32. package/sitemap.xml +1 -1
  33. package/smokeTest.ts +2 -2
  34. package/style.css +8 -8
  35. package/test/fixtures/buildFixtures.ts +3 -3
  36. package/test/fixtures/db/reports/260101T0000-mix.json +2 -2
  37. package/test/fixtures/db/reports/260202T0000-new.json +2 -2
  38. package/util.test.ts +31 -22
  39. package/util.ts +16 -5
  40. package/web/enqueueForm/index.html +1 -1
  41. package/web/hideReportForm/hideReportForm.test.ts +5 -0
  42. package/web/hideReportForm/index.ts +9 -6
  43. package/web/home/home.test.ts +19 -0
  44. package/{index.html → web/home/index.html} +2 -2
  45. package/web/home/index.ts +20 -0
  46. package/web/listDiagnoses/listDiagnoses.test.ts +1 -1
  47. package/web/listIssues/index.html +9 -10
  48. package/web/listIssues/index.ts +45 -3
  49. package/web/listIssues/listIssues.test.ts +82 -6
  50. package/web/{listReports → listPages}/index.html +1 -2
  51. package/web/listPages/index.ts +99 -0
  52. package/web/listPages/listPages.test.ts +196 -0
  53. package/web/listViolators/listViolators.test.ts +1 -1
  54. package/web/metrics/metrics.test.ts +2 -2
  55. package/web/requestNewTest/requestNewTest.test.ts +1 -1
  56. package/web/requestRetest/requestRetest.test.ts +1 -2
  57. package/web/requestRetestForm/index.ts +4 -2
  58. package/web/requestRetestForm/requestRetestForm.test.ts +12 -1
  59. package/web/unhideReportForm/index.ts +9 -6
  60. package/web/unhideReportForm/unhideReportForm.test.ts +8 -5
  61. package/web/util.test.ts +87 -23
  62. package/web/util.ts +27 -31
  63. package/api/listReports.test.ts +0 -109
  64. package/api/listReports.ts +0 -87
  65. package/web/listReports/index.ts +0 -182
  66. package/web/listReports/listReports.test.ts +0 -300
@@ -1,300 +0,0 @@
1
- /*
2
- listReports.test.ts
3
- UI tests for web/listReports/index.ts using the fixture corpus.
4
- */
5
-
6
- // IMPORTS
7
-
8
- import {test, before, after} from 'node:test';
9
- import assert from 'node:assert/strict';
10
- import 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
-
15
- // SETUP AND TEARDOWN
16
-
17
- const savedDBDir = process.env.DB_DIR;
18
-
19
- before(async () => {
20
- process.env.DB_DIR = (await import('../../test/dbFixture.ts')).fixtureDBDir;
21
- // Ensure job subdirectories exist (they are empty and not tracked by Git).
22
- for (const sub of ['queue', 'claimed', 'failed']) {
23
- await fs.mkdir(path.join(process.env.DB_DIR, 'jobs', sub), {recursive: true});
24
- }
25
- });
26
-
27
- after(() => {
28
- if (savedDBDir !== undefined) {
29
- process.env.DB_DIR = savedDBDir;
30
- }
31
- else {
32
- delete process.env.DB_DIR;
33
- }
34
- });
35
-
36
- // TESTS
37
-
38
- test('listReports returns an ok status with valid HTML', async () => {
39
- const result: any = await answer();
40
- assert.equal(result.status, 'ok');
41
- assert.ok(result.answerPage);
42
- const html = parse(result.answerPage);
43
- assert.equal(html.querySelector('title')?.text, 'Pages tested | Kilotest');
44
- });
45
-
46
- test('listReports includes the 8 non-hidden fixture reports as details elements', async () => {
47
- const result: any = await answer();
48
- const html = parse(result.answerPage);
49
- const details = html.querySelectorAll('details');
50
- assert.equal(details.length, 8);
51
- });
52
-
53
- test('listReports includes the page descriptions in summary elements', async () => {
54
- const result: any = await answer();
55
- const html = parse(result.answerPage);
56
- const summaries = html.querySelectorAll('details > summary').map(s => s.text);
57
- assert.ok(summaries.some(s => s.startsWith('Mixed Outcomes Page')));
58
- assert.ok(summaries.some(s => s.startsWith('All CantTell Page')));
59
- assert.ok(summaries.some(s => s.startsWith('No Outcomes Page')));
60
- assert.ok(summaries.some(s => s.startsWith('Empty Results Page')));
61
- assert.ok(summaries.some(s => s.startsWith('Prevented Page')));
62
- });
63
-
64
- test('listReports does not include the hidden report', async () => {
65
- const result: any = await answer();
66
- assert.ok(!result.answerPage.includes('Hidden Page'));
67
- });
68
-
69
- test('listReports includes links to listIssues for reports with issues', async () => {
70
- const result: any = await answer();
71
- const html = parse(result.answerPage);
72
- const issueLinks = html.querySelectorAll('a[href*="listIssues.html"]');
73
- assert.ok(issueLinks.length > 0);
74
- const hrefs = issueLinks.map(a => a.getAttribute('href'));
75
- assert.ok(hrefs.some(href => href?.includes('260101T0000/mix')));
76
- });
77
-
78
- test('listReports includes the page URLs in the report details', async () => {
79
- const result: any = await answer();
80
- assert.ok(result.answerPage.includes('https://example.com/mixed'));
81
- assert.ok(result.answerPage.includes('https://example.com/canttell'));
82
- });
83
-
84
- test('listReports includes a link to request testing a new page', async () => {
85
- const result: any = await answer();
86
- const html = parse(result.answerPage);
87
- const testLink = html.querySelector('a[href="requestNewTestForm.html"]');
88
- assert.ok(testLink);
89
- });
90
-
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
- try {
96
- const testRequests = {
97
- 'https://example.com/mixed': [
98
- {description: 'Mixed Outcomes Page', reason: 'Needs retesting for accessibility'}
99
- ]
100
- };
101
- await fs.writeFile(testRequestsPath, JSON.stringify(testRequests, null, 2));
102
- const result: any = await answer();
103
- assert.equal(result.status, 'ok');
104
- assert.ok(result.answerPage.includes('https://example.com/mixed'));
105
- assert.ok(result.answerPage.includes('Needs retesting for accessibility'));
106
- }
107
- finally {
108
- await fs.writeFile(testRequestsPath, originalTestRequests);
109
- }
110
- });
111
-
112
- test('listReports shows queued and claimed jobs when they exist', async () => {
113
- const dbDir = (await import('../../test/dbFixture.ts')).fixtureDBDir;
114
- const queueDir = path.join(dbDir, 'jobs', 'queue');
115
- const claimedDir = path.join(dbDir, 'jobs', 'claimed');
116
- const queueFile = path.join(queueDir, 'queuedJob.json');
117
- const claimedFile = path.join(claimedDir, 'claimedJob.json');
118
- try {
119
- const queuedJob = {
120
- target: {url: 'https://example.com/queued', what: 'Queued Page'}
121
- };
122
- const claimedJob = {
123
- target: {url: 'https://example.com/claimed', what: 'Claimed Page'}
124
- };
125
- await fs.writeFile(queueFile, JSON.stringify(queuedJob, null, 2));
126
- await fs.writeFile(claimedFile, JSON.stringify(claimedJob, null, 2));
127
- const result: any = await answer();
128
- assert.equal(result.status, 'ok');
129
- assert.ok(result.answerPage.includes('https://example.com/queued'));
130
- assert.ok(result.answerPage.includes('Queued Page'));
131
- assert.ok(result.answerPage.includes('https://example.com/claimed'));
132
- assert.ok(result.answerPage.includes('Claimed Page'));
133
- }
134
- finally {
135
- await fs.unlink(queueFile).catch(() => {});
136
- await fs.unlink(claimedFile).catch(() => {});
137
- }
138
- });
139
-
140
- test('listReports shows claimed retest status for a report with a matching claimed job', async () => {
141
- const dbDir = (await import('../../test/dbFixture.ts')).fixtureDBDir;
142
- const claimedDir = path.join(dbDir, 'jobs', 'claimed');
143
- const claimedFile = path.join(claimedDir, 'claimedRetest.json');
144
- try {
145
- const claimedJob = {
146
- target: {url: 'https://example.com/mixed', what: 'Mixed Outcomes Page'}
147
- };
148
- await fs.writeFile(claimedFile, JSON.stringify(claimedJob, null, 2));
149
- const result: any = await answer();
150
- assert.equal(result.status, 'ok');
151
- assert.ok(result.answerPage.includes('Currently being retested'));
152
- }
153
- finally {
154
- await fs.unlink(claimedFile).catch(() => {});
155
- }
156
- });
157
-
158
- test('listReports shows queued retest status for a report with a matching queued job', async () => {
159
- const dbDir = (await import('../../test/dbFixture.ts')).fixtureDBDir;
160
- const queueDir = path.join(dbDir, 'jobs', 'queue');
161
- const queueFile = path.join(queueDir, 'queuedRetest.json');
162
- try {
163
- const queuedJob = {
164
- target: {url: 'https://example.com/mixed', what: 'Mixed Outcomes Page'}
165
- };
166
- await fs.writeFile(queueFile, JSON.stringify(queuedJob, null, 2));
167
- const result: any = await answer();
168
- assert.equal(result.status, 'ok');
169
- assert.ok(result.answerPage.includes('Currently in the queue for retesting'));
170
- }
171
- finally {
172
- await fs.unlink(queueFile).catch(() => {});
173
- }
174
- });
175
-
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
- const reportsDir = path.join(dbDir, 'reports');
179
- const invalidReportPath = path.join(reportsDir, '260101T9999-bad.json');
180
- try {
181
- // Valid for getReportExtract (has target and jobData.endTime) but not usable by isUsableReport (no acts or catalog).
182
- const invalidReport = {
183
- target: {what: 'Invalid Report', url: 'https://example.com/invalid'},
184
- jobData: {endTime: '26-01-01T00:10'}
185
- };
186
- await fs.writeFile(invalidReportPath, JSON.stringify(invalidReport, null, 2));
187
- const result: any = await answer();
188
- assert.equal(result.status, 'error');
189
- assert.ok(result.message);
190
- }
191
- finally {
192
- await fs.unlink(invalidReportPath).catch(() => {});
193
- }
194
- });
195
-
196
- test('listReports shows no-reports message when the database is empty', async () => {
197
- // Create a temp DB with empty reports and jobs directories.
198
- const os = await import('node:os');
199
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'listreports-empty-'));
200
- await fs.mkdir(path.join(tmpDir, 'reports'), {recursive: true});
201
- await fs.mkdir(path.join(tmpDir, 'jobs', 'queue'), {recursive: true});
202
- await fs.mkdir(path.join(tmpDir, 'jobs', 'claimed'), {recursive: true});
203
- await fs.mkdir(path.join(tmpDir, 'jobs', 'failed'), {recursive: true});
204
- await fs.writeFile(path.join(tmpDir, 'jobs', 'testRequests.json'), '{}\n');
205
- const savedDBDir = process.env.DB_DIR;
206
- process.env.DB_DIR = tmpDir;
207
- try {
208
- const {answer} = await import('./index.ts');
209
- const result: any = await answer();
210
- assert.equal(result.status, 'ok');
211
- assert.ok(result.answerPage.includes('no'));
212
- assert.ok(result.answerPage.includes(' a '));
213
- }
214
- finally {
215
- process.env.DB_DIR = savedDBDir;
216
- await fs.rm(tmpDir, {recursive: true}).catch(() => {});
217
- }
218
- });
219
-
220
- // Returns the details element of the report whose summary starts with a description and whose issue link, if required, contains a fragment.
221
- const getReportDetails = (html: any, description: string, hrefFragment?: string) => {
222
- const matches = html.querySelectorAll('details').filter((d: any) => {
223
- const summary = d.querySelector('summary')?.text ?? '';
224
- if (!summary.startsWith(description)) {
225
- return false;
226
- }
227
- return hrefFragment === undefined
228
- || d.querySelectorAll('a').some((a: any) => a.getAttribute('href')?.includes(hrefFragment));
229
- });
230
- assert.equal(matches.length, 1, `exactly one report matches ${description} ${hrefFragment ?? ''}`);
231
- return matches[0];
232
- };
233
-
234
- // Returns the 3 rule-engine amounts and the 3 summary amounts of a details element.
235
- const getResultFacts = (details: any) => {
236
- const items = details.querySelectorAll('li');
237
- const itemTexts: string[] = items.map((li: any) => li.text.trim());
238
- const getAmount = (label: string) => {
239
- const text = itemTexts.find(t => t.startsWith(`${label}: `));
240
- assert.ok(text, `${label} item exists`);
241
- return Number(text.slice(label.length + 2));
242
- };
243
- // The summary item is the parent of the rule-engine item and the 3 amount items.
244
- const summaryItem = items.find((li: any) => li.text.trim().startsWith('Summary of results:'));
245
- assert.ok(summaryItem, 'Summary of results item exists');
246
- const summaryList = summaryItem.querySelector('ul');
247
- const amountLabels = summaryList.childNodes
248
- .filter((node: any) => node.tagName === 'LI')
249
- .map((li: any) => li.text.trim().split(':')[0]);
250
- return {
251
- called: getAmount('Called'),
252
- tested: getAmount('Were able to test'),
253
- reporters: getAmount('Reported any rule violations'),
254
- amountLabels,
255
- violations: getAmount('Violations'),
256
- violators: getAmount('Violators'),
257
- issues: getAmount('Issues')
258
- };
259
- };
260
-
261
- // Expected facts, hand-computed from the fixtures.
262
- const expectedFacts: [string, string | undefined, number, number, number, number, number, number][] = [
263
- // Description, issue-link fragment, engines called, engines able to test, reporting engines, violations, violators, issues.
264
- ['Mixed Outcomes Page', '260101T0000/mix', 2, 2, 2, 3, 2, 2],
265
- ['Mixed Outcomes Page', '260202T0000/new', 1, 1, 1, 1, 1, 1],
266
- ['All CantTell Page', undefined, 1, 1, 0, 0, 0, 0],
267
- ['No Outcomes Page', undefined, 1, 1, 1, 1, 1, 1],
268
- ['Empty Results Page', undefined, 1, 1, 0, 0, 0, 0],
269
- ['Prevented Page', undefined, 2, 1, 1, 1, 1, 1],
270
- ['Multi Violator Page', undefined, 2, 2, 2, 4, 3, 1],
271
- ['Branch Coverage Page', undefined, 3, 3, 1, 4, 4, 1]
272
- ];
273
-
274
- for (const [description, fragment, called, tested, reporters, violations, violators, issues] of expectedFacts) {
275
- test(`listReports gives correct rule-engine and summary amounts for ${description}${fragment ? ` (${fragment})` : ''}`, async () => {
276
- const result: any = await answer();
277
- const html = parse(result.answerPage);
278
- const facts = getResultFacts(getReportDetails(html, description, fragment));
279
- assert.equal(facts.called, called);
280
- assert.equal(facts.tested, tested);
281
- assert.equal(facts.reporters, reporters);
282
- assert.deepEqual(facts.amountLabels, ['Rule engines', 'Violations', 'Violators', 'Issues']);
283
- assert.equal(facts.violations, violations);
284
- assert.equal(facts.violators, violators);
285
- assert.equal(facts.issues, issues);
286
- });
287
- }
288
-
289
- test('listReports counts a prevented rule engine as called but not able to test', async () => {
290
- const result: any = await answer();
291
- const html = parse(result.answerPage);
292
- const prevented = getResultFacts(getReportDetails(html, 'Prevented Page'));
293
- assert.equal(prevented.called - prevented.tested, 1);
294
- const others = html.querySelectorAll('details').filter((d: any) => !d.text.includes('Prevented Page'));
295
- assert.equal(others.length, 7);
296
- assert.ok(others.every((d: any) => {
297
- const facts = getResultFacts(d);
298
- return facts.called === facts.tested;
299
- }));
300
- });