@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 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
@@ -9,7 +9,8 @@ export default defineConfig([
9
9
  {
10
10
  ignores: [
11
11
  "docs/IDEAS.md",
12
- "package-lock.json"
12
+ "package-lock.json",
13
+ "coverage/**"
13
14
  ]
14
15
  },
15
16
  {
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
- anyOf:
258
- - type: string
259
- - type: 'null'
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
- anyOf:
270
- - type: string
271
- - type: 'null'
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
- anyOf:
786
- - type: number
787
- - type: string
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
- anyOf:
1228
- - type: string
1229
- - type: 'null'
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
- anyOf:
1294
- - type: string
1295
- - type: 'null'
1293
+ type:
1294
+ - string
1295
+ - 'null'
1296
1296
  sponsor:
1297
- anyOf:
1298
- - type: string
1299
- - type: 'null'
1297
+ type:
1298
+ - string
1299
+ - 'null'
1300
1300
  required:
1301
1301
  - identifier
1302
1302
  - name
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jrpool/kilotest",
3
- "version": "46.0.1",
3
+ "version": "46.0.3",
4
4
  "description": "Ensemble testing for front-end web quality",
5
5
  "main": "index.js",
6
6
  "engines": {
@@ -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
- const dbPath = exports.dbPath = path.join(__dirname, 'db');
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
+ });
@@ -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
  }
@@ -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;