@jrpool/kilotest 46.0.7 → 46.0.11
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 +3 -1
- package/.github/workflows/ci.yml +32 -0
- 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 +8 -10
- package/api/util.test.js +68 -2
- package/apiIntegration.test.js +92 -0
- package/docs/CI.json +55 -0
- package/docs/SERVICE.md +25 -1
- package/index.js +109 -66
- 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/mcp.js +2 -2
- package/mcp.test.js +281 -0
- package/package.json +11 -5
- package/routes.test.js +69 -0
- package/routing.test.js +13 -3
- package/smokeTest.js +123 -0
- 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/tutorial.test.js +97 -0
- package/web/unhideReportForm/unhideReportForm.test.js +135 -0
package/routes.test.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/*
|
|
2
|
+
routes.test.js
|
|
3
|
+
Tests for the centralized routes table exported from index.js.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
const {test} = require('node:test');
|
|
9
|
+
const assert = require('node:assert/strict');
|
|
10
|
+
const {routes} = require('./index');
|
|
11
|
+
|
|
12
|
+
// TESTS
|
|
13
|
+
|
|
14
|
+
test('routes has GET and POST arrays', () => {
|
|
15
|
+
assert.ok(Array.isArray(routes.GET));
|
|
16
|
+
assert.ok(Array.isArray(routes.POST));
|
|
17
|
+
assert.ok(routes.GET.length > 0);
|
|
18
|
+
assert.ok(routes.POST.length > 0);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('routes GET includes all known GET paths', () => {
|
|
22
|
+
const required = [
|
|
23
|
+
'/mcp',
|
|
24
|
+
'/',
|
|
25
|
+
'/index.html',
|
|
26
|
+
'/robots.txt',
|
|
27
|
+
'/openapi.yaml',
|
|
28
|
+
'/openapi.json',
|
|
29
|
+
'/swagger.yaml',
|
|
30
|
+
'/swagger.json',
|
|
31
|
+
'/api-docs',
|
|
32
|
+
'/llms.txt',
|
|
33
|
+
'/llms-full.txt',
|
|
34
|
+
'/sitemap.xml',
|
|
35
|
+
'/style.css'
|
|
36
|
+
];
|
|
37
|
+
for (const path of required) {
|
|
38
|
+
assert.ok(routes.GET.includes(path), `routes.GET missing ${path}`);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('routes POST includes all known POST paths', () => {
|
|
43
|
+
const required = [
|
|
44
|
+
'/mcp',
|
|
45
|
+
'/requestTest.html',
|
|
46
|
+
'/recAction.html',
|
|
47
|
+
'/reannotate.html',
|
|
48
|
+
'/renewWCAG.html',
|
|
49
|
+
'/worker/job',
|
|
50
|
+
'/worker/report',
|
|
51
|
+
'/tutorialComment.html'
|
|
52
|
+
];
|
|
53
|
+
for (const path of required) {
|
|
54
|
+
assert.ok(routes.POST.includes(path), `routes.POST missing ${path}`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('routes GET includes wildcard patterns for dynamic paths', () => {
|
|
59
|
+
assert.ok(routes.GET.some(p => p.includes('fullReport.json')));
|
|
60
|
+
assert.ok(routes.GET.some(p => p.includes('/api/')));
|
|
61
|
+
assert.ok(routes.GET.some(p => p.includes('tutorial/images')));
|
|
62
|
+
assert.ok(routes.GET.some(p => p.includes('favicon')));
|
|
63
|
+
assert.ok(routes.GET.some(p => p.includes('.html')));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('routes POST includes wildcard patterns for dynamic paths', () => {
|
|
67
|
+
assert.ok(routes.POST.some(p => p.includes('requestRetest.html')));
|
|
68
|
+
assert.ok(routes.POST.some(p => p.includes('/api/')));
|
|
69
|
+
});
|
package/routing.test.js
CHANGED
|
@@ -23,7 +23,7 @@ let server;
|
|
|
23
23
|
before(async () => {
|
|
24
24
|
process.env.DB_DIR = path.join(__dirname, 'test', 'fixtures', 'db');
|
|
25
25
|
server = http.createServer(requestHandler);
|
|
26
|
-
await new Promise(resolve => server.listen(port, resolve));
|
|
26
|
+
await new Promise(resolve => server.listen(port, () => resolve()));
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
after(async () => {
|
|
@@ -33,7 +33,17 @@ after(async () => {
|
|
|
33
33
|
else {
|
|
34
34
|
delete process.env.DB_DIR;
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
server.closeAllConnections?.();
|
|
37
|
+
await new Promise(resolve => {
|
|
38
|
+
const timer = setTimeout(() => {
|
|
39
|
+
server.closeAllConnections?.();
|
|
40
|
+
resolve();
|
|
41
|
+
}, 1000);
|
|
42
|
+
server.close(() => {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
resolve();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
37
47
|
});
|
|
38
48
|
|
|
39
49
|
// HELPERS
|
|
@@ -73,5 +83,5 @@ test('GET /api/listReports returns a valid JSON response', async () => {
|
|
|
73
83
|
const body = JSON.parse(result.body);
|
|
74
84
|
assert.equal(body['tool name'], 'listReports');
|
|
75
85
|
const reports = body['response content']['basics about all available reports'];
|
|
76
|
-
assert.equal(reports.length,
|
|
86
|
+
assert.equal(reports.length, 8);
|
|
77
87
|
});
|
package/smokeTest.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/*
|
|
2
|
+
smokeTest.js
|
|
3
|
+
Sends requests to all valid GET and POST paths on the deployed server and verifies that Caddy forwards them to Kilotest (i.e., the response is not a bare Caddy 404).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
const https = require('https');
|
|
9
|
+
const {routes} = require('./index');
|
|
10
|
+
|
|
11
|
+
// CONSTANTS
|
|
12
|
+
|
|
13
|
+
const host = process.env.SMOKE_HOST || 'kilotest.com';
|
|
14
|
+
// Concrete paths matching each wildcard pattern in the routes table, for smoke testing.
|
|
15
|
+
const concretePaths = {
|
|
16
|
+
GET: {
|
|
17
|
+
'/mcp': '/mcp',
|
|
18
|
+
'/': '/',
|
|
19
|
+
'/index.html': '/index.html',
|
|
20
|
+
'/robots.txt': '/robots.txt',
|
|
21
|
+
'/openapi.yaml': '/openapi.yaml',
|
|
22
|
+
'/openapi.json': '/openapi.json',
|
|
23
|
+
'/swagger.yaml': '/swagger.yaml',
|
|
24
|
+
'/swagger.json': '/swagger.json',
|
|
25
|
+
'/api-docs': '/api-docs',
|
|
26
|
+
'/llms.txt': '/llms.txt',
|
|
27
|
+
'/llms-full.txt': '/llms-full.txt',
|
|
28
|
+
'/sitemap.xml': '/sitemap.xml',
|
|
29
|
+
'/style.css': '/style.css',
|
|
30
|
+
'/fullReport.json/*': '/fullReport.json/260101T0000/mix',
|
|
31
|
+
'/api/*': '/api/listReports',
|
|
32
|
+
'/tutorial/images/*': '/tutorial/images/diagram.png',
|
|
33
|
+
'/favicon.*': '/favicon.ico',
|
|
34
|
+
'*.html*': '/listReports.html'
|
|
35
|
+
},
|
|
36
|
+
POST: {
|
|
37
|
+
'/mcp': '/mcp',
|
|
38
|
+
'/requestTest.html': '/requestTest.html',
|
|
39
|
+
'/requestRetest.html/*': '/requestRetest.html/260101T0001/ct',
|
|
40
|
+
'/recAction.html': '/recAction.html',
|
|
41
|
+
'/reannotate.html': '/reannotate.html',
|
|
42
|
+
'/renewWCAG.html': '/renewWCAG.html',
|
|
43
|
+
'/worker/job': '/worker/job',
|
|
44
|
+
'/worker/report': '/worker/report',
|
|
45
|
+
'/api/*': '/api/requestFeature',
|
|
46
|
+
'/tutorialComment.html': '/tutorialComment.html'
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
// Minimal POST bodies for paths that require them.
|
|
50
|
+
const postBodies = {
|
|
51
|
+
'/api/requestFeature': {feature: 'smoke test'},
|
|
52
|
+
'/requestTest.html': {what: 'Smoke Test Page', url: 'https://smoketest.example.com', why: 'smoke test'},
|
|
53
|
+
'/requestRetest.html/260101T0001/ct': {why: 'smoke test'},
|
|
54
|
+
'/recAction.html': {target: 'https://smoketest.example.com\tSmoke Test Page', authCode: 'invalid'},
|
|
55
|
+
'/reannotate.html': {authCode: 'invalid'},
|
|
56
|
+
'/renewWCAG.html': {authCode: 'invalid'},
|
|
57
|
+
'/worker/job': {},
|
|
58
|
+
'/worker/report': {},
|
|
59
|
+
'/tutorialComment.html': {content: 'smoke test'},
|
|
60
|
+
'/mcp': {}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// FUNCTIONS
|
|
64
|
+
|
|
65
|
+
// Sends an HTTPS request and returns the status code and body length.
|
|
66
|
+
const sendRequest = (method, requestPath) => new Promise((resolve, reject) => {
|
|
67
|
+
const body = method === 'POST' ? JSON.stringify(postBodies[requestPath] || {}) : null;
|
|
68
|
+
const options = {
|
|
69
|
+
method,
|
|
70
|
+
host,
|
|
71
|
+
path: requestPath,
|
|
72
|
+
headers: body ? {
|
|
73
|
+
'content-type': 'application/json; charset=utf-8',
|
|
74
|
+
'content-length': Buffer.byteLength(body)
|
|
75
|
+
} : {}
|
|
76
|
+
};
|
|
77
|
+
const req = https.request(options, response => {
|
|
78
|
+
const chunks = [];
|
|
79
|
+
response.on('data', chunk => chunks.push(chunk));
|
|
80
|
+
response.on('end', () => {
|
|
81
|
+
const responseBody = chunks.join('');
|
|
82
|
+
resolve({statusCode: response.statusCode, bodyLength: responseBody.length});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
req.on('error', reject);
|
|
86
|
+
req.end(body || '');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Returns whether a response is a bare Caddy 404 (the failure this test detects).
|
|
90
|
+
const isCaddy404 = result => result.statusCode === 404 && result.bodyLength === 0;
|
|
91
|
+
|
|
92
|
+
// EXECUTION
|
|
93
|
+
|
|
94
|
+
(async () => {
|
|
95
|
+
let failures = 0;
|
|
96
|
+
for (const method of ['GET', 'POST']) {
|
|
97
|
+
console.log(`\n=== ${method} paths ===`);
|
|
98
|
+
for (const pattern of routes[method]) {
|
|
99
|
+
const requestPath = concretePaths[method][pattern];
|
|
100
|
+
if (!requestPath) {
|
|
101
|
+
console.log(`FAIL: no concrete path defined for pattern ${pattern}`);
|
|
102
|
+
failures++;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const result = await sendRequest(method, requestPath);
|
|
107
|
+
if (isCaddy404(result)) {
|
|
108
|
+
console.log(`FAIL: ${method} ${requestPath} -> Caddy 404 (path not forwarded)`);
|
|
109
|
+
failures++;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
console.log(`PASS: ${method} ${requestPath} -> ${result.statusCode} (body ${result.bodyLength} bytes)`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
console.log(`FAIL: ${method} ${requestPath} -> error: ${error.message}`);
|
|
117
|
+
failures++;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
console.log(`\n=== ${failures === 0 ? 'All paths forwarded' : `${failures} failure(s)`} ===`);
|
|
122
|
+
process.exit(failures === 0 ? 0 : 1);
|
|
123
|
+
})();
|
|
@@ -233,6 +233,89 @@ const main = async () => {
|
|
|
233
233
|
hiddenReport
|
|
234
234
|
);
|
|
235
235
|
|
|
236
|
+
// Fixture 8: multiViolator. Three violators for the same issue: two with 1 reporter each (exercising the catalogIndex tiebreaker) and one with 2 reporters (exercising the reporter-count descending sort).
|
|
237
|
+
const multiViolatorCatalog = {
|
|
238
|
+
'0': catalogItem('A', 'Home', '/html/body/a[1]', '10:10:40:20'),
|
|
239
|
+
'1': catalogItem('A', 'Contact', '/html/body/a[2]', '10:40:40:20'),
|
|
240
|
+
'2': catalogItem('A', 'Help', '/html/body/a[3]', '10:70:40:20')
|
|
241
|
+
};
|
|
242
|
+
const multiViolatorActs = [
|
|
243
|
+
testAct(engineAxe, [
|
|
244
|
+
instance('r11', 'The link does not have an accessible name', 'failed',
|
|
245
|
+
issueLinkNoText, 0, 2, 1),
|
|
246
|
+
instance('r11', 'The link does not have an accessible name', 'failed',
|
|
247
|
+
issueLinkNoText, 2, 2, 1)
|
|
248
|
+
]),
|
|
249
|
+
testAct(engineAlfa, [
|
|
250
|
+
instance('r11', 'The link does not have an accessible name', 'failed',
|
|
251
|
+
issueLinkNoText, 1, 2, 1),
|
|
252
|
+
instance('r11', 'The link does not have an accessible name', 'failed',
|
|
253
|
+
issueLinkNoText, 2, 2, 1)
|
|
254
|
+
])
|
|
255
|
+
];
|
|
256
|
+
await writeJSON(
|
|
257
|
+
path.join(reportsDir, '260101T0008-mul.json'),
|
|
258
|
+
report('260101T0008-mul', 'Multi Violator Page',
|
|
259
|
+
'https://example.com/multiviolator', multiViolatorActs, multiViolatorCatalog)
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// Fixture 9: branchCoverage. Covers remaining branch gaps in listDiagnoses and listIssues: an issue with a short WCAG code (guideline layer), a catalog item with no tagName or text, an instance where ruleID equals what and count is missing, a test act with null instances, and an instance with no issueID.
|
|
263
|
+
const issueDuplicateID = 'duplicateID';
|
|
264
|
+
const branchCatalog = {
|
|
265
|
+
'0': catalogItem('DIV', '', '/html/body/div[1]', '0:0:100:50'),
|
|
266
|
+
'1': {id: '', startTag: '<div>', text: null, textLinkable: false, boxID: '5:5:90:40', pathID: '/html/body/div[2]', headingIndex: '', checkpoint: 0},
|
|
267
|
+
'2': {id: '', text: 'No tags', textLinkable: false, headingIndex: '', checkpoint: 0}
|
|
268
|
+
};
|
|
269
|
+
const branchActs = [
|
|
270
|
+
testAct(engineAxe, [
|
|
271
|
+
instance('r99', 'r99', 'failed',
|
|
272
|
+
issueDuplicateID, 0, 2, 1),
|
|
273
|
+
{
|
|
274
|
+
ruleID: 'r100',
|
|
275
|
+
what: 'Element has no role',
|
|
276
|
+
ordinalSeverity: 1,
|
|
277
|
+
outcome: 'failed',
|
|
278
|
+
catalogIndex: '1',
|
|
279
|
+
checkpoint: 0,
|
|
280
|
+
issueID: issueDuplicateID
|
|
281
|
+
},
|
|
282
|
+
instance('r101', 'Duplicate ID found', 'failed',
|
|
283
|
+
issueDuplicateID, 2, 2, 1),
|
|
284
|
+
instance('r102', 'Duplicate ID in orphan', 'failed',
|
|
285
|
+
issueDuplicateID, 3, 2, 1)
|
|
286
|
+
]),
|
|
287
|
+
{
|
|
288
|
+
type: 'test',
|
|
289
|
+
which: engineAlfa,
|
|
290
|
+
startTime: '26-01-01T00:00',
|
|
291
|
+
endTime: '26-01-01T00:01',
|
|
292
|
+
result: {
|
|
293
|
+
standardResult: {}
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
testAct(engineIbm, [
|
|
297
|
+
{
|
|
298
|
+
ruleID: 'r50',
|
|
299
|
+
what: 'Link has no text',
|
|
300
|
+
ordinalSeverity: 2,
|
|
301
|
+
outcome: 'failed',
|
|
302
|
+
catalogIndex: '0',
|
|
303
|
+
checkpoint: 0
|
|
304
|
+
}
|
|
305
|
+
])
|
|
306
|
+
];
|
|
307
|
+
await writeJSON(
|
|
308
|
+
path.join(reportsDir, '260101T0009-brd.json'),
|
|
309
|
+
report('260101T0009-brd', 'Branch Coverage Page',
|
|
310
|
+
'https://example.com/branch', branchActs, branchCatalog)
|
|
311
|
+
);
|
|
312
|
+
// Remove preventions from the brd fixture to cover the preventions ?? {} branch in listIssues.
|
|
313
|
+
const brdReport = JSON.parse(
|
|
314
|
+
await fs.readFile(path.join(reportsDir, '260101T0009-brd.json'), 'utf8')
|
|
315
|
+
);
|
|
316
|
+
delete brdReport.jobData.preventions;
|
|
317
|
+
await writeJSON(path.join(reportsDir, '260101T0009-brd.json'), brdReport);
|
|
318
|
+
|
|
236
319
|
// Write an empty recs.json so getRecs does not try to create one.
|
|
237
320
|
await writeJSON(path.join(targetDir, 'jobs', 'recs.json'), {});
|
|
238
321
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "260101T0008-mul",
|
|
3
|
+
"what": "Multi Violator Page",
|
|
4
|
+
"strict": false,
|
|
5
|
+
"standard": "only",
|
|
6
|
+
"device": {
|
|
7
|
+
"id": "default"
|
|
8
|
+
},
|
|
9
|
+
"browserID": "chromium",
|
|
10
|
+
"creationTimeStamp": "260101T0008",
|
|
11
|
+
"executionTimeStamp": "260101T0008",
|
|
12
|
+
"target": {
|
|
13
|
+
"what": "Multi Violator Page",
|
|
14
|
+
"url": "https://example.com/multiviolator"
|
|
15
|
+
},
|
|
16
|
+
"sources": {
|
|
17
|
+
"worker": "test-worker"
|
|
18
|
+
},
|
|
19
|
+
"acts": [
|
|
20
|
+
{
|
|
21
|
+
"type": "test",
|
|
22
|
+
"which": "axe",
|
|
23
|
+
"startTime": "26-01-01T00:00",
|
|
24
|
+
"endTime": "26-01-01T00:01",
|
|
25
|
+
"result": {
|
|
26
|
+
"standardResult": {
|
|
27
|
+
"instances": [
|
|
28
|
+
{
|
|
29
|
+
"ruleID": "r11",
|
|
30
|
+
"what": "The link does not have an accessible name",
|
|
31
|
+
"ordinalSeverity": 2,
|
|
32
|
+
"outcome": "failed",
|
|
33
|
+
"count": 1,
|
|
34
|
+
"catalogIndex": "0",
|
|
35
|
+
"checkpoint": 0,
|
|
36
|
+
"issueID": "linkNoText"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"ruleID": "r11",
|
|
40
|
+
"what": "The link does not have an accessible name",
|
|
41
|
+
"ordinalSeverity": 2,
|
|
42
|
+
"outcome": "failed",
|
|
43
|
+
"count": 1,
|
|
44
|
+
"catalogIndex": "2",
|
|
45
|
+
"checkpoint": 0,
|
|
46
|
+
"issueID": "linkNoText"
|
|
47
|
+
}
|
|
48
|
+
],
|
|
49
|
+
"outcomeTotals": {
|
|
50
|
+
"failed": 2,
|
|
51
|
+
"cantTell": 0
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"type": "test",
|
|
58
|
+
"which": "alfa",
|
|
59
|
+
"startTime": "26-01-01T00:00",
|
|
60
|
+
"endTime": "26-01-01T00:01",
|
|
61
|
+
"result": {
|
|
62
|
+
"standardResult": {
|
|
63
|
+
"instances": [
|
|
64
|
+
{
|
|
65
|
+
"ruleID": "r11",
|
|
66
|
+
"what": "The link does not have an accessible name",
|
|
67
|
+
"ordinalSeverity": 2,
|
|
68
|
+
"outcome": "failed",
|
|
69
|
+
"count": 1,
|
|
70
|
+
"catalogIndex": "1",
|
|
71
|
+
"checkpoint": 0,
|
|
72
|
+
"issueID": "linkNoText"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"ruleID": "r11",
|
|
76
|
+
"what": "The link does not have an accessible name",
|
|
77
|
+
"ordinalSeverity": 2,
|
|
78
|
+
"outcome": "failed",
|
|
79
|
+
"count": 1,
|
|
80
|
+
"catalogIndex": "2",
|
|
81
|
+
"checkpoint": 0,
|
|
82
|
+
"issueID": "linkNoText"
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
"outcomeTotals": {
|
|
86
|
+
"failed": 2,
|
|
87
|
+
"cantTell": 0
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
"jobData": {
|
|
94
|
+
"startTime": "26-01-01T00:00",
|
|
95
|
+
"endTime": "26-01-01T00:10",
|
|
96
|
+
"elapsedSeconds": 600,
|
|
97
|
+
"preventions": {},
|
|
98
|
+
"issuelessRules": []
|
|
99
|
+
},
|
|
100
|
+
"catalog": {
|
|
101
|
+
"0": {
|
|
102
|
+
"tagName": "A",
|
|
103
|
+
"id": "",
|
|
104
|
+
"startTag": "<a>",
|
|
105
|
+
"text": "Home",
|
|
106
|
+
"textLinkable": false,
|
|
107
|
+
"boxID": "10:10:40:20",
|
|
108
|
+
"pathID": "/html/body/a[1]",
|
|
109
|
+
"headingIndex": "",
|
|
110
|
+
"checkpoint": 0
|
|
111
|
+
},
|
|
112
|
+
"1": {
|
|
113
|
+
"tagName": "A",
|
|
114
|
+
"id": "",
|
|
115
|
+
"startTag": "<a>",
|
|
116
|
+
"text": "Contact",
|
|
117
|
+
"textLinkable": false,
|
|
118
|
+
"boxID": "10:40:40:20",
|
|
119
|
+
"pathID": "/html/body/a[2]",
|
|
120
|
+
"headingIndex": "",
|
|
121
|
+
"checkpoint": 0
|
|
122
|
+
},
|
|
123
|
+
"2": {
|
|
124
|
+
"tagName": "A",
|
|
125
|
+
"id": "",
|
|
126
|
+
"startTag": "<a>",
|
|
127
|
+
"text": "Help",
|
|
128
|
+
"textLinkable": false,
|
|
129
|
+
"boxID": "10:70:40:20",
|
|
130
|
+
"pathID": "/html/body/a[3]",
|
|
131
|
+
"headingIndex": "",
|
|
132
|
+
"checkpoint": 0
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
"images": {},
|
|
136
|
+
"checkpoints": []
|
|
137
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "260101T0009-brd",
|
|
3
|
+
"what": "Branch Coverage Page",
|
|
4
|
+
"strict": false,
|
|
5
|
+
"standard": "only",
|
|
6
|
+
"device": {
|
|
7
|
+
"id": "default"
|
|
8
|
+
},
|
|
9
|
+
"browserID": "chromium",
|
|
10
|
+
"creationTimeStamp": "260101T0009",
|
|
11
|
+
"executionTimeStamp": "260101T0009",
|
|
12
|
+
"target": {
|
|
13
|
+
"what": "Branch Coverage Page",
|
|
14
|
+
"url": "https://example.com/branch"
|
|
15
|
+
},
|
|
16
|
+
"sources": {
|
|
17
|
+
"worker": "test-worker"
|
|
18
|
+
},
|
|
19
|
+
"acts": [
|
|
20
|
+
{
|
|
21
|
+
"type": "test",
|
|
22
|
+
"which": "axe",
|
|
23
|
+
"startTime": "26-01-01T00:00",
|
|
24
|
+
"endTime": "26-01-01T00:01",
|
|
25
|
+
"result": {
|
|
26
|
+
"standardResult": {
|
|
27
|
+
"instances": [
|
|
28
|
+
{
|
|
29
|
+
"ruleID": "r99",
|
|
30
|
+
"what": "r99",
|
|
31
|
+
"ordinalSeverity": 2,
|
|
32
|
+
"outcome": "failed",
|
|
33
|
+
"count": 1,
|
|
34
|
+
"catalogIndex": "0",
|
|
35
|
+
"checkpoint": 0,
|
|
36
|
+
"issueID": "duplicateID"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"ruleID": "r100",
|
|
40
|
+
"what": "Element has no role",
|
|
41
|
+
"ordinalSeverity": 1,
|
|
42
|
+
"outcome": "failed",
|
|
43
|
+
"catalogIndex": "1",
|
|
44
|
+
"checkpoint": 0,
|
|
45
|
+
"issueID": "duplicateID"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"ruleID": "r101",
|
|
49
|
+
"what": "Duplicate ID found",
|
|
50
|
+
"ordinalSeverity": 2,
|
|
51
|
+
"outcome": "failed",
|
|
52
|
+
"count": 1,
|
|
53
|
+
"catalogIndex": "2",
|
|
54
|
+
"checkpoint": 0,
|
|
55
|
+
"issueID": "duplicateID"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"ruleID": "r102",
|
|
59
|
+
"what": "Duplicate ID in orphan",
|
|
60
|
+
"ordinalSeverity": 2,
|
|
61
|
+
"outcome": "failed",
|
|
62
|
+
"count": 1,
|
|
63
|
+
"catalogIndex": "3",
|
|
64
|
+
"checkpoint": 0,
|
|
65
|
+
"issueID": "duplicateID"
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
"outcomeTotals": {
|
|
69
|
+
"failed": 4,
|
|
70
|
+
"cantTell": 0
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"type": "test",
|
|
77
|
+
"which": "alfa",
|
|
78
|
+
"startTime": "26-01-01T00:00",
|
|
79
|
+
"endTime": "26-01-01T00:01",
|
|
80
|
+
"result": {
|
|
81
|
+
"standardResult": {}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"type": "test",
|
|
86
|
+
"which": "ibm",
|
|
87
|
+
"startTime": "26-01-01T00:00",
|
|
88
|
+
"endTime": "26-01-01T00:01",
|
|
89
|
+
"result": {
|
|
90
|
+
"standardResult": {
|
|
91
|
+
"instances": [
|
|
92
|
+
{
|
|
93
|
+
"ruleID": "r50",
|
|
94
|
+
"what": "Link has no text",
|
|
95
|
+
"ordinalSeverity": 2,
|
|
96
|
+
"outcome": "failed",
|
|
97
|
+
"catalogIndex": "0",
|
|
98
|
+
"checkpoint": 0
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
"outcomeTotals": {
|
|
102
|
+
"failed": 1,
|
|
103
|
+
"cantTell": 0
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
],
|
|
109
|
+
"jobData": {
|
|
110
|
+
"startTime": "26-01-01T00:00",
|
|
111
|
+
"endTime": "26-01-01T00:10",
|
|
112
|
+
"elapsedSeconds": 600,
|
|
113
|
+
"issuelessRules": []
|
|
114
|
+
},
|
|
115
|
+
"catalog": {
|
|
116
|
+
"0": {
|
|
117
|
+
"tagName": "DIV",
|
|
118
|
+
"id": "",
|
|
119
|
+
"startTag": "<div>",
|
|
120
|
+
"text": "",
|
|
121
|
+
"textLinkable": false,
|
|
122
|
+
"boxID": "0:0:100:50",
|
|
123
|
+
"pathID": "/html/body/div[1]",
|
|
124
|
+
"headingIndex": "",
|
|
125
|
+
"checkpoint": 0
|
|
126
|
+
},
|
|
127
|
+
"1": {
|
|
128
|
+
"id": "",
|
|
129
|
+
"startTag": "<div>",
|
|
130
|
+
"text": null,
|
|
131
|
+
"textLinkable": false,
|
|
132
|
+
"boxID": "5:5:90:40",
|
|
133
|
+
"pathID": "/html/body/div[2]",
|
|
134
|
+
"headingIndex": "",
|
|
135
|
+
"checkpoint": 0
|
|
136
|
+
},
|
|
137
|
+
"2": {
|
|
138
|
+
"id": "",
|
|
139
|
+
"text": "No tags",
|
|
140
|
+
"textLinkable": false,
|
|
141
|
+
"headingIndex": "",
|
|
142
|
+
"checkpoint": 0
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
"images": {},
|
|
146
|
+
"checkpoints": []
|
|
147
|
+
}
|
package/test/setup.cjs
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/*
|
|
2
|
+
test/setup.cjs
|
|
3
|
+
Clears alert configuration env vars before any modules are required,
|
|
4
|
+
so that tests calling sendAlert indirectly never trigger real email alerts.
|
|
5
|
+
This file is loaded via --require before any test file runs.
|
|
6
|
+
|
|
7
|
+
The env vars are set to empty strings rather than deleted, because
|
|
8
|
+
dotenv does not override existing env vars. This prevents the .env
|
|
9
|
+
file loaded by index.js from re-arming the alert configuration.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
for (const key of ['MANAGER_EMAIL', 'ALERT_API_HOST', 'ALERT_API_PATH', 'ALERT_API_KEY', 'ALERT_FROM']) {
|
|
13
|
+
process.env[key] = '';
|
|
14
|
+
}
|
package/util.js
CHANGED
|
@@ -142,11 +142,18 @@ const getJobNames = exports.getJobNames = async () => {
|
|
|
142
142
|
const jobNames = {};
|
|
143
143
|
let fileNames;
|
|
144
144
|
for (const category of ['queue', 'claimed', 'failed']) {
|
|
145
|
+
const categoryPath = path.join(jobsPath(), category);
|
|
145
146
|
try {
|
|
146
|
-
fileNames = await fs.readdir(
|
|
147
|
+
fileNames = await fs.readdir(categoryPath);
|
|
147
148
|
}
|
|
148
149
|
catch(error) {
|
|
149
|
-
|
|
150
|
+
if (error.code === 'ENOENT') {
|
|
151
|
+
await fs.mkdir(categoryPath, {recursive: true});
|
|
152
|
+
fileNames = [];
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
return `ERROR: Job directory ${category} not readable (${error.message})`;
|
|
156
|
+
}
|
|
150
157
|
}
|
|
151
158
|
jobNames[category] = fileNames;
|
|
152
159
|
}
|
|
@@ -220,6 +227,9 @@ exports.getPOSTData = request => new Promise(resolve => {
|
|
|
220
227
|
const query = querystring.parse(body);
|
|
221
228
|
resolve(query);
|
|
222
229
|
}
|
|
230
|
+
else {
|
|
231
|
+
resolve(null);
|
|
232
|
+
}
|
|
223
233
|
});
|
|
224
234
|
});
|
|
225
235
|
// Returns the waiting test and retest recommendations.
|
|
@@ -604,9 +614,9 @@ const getPageData = exports.getPageData = async (timeStamp, jobID) => {
|
|
|
604
614
|
return report;
|
|
605
615
|
}
|
|
606
616
|
const {url, what} = report.target;
|
|
607
|
-
|
|
608
|
-
//
|
|
609
|
-
const daysAgo = getAgoDays(
|
|
617
|
+
// Get the elapsed time in days since the report was completed, using the
|
|
618
|
+
// report content rather than the file system birth time.
|
|
619
|
+
const daysAgo = getAgoDays(new Date(`20${report.jobData.endTime}Z`));
|
|
610
620
|
// Return the data.
|
|
611
621
|
return {
|
|
612
622
|
what,
|
|
@@ -640,7 +650,7 @@ exports.getPageDataStrings = async (timeStamp, jobID, pageData) => {
|
|
|
640
650
|
};
|
|
641
651
|
};
|
|
642
652
|
// Returns the creation time and size of a report.
|
|
643
|
-
|
|
653
|
+
exports.getReportStats = async (timeStamp, jobID) => {
|
|
644
654
|
let reportStat;
|
|
645
655
|
try {
|
|
646
656
|
reportStat = await fs.stat(path.join(reportsPath(), `${timeStamp}-${jobID}.json`));
|