@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.
- package/.devin/config.local.json +4 -1
- package/.github/workflows/{smoke-test.yml → ci.yml} +8 -2
- package/README.md +14 -8
- package/alerts.test.js +137 -0
- package/api/listDiagnoses.test.js +44 -0
- package/api/listIssues.js +1 -1
- package/api/listIssues.test.js +10 -0
- package/api/listReports.test.js +6 -2
- package/api/listViolators.test.js +35 -0
- package/api/requestTest.test.js +7 -0
- package/api/routes.test.js +63 -0
- package/api/util.js +9 -11
- package/api/util.test.js +68 -2
- package/apiIntegration.test.js +13 -3
- package/docs/AI-TOOLS.md +41 -158
- package/docs/{Smoke Test.json → CI.json} +4 -3
- package/index.html +3 -3
- package/index.js +64 -77
- package/index.test.js +1152 -0
- package/indexBalances.test.js +188 -0
- package/indexFallback.test.js +33 -0
- package/indexServe.test.js +59 -0
- package/indexWorkers.test.js +79 -0
- package/lhm.plugin.json +3218 -0
- package/llms-full.txt +6 -7
- package/llms.txt +1 -1
- package/mcp.js +2 -2
- package/mcp.test.js +281 -0
- package/openapi.yaml +1 -1
- package/package.json +10 -5
- package/routing.test.js +13 -3
- package/scripts/generate-openapi.js +1 -1
- package/test/fixtures/buildFixtures.js +83 -0
- package/test/fixtures/db/reports/260101T0008-mul.json +137 -0
- package/test/fixtures/db/reports/260101T0009-brd.json +147 -0
- package/test/setup.cjs +14 -0
- package/util.js +16 -6
- package/util.test.js +886 -3
- package/web/ai0BalanceForm/ai0BalanceForm.test.js +88 -0
- package/web/expungeReportsForm/expungeReportsForm.test.js +114 -0
- package/web/hideReportForm/hideReportForm.test.js +82 -0
- package/web/listDiagnoses/listDiagnoses.test.js +160 -0
- package/web/listIssues/listIssues.test.js +109 -0
- package/web/listReports/listReports.test.js +139 -3
- package/web/listRules/listRules.test.js +103 -0
- package/web/listTopIssues/listTopIssues.test.js +126 -0
- package/web/listViolators/index.js +1 -2
- package/web/listViolators/listViolators.test.js +256 -0
- package/web/pruneReportsForm/pruneReportsForm.test.js +86 -0
- package/web/reannotate/reannotate.test.js +144 -0
- package/web/reannotateForm/reannotateForm.test.js +134 -0
- package/web/renewWCAG/renewWCAG.test.js +124 -0
- package/web/requestRetest/requestRetest.test.js +119 -0
- package/web/requestRetestForm/requestRetestForm.test.js +62 -0
- package/web/requestTest/requestTest.test.js +115 -0
- package/web/requestTestForm/requestTestForm.test.js +40 -0
- package/web/rewindReportsForm/rewindReportsForm.test.js +86 -0
- package/web/tutorial/index.html +3 -3
- package/web/tutorial/tutorial.test.js +97 -0
- package/web/unhideReportForm/unhideReportForm.test.js +135 -0
package/util.test.js
CHANGED
|
@@ -1,19 +1,58 @@
|
|
|
1
1
|
/*
|
|
2
2
|
util.test.js
|
|
3
|
-
Tests for util.js data-path injection (Phase 1).
|
|
3
|
+
Tests for util.js data-path injection (Phase 1) and utility functions.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
const {test} = require('node:test');
|
|
8
|
+
const {test, before, after} = require('node:test');
|
|
9
9
|
const assert = require('node:assert/strict');
|
|
10
10
|
const path = require('node:path');
|
|
11
|
+
const fs = require('node:fs/promises');
|
|
11
12
|
const {
|
|
13
|
+
annotateReport,
|
|
14
|
+
createLock,
|
|
12
15
|
dbPath,
|
|
16
|
+
getAgoDays,
|
|
17
|
+
getAgoString,
|
|
18
|
+
getCountString,
|
|
19
|
+
getDateString,
|
|
20
|
+
getDateTime,
|
|
21
|
+
getEngineList,
|
|
22
|
+
getIssue,
|
|
23
|
+
getJSON,
|
|
24
|
+
getLatestReportExtracts,
|
|
25
|
+
getMultiReportWhats,
|
|
26
|
+
getNowStamp,
|
|
27
|
+
getObject,
|
|
28
|
+
getPageData,
|
|
29
|
+
getPageDataStrings,
|
|
30
|
+
getPlainText,
|
|
31
|
+
getRandomString,
|
|
32
|
+
getReportExtract,
|
|
33
|
+
getReportExtracts,
|
|
34
|
+
getReportPath,
|
|
35
|
+
getReportStats,
|
|
36
|
+
getTextFragmentHref,
|
|
37
|
+
getTimeStamp,
|
|
38
|
+
getWCAGLink,
|
|
39
|
+
getWeightName,
|
|
40
|
+
hiddenReportsPath,
|
|
41
|
+
htmlSafe,
|
|
42
|
+
isHidden,
|
|
43
|
+
isJobID,
|
|
44
|
+
isReportAvailable,
|
|
45
|
+
isTimeStamp,
|
|
46
|
+
isURL,
|
|
13
47
|
jobsPath,
|
|
48
|
+
makeBreakable,
|
|
49
|
+
minifyURL,
|
|
50
|
+
objectSort,
|
|
51
|
+
processTestRequest,
|
|
52
|
+
recsLock,
|
|
14
53
|
recsPath,
|
|
15
54
|
reportsPath,
|
|
16
|
-
|
|
55
|
+
updateRecs
|
|
17
56
|
} = require('./util');
|
|
18
57
|
|
|
19
58
|
// TESTS
|
|
@@ -83,3 +122,847 @@ test('changing DB_DIR between calls is reflected by the path functions', () => {
|
|
|
83
122
|
}
|
|
84
123
|
}
|
|
85
124
|
});
|
|
125
|
+
|
|
126
|
+
test('getAgoDays returns a positive number for a valid time stamp', () => {
|
|
127
|
+
const days = getAgoDays('260101T0000');
|
|
128
|
+
assert.ok(typeof days === 'number');
|
|
129
|
+
assert.ok(days > 0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('getAgoDays returns null for an invalid string', () => {
|
|
133
|
+
assert.equal(getAgoDays('invalid'), null);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('getAgoDays returns null for a non-string non-Date argument', () => {
|
|
137
|
+
assert.equal(getAgoDays(42), null);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('getAgoDays returns 0 for the current time', () => {
|
|
141
|
+
assert.equal(getAgoDays(new Date()), 0);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('getAgoString returns "1 day" for a 1-day-old timestamp', () => {
|
|
145
|
+
const date = new Date(Date.now() - 1 * 24 * 60 * 60 * 1000);
|
|
146
|
+
assert.equal(
|
|
147
|
+
getAgoString(date.toISOString().slice(2).replace(/[-:]/g, '').slice(0, 11)), '1 day'
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('getAgoString returns "3 days" for a 3-day-old timestamp', () => {
|
|
152
|
+
const date = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000);
|
|
153
|
+
assert.equal(
|
|
154
|
+
getAgoString(date.toISOString().slice(2).replace(/[-:]/g, '').slice(0, 11)), '3 days'
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('getCountString returns singular for count 1', () => {
|
|
159
|
+
assert.equal(getCountString(1, 'report', 'reports'), '1 report');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('getCountString returns plural for count 0', () => {
|
|
163
|
+
assert.equal(getCountString(0, 'report', 'reports'), '0 reports');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('getDateString returns a valid date string for a valid time stamp', () => {
|
|
167
|
+
assert.equal(getDateString('260101T0000'), '2026-01-01');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('getDateString returns empty string for an invalid time stamp', () => {
|
|
171
|
+
assert.equal(getDateString('999999T9999'), '');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('getDateTime returns a Date for a valid time stamp', () => {
|
|
175
|
+
const date = getDateTime('260101T0000');
|
|
176
|
+
assert.ok(date instanceof Date);
|
|
177
|
+
assert.equal(date.toISOString(), '2026-01-01T00:00:00.000Z');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('getDateTime returns null for an invalid time stamp', () => {
|
|
181
|
+
assert.equal(getDateTime('invalid'), null);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('getIssue returns an issue ID for a known engine and rule', () => {
|
|
185
|
+
const issueID = getIssue('axe', 'link-name');
|
|
186
|
+
assert.ok(typeof issueID === 'string' || issueID === null);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('getIssue returns null for an unknown engine', () => {
|
|
190
|
+
assert.equal(getIssue('nonexistentEngine', 'anyRule'), null);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('getJSON returns a JSON string with a trailing newline', () => {
|
|
194
|
+
assert.equal(getJSON({a: 1}), '{\n "a": 1\n}\n');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('getObject returns the parsed object for a valid JSON file', async () => {
|
|
198
|
+
const result = await getObject(path.join(__dirname, 'package.json'));
|
|
199
|
+
assert.ok(typeof result === 'object');
|
|
200
|
+
assert.equal(result.name, '@jrpool/kilotest');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('getObject returns an error string for a nonexistent file', async () => {
|
|
204
|
+
const result = await getObject('/tmp/nonexistent-file.json');
|
|
205
|
+
assert.equal(typeof result, 'string');
|
|
206
|
+
assert.ok(result.startsWith('ERROR'));
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('getNowStamp returns an 11-character time stamp', () => {
|
|
210
|
+
const stamp = getNowStamp();
|
|
211
|
+
assert.equal(stamp.length, 11);
|
|
212
|
+
assert.ok(/^\d{6}T\d{4}$/.test(stamp));
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('getPlainText replaces special characters', () => {
|
|
216
|
+
assert.equal(getPlainText('a<b>c&d'), 'a b c+d');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('getRandomString returns a string of the requested length', () => {
|
|
220
|
+
const s = getRandomString(10);
|
|
221
|
+
assert.equal(s.length, 10);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('getTextFragmentHref returns a text-fragment URL', () => {
|
|
225
|
+
const href = getTextFragmentHref('Hello', 'https://example.com/page');
|
|
226
|
+
assert.ok(href.startsWith('https://example.com/page#:~:text='));
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('getWCAGLink returns a URL for a numeric WCAG ID', () => {
|
|
230
|
+
const link = getWCAGLink('4.1.2');
|
|
231
|
+
assert.ok(link.startsWith('https://www.w3.org/WAI/WCAG22/Understanding/'));
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('getWeightName returns the correct name for each weight', () => {
|
|
235
|
+
assert.equal(getWeightName(1), 'lowest');
|
|
236
|
+
assert.equal(getWeightName(2), 'low');
|
|
237
|
+
assert.equal(getWeightName(3), 'high');
|
|
238
|
+
assert.equal(getWeightName(4), 'highest');
|
|
239
|
+
assert.equal(getWeightName(5), 'unknown');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('htmlSafe escapes HTML special characters', () => {
|
|
243
|
+
assert.equal(htmlSafe('<script>alert("x")</script>'), '<script>alert("x")</script>');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('htmlSafe returns empty string for falsy input', () => {
|
|
247
|
+
assert.equal(htmlSafe(''), '');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('isJobID returns true for a valid job ID', () => {
|
|
251
|
+
assert.equal(isJobID('abc'), true);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('isJobID returns false for an invalid job ID', () => {
|
|
255
|
+
assert.equal(isJobID('ABCD'), false);
|
|
256
|
+
assert.equal(isJobID('ab'), false);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('isTimeStamp returns true for a valid time stamp', () => {
|
|
260
|
+
assert.equal(isTimeStamp('260101T0000'), true);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('isTimeStamp returns false for an invalid time stamp', () => {
|
|
264
|
+
assert.equal(isTimeStamp('invalid'), false);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('isURL returns a URL object for a valid HTTPS URL', () => {
|
|
268
|
+
const result = isURL('https://example.com/page');
|
|
269
|
+
assert.ok(result instanceof URL);
|
|
270
|
+
assert.equal(result.hostname, 'example.com');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('isURL returns false for a non-HTTPS URL', () => {
|
|
274
|
+
assert.equal(isURL('http://example.com/page'), false);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test('isURL returns false for an invalid URL', () => {
|
|
278
|
+
assert.equal(isURL('not-a-url'), false);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('makeBreakable inserts wbr before non-initial slashes', () => {
|
|
282
|
+
assert.equal(makeBreakable('/api/listReports'), '/api<wbr>/listReports');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test('minifyURL removes www. and trailing slash and lowercases', () => {
|
|
286
|
+
assert.equal(minifyURL('https://www.Example.com/'), 'https://example.com');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('objectSort sorts objects alphabetically by a string property', () => {
|
|
290
|
+
const items = [{name: 'banana'}, {name: 'apple'}, {name: 'cherry'}];
|
|
291
|
+
const sorted = objectSort(items, 'name', 'alpha');
|
|
292
|
+
assert.deepEqual(sorted.map(i => i.name), ['apple', 'banana', 'cherry']);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('objectSort sorts objects numerically ascending', () => {
|
|
296
|
+
const items = [{count: 3}, {count: 1}, {count: 2}];
|
|
297
|
+
const sorted = objectSort(items, 'count', 'numericUp');
|
|
298
|
+
assert.deepEqual(sorted.map(i => i.count), [1, 2, 3]);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test('objectSort sorts objects numerically descending', () => {
|
|
302
|
+
const items = [{count: 1}, {count: 3}, {count: 2}];
|
|
303
|
+
const sorted = objectSort(items, 'count', 'numericDown');
|
|
304
|
+
assert.deepEqual(sorted.map(i => i.count), [3, 2, 1]);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// TESTS FOR processTestRequest, annotateReport, AND isReportAvailable
|
|
308
|
+
|
|
309
|
+
// These tests use the fixture database directory and the real web/requestTest template.
|
|
310
|
+
const fixtureDbDir = path.join(__dirname, 'test', 'fixtures', 'db');
|
|
311
|
+
const requestTestDir = path.join(__dirname, 'web', 'requestTest');
|
|
312
|
+
const savedDbDir = process.env.DB_DIR;
|
|
313
|
+
|
|
314
|
+
before(() => {
|
|
315
|
+
process.env.DB_DIR = fixtureDbDir;
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
after(() => {
|
|
319
|
+
if (savedDbDir !== undefined) {
|
|
320
|
+
process.env.DB_DIR = savedDbDir;
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
delete process.env.DB_DIR;
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test('getPageData returns page data for a valid report', async () => {
|
|
328
|
+
const data = await getPageData('260101T0000', 'mix');
|
|
329
|
+
assert.equal(data.what, 'Mixed Outcomes Page');
|
|
330
|
+
assert.equal(data.url, 'https://example.com/mixed');
|
|
331
|
+
assert.equal(typeof data.daysAgo, 'number');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test('getPageData returns an error for a nonexistent report', async () => {
|
|
335
|
+
const data = await getPageData('999999T9999', 'xxx');
|
|
336
|
+
assert.ok(data.error);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('getPageDataStrings returns HTML strings for a valid report', async () => {
|
|
340
|
+
const strings = await getPageDataStrings('260101T0000', 'mix');
|
|
341
|
+
assert.equal(strings.what, 'Mixed Outcomes Page');
|
|
342
|
+
assert.equal(strings.url, 'https://example.com/mixed');
|
|
343
|
+
assert.equal(strings.urlLink, '<a href="https://example.com/mixed">https://example.com/mixed</a>');
|
|
344
|
+
assert.ok(strings.testInfo.includes('by job <code>mix</code>'));
|
|
345
|
+
assert.ok(strings.testInfo.includes('2026-01-01 at 00:00'));
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test('getPageDataStrings returns different testInfo for a different timeStamp', async () => {
|
|
349
|
+
const strings = await getPageDataStrings('260101T0001', 'ct');
|
|
350
|
+
assert.equal(strings.what, 'All CantTell Page');
|
|
351
|
+
assert.ok(strings.testInfo.includes('by job <code>ct</code>'));
|
|
352
|
+
assert.ok(strings.testInfo.includes('2026-01-01 at 00:01'));
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test('getPageDataStrings returns an error for a nonexistent report', async () => {
|
|
356
|
+
const strings = await getPageDataStrings('999999T9999', 'xxx');
|
|
357
|
+
assert.ok(strings.error);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test('getPageDataStrings uses provided pageData instead of reading the report', async () => {
|
|
361
|
+
const strings = await getPageDataStrings('260101T0000', 'mix', {
|
|
362
|
+
what: 'Custom Page',
|
|
363
|
+
url: 'https://custom.com',
|
|
364
|
+
daysAgo: 1
|
|
365
|
+
});
|
|
366
|
+
assert.equal(strings.what, 'Custom Page');
|
|
367
|
+
assert.equal(strings.url, 'https://custom.com');
|
|
368
|
+
assert.ok(strings.testInfo.includes('1 day ago'));
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test('processTestRequest returns an error for an invalid test type', async () => {
|
|
372
|
+
const result = await processTestRequest('invalid', requestTestDir, 'Page', 'https://example.com', 'because');
|
|
373
|
+
assert.equal(result.status, 'error');
|
|
374
|
+
assert.equal(result.message, 'Invalid recommendation');
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test('processTestRequest returns an error for an invalid URL', async () => {
|
|
378
|
+
const result = await processTestRequest('test', requestTestDir, 'Page', 'not-a-url', 'because');
|
|
379
|
+
assert.equal(result.status, 'error');
|
|
380
|
+
assert.equal(result.message, 'Invalid recommendation');
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test('processTestRequest returns an error for a short reason', async () => {
|
|
384
|
+
const result = await processTestRequest('test', requestTestDir, 'Page', 'https://example.com', 'abc');
|
|
385
|
+
assert.equal(result.status, 'error');
|
|
386
|
+
assert.equal(result.message, 'Invalid recommendation');
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test('processTestRequest returns an error for a missing description', async () => {
|
|
390
|
+
const result = await processTestRequest('test', requestTestDir, '', 'https://example.com', 'because');
|
|
391
|
+
assert.equal(result.status, 'error');
|
|
392
|
+
assert.equal(result.message, 'Invalid recommendation');
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test('processTestRequest returns an error for a mismatched directory name', async () => {
|
|
396
|
+
const result = await processTestRequest('test', '/tmp/wrongDir', 'Page', 'https://example.com', 'because');
|
|
397
|
+
assert.equal(result.status, 'error');
|
|
398
|
+
assert.equal(result.message, 'Invalid recommendation');
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test('processTestRequest succeeds and populates the template for a valid request', async () => {
|
|
402
|
+
// Reset recs.json to empty before the test.
|
|
403
|
+
await fs.writeFile(recsPath(), '{}\n');
|
|
404
|
+
const result = await processTestRequest('test', requestTestDir, 'Example Page', 'https://example.com', 'because accessibility');
|
|
405
|
+
assert.equal(result.status, 'ok');
|
|
406
|
+
assert.ok(result.answerPage.includes('Example Page'));
|
|
407
|
+
assert.ok(result.answerPage.includes('because accessibility'));
|
|
408
|
+
// Clean up recs.json.
|
|
409
|
+
await fs.writeFile(recsPath(), '{}\n');
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test('processTestRequest returns a duplicate error for a repeated request', async () => {
|
|
413
|
+
// Reset recs.json to empty, then make a successful request.
|
|
414
|
+
await fs.writeFile(recsPath(), '{}\n');
|
|
415
|
+
await processTestRequest('test', requestTestDir, 'Example Page', 'https://example.com', 'because accessibility');
|
|
416
|
+
// Repeat the same request.
|
|
417
|
+
const result = await processTestRequest('test', requestTestDir, 'Example Page', 'https://example.com', 'another reason');
|
|
418
|
+
assert.equal(result.status, 'error');
|
|
419
|
+
assert.equal(result.message, 'Duplicate recommendation');
|
|
420
|
+
// Clean up recs.json.
|
|
421
|
+
await fs.writeFile(recsPath(), '{}\n');
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test('annotateReport returns an error for a nonexistent report', async () => {
|
|
425
|
+
const result = await annotateReport('990101T0000', 'xxx');
|
|
426
|
+
assert.ok(typeof result === 'string');
|
|
427
|
+
assert.ok(result.includes('missing'));
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test('annotateReport annotates a valid report and returns an empty string', async () => {
|
|
431
|
+
const reportPath = path.join(fixtureDbDir, 'reports', '260101T0000-mix.json');
|
|
432
|
+
const original = await fs.readFile(reportPath, 'utf8');
|
|
433
|
+
try {
|
|
434
|
+
const result = await annotateReport('260101T0000', 'mix');
|
|
435
|
+
assert.equal(result, '');
|
|
436
|
+
}
|
|
437
|
+
finally {
|
|
438
|
+
await fs.writeFile(reportPath, original);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test('isReportAvailable returns true for a known page description', async () => {
|
|
443
|
+
const result = await isReportAvailable('Mixed Outcomes Page', 'https://example.com/nonexistent');
|
|
444
|
+
assert.equal(result, true);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test('isReportAvailable returns true for a known URL', async () => {
|
|
448
|
+
const result = await isReportAvailable('Nonexistent Page', 'https://example.com/mixed');
|
|
449
|
+
assert.equal(result, true);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test('isReportAvailable returns false for an unknown page and URL', async () => {
|
|
453
|
+
const result = await isReportAvailable('Nonexistent Page', 'https://nonexistent.example.com/');
|
|
454
|
+
assert.equal(result, false);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// TESTS FOR REMAINING BRANCH COVERAGE
|
|
458
|
+
|
|
459
|
+
test('getJobNames creates missing job directories and returns empty arrays', async () => {
|
|
460
|
+
const os = require('node:os');
|
|
461
|
+
const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'kilotest-jobnames-'));
|
|
462
|
+
const tmpDbDir = path.join(tmpRoot, 'db');
|
|
463
|
+
const savedDbDir = process.env.DB_DIR;
|
|
464
|
+
process.env.DB_DIR = tmpDbDir;
|
|
465
|
+
try {
|
|
466
|
+
const {getJobNames} = require('./util');
|
|
467
|
+
const result = await getJobNames();
|
|
468
|
+
assert.equal(typeof result, 'object');
|
|
469
|
+
assert.deepEqual(result.queue, []);
|
|
470
|
+
assert.deepEqual(result.claimed, []);
|
|
471
|
+
assert.deepEqual(result.failed, []);
|
|
472
|
+
// Verify the directories were created.
|
|
473
|
+
for (const category of ['queue', 'claimed', 'failed']) {
|
|
474
|
+
const stat = await fs.stat(path.join(tmpDbDir, 'jobs', category));
|
|
475
|
+
assert.ok(stat.isDirectory());
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
finally {
|
|
479
|
+
process.env.DB_DIR = savedDbDir;
|
|
480
|
+
await fs.rm(tmpRoot, {recursive: true}).catch(() => {});
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test('getJobNames returns an error when a job directory is a file, not a directory', async () => {
|
|
485
|
+
const os = require('node:os');
|
|
486
|
+
const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'kilotest-jobnames-err-'));
|
|
487
|
+
const tmpJobsDir = path.join(tmpRoot, 'db', 'jobs');
|
|
488
|
+
await fs.mkdir(tmpJobsDir, {recursive: true});
|
|
489
|
+
// Create a file where the queue directory should be, causing ENOTDIR.
|
|
490
|
+
await fs.writeFile(path.join(tmpJobsDir, 'queue'), 'not a directory');
|
|
491
|
+
const savedDbDir = process.env.DB_DIR;
|
|
492
|
+
process.env.DB_DIR = path.join(tmpRoot, 'db');
|
|
493
|
+
try {
|
|
494
|
+
const {getJobNames} = require('./util');
|
|
495
|
+
const result = await getJobNames();
|
|
496
|
+
assert.equal(typeof result, 'string');
|
|
497
|
+
assert.ok(result.startsWith('ERROR'));
|
|
498
|
+
}
|
|
499
|
+
finally {
|
|
500
|
+
process.env.DB_DIR = savedDbDir;
|
|
501
|
+
await fs.rm(tmpRoot, {recursive: true}).catch(() => {});
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test('getObject returns an error for a file that is not valid JSON', async () => {
|
|
506
|
+
const tmpFile = path.join(require('node:os').tmpdir(), 'kilotest-test-invalid.json');
|
|
507
|
+
require('node:fs').writeFileSync(tmpFile, 'not json');
|
|
508
|
+
const result = await getObject(tmpFile);
|
|
509
|
+
assert.equal(typeof result, 'string');
|
|
510
|
+
assert.ok(result.startsWith('ERROR'));
|
|
511
|
+
require('node:fs').unlinkSync(tmpFile);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test('getRecs creates an empty recommendations file and returns an error when it is missing', async () => {
|
|
515
|
+
const tmpDir = require('node:os').tmpdir() + '/kilotest-missing-recs-test';
|
|
516
|
+
const fsSync = require('node:fs');
|
|
517
|
+
fsSync.mkdirSync(tmpDir + '/jobs', {recursive: true});
|
|
518
|
+
const savedDbDir = process.env.DB_DIR;
|
|
519
|
+
process.env.DB_DIR = tmpDir;
|
|
520
|
+
try {
|
|
521
|
+
const {getRecs} = require('./util');
|
|
522
|
+
const result = await getRecs();
|
|
523
|
+
assert.equal(typeof result, 'string');
|
|
524
|
+
assert.ok(result.startsWith('ERROR'));
|
|
525
|
+
// Verify the empty file was created.
|
|
526
|
+
assert.ok(fsSync.existsSync(tmpDir + '/jobs/recs.json'));
|
|
527
|
+
}
|
|
528
|
+
finally {
|
|
529
|
+
process.env.DB_DIR = savedDbDir;
|
|
530
|
+
fsSync.rmSync(tmpDir, {recursive: true});
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test('getRecs returns an error when the recommendations file is not JSON', async () => {
|
|
535
|
+
const tmpDir = require('node:os').tmpdir() + '/kilotest-recs-test';
|
|
536
|
+
require('node:fs').mkdirSync(tmpDir + '/jobs', {recursive: true});
|
|
537
|
+
require('node:fs').writeFileSync(tmpDir + '/jobs/recs.json', 'not json');
|
|
538
|
+
const savedDbDir = process.env.DB_DIR;
|
|
539
|
+
process.env.DB_DIR = tmpDir;
|
|
540
|
+
try {
|
|
541
|
+
const {getRecs} = require('./util');
|
|
542
|
+
const result = await getRecs();
|
|
543
|
+
assert.equal(typeof result, 'string');
|
|
544
|
+
assert.ok(result.startsWith('ERROR'));
|
|
545
|
+
}
|
|
546
|
+
finally {
|
|
547
|
+
process.env.DB_DIR = savedDbDir;
|
|
548
|
+
require('node:fs').rmSync(tmpDir, {recursive: true});
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test('getPOSTData resolves with parsed query for form-urlencoded requests', async () => {
|
|
553
|
+
const {Readable} = require('node:stream');
|
|
554
|
+
const {getPOSTData} = require('./util');
|
|
555
|
+
const body = 'target=Page&why=Because';
|
|
556
|
+
const req = Object.assign(new Readable({
|
|
557
|
+
read() {
|
|
558
|
+
this.push(Buffer.from(body));
|
|
559
|
+
this.push(null);
|
|
560
|
+
}
|
|
561
|
+
}), {headers: {'content-type': 'application/x-www-form-urlencoded'}});
|
|
562
|
+
const result = await getPOSTData(req);
|
|
563
|
+
assert.equal(result.target, 'Page');
|
|
564
|
+
assert.equal(result.why, 'Because');
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
test('isRecommendable returns "claimed" for a URL in a claimed job', async () => {
|
|
568
|
+
const tmpDir = require('node:os').tmpdir() + '/kilotest-recommendable-test';
|
|
569
|
+
const fsSync = require('node:fs');
|
|
570
|
+
fsSync.mkdirSync(tmpDir + '/jobs/claimed', {recursive: true});
|
|
571
|
+
fsSync.mkdirSync(tmpDir + '/jobs/queue', {recursive: true});
|
|
572
|
+
fsSync.mkdirSync(tmpDir + '/jobs/failed', {recursive: true});
|
|
573
|
+
fsSync.writeFileSync(tmpDir + '/jobs/claimed/job1.json', JSON.stringify({
|
|
574
|
+
target: {url: 'https://example.com/test', what: 'Test Page'}
|
|
575
|
+
}));
|
|
576
|
+
const savedDbDir = process.env.DB_DIR;
|
|
577
|
+
process.env.DB_DIR = tmpDir;
|
|
578
|
+
try {
|
|
579
|
+
const {isRecommendable} = require('./util');
|
|
580
|
+
const result = await isRecommendable('https://example.com/test');
|
|
581
|
+
assert.equal(result, 'claimed');
|
|
582
|
+
}
|
|
583
|
+
finally {
|
|
584
|
+
process.env.DB_DIR = savedDbDir;
|
|
585
|
+
fsSync.rmSync(tmpDir, {recursive: true});
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
test('isRecommendable returns "queued" for a URL in a queued job', async () => {
|
|
590
|
+
const tmpDir = require('node:os').tmpdir() + '/kilotest-recommendable-test';
|
|
591
|
+
const fsSync = require('node:fs');
|
|
592
|
+
fsSync.mkdirSync(tmpDir + '/jobs/claimed', {recursive: true});
|
|
593
|
+
fsSync.mkdirSync(tmpDir + '/jobs/queue', {recursive: true});
|
|
594
|
+
fsSync.mkdirSync(tmpDir + '/jobs/failed', {recursive: true});
|
|
595
|
+
fsSync.writeFileSync(tmpDir + '/jobs/queue/job1.json', JSON.stringify({
|
|
596
|
+
target: {url: 'https://example.com/test', what: 'Test Page'}
|
|
597
|
+
}));
|
|
598
|
+
const savedDbDir = process.env.DB_DIR;
|
|
599
|
+
process.env.DB_DIR = tmpDir;
|
|
600
|
+
try {
|
|
601
|
+
const {isRecommendable} = require('./util');
|
|
602
|
+
const result = await isRecommendable('https://example.com/test');
|
|
603
|
+
assert.equal(result, 'queued');
|
|
604
|
+
}
|
|
605
|
+
finally {
|
|
606
|
+
process.env.DB_DIR = savedDbDir;
|
|
607
|
+
fsSync.rmSync(tmpDir, {recursive: true});
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
test('isRecommendable returns empty string for a URL with no matching jobs', async () => {
|
|
612
|
+
const tmpDir = require('node:os').tmpdir() + '/kilotest-recommendable-test';
|
|
613
|
+
const fsSync = require('node:fs');
|
|
614
|
+
fsSync.mkdirSync(tmpDir + '/jobs/claimed', {recursive: true});
|
|
615
|
+
fsSync.mkdirSync(tmpDir + '/jobs/queue', {recursive: true});
|
|
616
|
+
fsSync.mkdirSync(tmpDir + '/jobs/failed', {recursive: true});
|
|
617
|
+
const savedDbDir = process.env.DB_DIR;
|
|
618
|
+
process.env.DB_DIR = tmpDir;
|
|
619
|
+
try {
|
|
620
|
+
const {isRecommendable} = require('./util');
|
|
621
|
+
const result = await isRecommendable('https://example.com/no-match');
|
|
622
|
+
assert.equal(result, '');
|
|
623
|
+
}
|
|
624
|
+
finally {
|
|
625
|
+
process.env.DB_DIR = savedDbDir;
|
|
626
|
+
fsSync.rmSync(tmpDir, {recursive: true});
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
test('isURL returns false for a malformed URL', () => {
|
|
631
|
+
assert.equal(isURL('https://[invalid'), false);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
test('objectSort returns 0 for an unknown sort type', () => {
|
|
635
|
+
const items = [{name: 'a'}, {name: 'b'}];
|
|
636
|
+
const sorted = objectSort(items, 'name', 'unknownType');
|
|
637
|
+
assert.equal(sorted.length, 2);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
test('getReport returns an error for an invalid report', async () => {
|
|
641
|
+
const tmpDir = require('node:os').tmpdir() + '/kilotest-invalid-report-test';
|
|
642
|
+
const fsSync = require('node:fs');
|
|
643
|
+
fsSync.mkdirSync(tmpDir + '/reports', {recursive: true});
|
|
644
|
+
fsSync.writeFileSync(tmpDir + '/reports/260101T0000-bad.json', JSON.stringify({notValid: true}));
|
|
645
|
+
const savedDbDir = process.env.DB_DIR;
|
|
646
|
+
process.env.DB_DIR = tmpDir;
|
|
647
|
+
try {
|
|
648
|
+
const {getReport} = require('./util');
|
|
649
|
+
const result = await getReport('260101T0000', 'bad');
|
|
650
|
+
assert.ok(result.error);
|
|
651
|
+
assert.ok(result.error.includes('invalid'));
|
|
652
|
+
}
|
|
653
|
+
finally {
|
|
654
|
+
process.env.DB_DIR = savedDbDir;
|
|
655
|
+
fsSync.rmSync(tmpDir, {recursive: true});
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test('getReportData returns an error for a nonexistent report', async () => {
|
|
660
|
+
const {getReportData} = require('./util');
|
|
661
|
+
const result = await getReportData('990101T0000', 'xxx');
|
|
662
|
+
assert.ok(result.error);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
// TESTS FOR REMAINING BRANCH COVERAGE IN util.js
|
|
666
|
+
|
|
667
|
+
test('getAgoDays returns null for an invalid Date object', () => {
|
|
668
|
+
assert.equal(getAgoDays(new Date('invalid')), null);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
test('getAgoString returns "1 day" for exactly 1 day ago', () => {
|
|
672
|
+
// Construct a time stamp 1 day and 1 hour ago, so Math.round gives exactly 1.
|
|
673
|
+
const date = new Date(Date.now() - (86400000 + 3600000));
|
|
674
|
+
const stamp = date.toISOString().slice(2).replace(/[-:]/g, '').slice(0, 11);
|
|
675
|
+
assert.equal(getAgoString(stamp), '1 day');
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
test('getIssue returns null for a known engine with an unknown variable rule', () => {
|
|
679
|
+
assert.equal(getIssue('axe', 'nonexistent-rule-id'), null);
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
test('getIssue returns an issue ID for a variable rule pattern match', () => {
|
|
683
|
+
const result = getIssue('nuVal', 'Duplicate attribute foo');
|
|
684
|
+
assert.ok(typeof result === 'string');
|
|
685
|
+
assert.equal(result, 'duplicateAttribute');
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test('getTimeString returns null for an invalid time portion', () => {
|
|
689
|
+
const {getDateTimeString} = require('./util');
|
|
690
|
+
const result = getDateTimeString('999999T9999');
|
|
691
|
+
assert.ok(result.includes('null'));
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
test('getPOSTData resolves with parsed JSON for application/json requests', async () => {
|
|
695
|
+
const {Readable} = require('node:stream');
|
|
696
|
+
const {getPOSTData} = require('./util');
|
|
697
|
+
const body = JSON.stringify({target: 'Page', why: 'Because'});
|
|
698
|
+
const req = Object.assign(new Readable({
|
|
699
|
+
read() {
|
|
700
|
+
this.push(Buffer.from(body));
|
|
701
|
+
this.push(null);
|
|
702
|
+
}
|
|
703
|
+
}), {headers: {'content-type': 'application/json'}});
|
|
704
|
+
const result = await getPOSTData(req);
|
|
705
|
+
assert.equal(result.target, 'Page');
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
test('getPOSTData resolves with parsed query for body-type form-urlencoded requests', async () => {
|
|
709
|
+
const {Readable} = require('node:stream');
|
|
710
|
+
const {getPOSTData} = require('./util');
|
|
711
|
+
const body = 'target=Page&why=Because';
|
|
712
|
+
const req = Object.assign(new Readable({
|
|
713
|
+
read() {
|
|
714
|
+
this.push(Buffer.from(body));
|
|
715
|
+
this.push(null);
|
|
716
|
+
}
|
|
717
|
+
}), {headers: {'body-type': 'application/x-www-form-urlencoded'}});
|
|
718
|
+
const result = await getPOSTData(req);
|
|
719
|
+
assert.equal(result.target, 'Page');
|
|
720
|
+
assert.equal(result.why, 'Because');
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
test('getPOSTData resolves with null for an unknown content type', async () => {
|
|
724
|
+
const {Readable} = require('node:stream');
|
|
725
|
+
const {getPOSTData} = require('./util');
|
|
726
|
+
const req = Object.assign(new Readable({
|
|
727
|
+
read() {
|
|
728
|
+
this.push(Buffer.from('data'));
|
|
729
|
+
this.push(null);
|
|
730
|
+
}
|
|
731
|
+
}), {headers: {}});
|
|
732
|
+
const result = await getPOSTData(req);
|
|
733
|
+
assert.equal(result, null);
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
test('getEngineNamesString falls back to the engine ID for an unknown engine', () => {
|
|
737
|
+
const {getEngineNamesString} = require('./util');
|
|
738
|
+
const result = getEngineNamesString(new Set(['unknownEngine']));
|
|
739
|
+
assert.equal(result, 'unknownEngine');
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
test('getPathID returns the catalog pathID when catalogIndex is truthy', () => {
|
|
743
|
+
const {getPathID} = require('./util');
|
|
744
|
+
const catalog = {'0': {pathID: '/html/body/div'}};
|
|
745
|
+
assert.equal(getPathID(catalog, '0', '/fallback'), '/html/body/div');
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
test('getPathID returns the fallback pathID when catalogIndex is truthy but catalogItem has no pathID', () => {
|
|
749
|
+
const {getPathID} = require('./util');
|
|
750
|
+
const catalog = {'0': {tagName: 'div'}};
|
|
751
|
+
assert.equal(getPathID(catalog, '0', '/fallback'), '/fallback');
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
test('getPathID returns /html when catalogIndex is truthy but catalogItem and pathID are both missing', () => {
|
|
755
|
+
const {getPathID} = require('./util');
|
|
756
|
+
const catalog = {};
|
|
757
|
+
assert.equal(getPathID(catalog, '0', null), '/html');
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
test('getPathID returns /html when catalogIndex is falsy and pathID is null', () => {
|
|
761
|
+
const {getPathID} = require('./util');
|
|
762
|
+
assert.equal(getPathID({}, null, null), '/html');
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
test('isValidReport returns false for a report with a test act using an unknown engine', () => {
|
|
766
|
+
const {isValidReport} = require('./util');
|
|
767
|
+
const report = {
|
|
768
|
+
target: {what: 'Test', url: 'https://example.com'},
|
|
769
|
+
acts: [{type: 'test', which: 'unknownEngine'}],
|
|
770
|
+
jobData: {endTime: '26-01-01T00:00'},
|
|
771
|
+
catalog: {}
|
|
772
|
+
};
|
|
773
|
+
assert.equal(isValidReport(report), false);
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
test('isValidReport returns true for a report with a non-test act', () => {
|
|
777
|
+
const {isValidReport} = require('./util');
|
|
778
|
+
const report = {
|
|
779
|
+
target: {what: 'Test', url: 'https://example.com'},
|
|
780
|
+
acts: [{type: 'other'}],
|
|
781
|
+
jobData: {endTime: '26-01-01T00:00'},
|
|
782
|
+
catalog: {}
|
|
783
|
+
};
|
|
784
|
+
assert.equal(isValidReport(report), true);
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
test('annotateReport handles a test act with no standardResult instances', async () => {
|
|
788
|
+
const tmpDir = require('node:os').tmpdir() + '/kilotest-no-instances-test';
|
|
789
|
+
const fsSync = require('node:fs');
|
|
790
|
+
fsSync.mkdirSync(tmpDir + '/reports', {recursive: true});
|
|
791
|
+
const report = {
|
|
792
|
+
target: {what: 'Test', url: 'https://example.com'},
|
|
793
|
+
acts: [
|
|
794
|
+
{type: 'test', which: 'axe', result: {standardResult: {instances: []}}},
|
|
795
|
+
{type: 'other'}
|
|
796
|
+
],
|
|
797
|
+
jobData: {endTime: '26-01-01T00:00'},
|
|
798
|
+
catalog: {}
|
|
799
|
+
};
|
|
800
|
+
fsSync.writeFileSync(tmpDir + '/reports/260101T0000-ni.json', JSON.stringify(report));
|
|
801
|
+
const savedDbDir = process.env.DB_DIR;
|
|
802
|
+
process.env.DB_DIR = tmpDir;
|
|
803
|
+
try {
|
|
804
|
+
const result = await annotateReport('260101T0000', 'ni');
|
|
805
|
+
assert.equal(result, '');
|
|
806
|
+
}
|
|
807
|
+
finally {
|
|
808
|
+
process.env.DB_DIR = savedDbDir;
|
|
809
|
+
fsSync.rmSync(tmpDir, {recursive: true});
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
test('annotateReport handles a test act with no standardResult', async () => {
|
|
814
|
+
const tmpDir = require('node:os').tmpdir() + '/kilotest-no-standard-test';
|
|
815
|
+
const fsSync = require('node:fs');
|
|
816
|
+
fsSync.mkdirSync(tmpDir + '/reports', {recursive: true});
|
|
817
|
+
const report = {
|
|
818
|
+
target: {what: 'Test', url: 'https://example.com'},
|
|
819
|
+
acts: [
|
|
820
|
+
{type: 'test', which: 'axe', result: {}}
|
|
821
|
+
],
|
|
822
|
+
jobData: {endTime: '26-01-01T00:00'},
|
|
823
|
+
catalog: {}
|
|
824
|
+
};
|
|
825
|
+
fsSync.writeFileSync(tmpDir + '/reports/260101T0000-ns.json', JSON.stringify(report));
|
|
826
|
+
const savedDbDir = process.env.DB_DIR;
|
|
827
|
+
process.env.DB_DIR = tmpDir;
|
|
828
|
+
try {
|
|
829
|
+
const result = await annotateReport('260101T0000', 'ns');
|
|
830
|
+
assert.equal(result, '');
|
|
831
|
+
}
|
|
832
|
+
finally {
|
|
833
|
+
process.env.DB_DIR = savedDbDir;
|
|
834
|
+
fsSync.rmSync(tmpDir, {recursive: true});
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
// UNIT TESTS FOR PREVIOUSLY UNTESTED EXPORTED FUNCTIONS
|
|
839
|
+
|
|
840
|
+
test('createLock returns a function that runs tasks sequentially', async () => {
|
|
841
|
+
const lock = createLock();
|
|
842
|
+
const order = [];
|
|
843
|
+
const p1 = lock(async () => {
|
|
844
|
+
order.push('start 1');
|
|
845
|
+
await new Promise(r => setTimeout(r, 10));
|
|
846
|
+
order.push('end 1');
|
|
847
|
+
return 1;
|
|
848
|
+
});
|
|
849
|
+
const p2 = lock(async () => {
|
|
850
|
+
order.push('start 2');
|
|
851
|
+
return 2;
|
|
852
|
+
});
|
|
853
|
+
const [r1, r2] = await Promise.all([p1, p2]);
|
|
854
|
+
assert.equal(r1, 1);
|
|
855
|
+
assert.equal(r2, 2);
|
|
856
|
+
assert.deepEqual(order, ['start 1', 'end 1', 'start 2']);
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
test('createLock propagates errors without blocking subsequent tasks', async () => {
|
|
860
|
+
const lock = createLock();
|
|
861
|
+
const p1 = lock(async () => {
|
|
862
|
+
throw new Error('first failed');
|
|
863
|
+
});
|
|
864
|
+
const p2 = lock(async () => 'second succeeded');
|
|
865
|
+
await p1.catch(() => {});
|
|
866
|
+
const r2 = await p2;
|
|
867
|
+
assert.equal(r2, 'second succeeded');
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
test('getTimeStamp returns an 11-character stamp from a Date', () => {
|
|
871
|
+
const date = new Date('2026-03-15T14:30:00.000Z');
|
|
872
|
+
const stamp = getTimeStamp(date);
|
|
873
|
+
assert.equal(stamp.length, 11);
|
|
874
|
+
assert.equal(stamp.slice(0, 6), '260315');
|
|
875
|
+
assert.equal(stamp.slice(6, 7), 'T');
|
|
876
|
+
assert.equal(stamp.slice(7), '1430');
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
test('getEngineList returns a sorted +-delimited list of engine names', () => {
|
|
880
|
+
const result = getEngineList(new Set(['axe', 'wave', 'nuVal']));
|
|
881
|
+
const names = result.split(' + ');
|
|
882
|
+
assert.ok(names.length === 3);
|
|
883
|
+
assert.ok(names.includes('WAVE'));
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
test('recsLock is a function (the lock returned by createLock)', () => {
|
|
887
|
+
assert.equal(typeof recsLock, 'function');
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
test('updateRecs adds a recommendation and returns success', async () => {
|
|
891
|
+
await fs.writeFile(recsPath(), '{}\n');
|
|
892
|
+
const result = await updateRecs('Test Page', 'https://example.com/test', 'because');
|
|
893
|
+
assert.equal(result.error, undefined);
|
|
894
|
+
const recs = JSON.parse(await fs.readFile(recsPath(), 'utf8'));
|
|
895
|
+
assert.ok(recs['https://example.com/test']);
|
|
896
|
+
assert.equal(recs['https://example.com/test'].length, 1);
|
|
897
|
+
assert.equal(recs['https://example.com/test'][0].what, 'Test Page');
|
|
898
|
+
await fs.writeFile(recsPath(), '{}\n');
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
test('updateRecs returns a duplicate error for a repeated recommendation', async () => {
|
|
902
|
+
await fs.writeFile(recsPath(), '{}\n');
|
|
903
|
+
await updateRecs('Test Page', 'https://example.com/test', 'because');
|
|
904
|
+
const result = await updateRecs('Test Page', 'https://example.com/test', 'another reason');
|
|
905
|
+
assert.equal(result.error, 'duplicate');
|
|
906
|
+
await fs.writeFile(recsPath(), '{}\n');
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
test('getReportPath returns the path of a report file', () => {
|
|
910
|
+
const result = getReportPath('260101T0000', 'mix');
|
|
911
|
+
assert.ok(result.endsWith('260101T0000-mix.json'));
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
test('getReportStats returns reportTime and reportSize for a valid report', async () => {
|
|
915
|
+
const stats = await getReportStats('260101T0000', 'mix');
|
|
916
|
+
assert.ok(stats.reportTime instanceof Date);
|
|
917
|
+
assert.equal(typeof stats.reportSize, 'number');
|
|
918
|
+
assert.ok(stats.reportSize > 0);
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
test('getReportStats returns null for a nonexistent report', async () => {
|
|
922
|
+
const stats = await getReportStats('999999T9999', 'xxx');
|
|
923
|
+
assert.equal(stats, null);
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
test('isHidden returns false for a non-hidden report', async () => {
|
|
927
|
+
const result = await isHidden('260101T0000', 'mix');
|
|
928
|
+
assert.equal(result, false);
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
test('isHidden returns true for a hidden report', async () => {
|
|
932
|
+
const result = await isHidden('260101T0007', 'hid');
|
|
933
|
+
assert.equal(result, true);
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
test('getReportExtract returns an extract for a valid report', async () => {
|
|
937
|
+
const extract = await getReportExtract('260101T0000', 'mix');
|
|
938
|
+
assert.equal(extract.timeStamp, '260101T0000');
|
|
939
|
+
assert.equal(extract.jobID, 'mix');
|
|
940
|
+
assert.equal(extract.what, 'Mixed Outcomes Page');
|
|
941
|
+
assert.equal(extract.url, 'https://example.com/mixed');
|
|
942
|
+
assert.ok(extract.reportTime);
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
test('getReportExtract returns an error for a nonexistent report', async () => {
|
|
946
|
+
const extract = await getReportExtract('999999T9999', 'xxx');
|
|
947
|
+
assert.ok(extract.error);
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
test('getReportExtracts returns extracts of all available reports', async () => {
|
|
951
|
+
const extracts = await getReportExtracts();
|
|
952
|
+
assert.ok(extracts.length >= 8);
|
|
953
|
+
const ids = extracts.map(e => `${e.timeStamp}-${e.jobID}`);
|
|
954
|
+
assert.ok(ids.includes('260101T0000-mix'));
|
|
955
|
+
assert.ok(ids.includes('260101T0001-ct'));
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
test('getLatestReportExtracts returns only the latest report for each page', async () => {
|
|
959
|
+
const latest = await getLatestReportExtracts();
|
|
960
|
+
const mixReports = latest.filter(e => e.what === 'Mixed Outcomes Page');
|
|
961
|
+
assert.equal(mixReports.length, 1);
|
|
962
|
+
assert.equal(mixReports[0].timeStamp, '260202T0000');
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
test('getMultiReportWhats returns descriptions that have multiple reports', async () => {
|
|
966
|
+
const whats = await getMultiReportWhats();
|
|
967
|
+
assert.ok(whats.includes('Mixed Outcomes Page'));
|
|
968
|
+
});
|