@jrpool/kilotest 57.2.0 → 57.2.3
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/package.json +1 -1
- package/util.test.ts +0 -205
- package/util.ts +2 -240
- package/web/deleteNotesForm/index.ts +8 -1
- package/web/listDiagnoses/index.ts +6 -4
- package/web/listDiagnoses/listDiagnoses.test.ts +8 -2
- package/web/listIssues/index.ts +7 -5
- package/web/listIssues/listIssues.test.ts +4 -3
- package/web/listReports/index.ts +6 -4
- package/web/listRules/index.ts +3 -1
- package/web/listTopIssues/index.ts +6 -4
- package/web/listViolators/index.ts +9 -7
- package/web/listViolators/listViolators.test.ts +8 -2
- package/web/metrics/index.ts +9 -2
- package/web/reportDeletion.ts +8 -1
- package/web/requestRetestForm/index.ts +2 -1
- package/web/tutorialAI/index.html +25 -22
- package/web/tutorialAI/index.ts +2 -1
- package/web/tutorialWeb/index.ts +2 -1
- package/web/util.test.ts +240 -0
- package/web/util.ts +266 -0
package/package.json
CHANGED
package/util.test.ts
CHANGED
|
@@ -23,36 +23,26 @@ import {
|
|
|
23
23
|
annotateReportObject,
|
|
24
24
|
awaitJob,
|
|
25
25
|
checkCommentDuplicate,
|
|
26
|
-
checkCommentLength,
|
|
27
26
|
checkLength,
|
|
28
27
|
createLock,
|
|
29
28
|
dbPath,
|
|
30
29
|
describeMax,
|
|
31
30
|
getAgoDays,
|
|
32
|
-
getAgoString,
|
|
33
|
-
getCountString,
|
|
34
31
|
getDateString,
|
|
35
32
|
getDateTime,
|
|
36
|
-
getEngineNamesString,
|
|
37
33
|
getEnvMax,
|
|
38
34
|
getIssue,
|
|
39
35
|
errorMessage,
|
|
40
36
|
getJSON,
|
|
41
|
-
getMultiReportWhats,
|
|
42
37
|
getNowStamp,
|
|
43
38
|
getObject,
|
|
44
|
-
getPageData,
|
|
45
|
-
getPageDataStrings,
|
|
46
39
|
getPlainText,
|
|
47
40
|
getRandomString,
|
|
48
41
|
getReportExtract,
|
|
49
42
|
getReportExtracts,
|
|
50
43
|
getReportPath,
|
|
51
44
|
getReportStats,
|
|
52
|
-
getTextFragmentHref,
|
|
53
45
|
getTimeStamp,
|
|
54
|
-
getWCAGLink,
|
|
55
|
-
getWeightName,
|
|
56
46
|
hiddenReportsPath,
|
|
57
47
|
htmlSafe,
|
|
58
48
|
isAllowedRedirectTarget,
|
|
@@ -64,7 +54,6 @@ import {
|
|
|
64
54
|
isURL,
|
|
65
55
|
isValidAuthCode,
|
|
66
56
|
jobsPath,
|
|
67
|
-
makeBreakable,
|
|
68
57
|
minifyURL,
|
|
69
58
|
objectSort,
|
|
70
59
|
orderJob,
|
|
@@ -76,32 +65,6 @@ import {
|
|
|
76
65
|
|
|
77
66
|
// TESTS
|
|
78
67
|
|
|
79
|
-
test('checkCommentLength accepts a comment of exactly 20 characters', () => {
|
|
80
|
-
const result = checkCommentLength('a'.repeat(20));
|
|
81
|
-
assert.deepEqual(result, {status: 'ok'});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test('checkCommentLength accepts a comment of exactly 1000 characters', () => {
|
|
85
|
-
const result = checkCommentLength('a'.repeat(1000));
|
|
86
|
-
assert.deepEqual(result, {status: 'ok'});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test('checkCommentLength rejects a comment shorter than 20 characters', () => {
|
|
90
|
-
const result = checkCommentLength('a'.repeat(19));
|
|
91
|
-
assert.deepEqual(result, {
|
|
92
|
-
status: 'error',
|
|
93
|
-
message: 'Your comment was shorter than 20 characters'
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test('checkCommentLength rejects a comment longer than 1000 characters', () => {
|
|
98
|
-
const result = checkCommentLength('a'.repeat(1001));
|
|
99
|
-
assert.deepEqual(result, {
|
|
100
|
-
status: 'error',
|
|
101
|
-
message: 'Your comment was longer than 1000 characters'
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
68
|
test('checkLength accepts text at the minimum bound', () => {
|
|
106
69
|
const result = checkLength('a'.repeat(5), 5, 10, 'reason');
|
|
107
70
|
assert.deepEqual(result, {status: 'ok'});
|
|
@@ -233,28 +196,6 @@ test('getAgoDays returns 0 for the current time', () => {
|
|
|
233
196
|
assert.equal(getAgoDays(new Date()), 0);
|
|
234
197
|
});
|
|
235
198
|
|
|
236
|
-
test('getAgoString returns "1 day" for a 1-day-old timestamp', () => {
|
|
237
|
-
const date = new Date(Date.now() - 1 * 24 * 60 * 60 * 1000);
|
|
238
|
-
assert.equal(
|
|
239
|
-
getAgoString(date.toISOString().slice(2).replace(/[-:]/g, '').slice(0, 11)), '1 day'
|
|
240
|
-
);
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
test('getAgoString returns "3 days" for a 3-day-old timestamp', () => {
|
|
244
|
-
const date = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000);
|
|
245
|
-
assert.equal(
|
|
246
|
-
getAgoString(date.toISOString().slice(2).replace(/[-:]/g, '').slice(0, 11)), '3 days'
|
|
247
|
-
);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
test('getCountString returns singular for count 1', () => {
|
|
251
|
-
assert.equal(getCountString(1, 'report', 'reports'), '1 report');
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
test('getCountString returns plural for count 0', () => {
|
|
255
|
-
assert.equal(getCountString(0, 'report', 'reports'), '0 reports');
|
|
256
|
-
});
|
|
257
|
-
|
|
258
199
|
test('getDateString returns a valid date string for a valid time stamp', () => {
|
|
259
200
|
assert.equal(getDateString('260101T0000'), '2026-01-01');
|
|
260
201
|
});
|
|
@@ -359,24 +300,6 @@ test('getRandomString returns a string of the requested length', () => {
|
|
|
359
300
|
assert.equal(s.length, 10);
|
|
360
301
|
});
|
|
361
302
|
|
|
362
|
-
test('getTextFragmentHref returns a text-fragment URL', () => {
|
|
363
|
-
const href = getTextFragmentHref('Hello', 'https://example.com/page');
|
|
364
|
-
assert.ok(href.startsWith('https://example.com/page#:~:text='));
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
test('getWCAGLink returns a URL for a numeric WCAG ID', () => {
|
|
368
|
-
const link = getWCAGLink('4.1.2');
|
|
369
|
-
assert.ok(link.startsWith('https://www.w3.org/WAI/WCAG22/Understanding/'));
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
test('getWeightName returns the correct name for each weight', () => {
|
|
373
|
-
assert.equal(getWeightName(1), 'lowest');
|
|
374
|
-
assert.equal(getWeightName(2), 'low');
|
|
375
|
-
assert.equal(getWeightName(3), 'high');
|
|
376
|
-
assert.equal(getWeightName(4), 'highest');
|
|
377
|
-
assert.equal(getWeightName(5), 'unknown');
|
|
378
|
-
});
|
|
379
|
-
|
|
380
303
|
test('htmlSafe escapes HTML special characters', () => {
|
|
381
304
|
assert.equal(htmlSafe('<script>alert("x")</script>'), '<script>alert("x")</script>');
|
|
382
305
|
});
|
|
@@ -776,10 +699,6 @@ test('isValidAuthCode returns false for an undefined code', () => {
|
|
|
776
699
|
assert.equal(isValidAuthCode(undefined), false);
|
|
777
700
|
});
|
|
778
701
|
|
|
779
|
-
test('makeBreakable inserts wbr before non-initial slashes', () => {
|
|
780
|
-
assert.equal(makeBreakable('/api/listReports'), '/api<wbr>/listReports');
|
|
781
|
-
});
|
|
782
|
-
|
|
783
702
|
test('minifyURL removes www. and trailing slash and lowercases', () => {
|
|
784
703
|
assert.equal(minifyURL('https://www.Example.com/'), 'https://example.com');
|
|
785
704
|
});
|
|
@@ -821,50 +740,6 @@ after(() => {
|
|
|
821
740
|
}
|
|
822
741
|
});
|
|
823
742
|
|
|
824
|
-
test('getPageData returns page data for a valid report', async () => {
|
|
825
|
-
const data = await getPageData('260101T0000', 'mix') as any;
|
|
826
|
-
assert.equal(data.description, 'Mixed Outcomes Page');
|
|
827
|
-
assert.equal(data.url, 'https://example.com/mixed');
|
|
828
|
-
assert.equal(typeof data.daysAgo, 'number');
|
|
829
|
-
});
|
|
830
|
-
|
|
831
|
-
test('getPageData returns an error for a nonexistent report', async () => {
|
|
832
|
-
const data = await getPageData('999999T9999', 'xxx') as any;
|
|
833
|
-
assert.ok(data.error);
|
|
834
|
-
});
|
|
835
|
-
|
|
836
|
-
test('getPageDataStrings returns HTML strings for a valid report', async () => {
|
|
837
|
-
const strings = await getPageDataStrings('260101T0000', 'mix') as any;
|
|
838
|
-
assert.equal(strings.description, 'Mixed Outcomes Page');
|
|
839
|
-
assert.equal(strings.url, 'https://example.com/mixed');
|
|
840
|
-
assert.equal(strings.urlLink, '<a href="https://example.com/mixed">https://example.com/mixed</a>');
|
|
841
|
-
assert.ok(strings.testInfo.includes('by job <code>mix</code>'));
|
|
842
|
-
assert.ok(strings.testInfo.includes('2026-01-01 at 00:00'));
|
|
843
|
-
});
|
|
844
|
-
|
|
845
|
-
test('getPageDataStrings returns different testInfo for a different timeStamp', async () => {
|
|
846
|
-
const strings = await getPageDataStrings('260101T0001', 'ct') as any;
|
|
847
|
-
assert.equal(strings.description, 'All CantTell Page');
|
|
848
|
-
assert.ok(strings.testInfo.includes('by job <code>ct</code>'));
|
|
849
|
-
assert.ok(strings.testInfo.includes('2026-01-01 at 00:01'));
|
|
850
|
-
});
|
|
851
|
-
|
|
852
|
-
test('getPageDataStrings returns an error for a nonexistent report', async () => {
|
|
853
|
-
const strings = await getPageDataStrings('999999T9999', 'xxx');
|
|
854
|
-
assert.ok(strings.error);
|
|
855
|
-
});
|
|
856
|
-
|
|
857
|
-
test('getPageDataStrings uses provided pageData instead of reading the report', async () => {
|
|
858
|
-
const strings = await getPageDataStrings('260101T0000', 'mix', {
|
|
859
|
-
description: 'Custom Page',
|
|
860
|
-
url: 'https://custom.com',
|
|
861
|
-
daysAgo: 1
|
|
862
|
-
}) as any;
|
|
863
|
-
assert.equal(strings.description, 'Custom Page');
|
|
864
|
-
assert.equal(strings.url, 'https://custom.com');
|
|
865
|
-
assert.ok(strings.testInfo.includes('1 day ago'));
|
|
866
|
-
});
|
|
867
|
-
|
|
868
743
|
test('processTestRequest succeeds for a valid new-test request', {timeout: 500}, async () => {
|
|
869
744
|
// Reset testRequests.json to empty before the test.
|
|
870
745
|
await fs.writeFile(testRequestsPath(), '{}\n');
|
|
@@ -1358,41 +1233,12 @@ test('getReport returns an error for an invalid report', async () => {
|
|
|
1358
1233
|
}
|
|
1359
1234
|
});
|
|
1360
1235
|
|
|
1361
|
-
test('getReportData returns an error for a nonexistent report', async () => {
|
|
1362
|
-
const {getReportData} = await import('./util.ts');
|
|
1363
|
-
const result: any = await getReportData('990101T0000', 'xxx');
|
|
1364
|
-
assert.ok(result.error);
|
|
1365
|
-
});
|
|
1366
|
-
|
|
1367
|
-
test('getReportData falls back to the engine ID for an unknown prevented engine', async () => {
|
|
1368
|
-
const {getReportData} = await import('./util.ts');
|
|
1369
|
-
const prvJSON = await fs.readFile(path.join(reportsPath(), '260101T0006-prv.json'), 'utf8');
|
|
1370
|
-
const report = JSON.parse(prvJSON);
|
|
1371
|
-
report.jobData.preventions.unknownEngine = 'mystery failure';
|
|
1372
|
-
const reportPath = path.join(reportsPath(), '260103T0000-unk.json');
|
|
1373
|
-
await fs.writeFile(reportPath, JSON.stringify(report));
|
|
1374
|
-
try {
|
|
1375
|
-
const result: any = await getReportData('260103T0000', 'unk');
|
|
1376
|
-
assert.ok(result.preventedEngineNames.includes('unknownEngine'));
|
|
1377
|
-
}
|
|
1378
|
-
finally {
|
|
1379
|
-
await fs.unlink(reportPath);
|
|
1380
|
-
}
|
|
1381
|
-
});
|
|
1382
|
-
|
|
1383
1236
|
// TESTS FOR REMAINING BRANCH COVERAGE IN util.js
|
|
1384
1237
|
|
|
1385
1238
|
test('getAgoDays returns null for an invalid Date object', () => {
|
|
1386
1239
|
assert.equal(getAgoDays(new Date('invalid')), null);
|
|
1387
1240
|
});
|
|
1388
1241
|
|
|
1389
|
-
test('getAgoString returns "1 day" for exactly 1 day ago', () => {
|
|
1390
|
-
// Construct a time stamp 1 day and 1 hour ago, so Math.round gives exactly 1.
|
|
1391
|
-
const date = new Date(Date.now() - (86400000 + 3600000));
|
|
1392
|
-
const stamp = date.toISOString().slice(2).replace(/[-:]/g, '').slice(0, 11);
|
|
1393
|
-
assert.equal(getAgoString(stamp), '1 day');
|
|
1394
|
-
});
|
|
1395
|
-
|
|
1396
1242
|
test('getIssue returns null for a known engine with an unknown variable rule', () => {
|
|
1397
1243
|
assert.equal(getIssue('axe', 'nonexistent-rule-id'), null);
|
|
1398
1244
|
});
|
|
@@ -1403,12 +1249,6 @@ test('getIssue returns an issue ID for a variable rule pattern match', () => {
|
|
|
1403
1249
|
assert.equal(result, 'duplicateAttribute');
|
|
1404
1250
|
});
|
|
1405
1251
|
|
|
1406
|
-
test('getDateTimeString describes an invalid time stamp as unknown', async () => {
|
|
1407
|
-
const {getDateTimeString} = await import('./util.ts');
|
|
1408
|
-
const result = getDateTimeString('999999T9999');
|
|
1409
|
-
assert.equal(result, 'an unknown date at an unknown time');
|
|
1410
|
-
});
|
|
1411
|
-
|
|
1412
1252
|
test('getPOSTData resolves with parsed JSON for application/json requests', async () => {
|
|
1413
1253
|
const {Readable} = await import('node:stream');
|
|
1414
1254
|
const {getPOSTData} = await import('./util.ts');
|
|
@@ -1464,35 +1304,6 @@ test('getPOSTData resolves with null for a malformed JSON body', async () => {
|
|
|
1464
1304
|
assert.equal(result, null);
|
|
1465
1305
|
});
|
|
1466
1306
|
|
|
1467
|
-
test('getEngineNamesString falls back to the engine ID for an unknown engine', async () => {
|
|
1468
|
-
const {getEngineNamesString} = await import('./util.ts');
|
|
1469
|
-
const result = getEngineNamesString(new Set(['unknownEngine']));
|
|
1470
|
-
assert.equal(result, 'unknownEngine');
|
|
1471
|
-
});
|
|
1472
|
-
|
|
1473
|
-
test('getPathID returns the catalog pathID when catalogIndex is truthy', async () => {
|
|
1474
|
-
const {getPathID} = await import('./util.ts');
|
|
1475
|
-
const catalog = {'0': {tagName: 'div', pathID: '/html/body/div'}};
|
|
1476
|
-
assert.equal(getPathID(catalog, '0', '/fallback'), '/html/body/div');
|
|
1477
|
-
});
|
|
1478
|
-
|
|
1479
|
-
test('getPathID returns the fallback pathID when catalogIndex is truthy but catalogItem has no pathID', async () => {
|
|
1480
|
-
const {getPathID} = await import('./util.ts');
|
|
1481
|
-
const catalog = {'0': {tagName: 'div', pathID: ''}};
|
|
1482
|
-
assert.equal(getPathID(catalog, '0', '/fallback'), '/fallback');
|
|
1483
|
-
});
|
|
1484
|
-
|
|
1485
|
-
test('getPathID returns /html when catalogIndex is truthy but catalogItem and pathID are both missing', async () => {
|
|
1486
|
-
const {getPathID} = await import('./util.ts');
|
|
1487
|
-
const catalog = {};
|
|
1488
|
-
assert.equal(getPathID(catalog, '0', null as any), '/html');
|
|
1489
|
-
});
|
|
1490
|
-
|
|
1491
|
-
test('getPathID returns /html when catalogIndex is falsy and pathID is null', async () => {
|
|
1492
|
-
const {getPathID} = await import('./util.ts');
|
|
1493
|
-
assert.equal(getPathID({}, null as any, null as any), '/html');
|
|
1494
|
-
});
|
|
1495
|
-
|
|
1496
1307
|
test('isUsableReport returns false for a report with a test act using an unknown engine', async () => {
|
|
1497
1308
|
const {isUsableReport} = await import('./util.ts');
|
|
1498
1309
|
const report = {
|
|
@@ -1556,17 +1367,6 @@ test('getTimeStamp returns an 11-character stamp from a Date', () => {
|
|
|
1556
1367
|
assert.equal(stamp.slice(7), '1430');
|
|
1557
1368
|
});
|
|
1558
1369
|
|
|
1559
|
-
test('getEngineNamesString returns a sorted +-delimited list of engine names', () => {
|
|
1560
|
-
const result = getEngineNamesString(new Set(['axe', 'wave', 'nuVal']));
|
|
1561
|
-
const names = result.split(' + ');
|
|
1562
|
-
assert.ok(names.length === 3);
|
|
1563
|
-
assert.ok(names.includes('WAVE'));
|
|
1564
|
-
});
|
|
1565
|
-
|
|
1566
|
-
test('getEngineNamesString falls back to the ID for an unknown engine', () => {
|
|
1567
|
-
assert.equal(getEngineNamesString(new Set(['unknownEngine'])), 'unknownEngine');
|
|
1568
|
-
});
|
|
1569
|
-
|
|
1570
1370
|
test('testRequestsLock is a function (the lock returned by createLock)', () => {
|
|
1571
1371
|
assert.equal(typeof testRequestsLock, 'function');
|
|
1572
1372
|
});
|
|
@@ -1619,11 +1419,6 @@ test('getReportExtracts with onlyLatest returns only the latest report for each
|
|
|
1619
1419
|
assert.equal(mixReports[0]!.timeStamp, '260202T0000');
|
|
1620
1420
|
});
|
|
1621
1421
|
|
|
1622
|
-
test('getMultiReportWhats returns descriptions that have multiple reports', async () => {
|
|
1623
|
-
const whats = await getMultiReportWhats();
|
|
1624
|
-
assert.ok(whats.includes('Mixed Outcomes Page'));
|
|
1625
|
-
});
|
|
1626
|
-
|
|
1627
1422
|
test('getExclusionCookieValue is deterministic for a given AUTH_CODE', async () => {
|
|
1628
1423
|
const savedAuthCode = process.env.AUTH_CODE;
|
|
1629
1424
|
process.env.AUTH_CODE = 'test-auth-code';
|
package/util.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// c8 intermittently reports import lines as uncovered due to a range-merge
|
|
10
10
|
// artifact in its remapping of Node's type-stripped source.
|
|
11
11
|
import {sendAlert} from './alerts.ts';
|
|
12
|
-
import {
|
|
12
|
+
import {rules as ruleSpecs} from 'testaro-issues';
|
|
13
13
|
import type {Act, Catalog, Report, StandardInstance} from 'testaro';
|
|
14
14
|
import crypto from 'node:crypto';
|
|
15
15
|
import dns from 'node:dns/promises';
|
|
@@ -19,7 +19,6 @@ import net from 'node:net';
|
|
|
19
19
|
import path from 'node:path';
|
|
20
20
|
import querystring from 'node:querystring';
|
|
21
21
|
/* c8 ignore stop */
|
|
22
|
-
import wcagMap from './wcagMap.json' with {type: 'json'};
|
|
23
22
|
|
|
24
23
|
// CONSTANTS
|
|
25
24
|
|
|
@@ -139,47 +138,10 @@ export type ReportExtract = {
|
|
|
139
138
|
superseded?: boolean;
|
|
140
139
|
};
|
|
141
140
|
|
|
142
|
-
// Page data from an available report.
|
|
143
|
-
export type PageData = {
|
|
144
|
-
description: string;
|
|
145
|
-
url: string;
|
|
146
|
-
daysAgo: number | null;
|
|
147
|
-
error?: never;
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
// HTML strings describing the page data of an available report.
|
|
151
|
-
export type PageDataStrings = {
|
|
152
|
-
description: string;
|
|
153
|
-
url: string;
|
|
154
|
-
urlLink: string;
|
|
155
|
-
testInfo: string;
|
|
156
|
-
error?: never;
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
// Basics about an available report.
|
|
160
|
-
export type ReportData = {
|
|
161
|
-
description: string;
|
|
162
|
-
url: string;
|
|
163
|
-
jobName: unknown;
|
|
164
|
-
creationDate: Date | null;
|
|
165
|
-
daysAgo: number | null;
|
|
166
|
-
issueCount: number;
|
|
167
|
-
engineNames: string[];
|
|
168
|
-
engineCount: number;
|
|
169
|
-
reporterNames: string[];
|
|
170
|
-
reporterCount: number;
|
|
171
|
-
violatorCount: number;
|
|
172
|
-
preventedEngineNames: string[];
|
|
173
|
-
preventedEngineCount: number;
|
|
174
|
-
error?: never;
|
|
175
|
-
};
|
|
176
|
-
|
|
177
141
|
// MISCELLANEOUS FUNCTIONS
|
|
178
142
|
|
|
179
143
|
// Compares strings alphabetically and case-insensitively.
|
|
180
|
-
const alphaCompare = (a: string, b: string) => a.localeCompare(b, 'en', {sensitivity: 'base'});
|
|
181
|
-
// Sorts strings alphabetically and case-insensitively.
|
|
182
|
-
const alphaSort = (strings: string[]) => strings.sort((a, b) => alphaCompare(a, b));
|
|
144
|
+
export const alphaCompare = (a: string, b: string) => a.localeCompare(b, 'en', {sensitivity: 'base'});
|
|
183
145
|
// Returns whether a string's length is within the given bounds. Generic length check
|
|
184
146
|
// shared by any field with a min/max character-count rule (e.g. a request description
|
|
185
147
|
// or reason), so each caller states its own bounds and label instead of hardcoding an
|
|
@@ -198,22 +160,6 @@ export const checkLength = (
|
|
|
198
160
|
}
|
|
199
161
|
return {status: 'ok'};
|
|
200
162
|
};
|
|
201
|
-
// Returns whether a comment's raw length is within the 20-to-1000-character bounds.
|
|
202
|
-
export const checkCommentLength = (
|
|
203
|
-
content: string
|
|
204
|
-
): {status: 'ok'} | {status: 'error'; message: string} => {
|
|
205
|
-
const contentLength = content.length;
|
|
206
|
-
// If the length is invalid:
|
|
207
|
-
if (contentLength < 20 || contentLength > 1000) {
|
|
208
|
-
// Return the applicable error message.
|
|
209
|
-
const messageSpec = contentLength < 20 ? 'shorter than 20' : 'longer than 1000';
|
|
210
|
-
return {
|
|
211
|
-
status: 'error',
|
|
212
|
-
message: `Your comment was ${messageSpec} characters`
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
return {status: 'ok'};
|
|
216
|
-
};
|
|
217
163
|
// Returns whether a comment repeats, verbatim, one of the given comments, regardless
|
|
218
164
|
// of when the earlier one was submitted: a legitimate need to resubmit exactly the
|
|
219
165
|
// same content never arises (the near-term case is a browser "Back" resubmission),
|
|
@@ -244,10 +190,6 @@ export const createLock = (): (<T>(fn: () => T | Promise<T>) => Promise<T>) => {
|
|
|
244
190
|
return result;
|
|
245
191
|
};
|
|
246
192
|
};
|
|
247
|
-
// Returns a string encoded for use as a URL fragment.
|
|
248
|
-
const fragmentEncode = (string: string) => {
|
|
249
|
-
return encodeURIComponent(string).replace(/-/g, '%2D');
|
|
250
|
-
};
|
|
251
193
|
// Returns the time in days since a Date or time stamp, or null if the argument is invalid.
|
|
252
194
|
export const getAgoDays = (timeArg: string | Date): number | null => {
|
|
253
195
|
let dateTime;
|
|
@@ -274,16 +216,6 @@ export const getAgoDays = (timeArg: string | Date): number | null => {
|
|
|
274
216
|
// Otherwise, i.e. if it is valid, return the elapsed days since then.
|
|
275
217
|
return Math.round((Date.now() - dateTime.getTime()) / (1000 * 60 * 60 * 24));
|
|
276
218
|
};
|
|
277
|
-
// Returns a string describing the time in days since a time stamp.
|
|
278
|
-
export const getAgoString = (timeStamp: string): string => {
|
|
279
|
-
const agoDays = getAgoDays(timeStamp);
|
|
280
|
-
if (agoDays === null) {
|
|
281
|
-
return 'an unknown number of days';
|
|
282
|
-
}
|
|
283
|
-
return agoDays === 1 ? '1 day' : `${agoDays} days`;
|
|
284
|
-
};
|
|
285
|
-
// Returns a string describing a count.
|
|
286
|
-
export const getCountString = (count: number, singular: string, plural: string): string => count === 1 ? `1 ${singular}` : `${count} ${plural}`;
|
|
287
219
|
// Returns a date string from a time stamp.
|
|
288
220
|
export const getDateString = (timeStamp: string): string => {
|
|
289
221
|
const dateString = `20${timeStamp.slice(0, 2)}-${timeStamp.slice(2, 4)}-${timeStamp.slice(4,6)}`;
|
|
@@ -435,19 +367,6 @@ export const getTimeStamp = (date: Date): string => {
|
|
|
435
367
|
export const getNowStamp = (): string => {
|
|
436
368
|
return getTimeStamp(new Date());
|
|
437
369
|
};
|
|
438
|
-
// Returns a time string from a time stamp.
|
|
439
|
-
const getTimeString = (timeStamp: string) => {
|
|
440
|
-
const timeString = `${timeStamp.slice(7, 9)}:${timeStamp.slice(9, 11)}`;
|
|
441
|
-
// Return the time string if valid, or null if not.
|
|
442
|
-
return (!isNaN(Date.parse(`2000-01-01T${timeString}Z`))) ? timeString : null;
|
|
443
|
-
};
|
|
444
|
-
// Returns a date-and-time string.
|
|
445
|
-
export const getDateTimeString = (timeStamp: string): string => {
|
|
446
|
-
const dateString = getDateString(timeStamp) || 'an unknown date';
|
|
447
|
-
const timeString = getTimeString(timeStamp) || 'an unknown time';
|
|
448
|
-
const dateTimeString = `${dateString} at ${timeString}`;
|
|
449
|
-
return dateTimeString;
|
|
450
|
-
}
|
|
451
370
|
// Converts a string to a plain-text 1-line ASCII string.
|
|
452
371
|
export const getPlainText = (string: string): string => string
|
|
453
372
|
.replace(/&/g, '+')
|
|
@@ -512,26 +431,6 @@ export const getTestRequests = async (): Promise<TestRequests> => {
|
|
|
512
431
|
throw new Error('Test-requests file not JSON', {cause: error});
|
|
513
432
|
}
|
|
514
433
|
};
|
|
515
|
-
// Converts a catalog item text to a text-fragment link destination.
|
|
516
|
-
export const getTextFragmentHref = (text: string, url: string): string => {
|
|
517
|
-
const fragmentList = text
|
|
518
|
-
.split('\n')
|
|
519
|
-
.map(fragment => fragmentEncode(fragment))
|
|
520
|
-
.join(',');
|
|
521
|
-
// Return a text-fragment link.
|
|
522
|
-
return `${url}#:~:text=${fragmentList}`;
|
|
523
|
-
};
|
|
524
|
-
// Returns a +-delimited list of sorted names of rule engines.
|
|
525
|
-
export const getEngineNamesString = (engineIDSet: Iterable<string>): string => alphaSort(
|
|
526
|
-
Array.from(engineIDSet).map(engineID => ruleEngines[engineID]?.[0] || engineID)
|
|
527
|
-
).join(' + ');
|
|
528
|
-
// Gets the WCAG Understanding link for a numeric WCAG standard identifier.
|
|
529
|
-
export const getWCAGLink = (numericID: string): string => {
|
|
530
|
-
// Return the link.
|
|
531
|
-
return `https://www.w3.org/WAI/WCAG22/Understanding/${(wcagMap as Record<string, string>)[numericID]}`;
|
|
532
|
-
};
|
|
533
|
-
// Gets the name of an issue weight.
|
|
534
|
-
export const getWeightName = (weight: number): string => ['lowest', 'low', 'high', 'highest'][weight - 1] ?? 'unknown';
|
|
535
434
|
// Makes a string HTML-safe.
|
|
536
435
|
export const htmlSafe = (string: string): string => string ? string
|
|
537
436
|
.replace(/&/g, '&')
|
|
@@ -674,8 +573,6 @@ export const isAllowedRedirectTarget = async (
|
|
|
674
573
|
export const isValidAuthCode = (authCode: string | undefined | null): boolean => {
|
|
675
574
|
return !!authCode && authCode === process.env.AUTH_CODE;
|
|
676
575
|
};
|
|
677
|
-
// Makes a string breakable before non-initial slashes.
|
|
678
|
-
export const makeBreakable = (string: string): string => string.replace(/\//g, '<wbr>/').replace(/^<wbr>/, '');
|
|
679
576
|
// Minifies a URL for duplicate detection.
|
|
680
577
|
export const minifyURL = (url: string): string => url.replace(/www\.|\/$/g, '').toLowerCase();
|
|
681
578
|
// Sorts objects by a property value and returns the sorted array.
|
|
@@ -770,16 +667,6 @@ export const getTestActInstances = (
|
|
|
770
667
|
}
|
|
771
668
|
return pairs;
|
|
772
669
|
};
|
|
773
|
-
// Returns the path ID of the element of a standard instance.
|
|
774
|
-
export const getPathID = (catalog: Catalog, catalogIndex: string, pathID?: string) => {
|
|
775
|
-
if (catalogIndex) {
|
|
776
|
-
const pathIDFromCatalog = (catalog[catalogIndex] || {}).pathID;
|
|
777
|
-
if (pathIDFromCatalog) {
|
|
778
|
-
return pathIDFromCatalog;
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
return pathID ?? '/html';
|
|
782
|
-
};
|
|
783
670
|
// Returns the path of an available report file.
|
|
784
671
|
export const getReportPath = (timeStamp: string, jobID: string): string => path
|
|
785
672
|
.join(reportsPath(), `${timeStamp}-${jobID}.json`);
|
|
@@ -866,122 +753,6 @@ export const annotateReportObject = async (report: UsableReport): Promise<void>
|
|
|
866
753
|
);
|
|
867
754
|
}
|
|
868
755
|
};
|
|
869
|
-
// Returns basics about an available report.
|
|
870
|
-
export const getReportData = async (timeStamp: string, jobID: string): Promise<ReportData | {error: string}> => {
|
|
871
|
-
// Get the report.
|
|
872
|
-
const report = await getReport(timeStamp, jobID);
|
|
873
|
-
// If this failed:
|
|
874
|
-
if (isReportError(report)) {
|
|
875
|
-
// Return why.
|
|
876
|
-
return {error: report.error};
|
|
877
|
-
}
|
|
878
|
-
// Otherwise, i.e. if it succeeded, initialize the data.
|
|
879
|
-
const data = {
|
|
880
|
-
description: report.target.what,
|
|
881
|
-
url: report.target.url,
|
|
882
|
-
jobName: report.id,
|
|
883
|
-
creationDate: getDateTime(timeStamp),
|
|
884
|
-
daysAgo: getAgoDays(timeStamp),
|
|
885
|
-
issueCount: 0,
|
|
886
|
-
engineNames: [] as string[],
|
|
887
|
-
engineCount: 0,
|
|
888
|
-
reporterNames: [] as string[],
|
|
889
|
-
reporterCount: 0,
|
|
890
|
-
violatorCount: 0,
|
|
891
|
-
preventedEngineNames: [] as string[],
|
|
892
|
-
preventedEngineCount: 0
|
|
893
|
-
};
|
|
894
|
-
const issueIDSet = new Set<string>();
|
|
895
|
-
const engineNameSet = new Set<string>();
|
|
896
|
-
const reporterIDSet = new Set<string>();
|
|
897
|
-
const violatorIndexSet = new Set<string>();
|
|
898
|
-
// For each test act of the report:
|
|
899
|
-
getTestActs(report).forEach(act => {
|
|
900
|
-
// Ensure that the rule engine is in the temporary data.
|
|
901
|
-
engineNameSet.add(ruleEngines[act.which!]![0]);
|
|
902
|
-
});
|
|
903
|
-
// For each violating standard instance of each test act:
|
|
904
|
-
getTestActInstances(report, {violationsOnly: true}).forEach(({act, instance}) => {
|
|
905
|
-
const {catalogIndex, issueID} = instance;
|
|
906
|
-
// If it has a non-ignorable classified issue ID:
|
|
907
|
-
if (issueID && issueSpecs[issueID] && issueID !== 'ignorable') {
|
|
908
|
-
// Ensure that the rule engine is in the temporary data.
|
|
909
|
-
reporterIDSet.add(act.which!);
|
|
910
|
-
// Ensure that the issue is in the temporary data.
|
|
911
|
-
issueIDSet.add(issueID);
|
|
912
|
-
// If the violator has a catalog index:
|
|
913
|
-
if (catalogIndex) {
|
|
914
|
-
// Ensure that the violator is in the temporary data.
|
|
915
|
-
violatorIndexSet.add(String(catalogIndex));
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
});
|
|
919
|
-
// Populate the data with the act data.
|
|
920
|
-
data.issueCount = issueIDSet.size;
|
|
921
|
-
data.engineNames = Array
|
|
922
|
-
.from(engineNameSet)
|
|
923
|
-
.sort((a, b) => a.localeCompare(b, 'en', {sensitivity: 'base'}));
|
|
924
|
-
data.engineCount = engineNameSet.size;
|
|
925
|
-
data.reporterNames = Array
|
|
926
|
-
.from(reporterIDSet)
|
|
927
|
-
.map(id => ruleEngines[id]![0])
|
|
928
|
-
.sort((a, b) => a.localeCompare(b, 'en', {sensitivity: 'base'}));
|
|
929
|
-
data.reporterCount = data.reporterNames.length;
|
|
930
|
-
data.violatorCount = violatorIndexSet.size;
|
|
931
|
-
// Add the names of any prevented rule engines to the data.
|
|
932
|
-
data.preventedEngineNames = Object.keys(report.jobData?.preventions || {})
|
|
933
|
-
.map(engineID => ruleEngines[engineID]?.[0] || engineID)
|
|
934
|
-
.sort((a, b) => a.localeCompare(b, 'en', {sensitivity: 'base'}));
|
|
935
|
-
data.preventedEngineCount = data.preventedEngineNames.length;
|
|
936
|
-
// Return the data.
|
|
937
|
-
return data;
|
|
938
|
-
}
|
|
939
|
-
// Returns page data from an available report.
|
|
940
|
-
export const getPageData = async (timeStamp: string, jobID: string): Promise<PageData | {error: string}> => {
|
|
941
|
-
// Get the report.
|
|
942
|
-
const report = await getReport(timeStamp, jobID);
|
|
943
|
-
// If this failed:
|
|
944
|
-
if (isReportError(report)) {
|
|
945
|
-
// Return why.
|
|
946
|
-
return report;
|
|
947
|
-
}
|
|
948
|
-
const {what: description, url} = report.target;
|
|
949
|
-
// Get the elapsed time in days since the report was completed, using the
|
|
950
|
-
// report content rather than the file system birth time.
|
|
951
|
-
const daysAgo = getAgoDays(new Date(`20${report.jobData.endTime}Z`));
|
|
952
|
-
// Return the data.
|
|
953
|
-
return {
|
|
954
|
-
description,
|
|
955
|
-
url,
|
|
956
|
-
daysAgo
|
|
957
|
-
};
|
|
958
|
-
};
|
|
959
|
-
// Gets HTML strings for page data from a report.
|
|
960
|
-
export const getPageDataStrings = async (
|
|
961
|
-
timeStamp: string,
|
|
962
|
-
jobID: string,
|
|
963
|
-
pageData?: PageData | {error: string}
|
|
964
|
-
): Promise<PageDataStrings | {error: string}> => {
|
|
965
|
-
// Get the page data if they were not specified.
|
|
966
|
-
const data = pageData ?? await getPageData(timeStamp, jobID);
|
|
967
|
-
// If the page data are invalid:
|
|
968
|
-
if (data.error !== undefined) {
|
|
969
|
-
// Return why.
|
|
970
|
-
return {
|
|
971
|
-
error: data.error
|
|
972
|
-
};
|
|
973
|
-
}
|
|
974
|
-
const {daysAgo, url, description} = data;
|
|
975
|
-
// Otherwise, i.e. if they are valid, get a description of the timestamp.
|
|
976
|
-
const when = getDateTimeString(timeStamp);
|
|
977
|
-
// Return the HTML strings.
|
|
978
|
-
return {
|
|
979
|
-
description,
|
|
980
|
-
url,
|
|
981
|
-
urlLink: `<a href="${url}">${url}</a>`,
|
|
982
|
-
testInfo: `Tested ${daysAgo === 1 ? '1 day' : `${daysAgo} days`} ago by job <code>${jobID}</code> on ${when}`
|
|
983
|
-
};
|
|
984
|
-
};
|
|
985
756
|
// Returns the creation time and size of a report.
|
|
986
757
|
export const getReportStats = async (timeStamp: string, jobID: string) => {
|
|
987
758
|
let reportStat;
|
|
@@ -1058,15 +829,6 @@ export const isReportAvailable = async (description: string, url: string): Promi
|
|
|
1058
829
|
const miniURLs = reportExtracts.map(reportExtract => minifyURL(reportExtract.url));
|
|
1059
830
|
return descriptions.includes(description) || miniURLs.includes(minifyURL(url));
|
|
1060
831
|
};
|
|
1061
|
-
// Gets the descriptions of multi-report pages.
|
|
1062
|
-
export const getMultiReportWhats = async (): Promise<string[]> => {
|
|
1063
|
-
const reportExtracts = await getReportExtracts();
|
|
1064
|
-
const sortedDescriptions = reportExtracts.map(extract => extract.description).sort();
|
|
1065
|
-
const multiReportDescriptions = sortedDescriptions.filter(
|
|
1066
|
-
(description, index) => description !== sortedDescriptions[index - 1] && description === sortedDescriptions[index + 1]
|
|
1067
|
-
);
|
|
1068
|
-
return multiReportDescriptions;
|
|
1069
|
-
};
|
|
1070
832
|
// Returns the property that an approved job in a category has.
|
|
1071
833
|
const getApprovedJobProperty = async (
|
|
1072
834
|
description: string, url: string, category: 'claimed' | 'queue'
|
|
@@ -7,7 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
// IMPORTS
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
errorMessage,
|
|
12
|
+
getJSON,
|
|
13
|
+
getPlainText,
|
|
14
|
+
isValidAuthCode,
|
|
15
|
+
populateTemplate
|
|
16
|
+
} from '../../util.ts';
|
|
17
|
+
import {getDateTimeString} from '../util.ts';
|
|
11
18
|
import fs from 'node:fs/promises';
|
|
12
19
|
import path from 'node:path';
|
|
13
20
|
|
|
@@ -6,17 +6,19 @@
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
-
getPageDataStrings,
|
|
10
9
|
getReport,
|
|
11
10
|
getTestActInstances,
|
|
12
|
-
getTextFragmentHref,
|
|
13
|
-
getWCAGLink,
|
|
14
|
-
getWeightName,
|
|
15
11
|
htmlSafe,
|
|
16
12
|
isReportError,
|
|
17
13
|
populateTemplate,
|
|
18
14
|
ruleEngines
|
|
19
15
|
} from '../../util.ts';
|
|
16
|
+
import {
|
|
17
|
+
getPageDataStrings,
|
|
18
|
+
getTextFragmentHref,
|
|
19
|
+
getWCAGLink,
|
|
20
|
+
getWeightName
|
|
21
|
+
} from '../util.ts';
|
|
20
22
|
import {issues as issueSpecs} from 'testaro-issues';
|
|
21
23
|
|
|
22
24
|
// FUNCTIONS
|