@jrpool/kilotest 46.0.1 → 46.0.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/env.example +3 -0
- package/eslint.config.mjs +2 -1
- package/index.js +12 -12
- package/openapi.yaml +18 -18
- package/package.json +1 -1
- package/test/fixtures/buildFixtures.js +244 -0
- package/test/fixtures/db/jobs/recs.json +1 -0
- package/test/fixtures/db/reports/260101T0000-mix.json +126 -0
- package/test/fixtures/db/reports/260101T0001-ct.json +69 -0
- package/test/fixtures/db/reports/260101T0002-no.json +68 -0
- package/test/fixtures/db/reports/260101T0005-emp.json +46 -0
- package/test/fixtures/db/reports/260101T0006-prv.json +71 -0
- package/test/fixtures/db/reports/260202T0000-new.json +80 -0
- package/util.js +23 -22
- package/util.test.js +87 -0
- package/web/enqueue/index.js +2 -2
- package/web/expungeReportsForm/index.js +2 -2
- package/web/hideReportForm/index.js +1 -1
- package/web/listReports/index.js +1 -1
- package/web/pruneReportsForm/index.js +2 -2
- package/web/rewindReportsForm/index.js +2 -2
- package/web/unhideReportForm/index.js +3 -3
package/env.example
CHANGED
|
@@ -12,6 +12,9 @@ THIS_KILOTEST_HOST=__placeholder__
|
|
|
12
12
|
LOCAL_KILOTEST_HOST=http://localhost:3000
|
|
13
13
|
# URL of the deployed host.
|
|
14
14
|
DEPLOYED_KILOTEST_HOST=https://kilotest.com
|
|
15
|
+
# Path of the data directory (defaults to the db subdirectory of the project root). Tests set
|
|
16
|
+
# this to a fixture directory so they never read or write the real db.
|
|
17
|
+
DB_DIR=__placeholder__
|
|
15
18
|
|
|
16
19
|
# Alert configuration:
|
|
17
20
|
|
package/eslint.config.mjs
CHANGED
package/index.js
CHANGED
|
@@ -65,9 +65,9 @@ const answer = {
|
|
|
65
65
|
// CONSTANTS
|
|
66
66
|
|
|
67
67
|
const protocol = process.env.PROTOCOL || 'http';
|
|
68
|
-
const queuePath = path.join(jobsPath, 'queue');
|
|
69
|
-
const claimedPath = path.join(jobsPath, 'claimed');
|
|
70
|
-
const failedPath = path.join(jobsPath, 'failed');
|
|
68
|
+
const queuePath = () => path.join(jobsPath(), 'queue');
|
|
69
|
+
const claimedPath = () => path.join(jobsPath(), 'claimed');
|
|
70
|
+
const failedPath = () => path.join(jobsPath(), 'failed');
|
|
71
71
|
// Credentials of the Testaro workers, by worker ID, from a JSON-object environment variable.
|
|
72
72
|
// Each worker ID maps to a secret (used only to authenticate the worker, never published) and a
|
|
73
73
|
// name (a non-secret label safe to publish, e.g. in report data and logs).
|
|
@@ -238,7 +238,7 @@ const processJobRequest = async (request, response, workerName) => jobLock(async
|
|
|
238
238
|
const claimedJobNames = jobNames.claimed;
|
|
239
239
|
// For each claimed job:
|
|
240
240
|
for (const jobName of claimedJobNames) {
|
|
241
|
-
const job = await getObject(path.join(jobsPath, 'claimed', jobName));
|
|
241
|
+
const job = await getObject(path.join(jobsPath(), 'claimed', jobName));
|
|
242
242
|
const {id, sources} = job;
|
|
243
243
|
const {worker} = sources;
|
|
244
244
|
// If its assignee is the worker:
|
|
@@ -248,7 +248,7 @@ const processJobRequest = async (request, response, workerName) => jobLock(async
|
|
|
248
248
|
await serveError({message: `${messageStart}${messageEnd}`}, response, false);
|
|
249
249
|
// Reclassify the job as failed.
|
|
250
250
|
await fs.rename(
|
|
251
|
-
path.join(claimedPath, jobName), path.join(failedPath, jobName)
|
|
251
|
+
path.join(claimedPath(), jobName), path.join(failedPath(), jobName)
|
|
252
252
|
);
|
|
253
253
|
clean = false;
|
|
254
254
|
// Stop checking claimed jobs.
|
|
@@ -262,7 +262,7 @@ const processJobRequest = async (request, response, workerName) => jobLock(async
|
|
|
262
262
|
if (queuedJobNames.length) {
|
|
263
263
|
const oldestJobName = queuedJobNames[0];
|
|
264
264
|
// Get the first one.
|
|
265
|
-
const firstJob = await getObject(path.join(queuePath, oldestJobName));
|
|
265
|
+
const firstJob = await getObject(path.join(queuePath(), oldestJobName));
|
|
266
266
|
// Add the public worker name to the job, in a property Testaro does not read or alter.
|
|
267
267
|
firstJob.sources.worker = workerName;
|
|
268
268
|
console.log(
|
|
@@ -278,10 +278,10 @@ const processJobRequest = async (request, response, workerName) => jobLock(async
|
|
|
278
278
|
console.log(`${messageStart}${messageEnd}`);
|
|
279
279
|
// Save the job in the claimed-jobs directory.
|
|
280
280
|
await fs.writeFile(
|
|
281
|
-
path.join(claimedPath, oldestJobName), getJSON(firstJob)
|
|
281
|
+
path.join(claimedPath(), oldestJobName), getJSON(firstJob)
|
|
282
282
|
);
|
|
283
283
|
// Delete it from the queue.
|
|
284
|
-
await fs.unlink(path.join(queuePath, oldestJobName));
|
|
284
|
+
await fs.unlink(path.join(queuePath(), oldestJobName));
|
|
285
285
|
}
|
|
286
286
|
// Otherwise, i.e. if no jobs are queued:
|
|
287
287
|
else {
|
|
@@ -661,7 +661,7 @@ const requestHandler = async (request, response) => {
|
|
|
661
661
|
// Delete the rejected URL.
|
|
662
662
|
delete recs[url];
|
|
663
663
|
// Save the revised recommendations.
|
|
664
|
-
await fs.writeFile(path.join(jobsPath, 'recs.json'), getJSON(recs));
|
|
664
|
+
await fs.writeFile(path.join(jobsPath(), 'recs.json'), getJSON(recs));
|
|
665
665
|
});
|
|
666
666
|
// Set a location header for a response.
|
|
667
667
|
response.setHeader('content-location', '/enqueueForm.html');
|
|
@@ -739,7 +739,7 @@ const requestHandler = async (request, response) => {
|
|
|
739
739
|
if (id && isTimeStamp(timeStamp) && isJobID(jobID) && what && url) {
|
|
740
740
|
const [timeStamp, jobID] = id.split('-');
|
|
741
741
|
// Get the job the report is from.
|
|
742
|
-
const claimedJob = await getObject(path.join(claimedPath, `${id}.json`));
|
|
742
|
+
const claimedJob = await getObject(path.join(claimedPath(), `${id}.json`));
|
|
743
743
|
// If the job was actually assigned to this worker:
|
|
744
744
|
if (typeof claimedJob === 'object' && claimedJob.sources?.worker === workerName) {
|
|
745
745
|
console.log(`Testaro report ${id} was received from worker ${workerName}`);
|
|
@@ -755,7 +755,7 @@ const requestHandler = async (request, response) => {
|
|
|
755
755
|
// Check the monetary balances and send alerts if nearing exhaustion.
|
|
756
756
|
await checkBalancesForAlerts(report);
|
|
757
757
|
// Delete the job.
|
|
758
|
-
await fs.unlink(path.join(claimedPath, `${id}.json`));
|
|
758
|
+
await fs.unlink(path.join(claimedPath(), `${id}.json`));
|
|
759
759
|
console.log(`Completed job ${id} deleted`);
|
|
760
760
|
// Acknowledge receipt.
|
|
761
761
|
response.setHeader('content-type', 'application/json; charset=utf-8');
|
|
@@ -868,7 +868,7 @@ const requestHandler = async (request, response) => {
|
|
|
868
868
|
|
|
869
869
|
const serve = async (protocolModule, options) => {
|
|
870
870
|
// Create any missing directories.
|
|
871
|
-
for (const path of [queuePath, claimedPath, failedPath, hiddenReportsPath, reportsPath]) {
|
|
871
|
+
for (const path of [queuePath(), claimedPath(), failedPath(), hiddenReportsPath(), reportsPath()]) {
|
|
872
872
|
await fs.mkdir(path, {recursive: true});
|
|
873
873
|
}
|
|
874
874
|
const server = protocolModule === 'https'
|
package/openapi.yaml
CHANGED
|
@@ -254,9 +254,9 @@ paths:
|
|
|
254
254
|
type: object
|
|
255
255
|
properties:
|
|
256
256
|
name:
|
|
257
|
-
|
|
258
|
-
-
|
|
259
|
-
-
|
|
257
|
+
type:
|
|
258
|
+
- string
|
|
259
|
+
- 'null'
|
|
260
260
|
reason for failure:
|
|
261
261
|
description: Copied verbatim from the Testaro report; shape may vary.
|
|
262
262
|
required:
|
|
@@ -266,9 +266,9 @@ paths:
|
|
|
266
266
|
names of rule engines that reported rule violations:
|
|
267
267
|
type: array
|
|
268
268
|
items:
|
|
269
|
-
|
|
270
|
-
-
|
|
271
|
-
-
|
|
269
|
+
type:
|
|
270
|
+
- string
|
|
271
|
+
- 'null'
|
|
272
272
|
counts of issues by priority:
|
|
273
273
|
type: object
|
|
274
274
|
properties:
|
|
@@ -782,9 +782,9 @@ paths:
|
|
|
782
782
|
type: object
|
|
783
783
|
properties:
|
|
784
784
|
size of the report in bytes:
|
|
785
|
-
|
|
786
|
-
-
|
|
787
|
-
-
|
|
785
|
+
type:
|
|
786
|
+
- number
|
|
787
|
+
- string
|
|
788
788
|
full report:
|
|
789
789
|
description: The full raw Testaro/Testilo report JSON, copied verbatim, or an error object if it could not be retrieved.
|
|
790
790
|
required:
|
|
@@ -1224,9 +1224,9 @@ components:
|
|
|
1224
1224
|
type: string
|
|
1225
1225
|
description: URL of the equivalent web UI page for this request.
|
|
1226
1226
|
closest ancestor request:
|
|
1227
|
-
|
|
1228
|
-
-
|
|
1229
|
-
-
|
|
1227
|
+
type:
|
|
1228
|
+
- string
|
|
1229
|
+
- 'null'
|
|
1230
1230
|
required:
|
|
1231
1231
|
- this request
|
|
1232
1232
|
- closest ancestor request
|
|
@@ -1290,13 +1290,13 @@ components:
|
|
|
1290
1290
|
identifier:
|
|
1291
1291
|
type: string
|
|
1292
1292
|
name:
|
|
1293
|
-
|
|
1294
|
-
-
|
|
1295
|
-
-
|
|
1293
|
+
type:
|
|
1294
|
+
- string
|
|
1295
|
+
- 'null'
|
|
1296
1296
|
sponsor:
|
|
1297
|
-
|
|
1298
|
-
-
|
|
1299
|
-
-
|
|
1297
|
+
type:
|
|
1298
|
+
- string
|
|
1299
|
+
- 'null'
|
|
1300
1300
|
required:
|
|
1301
1301
|
- identifier
|
|
1302
1302
|
- name
|
package/package.json
CHANGED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/*
|
|
2
|
+
buildFixtures.js
|
|
3
|
+
Builds the fixture corpus for Kilotest tests.
|
|
4
|
+
|
|
5
|
+
Each fixture is a minimal Testaro report in the current format, crafted so
|
|
6
|
+
that the expected API response can be hand-computed and hard-coded into
|
|
7
|
+
tests. The fixtures collectively cover the `outcome` property values
|
|
8
|
+
(`failed`, `cantTell`, and missing/undefined), superseded reports, empty
|
|
9
|
+
reports, and prevented rule engines.
|
|
10
|
+
|
|
11
|
+
Run with: node test/fixtures/buildFixtures.js [targetDir]
|
|
12
|
+
Defaults to test/fixtures/db under the project root.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// IMPORTS
|
|
16
|
+
|
|
17
|
+
const fs = require('fs/promises');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
// CONSTANTS
|
|
21
|
+
|
|
22
|
+
// Issue IDs with known specs in testaro-issues, chosen for different weights.
|
|
23
|
+
const ISSUE_LINK_NO_TEXT = 'linkNoText'; // weight 4 (highest)
|
|
24
|
+
const ISSUE_FOCUS_INDICATION = 'focusIndicationBad'; // weight 4
|
|
25
|
+
const ISSUE_ALL_CAPS = 'allCaps'; // weight 1 (lowest)
|
|
26
|
+
|
|
27
|
+
// Rule engine IDs that exist in util.js ruleEngines.
|
|
28
|
+
const ENGINE_AXE = 'axe';
|
|
29
|
+
const ENGINE_ALFA = 'alfa';
|
|
30
|
+
const ENGINE_IBM = 'ibm';
|
|
31
|
+
|
|
32
|
+
// FUNCTIONS
|
|
33
|
+
|
|
34
|
+
// Returns a minimal catalog item.
|
|
35
|
+
const catalogItem = (tagName, text, pathID = '/html', boxID = '0:0:100:50') => ({
|
|
36
|
+
tagName,
|
|
37
|
+
id: '',
|
|
38
|
+
startTag: `<${tagName.toLowerCase()}>`,
|
|
39
|
+
text,
|
|
40
|
+
textLinkable: false,
|
|
41
|
+
boxID,
|
|
42
|
+
pathID,
|
|
43
|
+
headingIndex: '',
|
|
44
|
+
checkpoint: 0
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Returns a minimal standard instance.
|
|
48
|
+
const instance = (
|
|
49
|
+
ruleID, what, outcome, issueID, catalogIndex, ordinalSeverity = 2, count = 1
|
|
50
|
+
) => ({
|
|
51
|
+
ruleID,
|
|
52
|
+
what,
|
|
53
|
+
ordinalSeverity,
|
|
54
|
+
outcome,
|
|
55
|
+
count,
|
|
56
|
+
catalogIndex: String(catalogIndex),
|
|
57
|
+
checkpoint: 0,
|
|
58
|
+
issueID
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Returns a minimal test act.
|
|
62
|
+
const testAct = (which, instances) => ({
|
|
63
|
+
type: 'test',
|
|
64
|
+
which,
|
|
65
|
+
startTime: '26-01-01T00:00',
|
|
66
|
+
endTime: '26-01-01T00:01',
|
|
67
|
+
result: {
|
|
68
|
+
standardResult: {
|
|
69
|
+
instances,
|
|
70
|
+
outcomeTotals: {
|
|
71
|
+
failed: instances.filter(i => i.outcome === 'failed').length,
|
|
72
|
+
cantTell: instances.filter(i => i.outcome === 'cantTell').length
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Returns a minimal valid report.
|
|
79
|
+
const report = (id, what, url, acts, catalog, endTime = '26-01-01T00:10') => ({
|
|
80
|
+
id,
|
|
81
|
+
what,
|
|
82
|
+
strict: false,
|
|
83
|
+
standard: 'only',
|
|
84
|
+
device: {id: 'default'},
|
|
85
|
+
browserID: 'chromium',
|
|
86
|
+
creationTimeStamp: id.slice(0, 11),
|
|
87
|
+
executionTimeStamp: id.slice(0, 11),
|
|
88
|
+
target: {what, url},
|
|
89
|
+
sources: {worker: 'test-worker'},
|
|
90
|
+
acts,
|
|
91
|
+
jobData: {
|
|
92
|
+
startTime: '26-01-01T00:00',
|
|
93
|
+
endTime,
|
|
94
|
+
elapsedSeconds: 600,
|
|
95
|
+
preventions: {},
|
|
96
|
+
issuelessRules: []
|
|
97
|
+
},
|
|
98
|
+
catalog,
|
|
99
|
+
images: {},
|
|
100
|
+
checkpoints: []
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Writes a JSON file with a trailing newline.
|
|
104
|
+
const writeJSON = async (filePath, object) => {
|
|
105
|
+
await fs.writeFile(filePath, `${JSON.stringify(object, null, 2)}\n`);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// MAIN
|
|
109
|
+
|
|
110
|
+
const main = async () => {
|
|
111
|
+
const targetDir = process.argv[2] || path.join(__dirname, 'db');
|
|
112
|
+
const reportsDir = path.join(targetDir, 'reports');
|
|
113
|
+
await fs.mkdir(reportsDir, {recursive: true});
|
|
114
|
+
await fs.mkdir(path.join(targetDir, 'jobs', 'queue'), {recursive: true});
|
|
115
|
+
await fs.mkdir(path.join(targetDir, 'jobs', 'claimed'), {recursive: true});
|
|
116
|
+
await fs.mkdir(path.join(targetDir, 'jobs', 'failed'), {recursive: true});
|
|
117
|
+
await fs.mkdir(path.join(targetDir, 'hiddenReports'), {recursive: true});
|
|
118
|
+
|
|
119
|
+
// Fixture 1: mixedOutcomes
|
|
120
|
+
// Two rule engines, three instances: one failed, one cantTell, one failed.
|
|
121
|
+
// The cantTell instance must be excluded from issue/violator counts.
|
|
122
|
+
// Two distinct issues (linkNoText weight 4, allCaps weight 1) and two
|
|
123
|
+
// distinct violator catalog indexes (0 and 1).
|
|
124
|
+
const mixedCatalog = {
|
|
125
|
+
'0': catalogItem('A', 'About Us', '/html/body/a[1]', '10:20:80:30'),
|
|
126
|
+
'1': catalogItem('P', 'ALL ABOUT US', '/html/body/p[1]', '10:60:80:20')
|
|
127
|
+
};
|
|
128
|
+
const mixedActs = [
|
|
129
|
+
testAct(ENGINE_AXE, [
|
|
130
|
+
instance('r11', 'The link does not have an accessible name', 'failed',
|
|
131
|
+
ISSUE_LINK_NO_TEXT, 0, 2, 1),
|
|
132
|
+
instance('r65', 'Focus Visible', 'cantTell',
|
|
133
|
+
ISSUE_FOCUS_INDICATION, 0, 0, 1)
|
|
134
|
+
]),
|
|
135
|
+
testAct(ENGINE_ALFA, [
|
|
136
|
+
instance('r11', 'The link does not have an accessible name', 'failed',
|
|
137
|
+
ISSUE_LINK_NO_TEXT, 0, 2, 1),
|
|
138
|
+
instance('r3', 'Text is all-capital', 'failed',
|
|
139
|
+
ISSUE_ALL_CAPS, 1, 1, 1)
|
|
140
|
+
])
|
|
141
|
+
];
|
|
142
|
+
await writeJSON(
|
|
143
|
+
path.join(reportsDir, '260101T0000-mix.json'),
|
|
144
|
+
report('260101T0000-mix', 'Mixed Outcomes Page',
|
|
145
|
+
'https://example.com/mixed', mixedActs, mixedCatalog)
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// Fixture 2: allCantTell
|
|
149
|
+
// All instances have outcome cantTell, so no issues or violators should be
|
|
150
|
+
// reported by listIssues, listViolators, or listDiagnoses.
|
|
151
|
+
const cantTellCatalog = {
|
|
152
|
+
'0': catalogItem('A', 'Click here', '/html/body/a[1]', '5:10:60:20')
|
|
153
|
+
};
|
|
154
|
+
const cantTellActs = [
|
|
155
|
+
testAct(ENGINE_AXE, [
|
|
156
|
+
instance('r65', 'Focus Visible', 'cantTell',
|
|
157
|
+
ISSUE_FOCUS_INDICATION, 0, 0, 1)
|
|
158
|
+
])
|
|
159
|
+
];
|
|
160
|
+
await writeJSON(
|
|
161
|
+
path.join(reportsDir, '260101T0001-ct.json'),
|
|
162
|
+
report('260101T0001-ct', 'All CantTell Page',
|
|
163
|
+
'https://example.com/canttell', cantTellActs, cantTellCatalog)
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// Fixture 3: noOutcomes
|
|
167
|
+
// Instances with no outcome property. Testaro defaults to 'failed', but
|
|
168
|
+
// Kilotest code checks `outcome !== 'cantTell'`, so missing outcome (undefined)
|
|
169
|
+
// is treated as a violation. This verifies that behavior.
|
|
170
|
+
const noOutcomeCatalog = {
|
|
171
|
+
'0': catalogItem('BUTTON', 'Submit', '/html/body/button[1]', '15:25:70:30')
|
|
172
|
+
};
|
|
173
|
+
const noOutcomeActs = [
|
|
174
|
+
testAct(ENGINE_IBM, [
|
|
175
|
+
{
|
|
176
|
+
ruleID: 'r1',
|
|
177
|
+
what: 'Button has no accessible name',
|
|
178
|
+
ordinalSeverity: 3,
|
|
179
|
+
count: 1,
|
|
180
|
+
catalogIndex: '0',
|
|
181
|
+
checkpoint: 0,
|
|
182
|
+
issueID: ISSUE_LINK_NO_TEXT
|
|
183
|
+
}
|
|
184
|
+
])
|
|
185
|
+
];
|
|
186
|
+
await writeJSON(
|
|
187
|
+
path.join(reportsDir, '260101T0002-no.json'),
|
|
188
|
+
report('260101T0002-no', 'No Outcomes Page',
|
|
189
|
+
'https://example.com/nooutcomes', noOutcomeActs, noOutcomeCatalog)
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Fixture 4: superseded
|
|
193
|
+
// A report about the same page as mixedOutcomes but with a later timestamp,
|
|
194
|
+
// so mixedOutcomes is superseded by this one.
|
|
195
|
+
const newerActs = [
|
|
196
|
+
testAct(ENGINE_AXE, [
|
|
197
|
+
instance('r11', 'The link does not have an accessible name', 'failed',
|
|
198
|
+
ISSUE_LINK_NO_TEXT, 0, 2, 1)
|
|
199
|
+
])
|
|
200
|
+
];
|
|
201
|
+
await writeJSON(
|
|
202
|
+
path.join(reportsDir, '260202T0000-new.json'),
|
|
203
|
+
report('260202T0000-new', 'Mixed Outcomes Page',
|
|
204
|
+
'https://example.com/mixed', newerActs, mixedCatalog, '26-02-02T00:10')
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// Fixture 5: empty
|
|
208
|
+
// A valid report with no test acts that have instances, so listIssues
|
|
209
|
+
// returns zero issues.
|
|
210
|
+
await writeJSON(
|
|
211
|
+
path.join(reportsDir, '260101T0005-emp.json'),
|
|
212
|
+
report('260101T0005-emp', 'Empty Results Page',
|
|
213
|
+
'https://example.com/empty', [testAct(ENGINE_AXE, [])], {})
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// Fixture 6: prevented
|
|
217
|
+
// A report where one rule engine was prevented from testing.
|
|
218
|
+
const preventedCatalog = {
|
|
219
|
+
'0': catalogItem('IMG', 'An image', '/html/body/img[1]', '0:0:200:100')
|
|
220
|
+
};
|
|
221
|
+
const preventedActs = [
|
|
222
|
+
testAct(ENGINE_AXE, [
|
|
223
|
+
instance('r11', 'The image has no alt text', 'failed',
|
|
224
|
+
ISSUE_LINK_NO_TEXT, 0, 2, 1)
|
|
225
|
+
])
|
|
226
|
+
];
|
|
227
|
+
const preventedReport = report('260101T0006-prv', 'Prevented Page',
|
|
228
|
+
'https://example.com/prevented', preventedActs, preventedCatalog);
|
|
229
|
+
preventedReport.jobData.preventions = {alfa: 'page timed out'};
|
|
230
|
+
await writeJSON(
|
|
231
|
+
path.join(reportsDir, '260101T0006-prv.json'),
|
|
232
|
+
preventedReport
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
// Write an empty recs.json so getRecs does not try to create one.
|
|
236
|
+
await writeJSON(path.join(targetDir, 'jobs', 'recs.json'), {});
|
|
237
|
+
|
|
238
|
+
console.log(`Fixtures built in ${targetDir}`);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
main().catch(error => {
|
|
242
|
+
console.error(error);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "260101T0000-mix",
|
|
3
|
+
"what": "Mixed Outcomes Page",
|
|
4
|
+
"strict": false,
|
|
5
|
+
"standard": "only",
|
|
6
|
+
"device": {
|
|
7
|
+
"id": "default"
|
|
8
|
+
},
|
|
9
|
+
"browserID": "chromium",
|
|
10
|
+
"creationTimeStamp": "260101T0000",
|
|
11
|
+
"executionTimeStamp": "260101T0000",
|
|
12
|
+
"target": {
|
|
13
|
+
"what": "Mixed Outcomes Page",
|
|
14
|
+
"url": "https://example.com/mixed"
|
|
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": "r65",
|
|
40
|
+
"what": "Focus Visible",
|
|
41
|
+
"ordinalSeverity": 0,
|
|
42
|
+
"outcome": "cantTell",
|
|
43
|
+
"count": 1,
|
|
44
|
+
"catalogIndex": "0",
|
|
45
|
+
"checkpoint": 0,
|
|
46
|
+
"issueID": "focusIndicationBad"
|
|
47
|
+
}
|
|
48
|
+
],
|
|
49
|
+
"outcomeTotals": {
|
|
50
|
+
"failed": 1,
|
|
51
|
+
"cantTell": 1
|
|
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": "0",
|
|
71
|
+
"checkpoint": 0,
|
|
72
|
+
"issueID": "linkNoText"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"ruleID": "r3",
|
|
76
|
+
"what": "Text is all-capital",
|
|
77
|
+
"ordinalSeverity": 1,
|
|
78
|
+
"outcome": "failed",
|
|
79
|
+
"count": 1,
|
|
80
|
+
"catalogIndex": "1",
|
|
81
|
+
"checkpoint": 0,
|
|
82
|
+
"issueID": "allCaps"
|
|
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": "About Us",
|
|
106
|
+
"textLinkable": false,
|
|
107
|
+
"boxID": "10:20:80:30",
|
|
108
|
+
"pathID": "/html/body/a[1]",
|
|
109
|
+
"headingIndex": "",
|
|
110
|
+
"checkpoint": 0
|
|
111
|
+
},
|
|
112
|
+
"1": {
|
|
113
|
+
"tagName": "P",
|
|
114
|
+
"id": "",
|
|
115
|
+
"startTag": "<p>",
|
|
116
|
+
"text": "ALL ABOUT US",
|
|
117
|
+
"textLinkable": false,
|
|
118
|
+
"boxID": "10:60:80:20",
|
|
119
|
+
"pathID": "/html/body/p[1]",
|
|
120
|
+
"headingIndex": "",
|
|
121
|
+
"checkpoint": 0
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
"images": {},
|
|
125
|
+
"checkpoints": []
|
|
126
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "260101T0001-ct",
|
|
3
|
+
"what": "All CantTell Page",
|
|
4
|
+
"strict": false,
|
|
5
|
+
"standard": "only",
|
|
6
|
+
"device": {
|
|
7
|
+
"id": "default"
|
|
8
|
+
},
|
|
9
|
+
"browserID": "chromium",
|
|
10
|
+
"creationTimeStamp": "260101T0001",
|
|
11
|
+
"executionTimeStamp": "260101T0001",
|
|
12
|
+
"target": {
|
|
13
|
+
"what": "All CantTell Page",
|
|
14
|
+
"url": "https://example.com/canttell"
|
|
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": "r65",
|
|
30
|
+
"what": "Focus Visible",
|
|
31
|
+
"ordinalSeverity": 0,
|
|
32
|
+
"outcome": "cantTell",
|
|
33
|
+
"count": 1,
|
|
34
|
+
"catalogIndex": "0",
|
|
35
|
+
"checkpoint": 0,
|
|
36
|
+
"issueID": "focusIndicationBad"
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"outcomeTotals": {
|
|
40
|
+
"failed": 0,
|
|
41
|
+
"cantTell": 1
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"jobData": {
|
|
48
|
+
"startTime": "26-01-01T00:00",
|
|
49
|
+
"endTime": "26-01-01T00:10",
|
|
50
|
+
"elapsedSeconds": 600,
|
|
51
|
+
"preventions": {},
|
|
52
|
+
"issuelessRules": []
|
|
53
|
+
},
|
|
54
|
+
"catalog": {
|
|
55
|
+
"0": {
|
|
56
|
+
"tagName": "A",
|
|
57
|
+
"id": "",
|
|
58
|
+
"startTag": "<a>",
|
|
59
|
+
"text": "Click here",
|
|
60
|
+
"textLinkable": false,
|
|
61
|
+
"boxID": "5:10:60:20",
|
|
62
|
+
"pathID": "/html/body/a[1]",
|
|
63
|
+
"headingIndex": "",
|
|
64
|
+
"checkpoint": 0
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"images": {},
|
|
68
|
+
"checkpoints": []
|
|
69
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "260101T0002-no",
|
|
3
|
+
"what": "No Outcomes Page",
|
|
4
|
+
"strict": false,
|
|
5
|
+
"standard": "only",
|
|
6
|
+
"device": {
|
|
7
|
+
"id": "default"
|
|
8
|
+
},
|
|
9
|
+
"browserID": "chromium",
|
|
10
|
+
"creationTimeStamp": "260101T0002",
|
|
11
|
+
"executionTimeStamp": "260101T0002",
|
|
12
|
+
"target": {
|
|
13
|
+
"what": "No Outcomes Page",
|
|
14
|
+
"url": "https://example.com/nooutcomes"
|
|
15
|
+
},
|
|
16
|
+
"sources": {
|
|
17
|
+
"worker": "test-worker"
|
|
18
|
+
},
|
|
19
|
+
"acts": [
|
|
20
|
+
{
|
|
21
|
+
"type": "test",
|
|
22
|
+
"which": "ibm",
|
|
23
|
+
"startTime": "26-01-01T00:00",
|
|
24
|
+
"endTime": "26-01-01T00:01",
|
|
25
|
+
"result": {
|
|
26
|
+
"standardResult": {
|
|
27
|
+
"instances": [
|
|
28
|
+
{
|
|
29
|
+
"ruleID": "r1",
|
|
30
|
+
"what": "Button has no accessible name",
|
|
31
|
+
"ordinalSeverity": 3,
|
|
32
|
+
"count": 1,
|
|
33
|
+
"catalogIndex": "0",
|
|
34
|
+
"checkpoint": 0,
|
|
35
|
+
"issueID": "linkNoText"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"outcomeTotals": {
|
|
39
|
+
"failed": 0,
|
|
40
|
+
"cantTell": 0
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"jobData": {
|
|
47
|
+
"startTime": "26-01-01T00:00",
|
|
48
|
+
"endTime": "26-01-01T00:10",
|
|
49
|
+
"elapsedSeconds": 600,
|
|
50
|
+
"preventions": {},
|
|
51
|
+
"issuelessRules": []
|
|
52
|
+
},
|
|
53
|
+
"catalog": {
|
|
54
|
+
"0": {
|
|
55
|
+
"tagName": "BUTTON",
|
|
56
|
+
"id": "",
|
|
57
|
+
"startTag": "<button>",
|
|
58
|
+
"text": "Submit",
|
|
59
|
+
"textLinkable": false,
|
|
60
|
+
"boxID": "15:25:70:30",
|
|
61
|
+
"pathID": "/html/body/button[1]",
|
|
62
|
+
"headingIndex": "",
|
|
63
|
+
"checkpoint": 0
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"images": {},
|
|
67
|
+
"checkpoints": []
|
|
68
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "260101T0005-emp",
|
|
3
|
+
"what": "Empty Results Page",
|
|
4
|
+
"strict": false,
|
|
5
|
+
"standard": "only",
|
|
6
|
+
"device": {
|
|
7
|
+
"id": "default"
|
|
8
|
+
},
|
|
9
|
+
"browserID": "chromium",
|
|
10
|
+
"creationTimeStamp": "260101T0005",
|
|
11
|
+
"executionTimeStamp": "260101T0005",
|
|
12
|
+
"target": {
|
|
13
|
+
"what": "Empty Results Page",
|
|
14
|
+
"url": "https://example.com/empty"
|
|
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
|
+
"outcomeTotals": {
|
|
29
|
+
"failed": 0,
|
|
30
|
+
"cantTell": 0
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"jobData": {
|
|
37
|
+
"startTime": "26-01-01T00:00",
|
|
38
|
+
"endTime": "26-01-01T00:10",
|
|
39
|
+
"elapsedSeconds": 600,
|
|
40
|
+
"preventions": {},
|
|
41
|
+
"issuelessRules": []
|
|
42
|
+
},
|
|
43
|
+
"catalog": {},
|
|
44
|
+
"images": {},
|
|
45
|
+
"checkpoints": []
|
|
46
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "260101T0006-prv",
|
|
3
|
+
"what": "Prevented Page",
|
|
4
|
+
"strict": false,
|
|
5
|
+
"standard": "only",
|
|
6
|
+
"device": {
|
|
7
|
+
"id": "default"
|
|
8
|
+
},
|
|
9
|
+
"browserID": "chromium",
|
|
10
|
+
"creationTimeStamp": "260101T0006",
|
|
11
|
+
"executionTimeStamp": "260101T0006",
|
|
12
|
+
"target": {
|
|
13
|
+
"what": "Prevented Page",
|
|
14
|
+
"url": "https://example.com/prevented"
|
|
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 image has no alt text",
|
|
31
|
+
"ordinalSeverity": 2,
|
|
32
|
+
"outcome": "failed",
|
|
33
|
+
"count": 1,
|
|
34
|
+
"catalogIndex": "0",
|
|
35
|
+
"checkpoint": 0,
|
|
36
|
+
"issueID": "linkNoText"
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"outcomeTotals": {
|
|
40
|
+
"failed": 1,
|
|
41
|
+
"cantTell": 0
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"jobData": {
|
|
48
|
+
"startTime": "26-01-01T00:00",
|
|
49
|
+
"endTime": "26-01-01T00:10",
|
|
50
|
+
"elapsedSeconds": 600,
|
|
51
|
+
"preventions": {
|
|
52
|
+
"alfa": "page timed out"
|
|
53
|
+
},
|
|
54
|
+
"issuelessRules": []
|
|
55
|
+
},
|
|
56
|
+
"catalog": {
|
|
57
|
+
"0": {
|
|
58
|
+
"tagName": "IMG",
|
|
59
|
+
"id": "",
|
|
60
|
+
"startTag": "<img>",
|
|
61
|
+
"text": "An image",
|
|
62
|
+
"textLinkable": false,
|
|
63
|
+
"boxID": "0:0:200:100",
|
|
64
|
+
"pathID": "/html/body/img[1]",
|
|
65
|
+
"headingIndex": "",
|
|
66
|
+
"checkpoint": 0
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"images": {},
|
|
70
|
+
"checkpoints": []
|
|
71
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "260202T0000-new",
|
|
3
|
+
"what": "Mixed Outcomes Page",
|
|
4
|
+
"strict": false,
|
|
5
|
+
"standard": "only",
|
|
6
|
+
"device": {
|
|
7
|
+
"id": "default"
|
|
8
|
+
},
|
|
9
|
+
"browserID": "chromium",
|
|
10
|
+
"creationTimeStamp": "260202T0000",
|
|
11
|
+
"executionTimeStamp": "260202T0000",
|
|
12
|
+
"target": {
|
|
13
|
+
"what": "Mixed Outcomes Page",
|
|
14
|
+
"url": "https://example.com/mixed"
|
|
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
|
+
"outcomeTotals": {
|
|
40
|
+
"failed": 1,
|
|
41
|
+
"cantTell": 0
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"jobData": {
|
|
48
|
+
"startTime": "26-01-01T00:00",
|
|
49
|
+
"endTime": "26-02-02T00:10",
|
|
50
|
+
"elapsedSeconds": 600,
|
|
51
|
+
"preventions": {},
|
|
52
|
+
"issuelessRules": []
|
|
53
|
+
},
|
|
54
|
+
"catalog": {
|
|
55
|
+
"0": {
|
|
56
|
+
"tagName": "A",
|
|
57
|
+
"id": "",
|
|
58
|
+
"startTag": "<a>",
|
|
59
|
+
"text": "About Us",
|
|
60
|
+
"textLinkable": false,
|
|
61
|
+
"boxID": "10:20:80:30",
|
|
62
|
+
"pathID": "/html/body/a[1]",
|
|
63
|
+
"headingIndex": "",
|
|
64
|
+
"checkpoint": 0
|
|
65
|
+
},
|
|
66
|
+
"1": {
|
|
67
|
+
"tagName": "P",
|
|
68
|
+
"id": "",
|
|
69
|
+
"startTag": "<p>",
|
|
70
|
+
"text": "ALL ABOUT US",
|
|
71
|
+
"textLinkable": false,
|
|
72
|
+
"boxID": "10:60:80:20",
|
|
73
|
+
"pathID": "/html/body/p[1]",
|
|
74
|
+
"headingIndex": "",
|
|
75
|
+
"checkpoint": 0
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"images": {},
|
|
79
|
+
"checkpoints": []
|
|
80
|
+
}
|
package/util.js
CHANGED
|
@@ -14,18 +14,19 @@ const wcagMap = require('./wcagMap.json');
|
|
|
14
14
|
|
|
15
15
|
// CONSTANTS
|
|
16
16
|
|
|
17
|
-
// Path of the data directory.
|
|
18
|
-
|
|
17
|
+
// Path of the data directory. Read afresh on every call (rather than cached at module load) so
|
|
18
|
+
// that tests can point Kilotest at a fixture directory via the DB_DIR environment variable.
|
|
19
|
+
const dbPath = exports.dbPath = () => process.env.DB_DIR || path.join(__dirname, 'db');
|
|
19
20
|
// Path of the jobs directory.
|
|
20
|
-
const jobsPath = exports.jobsPath = path.join(dbPath, 'jobs');
|
|
21
|
+
const jobsPath = exports.jobsPath = () => path.join(dbPath(), 'jobs');
|
|
21
22
|
// Path of the recommendations file.
|
|
22
|
-
const recsPath = exports.recsPath = path.join(jobsPath, 'recs.json');
|
|
23
|
+
const recsPath = exports.recsPath = () => path.join(jobsPath(), 'recs.json');
|
|
23
24
|
// Path of the reports directory.
|
|
24
|
-
const reportsPath = exports.reportsPath = path.join(dbPath, 'reports');
|
|
25
|
+
const reportsPath = exports.reportsPath = () => path.join(dbPath(), 'reports');
|
|
25
26
|
// Path of the hidden-reports directory.
|
|
26
|
-
const hiddenReportsPath = exports.hiddenReportsPath = path.join(dbPath, 'hiddenReports');
|
|
27
|
+
const hiddenReportsPath = exports.hiddenReportsPath = () => path.join(dbPath(), 'hiddenReports');
|
|
27
28
|
// Path of the reports data file.
|
|
28
|
-
const reportsExtractPath = exports.reportsExtractPath = path.join(dbPath, 'reportsExtract.json');
|
|
29
|
+
const reportsExtractPath = exports.reportsExtractPath = () => path.join(dbPath(), 'reportsExtract.json');
|
|
29
30
|
// IDs, names, and sponsors of Testaro rule engines.
|
|
30
31
|
const ruleEngines = exports.ruleEngines = {
|
|
31
32
|
alfa: ['Alfa', 'Siteimprove'],
|
|
@@ -145,7 +146,7 @@ const getJobNames = exports.getJobNames = async () => {
|
|
|
145
146
|
let fileNames;
|
|
146
147
|
for (const category of ['queue', 'claimed', 'failed']) {
|
|
147
148
|
try {
|
|
148
|
-
fileNames = await fs.readdir(path.join(jobsPath, category));
|
|
149
|
+
fileNames = await fs.readdir(path.join(jobsPath(), category));
|
|
149
150
|
}
|
|
150
151
|
catch(error) {
|
|
151
152
|
return `ERROR: Job directory ${category} not readable (${error.message})`;
|
|
@@ -229,10 +230,10 @@ const getRecs = exports.getRecs = async () => {
|
|
|
229
230
|
let recs;
|
|
230
231
|
let recsJSON;
|
|
231
232
|
try {
|
|
232
|
-
recsJSON = await fs.readFile(recsPath, 'utf8');
|
|
233
|
+
recsJSON = await fs.readFile(recsPath(), 'utf8');
|
|
233
234
|
}
|
|
234
235
|
catch(error) {
|
|
235
|
-
await fs.writeFile(recsPath, '{}\n');
|
|
236
|
+
await fs.writeFile(recsPath(), '{}\n');
|
|
236
237
|
return `ERROR: recommendations file not readable, so created an empty one (${error.message})`;
|
|
237
238
|
}
|
|
238
239
|
try {
|
|
@@ -285,7 +286,7 @@ exports.isRecommendable = async url => {
|
|
|
285
286
|
const jobNames = await getJobNames();
|
|
286
287
|
// For each claimed job:
|
|
287
288
|
for (const fileName of jobNames.claimed) {
|
|
288
|
-
const job = await getObject(path.join(jobsPath, 'claimed', fileName));
|
|
289
|
+
const job = await getObject(path.join(jobsPath(), 'claimed', fileName));
|
|
289
290
|
// If its URL is that of the recommended target:
|
|
290
291
|
if (job.target.url === url) {
|
|
291
292
|
// Return this.
|
|
@@ -294,7 +295,7 @@ exports.isRecommendable = async url => {
|
|
|
294
295
|
}
|
|
295
296
|
// If no claimed job has the URL of the target, for each queued job:
|
|
296
297
|
for (const fileName of jobNames.queue) {
|
|
297
|
-
const job = await getObject(path.join(jobsPath, 'queue', fileName));
|
|
298
|
+
const job = await getObject(path.join(jobsPath(), 'queue', fileName));
|
|
298
299
|
// If its URL is that of the recommended target:
|
|
299
300
|
if (job.target.url === url) {
|
|
300
301
|
// Return this.
|
|
@@ -415,7 +416,7 @@ const updateRecs = exports.updateRecs = (what, url, why) => recsLock(async () =>
|
|
|
415
416
|
why
|
|
416
417
|
});
|
|
417
418
|
// Save the revised recommendations.
|
|
418
|
-
await fs.writeFile(recsPath, getJSON(recs));
|
|
419
|
+
await fs.writeFile(recsPath(), getJSON(recs));
|
|
419
420
|
// Return success.
|
|
420
421
|
return {};
|
|
421
422
|
});
|
|
@@ -435,7 +436,7 @@ exports.getPathID = (catalog, catalogIndex, pathID) => {
|
|
|
435
436
|
};
|
|
436
437
|
// Returns the path of an available report file.
|
|
437
438
|
const getReportPath = exports.getReportPath = (timeStamp, jobID) => path
|
|
438
|
-
.join(reportsPath, `${timeStamp}-${jobID}.json`);
|
|
439
|
+
.join(reportsPath(), `${timeStamp}-${jobID}.json`);
|
|
439
440
|
// Returns whether a report is valid.
|
|
440
441
|
const isValidReport = exports.isValidReport = report => {
|
|
441
442
|
// Return whether it has the type and properties required by Kilotest:
|
|
@@ -644,7 +645,7 @@ exports.getPageDataStrings = async (timeStamp, jobID, pageData) => {
|
|
|
644
645
|
// Returns the creation time and size of a report.
|
|
645
646
|
const getReportStats = exports.getReportStats = async (timeStamp, jobID) => {
|
|
646
647
|
const reportStat = await fs.stat(
|
|
647
|
-
path.join(reportsPath, `${timeStamp}-${jobID}.json`),
|
|
648
|
+
path.join(reportsPath(), `${timeStamp}-${jobID}.json`),
|
|
648
649
|
{throwIfNoEntry: false}
|
|
649
650
|
);
|
|
650
651
|
if (!reportStat) {
|
|
@@ -656,9 +657,9 @@ const getReportStats = exports.getReportStats = async (timeStamp, jobID) => {
|
|
|
656
657
|
};
|
|
657
658
|
// Returns whether a report is hidden.
|
|
658
659
|
exports.isHidden = async (timeStamp, jobID) => {
|
|
659
|
-
await fs.mkdir(hiddenReportsPath, {recursive: true});
|
|
660
|
+
await fs.mkdir(hiddenReportsPath(), {recursive: true});
|
|
660
661
|
// Get the names of the hidden report files.
|
|
661
|
-
const hiddenReportFileNames = await fs.readdir(hiddenReportsPath);
|
|
662
|
+
const hiddenReportFileNames = await fs.readdir(hiddenReportsPath());
|
|
662
663
|
// Return whether the report is among them.
|
|
663
664
|
return hiddenReportFileNames.includes(`${timeStamp}-${jobID}.json`);
|
|
664
665
|
};
|
|
@@ -667,11 +668,11 @@ const makeReportsExtract = exports.makeReportsExtract = async () => {
|
|
|
667
668
|
// Initialize the reports data.
|
|
668
669
|
const reportsExtract = {};
|
|
669
670
|
// Get the names of all report files.
|
|
670
|
-
const reportFileNames = await fs.readdir(reportsPath);
|
|
671
|
+
const reportFileNames = await fs.readdir(reportsPath());
|
|
671
672
|
// For each of them:
|
|
672
673
|
for (const reportFileName of reportFileNames) {
|
|
673
674
|
// Get its content.
|
|
674
|
-
const reportJSON = await fs.readFile(path.join(reportsPath, reportFileName), 'utf8');
|
|
675
|
+
const reportJSON = await fs.readFile(path.join(reportsPath(), reportFileName), 'utf8');
|
|
675
676
|
// Get the object that it represents.
|
|
676
677
|
const report = JSON.parse(reportJSON);
|
|
677
678
|
const {target, jobData} = report;
|
|
@@ -690,9 +691,9 @@ const makeReportsExtract = exports.makeReportsExtract = async () => {
|
|
|
690
691
|
reportsExtract[jobName] = reportExtract;
|
|
691
692
|
}
|
|
692
693
|
// Ensure the data directory exists.
|
|
693
|
-
await fs.mkdir(dbPath, {recursive: true});
|
|
694
|
+
await fs.mkdir(dbPath(), {recursive: true});
|
|
694
695
|
// Save the extract of the reports.
|
|
695
|
-
await fs.writeFile(reportsExtractPath, getJSON(reportsExtract));
|
|
696
|
+
await fs.writeFile(reportsExtractPath(), getJSON(reportsExtract));
|
|
696
697
|
// Return it.
|
|
697
698
|
return reportsExtract;
|
|
698
699
|
};
|
|
@@ -701,7 +702,7 @@ const getReportsExtract = exports.getReportsExtract = async () => {
|
|
|
701
702
|
let reportsExtract;
|
|
702
703
|
try {
|
|
703
704
|
// Get the reports extract file.
|
|
704
|
-
const reportsExtractJSON = await fs.readFile(reportsExtractPath, 'utf8');
|
|
705
|
+
const reportsExtractJSON = await fs.readFile(reportsExtractPath(), 'utf8');
|
|
705
706
|
// Get its data.
|
|
706
707
|
reportsExtract = JSON.parse(reportsExtractJSON);
|
|
707
708
|
// If it is empty:
|
package/util.test.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/*
|
|
2
|
+
util.test.js
|
|
3
|
+
Tests for util.js data-path injection (Phase 1).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
const {test} = require('node:test');
|
|
9
|
+
const assert = require('node:assert/strict');
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
const {
|
|
12
|
+
dbPath,
|
|
13
|
+
jobsPath,
|
|
14
|
+
recsPath,
|
|
15
|
+
reportsPath,
|
|
16
|
+
hiddenReportsPath,
|
|
17
|
+
reportsExtractPath
|
|
18
|
+
} = require('./util');
|
|
19
|
+
|
|
20
|
+
// TESTS
|
|
21
|
+
|
|
22
|
+
test('dbPath defaults to the project db directory when DB_DIR is unset', () => {
|
|
23
|
+
const saved = process.env.DB_DIR;
|
|
24
|
+
delete process.env.DB_DIR;
|
|
25
|
+
try {
|
|
26
|
+
assert.equal(dbPath(), path.join(__dirname, 'db'));
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
if (saved !== undefined) {
|
|
30
|
+
process.env.DB_DIR = saved;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('dbPath honors DB_DIR when it is set', () => {
|
|
36
|
+
const saved = process.env.DB_DIR;
|
|
37
|
+
process.env.DB_DIR = '/tmp/kilotest-fixtures';
|
|
38
|
+
try {
|
|
39
|
+
assert.equal(dbPath(), '/tmp/kilotest-fixtures');
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
if (saved !== undefined) {
|
|
43
|
+
process.env.DB_DIR = saved;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
delete process.env.DB_DIR;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('jobsPath, recsPath, reportsPath, hiddenReportsPath, and reportsExtractPath derive from DB_DIR', () => {
|
|
52
|
+
const saved = process.env.DB_DIR;
|
|
53
|
+
process.env.DB_DIR = '/tmp/kilotest-fixtures';
|
|
54
|
+
try {
|
|
55
|
+
assert.equal(jobsPath(), path.join('/tmp/kilotest-fixtures', 'jobs'));
|
|
56
|
+
assert.equal(recsPath(), path.join('/tmp/kilotest-fixtures', 'jobs', 'recs.json'));
|
|
57
|
+
assert.equal(reportsPath(), path.join('/tmp/kilotest-fixtures', 'reports'));
|
|
58
|
+
assert.equal(hiddenReportsPath(), path.join('/tmp/kilotest-fixtures', 'hiddenReports'));
|
|
59
|
+
assert.equal(reportsExtractPath(), path.join('/tmp/kilotest-fixtures', 'reportsExtract.json'));
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
if (saved !== undefined) {
|
|
63
|
+
process.env.DB_DIR = saved;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
delete process.env.DB_DIR;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('changing DB_DIR between calls is reflected by the path functions', () => {
|
|
72
|
+
const saved = process.env.DB_DIR;
|
|
73
|
+
try {
|
|
74
|
+
process.env.DB_DIR = '/tmp/a';
|
|
75
|
+
assert.equal(reportsPath(), path.join('/tmp/a', 'reports'));
|
|
76
|
+
process.env.DB_DIR = '/tmp/b';
|
|
77
|
+
assert.equal(reportsPath(), path.join('/tmp/b', 'reports'));
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
if (saved !== undefined) {
|
|
81
|
+
process.env.DB_DIR = saved;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
delete process.env.DB_DIR;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
package/web/enqueue/index.js
CHANGED
|
@@ -35,7 +35,7 @@ exports.answer = async (url, what, authCode) => {
|
|
|
35
35
|
};
|
|
36
36
|
// Save the job in the queue.
|
|
37
37
|
await fs.writeFile(
|
|
38
|
-
path.join(jobsPath, 'queue', `${jobName}.json`), getJSON(job)
|
|
38
|
+
path.join(jobsPath(), 'queue', `${jobName}.json`), getJSON(job)
|
|
39
39
|
);
|
|
40
40
|
console.log(`Retest queued for ${what} as job ${jobName}`);
|
|
41
41
|
// Isolate this revision.
|
|
@@ -45,7 +45,7 @@ exports.answer = async (url, what, authCode) => {
|
|
|
45
45
|
// Delete the recommendations to test the target.
|
|
46
46
|
delete recs[url];
|
|
47
47
|
// Save the revised recommendations.
|
|
48
|
-
await fs.writeFile(path.join(jobsPath, 'recs.json'), getJSON(recs));
|
|
48
|
+
await fs.writeFile(path.join(jobsPath(), 'recs.json'), getJSON(recs));
|
|
49
49
|
});
|
|
50
50
|
// Get the answer template.
|
|
51
51
|
let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
|
|
@@ -24,7 +24,7 @@ exports.answer = async (_, search) => {
|
|
|
24
24
|
// For each report to be deleted:
|
|
25
25
|
for (const jobName of jobNames) {
|
|
26
26
|
// Delete it.
|
|
27
|
-
await fs.unlink(path.join(reportsPath, `${jobName}.json`));
|
|
27
|
+
await fs.unlink(path.join(reportsPath(), `${jobName}.json`));
|
|
28
28
|
}
|
|
29
29
|
// Update the extract of the available reports.
|
|
30
30
|
await makeReportsExtract();
|
|
@@ -47,7 +47,7 @@ exports.answer = async (_, search) => {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
-
const reportNames = await fs.readdir(reportsPath);
|
|
50
|
+
const reportNames = await fs.readdir(reportsPath());
|
|
51
51
|
const reportSpecs = [];
|
|
52
52
|
// For each report:
|
|
53
53
|
for (const reportName of reportNames) {
|
|
@@ -23,7 +23,7 @@ exports.answer = async (_, search) => {
|
|
|
23
23
|
const fileName = `${jobName}.json`;
|
|
24
24
|
try {
|
|
25
25
|
// Move the specified report to the directory of hidden reports.
|
|
26
|
-
await fs.rename(path.join(reportsPath, fileName), path.join(hiddenReportsPath, fileName));
|
|
26
|
+
await fs.rename(path.join(reportsPath(), fileName), path.join(hiddenReportsPath(), fileName));
|
|
27
27
|
// Update the data on the available reports.
|
|
28
28
|
await makeReportsExtract();
|
|
29
29
|
}
|
package/web/listReports/index.js
CHANGED
|
@@ -60,7 +60,7 @@ const populateQuery = async query => {
|
|
|
60
60
|
// For each job in the category:
|
|
61
61
|
for (const fileName of jobFileNames[category]) {
|
|
62
62
|
// Get the job.
|
|
63
|
-
const job = await getObject(path.join(jobsPath, category, fileName));
|
|
63
|
+
const job = await getObject(path.join(jobsPath(), category, fileName));
|
|
64
64
|
// Add a line.
|
|
65
65
|
lines[category].push(`${margin}<li><code>${job.target.url}</code> (${job.target.what})</li>`);
|
|
66
66
|
}
|
|
@@ -24,7 +24,7 @@ exports.answer = async (_, search) => {
|
|
|
24
24
|
// For each report to be deleted:
|
|
25
25
|
for (const jobName of jobNames) {
|
|
26
26
|
// Delete it.
|
|
27
|
-
await fs.unlink(path.join(reportsPath, `${jobName}.json`));
|
|
27
|
+
await fs.unlink(path.join(reportsPath(), `${jobName}.json`));
|
|
28
28
|
}
|
|
29
29
|
// Update the extract of the available reports.
|
|
30
30
|
await makeReportsExtract();
|
|
@@ -47,7 +47,7 @@ exports.answer = async (_, search) => {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
-
const reportNames = await fs.readdir(reportsPath);
|
|
50
|
+
const reportNames = await fs.readdir(reportsPath());
|
|
51
51
|
// Initialize an array of report summaries.
|
|
52
52
|
const reportSpecs = [];
|
|
53
53
|
// For each report:
|
|
@@ -24,7 +24,7 @@ exports.answer = async (_, search) => {
|
|
|
24
24
|
// For each report to be deleted:
|
|
25
25
|
for (const jobName of jobNames) {
|
|
26
26
|
// Delete it.
|
|
27
|
-
await fs.unlink(path.join(reportsPath, `${jobName}.json`));
|
|
27
|
+
await fs.unlink(path.join(reportsPath(), `${jobName}.json`));
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
// If this failed:
|
|
@@ -45,7 +45,7 @@ exports.answer = async (_, search) => {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
-
const reportNames = await fs.readdir(reportsPath);
|
|
48
|
+
const reportNames = await fs.readdir(reportsPath());
|
|
49
49
|
const reportSpecs = [];
|
|
50
50
|
// For each report:
|
|
51
51
|
for (const reportName of reportNames) {
|
|
@@ -23,7 +23,7 @@ exports.answer = async (_, search) => {
|
|
|
23
23
|
const fileName = `${jobName}.json`;
|
|
24
24
|
try {
|
|
25
25
|
// Move the report to the reports directory.
|
|
26
|
-
await fs.rename(path.join(hiddenReportsPath, fileName), path.join(reportsPath, fileName));
|
|
26
|
+
await fs.rename(path.join(hiddenReportsPath(), fileName), path.join(reportsPath(), fileName));
|
|
27
27
|
// Update the data on the available reports.
|
|
28
28
|
await makeReportsExtract();
|
|
29
29
|
}
|
|
@@ -47,11 +47,11 @@ exports.answer = async (_, search) => {
|
|
|
47
47
|
// Initialize an array of data on reports to be unhidden.
|
|
48
48
|
const reportsData = [];
|
|
49
49
|
// Get the names of the hidden report files.
|
|
50
|
-
const hiddenReportFileNames = await fs.readdir(hiddenReportsPath);
|
|
50
|
+
const hiddenReportFileNames = await fs.readdir(hiddenReportsPath());
|
|
51
51
|
// For each hidden report:
|
|
52
52
|
for (const reportFileName of hiddenReportFileNames) {
|
|
53
53
|
// Get its file.
|
|
54
|
-
const reportJSON = await fs.readFile(path.join(hiddenReportsPath, reportFileName), 'utf8');
|
|
54
|
+
const reportJSON = await fs.readFile(path.join(hiddenReportsPath(), reportFileName), 'utf8');
|
|
55
55
|
// Get the report.
|
|
56
56
|
const report = JSON.parse(reportJSON);
|
|
57
57
|
const {id, target} = report;
|