@jrpool/kilotest 24.0.4
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/.claude/settings.local.json +9 -0
- package/DEVELOPMENT.md +5860 -0
- package/LICENSE +21 -0
- package/README.md +44 -0
- package/SERVICE.md +268 -0
- package/aceconfig.js +28 -0
- package/ai0BalanceForm/index.html +30 -0
- package/ai0BalanceForm/index.js +79 -0
- package/alerts.js +73 -0
- package/diagnoses/index.html +46 -0
- package/diagnoses/index.js +140 -0
- package/env.example +21 -0
- package/env.testaro +17 -0
- package/error.html +18 -0
- package/eslint.config.mjs +53 -0
- package/favicon.ico +0 -0
- package/index.html +39 -0
- package/index.js +639 -0
- package/issues/index.html +20 -0
- package/issues/index.js +173 -0
- package/job.json +100 -0
- package/manage/index.html +32 -0
- package/manage/index.js +22 -0
- package/package.json +38 -0
- package/pm2.config.js +15 -0
- package/reannotate/index.html +19 -0
- package/reannotate/index.js +39 -0
- package/reannotateForm/index.html +29 -0
- package/reannotateForm/index.js +114 -0
- package/recActionForm/index.html +33 -0
- package/recActionForm/index.js +49 -0
- package/reportHideForm/index.html +29 -0
- package/reportHideForm/index.js +89 -0
- package/reportIssue/index.html +38 -0
- package/reportIssue/index.js +181 -0
- package/reportIssues/index.html +47 -0
- package/reportIssues/index.js +259 -0
- package/reportUnhideForm/index.html +29 -0
- package/reportUnhideForm/index.js +89 -0
- package/reportsExpungeForm/index.html +29 -0
- package/reportsExpungeForm/index.js +105 -0
- package/reportsPruneForm/index.html +29 -0
- package/reportsPruneForm/index.js +105 -0
- package/reportsRewindForm/index.html +29 -0
- package/reportsRewindForm/index.js +105 -0
- package/retestRec/index.html +23 -0
- package/retestRec/index.js +19 -0
- package/retestRecForm/index.html +27 -0
- package/retestRecForm/index.js +36 -0
- package/rules/index.html +28 -0
- package/rules/index.js +71 -0
- package/style.css +196 -0
- package/targets/index.html +37 -0
- package/targets/index.js +170 -0
- package/testOrder/index.html +23 -0
- package/testOrder/index.js +62 -0
- package/testRec/index.html +23 -0
- package/testRec/index.js +25 -0
- package/testRecForm/index.html +34 -0
- package/testRecForm/index.js +22 -0
- package/tutorial/images/newsletter-form.png +0 -0
- package/tutorial/index.html +796 -0
- package/tutorial/index.js +53 -0
- package/util.js +686 -0
- package/wcagMap.json +102 -0
- package/wcagRenew/index.html +19 -0
- package/wcagRenew/index.js +70 -0
- package/wcagRenewForm/index.html +25 -0
- package/wcagRenewForm/index.js +22 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/*
|
|
2
|
+
index.js
|
|
3
|
+
Serves a form for unhiding a report.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
const {getJSON, getLog, logsPath, reportsPath} = require('../util');
|
|
9
|
+
const fs = require('fs/promises');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
// FUNCTIONS
|
|
13
|
+
|
|
14
|
+
// Returns a form for unhiding a report.
|
|
15
|
+
exports.answer = async (_, search) => {
|
|
16
|
+
const searchParams = new URLSearchParams(search);
|
|
17
|
+
const authCode = searchParams?.get('authCode');
|
|
18
|
+
const jobName = searchParams?.get('report');
|
|
19
|
+
// If the form has been displayed by itself after a submission and a report is to be unhidden:
|
|
20
|
+
if (jobName) {
|
|
21
|
+
// If the authorization code is valid:
|
|
22
|
+
if (authCode === process.env.AUTH_CODE) {
|
|
23
|
+
// Get the log of the report.
|
|
24
|
+
const log = await getLog(... jobName.split('-'));
|
|
25
|
+
// Reverse the hiddenness property of the log.
|
|
26
|
+
log.hidden = false;
|
|
27
|
+
// Save the updated log.
|
|
28
|
+
await fs.writeFile(path.join(logsPath, `${jobName}.json`), getJSON(log));
|
|
29
|
+
}
|
|
30
|
+
// Otherwise, i.e. if the authorization code is invalid:
|
|
31
|
+
else {
|
|
32
|
+
// Report the error.
|
|
33
|
+
return {
|
|
34
|
+
status: 'error',
|
|
35
|
+
error: 'Invalid authorization code'
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const reportFileNames = await fs.readdir(reportsPath);
|
|
40
|
+
const reportSpecs = [];
|
|
41
|
+
// For each report:
|
|
42
|
+
for (const reportFileName of reportFileNames) {
|
|
43
|
+
const [timeStamp, jobID] = reportFileName.slice(0, -5).split('-');
|
|
44
|
+
// Get its log.
|
|
45
|
+
const log = await getLog(timeStamp, jobID);
|
|
46
|
+
const {what, hidden} = log;
|
|
47
|
+
reportSpecs.push({
|
|
48
|
+
what,
|
|
49
|
+
timeStamp,
|
|
50
|
+
jobID,
|
|
51
|
+
hidden
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
// Sort the logs by page name and then by time stamp.
|
|
55
|
+
reportSpecs.sort((a, b) => {
|
|
56
|
+
if (a.what === b.what) {
|
|
57
|
+
return a.timeStamp.localeCompare(b.timeStamp);
|
|
58
|
+
}
|
|
59
|
+
return a.what.localeCompare(b.what);
|
|
60
|
+
});
|
|
61
|
+
const lines = [];
|
|
62
|
+
const margin = ' '.repeat(12);
|
|
63
|
+
// For each report:
|
|
64
|
+
reportSpecs.forEach(spec => {
|
|
65
|
+
const {hidden, what, timeStamp, jobID} = spec;
|
|
66
|
+
// If it is hidden:
|
|
67
|
+
if (hidden) {
|
|
68
|
+
const specString = `${what} (job <code>${jobID}</code> at ${timeStamp})`;
|
|
69
|
+
// Add a line with a radio button to unhide it.
|
|
70
|
+
lines.push(
|
|
71
|
+
`${margin}<p><input type="radio" name="report" value="${timeStamp}-${jobID}"> ${specString}</p>`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
const query = {
|
|
76
|
+
reports: lines.join('\n'),
|
|
77
|
+
};
|
|
78
|
+
// Get the unhiding form template.
|
|
79
|
+
let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
|
|
80
|
+
// Replace its placeholders.
|
|
81
|
+
Object.keys(query).forEach(param => {
|
|
82
|
+
answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
|
|
83
|
+
});
|
|
84
|
+
// Return the populated page.
|
|
85
|
+
return {
|
|
86
|
+
status: 'ok',
|
|
87
|
+
answerPage
|
|
88
|
+
};
|
|
89
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en-US">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta name="publisher" content="Jonathan Robert Pool">
|
|
7
|
+
<meta name="creator" content="Jonathan Robert Pool">
|
|
8
|
+
<meta name="keywords" content="report,accessibility,a11y">
|
|
9
|
+
<title>Expunge reports | Kilotest</title>
|
|
10
|
+
<link rel="icon" href="/favicon.ico">
|
|
11
|
+
<link rel="stylesheet" href="/style.css">
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<main>
|
|
15
|
+
<h1><a href="/">Kilotest</a>: Expunge reports</h1>
|
|
16
|
+
<p>__intro__</p>
|
|
17
|
+
<form action="/reportsExpungeForm.html">
|
|
18
|
+
<fieldset>
|
|
19
|
+
<legend>Reports</legend>
|
|
20
|
+
__reports__
|
|
21
|
+
</fieldset>
|
|
22
|
+
<p><label>
|
|
23
|
+
Authorization code: <input size="3" minLength="3" maxlength="3" name="authCode" required__disabled__>
|
|
24
|
+
</label></p>
|
|
25
|
+
<p><button type="submit"__disabled__>Submit</button></p>
|
|
26
|
+
</form>
|
|
27
|
+
</main>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/*
|
|
2
|
+
index.js
|
|
3
|
+
Serves a form for deleting sole reports.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
const {getTargetData, logsPath, reportsPath} = require('../util');
|
|
9
|
+
const fs = require('fs/promises');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
// FUNCTIONS
|
|
13
|
+
|
|
14
|
+
// Returns a form for deleting sole reports.
|
|
15
|
+
exports.answer = async (_, search) => {
|
|
16
|
+
const searchParams = new URLSearchParams(search);
|
|
17
|
+
const authCode = searchParams?.get('authCode');
|
|
18
|
+
const jobNames = searchParams?.getAll('report');
|
|
19
|
+
// If the form has been displayed by itself after a submission and any reports are to be deleted:
|
|
20
|
+
if (jobNames?.length) {
|
|
21
|
+
// If the authorization code is valid:
|
|
22
|
+
if (authCode === process.env.AUTH_CODE) {
|
|
23
|
+
// For each report to be deleted:
|
|
24
|
+
for (const jobName of jobNames) {
|
|
25
|
+
// Delete it.
|
|
26
|
+
await fs.unlink(path.join(reportsPath, `${jobName}.json`));
|
|
27
|
+
// Delete its log.
|
|
28
|
+
await fs.unlink(path.join(logsPath, `${jobName}.json`));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Otherwise, i.e. if the authorization code is invalid:
|
|
32
|
+
else {
|
|
33
|
+
// Report the error.
|
|
34
|
+
return {
|
|
35
|
+
status: 'error',
|
|
36
|
+
error: 'Invalid authorization code'
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const reportNames = await fs.readdir(reportsPath);
|
|
41
|
+
const reportSpecs = [];
|
|
42
|
+
// For each report:
|
|
43
|
+
for (const reportName of reportNames) {
|
|
44
|
+
const [timeStamp, jobID] = reportName.slice(0, -5).split('-');
|
|
45
|
+
// Get a summary of it.
|
|
46
|
+
const reportSummary = await getTargetData(timeStamp, jobID);
|
|
47
|
+
const {issueSet, preventedTools, url} = reportSummary;
|
|
48
|
+
reportSpecs.push({
|
|
49
|
+
timeStamp,
|
|
50
|
+
jobID,
|
|
51
|
+
issueCount: issueSet.size,
|
|
52
|
+
preventionCount: preventedTools?.length ?? 0,
|
|
53
|
+
url
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
// Sort the summaries by URL and then by time stamp.
|
|
57
|
+
reportSpecs.sort((a, b) => {
|
|
58
|
+
if (a.url === b.url) {
|
|
59
|
+
return a.timeStamp.localeCompare(b.timeStamp);
|
|
60
|
+
}
|
|
61
|
+
return a.url.localeCompare(b.url);
|
|
62
|
+
});
|
|
63
|
+
const lines = [];
|
|
64
|
+
const margin = ' '.repeat(12);
|
|
65
|
+
let anyDeletable = false;
|
|
66
|
+
// For each summary:
|
|
67
|
+
reportSpecs.forEach((spec, index) => {
|
|
68
|
+
const {timeStamp, jobID, issueCount, preventionCount, url} = spec;
|
|
69
|
+
const jobName = `${timeStamp}-${jobID}`;
|
|
70
|
+
const specString = `<code>${url}</code> (<code>${jobName}</code>): preventions ${preventionCount}, issues ${issueCount}`;
|
|
71
|
+
// If its report is the sole report on a target:
|
|
72
|
+
if (reportSpecs[index - 1]?.url !== url && reportSpecs[index + 1]?.url !== url) {
|
|
73
|
+
// Add a line with a deletion checkbox.
|
|
74
|
+
lines.push(
|
|
75
|
+
`${margin}<p><input type="checkbox" name="report" value="${jobName}"> ${specString}</p>`
|
|
76
|
+
);
|
|
77
|
+
anyDeletable = true;
|
|
78
|
+
}
|
|
79
|
+
// Otherwise, i.e. if its report is not the sole report on a target:
|
|
80
|
+
else {
|
|
81
|
+
// Add a line without a deletion checkbox.
|
|
82
|
+
lines.push(`${margin}<p>${specString}</p>`);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
const intro = anyDeletable
|
|
86
|
+
? 'Choose the sole reports to delete.'
|
|
87
|
+
: 'Each target has at least 2 reports, so there are no reports to delete.';
|
|
88
|
+
const disabled = anyDeletable ? '' : ' disabled';
|
|
89
|
+
const query = {
|
|
90
|
+
reports: lines.join('\n'),
|
|
91
|
+
intro,
|
|
92
|
+
disabled
|
|
93
|
+
};
|
|
94
|
+
// Get the order form template.
|
|
95
|
+
let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
|
|
96
|
+
// Replace its placeholders.
|
|
97
|
+
Object.keys(query).forEach(param => {
|
|
98
|
+
answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
|
|
99
|
+
});
|
|
100
|
+
// Return the populated page.
|
|
101
|
+
return {
|
|
102
|
+
status: 'ok',
|
|
103
|
+
answerPage
|
|
104
|
+
};
|
|
105
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en-US">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta name="publisher" content="Jonathan Robert Pool">
|
|
7
|
+
<meta name="creator" content="Jonathan Robert Pool">
|
|
8
|
+
<meta name="keywords" content="report,accessibility,a11y">
|
|
9
|
+
<title>Prune reports | Kilotest</title>
|
|
10
|
+
<link rel="icon" href="/favicon.ico">
|
|
11
|
+
<link rel="stylesheet" href="/style.css">
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<main>
|
|
15
|
+
<h1><a href="/">Kilotest</a>: Prune reports</h1>
|
|
16
|
+
<p>__intro__</p>
|
|
17
|
+
<form action="/reportsPruneForm.html">
|
|
18
|
+
<fieldset>
|
|
19
|
+
<legend>Reports</legend>
|
|
20
|
+
__reports__
|
|
21
|
+
</fieldset>
|
|
22
|
+
<p><label>
|
|
23
|
+
Authorization code: <input size="3" minLength="3" maxlength="3" name="authCode" required__disabled__>
|
|
24
|
+
</label></p>
|
|
25
|
+
<p><button type="submit"__disabled__>Submit</button></p>
|
|
26
|
+
</form>
|
|
27
|
+
</main>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/*
|
|
2
|
+
index.js
|
|
3
|
+
Serves a form for deleting superseded reports.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
const {getTargetData, logsPath, reportsPath} = require('../util');
|
|
9
|
+
const fs = require('fs/promises');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
// FUNCTIONS
|
|
13
|
+
|
|
14
|
+
// Returns a form for deleting non-latest reports.
|
|
15
|
+
exports.answer = async (_, search) => {
|
|
16
|
+
const searchParams = new URLSearchParams(search);
|
|
17
|
+
const authCode = searchParams?.get('authCode');
|
|
18
|
+
const jobNames = searchParams?.getAll('report');
|
|
19
|
+
// If the form has been displayed by itself after a submission and any reports are to be deleted:
|
|
20
|
+
if (jobNames?.length) {
|
|
21
|
+
// If the authorization code is valid:
|
|
22
|
+
if (authCode === process.env.AUTH_CODE) {
|
|
23
|
+
// For each report to be deleted:
|
|
24
|
+
for (const jobName of jobNames) {
|
|
25
|
+
// Delete it.
|
|
26
|
+
await fs.unlink(path.join(reportsPath, `${jobName}.json`));
|
|
27
|
+
// Delete its log.
|
|
28
|
+
await fs.unlink(path.join(logsPath, `${jobName}.json`));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Otherwise, i.e. if the authorization code is invalid:
|
|
32
|
+
else {
|
|
33
|
+
// Report the error.
|
|
34
|
+
return {
|
|
35
|
+
status: 'error',
|
|
36
|
+
error: 'Invalid authorization code'
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const reportNames = await fs.readdir(reportsPath);
|
|
41
|
+
const reportSpecs = [];
|
|
42
|
+
// For each report:
|
|
43
|
+
for (const reportName of reportNames) {
|
|
44
|
+
const [timeStamp, jobID] = reportName.slice(0, -5).split('-');
|
|
45
|
+
// Get a summary of it.
|
|
46
|
+
const reportSummary = await getTargetData(timeStamp, jobID);
|
|
47
|
+
const {issueSet, preventedTools, url} = reportSummary;
|
|
48
|
+
reportSpecs.push({
|
|
49
|
+
timeStamp,
|
|
50
|
+
jobID,
|
|
51
|
+
issueCount: issueSet.size,
|
|
52
|
+
preventionCount: preventedTools?.length ?? 0,
|
|
53
|
+
url
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
// Sort the summaries by URL and then by time stamp.
|
|
57
|
+
reportSpecs.sort((a, b) => {
|
|
58
|
+
if (a.url === b.url) {
|
|
59
|
+
return a.timeStamp.localeCompare(b.timeStamp);
|
|
60
|
+
}
|
|
61
|
+
return a.url.localeCompare(b.url);
|
|
62
|
+
});
|
|
63
|
+
const lines = [];
|
|
64
|
+
const margin = ' '.repeat(12);
|
|
65
|
+
let anyDeletable = false;
|
|
66
|
+
// For each summary:
|
|
67
|
+
reportSpecs.forEach((spec, index) => {
|
|
68
|
+
const {timeStamp, jobID, issueCount, preventionCount, url} = spec;
|
|
69
|
+
const jobName = `${timeStamp}-${jobID}`;
|
|
70
|
+
const specString = `<code>${url}</code> (<code>${jobName}</code>): preventions ${preventionCount}, issues ${issueCount}`;
|
|
71
|
+
// If its report is a non-latest report:
|
|
72
|
+
if (reportSpecs[index + 1]?.url === url) {
|
|
73
|
+
// Add a line with a deletion checkbox.
|
|
74
|
+
lines.push(
|
|
75
|
+
`${margin}<p><input type="checkbox" name="report" value="${jobName}"> ${specString}</p>`
|
|
76
|
+
);
|
|
77
|
+
anyDeletable = true;
|
|
78
|
+
}
|
|
79
|
+
// Otherwise, i.e. if its report is a latest report:
|
|
80
|
+
else {
|
|
81
|
+
// Add a line without a deletion checkbox.
|
|
82
|
+
lines.push(`${margin}<p>${specString}</p>`);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
const intro = anyDeletable
|
|
86
|
+
? 'Choose the superseded reports to delete.'
|
|
87
|
+
: 'Each target has only 1 report, so there are no superseded reports to delete.';
|
|
88
|
+
const disabled = anyDeletable ? '' : ' disabled';
|
|
89
|
+
const query = {
|
|
90
|
+
reports: lines.join('\n'),
|
|
91
|
+
intro,
|
|
92
|
+
disabled
|
|
93
|
+
};
|
|
94
|
+
// Get the order form template.
|
|
95
|
+
let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
|
|
96
|
+
// Replace its placeholders.
|
|
97
|
+
Object.keys(query).forEach(param => {
|
|
98
|
+
answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
|
|
99
|
+
});
|
|
100
|
+
// Return the populated page.
|
|
101
|
+
return {
|
|
102
|
+
status: 'ok',
|
|
103
|
+
answerPage
|
|
104
|
+
};
|
|
105
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en-US">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta name="publisher" content="Jonathan Robert Pool">
|
|
7
|
+
<meta name="creator" content="Jonathan Robert Pool">
|
|
8
|
+
<meta name="keywords" content="report,accessibility,a11y">
|
|
9
|
+
<title>Rewind reports | Kilotest</title>
|
|
10
|
+
<link rel="icon" href="/favicon.ico">
|
|
11
|
+
<link rel="stylesheet" href="/style.css">
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<main>
|
|
15
|
+
<h1><a href="/">Kilotest</a>: Rewind reports</h1>
|
|
16
|
+
<p>__intro__</p>
|
|
17
|
+
<form action="/reportsRewindForm.html">
|
|
18
|
+
<fieldset>
|
|
19
|
+
<legend>Reports</legend>
|
|
20
|
+
__reports__
|
|
21
|
+
</fieldset>
|
|
22
|
+
<p><label>
|
|
23
|
+
Authorization code: <input size="3" minLength="3" maxlength="3" name="authCode" required__disabled__>
|
|
24
|
+
</label></p>
|
|
25
|
+
<p><button type="submit"__disabled__>Submit</button></p>
|
|
26
|
+
</form>
|
|
27
|
+
</main>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/*
|
|
2
|
+
index.js
|
|
3
|
+
Serves a form for deleting latest superseding reports.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
const {getTargetData, logsPath, reportsPath} = require('../util');
|
|
9
|
+
const fs = require('fs/promises');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
// FUNCTIONS
|
|
13
|
+
|
|
14
|
+
// Returns a form for deleting latest superseding reports.
|
|
15
|
+
exports.answer = async (_, search) => {
|
|
16
|
+
const searchParams = new URLSearchParams(search);
|
|
17
|
+
const authCode = searchParams?.get('authCode');
|
|
18
|
+
const jobNames = searchParams?.getAll('report');
|
|
19
|
+
// If the form has been displayed by itself after a submission and any reports are to be deleted:
|
|
20
|
+
if (jobNames?.length) {
|
|
21
|
+
// If the authorization code is valid:
|
|
22
|
+
if (authCode === process.env.AUTH_CODE) {
|
|
23
|
+
// For each report to be deleted:
|
|
24
|
+
for (const jobName of jobNames) {
|
|
25
|
+
// Delete it.
|
|
26
|
+
await fs.unlink(path.join(reportsPath, `${jobName}.json`));
|
|
27
|
+
// Delete its log.
|
|
28
|
+
await fs.unlink(path.join(logsPath, `${jobName}.json`));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Otherwise, i.e. if the authorization code is invalid:
|
|
32
|
+
else {
|
|
33
|
+
// Report the error.
|
|
34
|
+
return {
|
|
35
|
+
status: 'error',
|
|
36
|
+
error: 'Invalid authorization code'
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const reportNames = await fs.readdir(reportsPath);
|
|
41
|
+
const reportSpecs = [];
|
|
42
|
+
// For each report:
|
|
43
|
+
for (const reportName of reportNames) {
|
|
44
|
+
const [timeStamp, jobID] = reportName.slice(0, -5).split('-');
|
|
45
|
+
// Get a summary of it.
|
|
46
|
+
const reportSummary = await getTargetData(timeStamp, jobID);
|
|
47
|
+
const {issueSet, preventedTools, url} = reportSummary;
|
|
48
|
+
reportSpecs.push({
|
|
49
|
+
timeStamp,
|
|
50
|
+
jobID,
|
|
51
|
+
issueCount: issueSet.size,
|
|
52
|
+
preventionCount: preventedTools?.length ?? 0,
|
|
53
|
+
url
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
// Sort the summaries by URL and then by time stamp.
|
|
57
|
+
reportSpecs.sort((a, b) => {
|
|
58
|
+
if (a.url === b.url) {
|
|
59
|
+
return a.timeStamp.localeCompare(b.timeStamp);
|
|
60
|
+
}
|
|
61
|
+
return a.url.localeCompare(b.url);
|
|
62
|
+
});
|
|
63
|
+
const lines = [];
|
|
64
|
+
const margin = ' '.repeat(12);
|
|
65
|
+
let anyDeletable = false;
|
|
66
|
+
// For each summary:
|
|
67
|
+
reportSpecs.forEach((spec, index) => {
|
|
68
|
+
const {timeStamp, jobID, issueCount, preventionCount, url} = spec;
|
|
69
|
+
const jobName = `${timeStamp}-${jobID}`;
|
|
70
|
+
const specString = `<code>${url}</code> (<code>${jobName}</code>): preventions ${preventionCount}, issues ${issueCount}`;
|
|
71
|
+
// If its report is the latest report on a target with at least 2 reports:
|
|
72
|
+
if (reportSpecs[index - 1]?.url === url && reportSpecs[index + 1]?.url !== url) {
|
|
73
|
+
// Add a line with a deletion checkbox.
|
|
74
|
+
lines.push(
|
|
75
|
+
`${margin}<p><input type="checkbox" name="report" value="${jobName}"> ${specString}</p>`
|
|
76
|
+
);
|
|
77
|
+
anyDeletable = true;
|
|
78
|
+
}
|
|
79
|
+
// Otherwise, i.e. if its report is a superseded report or the only report on its target:
|
|
80
|
+
else {
|
|
81
|
+
// Add a line without a deletion checkbox.
|
|
82
|
+
lines.push(`${margin}<p>${specString}</p>`);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
const intro = anyDeletable
|
|
86
|
+
? 'Choose the latest superseding reports to delete.'
|
|
87
|
+
: 'Each target has only 1 report, so there are no reports to delete.';
|
|
88
|
+
const disabled = anyDeletable ? '' : ' disabled';
|
|
89
|
+
const query = {
|
|
90
|
+
reports: lines.join('\n'),
|
|
91
|
+
intro,
|
|
92
|
+
disabled
|
|
93
|
+
};
|
|
94
|
+
// Get the order form template.
|
|
95
|
+
let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
|
|
96
|
+
// Replace its placeholders.
|
|
97
|
+
Object.keys(query).forEach(param => {
|
|
98
|
+
answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
|
|
99
|
+
});
|
|
100
|
+
// Return the populated page.
|
|
101
|
+
return {
|
|
102
|
+
status: 'ok',
|
|
103
|
+
answerPage
|
|
104
|
+
};
|
|
105
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en-US">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta name="publisher" content="Jonathan Robert Pool">
|
|
7
|
+
<meta name="creator" content="Jonathan Robert Pool">
|
|
8
|
+
<meta name="keywords" content="report,accessibility,a11y">
|
|
9
|
+
<title>__target__ retest recommendation received | Kilotest</title>
|
|
10
|
+
<link rel="icon" href="/favicon.ico">
|
|
11
|
+
<link rel="stylesheet" href="/style.css">
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<main>
|
|
15
|
+
<h1><a href="/">Kilotest</a>: Retest recommendation received for __target__</h1>
|
|
16
|
+
<p>Your recommendation to retest the __target__ page has been received.</p>
|
|
17
|
+
<p>The reason for your recommendation has been recorded as <q>__why__</q>.</p>
|
|
18
|
+
<p>Thank you for your recommendation.</p>
|
|
19
|
+
<p>If a <a href="/recActionForm.html">Kilotest manager approves this recommendation</a>, a test order will be added to the queue.</p>
|
|
20
|
+
<p>In most cases, if you check Kilotest again in 1 day, you will find that the recommended test has been performed.</p>
|
|
21
|
+
</main>
|
|
22
|
+
</body>
|
|
23
|
+
</html>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
index.js
|
|
3
|
+
Answers the retest question.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
const {getLog, processRec} = require('../util');
|
|
9
|
+
|
|
10
|
+
// FUNCTIONS
|
|
11
|
+
|
|
12
|
+
// Records a retest recommendation and returns an acknowledgement page.
|
|
13
|
+
exports.answer = async (pageArgs, why) => {
|
|
14
|
+
const [timeStamp, jobID] = pageArgs.split('/');
|
|
15
|
+
const log = await getLog(timeStamp, jobID);
|
|
16
|
+
const {url, what} = log;
|
|
17
|
+
// Process the recommendation.
|
|
18
|
+
return await processRec('retest', __dirname, what, url, why);
|
|
19
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en-US">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta name="publisher" content="Jonathan Robert Pool">
|
|
7
|
+
<meta name="creator" content="Jonathan Robert Pool">
|
|
8
|
+
<meta name="keywords" content="report,accessibility,a11y">
|
|
9
|
+
<title>__target__ retest recommendation | Kilotest</title>
|
|
10
|
+
<link rel="icon" href="/favicon.ico">
|
|
11
|
+
<link rel="stylesheet" href="/style.css">
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<main>
|
|
15
|
+
<h1><a href="/">Kilotest</a>: Retest recommendation for __target__</h1>
|
|
16
|
+
<p>The __target__ page was <a href="/reportIssues.html/__timeStamp__/__jobID__">last tested</a> __ago__ ago by job <code>__jobID__</code> on __dateTime__.</p>
|
|
17
|
+
<p>To recommend a retest, submit the following form.</p>
|
|
18
|
+
<form action="/retestRec.html/__timeStamp__/__jobID__" method="post">
|
|
19
|
+
<p><label>
|
|
20
|
+
Why retest the page (answer required)?<br>
|
|
21
|
+
<input size="70" minlength="5" maxlength="100" name="why" required>
|
|
22
|
+
</label></p>
|
|
23
|
+
<p><button type="submit">Submit</button></p>
|
|
24
|
+
</form>
|
|
25
|
+
</main>
|
|
26
|
+
</body>
|
|
27
|
+
</html>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
index.js
|
|
3
|
+
Answers the retest question.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
const {getAgoString, getDateTimeString, getLog} = require('../util');
|
|
9
|
+
const fs = require('fs/promises');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
// FUNCTIONS
|
|
13
|
+
|
|
14
|
+
// Returns a retest recommendation form.
|
|
15
|
+
exports.answer = async pageArgs => {
|
|
16
|
+
const [timeStamp, jobID] = pageArgs.split('/');
|
|
17
|
+
const log = await getLog(timeStamp, jobID);
|
|
18
|
+
const query = {
|
|
19
|
+
target: log.what,
|
|
20
|
+
timeStamp,
|
|
21
|
+
jobID,
|
|
22
|
+
ago: getAgoString(timeStamp),
|
|
23
|
+
dateTime: getDateTimeString(timeStamp)
|
|
24
|
+
};
|
|
25
|
+
// Get the recommendation form template.
|
|
26
|
+
let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
|
|
27
|
+
// Replace its placeholders.
|
|
28
|
+
Object.keys(query).forEach(param => {
|
|
29
|
+
answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
|
|
30
|
+
});
|
|
31
|
+
// Return the populated page.
|
|
32
|
+
return {
|
|
33
|
+
status: 'ok',
|
|
34
|
+
answerPage
|
|
35
|
+
};
|
|
36
|
+
};
|
package/rules/index.html
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en-US">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta name="publisher" content="Jonathan Robert Pool">
|
|
7
|
+
<meta name="creator" content="Jonathan Robert Pool">
|
|
8
|
+
<meta name="keywords" content="report,accessibility,a11y">
|
|
9
|
+
<title>Rules belonging to __issue__ issue| Kilotest</title>
|
|
10
|
+
<link rel="icon" href="/favicon.ico">
|
|
11
|
+
<link rel="stylesheet" href="/style.css">
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<main>
|
|
15
|
+
<h1><a href="/">Kilotest</a>: Rules belonging to <q>__issue__</q> issue</h1>
|
|
16
|
+
<h2>About the <q>__issue__</q> issue</h2>
|
|
17
|
+
<ul>
|
|
18
|
+
<li>Why it matters: __why__</li>
|
|
19
|
+
<li>Priority: __priority__</li>
|
|
20
|
+
<li>Related WCAG standard: __wcag__</li>
|
|
21
|
+
</ul>
|
|
22
|
+
<h2>Rules</h2>
|
|
23
|
+
<ul>
|
|
24
|
+
__rules__
|
|
25
|
+
</ul>
|
|
26
|
+
</main>
|
|
27
|
+
</body>
|
|
28
|
+
</html>
|