@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
package/issues/index.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/*
|
|
2
|
+
index.js
|
|
3
|
+
Answers the targets question.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
const {sendAlert} = require('../alerts');
|
|
9
|
+
const {
|
|
10
|
+
annotateReport,
|
|
11
|
+
getReport,
|
|
12
|
+
getToolNamesString,
|
|
13
|
+
getLogs,
|
|
14
|
+
getWCAGLink,
|
|
15
|
+
getWeightName,
|
|
16
|
+
objectSort,
|
|
17
|
+
ruleIDs
|
|
18
|
+
} = require('../util');
|
|
19
|
+
const {issues} = require('testilo/procs/score/tic');
|
|
20
|
+
const fs = require('fs/promises');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
|
|
23
|
+
// FUNCTIONS
|
|
24
|
+
|
|
25
|
+
// Gets summary data on the issues reported in a set of reports.
|
|
26
|
+
const getIssuesSummary = async logs => {
|
|
27
|
+
// Initialize data for a summary.
|
|
28
|
+
const issuesData = {};
|
|
29
|
+
// For each log of a report to be inspected:
|
|
30
|
+
for (const log of logs) {
|
|
31
|
+
const {annotated, jobName} = log;
|
|
32
|
+
const [timeStamp, jobID] = jobName.split('-');
|
|
33
|
+
// If the corresponding report is not yet annotated:
|
|
34
|
+
if (! annotated) {
|
|
35
|
+
// Annotate it and mark it as annotated in the log.
|
|
36
|
+
await annotateReport(ruleIDs, timeStamp, jobID);
|
|
37
|
+
}
|
|
38
|
+
// Get the corresponding report.
|
|
39
|
+
const report = await getReport(timeStamp, jobID);
|
|
40
|
+
// For each act in it:
|
|
41
|
+
report.acts.forEach(act => {
|
|
42
|
+
// If it is a test act:
|
|
43
|
+
if (act.type === 'test') {
|
|
44
|
+
const {result, which} = act;
|
|
45
|
+
const instances = result?.standardResult?.instances ?? [];
|
|
46
|
+
// For each of its standard instances:
|
|
47
|
+
instances.forEach(instance => {
|
|
48
|
+
const {count, issueID} = instance;
|
|
49
|
+
// If the instance has a non-ignorable issue ID:
|
|
50
|
+
if (issueID && issueID !== 'ignorable') {
|
|
51
|
+
issuesData[issueID] ??= {
|
|
52
|
+
count: 0,
|
|
53
|
+
reporters: new Set()
|
|
54
|
+
};
|
|
55
|
+
// Increment the data with the count and reporter of the instance.
|
|
56
|
+
issuesData[issueID].count += count ?? 1;
|
|
57
|
+
issuesData[issueID].reporters.add(which);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// Initialize the summary.
|
|
64
|
+
const summary = {
|
|
65
|
+
totalCount: 0,
|
|
66
|
+
issues: []
|
|
67
|
+
};
|
|
68
|
+
// For each issue:
|
|
69
|
+
Object.entries(issuesData).forEach(([issueID, data]) => {
|
|
70
|
+
const {count, reporters} = data;
|
|
71
|
+
// If the issue is still classified:
|
|
72
|
+
if (issues[issueID]) {
|
|
73
|
+
// Increment the report violation count by the issue violation count.
|
|
74
|
+
summary.totalCount += count;
|
|
75
|
+
// Add the issue data and an initilized percentage to the summary.
|
|
76
|
+
summary.issues.push({
|
|
77
|
+
issueID,
|
|
78
|
+
weight: issues[issueID].weight,
|
|
79
|
+
count,
|
|
80
|
+
percentage: 0,
|
|
81
|
+
reporters: getToolNamesString(reporters)
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// Otherwise, i.e. if it is no longer classified:
|
|
85
|
+
else {
|
|
86
|
+
// Report this.
|
|
87
|
+
console.log(`ERROR: Annotations obsolete for issue ${issueID}; reannotate`);
|
|
88
|
+
// Notify a manager.
|
|
89
|
+
sendAlert('Annotations obsolete', `Annotations for issue ${issueID} obsolete; reannotate.`);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
// For each summarized issue:
|
|
93
|
+
summary.issues.forEach(issue => {
|
|
94
|
+
// Add its percentage to its entry.
|
|
95
|
+
issue.percentage = Math.round(100 * (issue.count / summary.totalCount));
|
|
96
|
+
});
|
|
97
|
+
// Sort the issues in descending count order.
|
|
98
|
+
objectSort(summary.issues, 'count', 'numericDown');
|
|
99
|
+
// Sort the issues in descending priority order.
|
|
100
|
+
objectSort(summary.issues, 'weight', 'numericDown');
|
|
101
|
+
// Return the summary.
|
|
102
|
+
return summary;
|
|
103
|
+
};
|
|
104
|
+
// Adds parameters to a query for the answer page.
|
|
105
|
+
const populateQuery = async query => {
|
|
106
|
+
// Get the logs of the latest reports on the tested targets.
|
|
107
|
+
const targetLogs = (await getLogs()).filter(log => ! log.superseded);
|
|
108
|
+
// Get summary data on the issues.
|
|
109
|
+
const issuesSummary = await getIssuesSummary(targetLogs);
|
|
110
|
+
// Initialize the lines.
|
|
111
|
+
const lines = [];
|
|
112
|
+
const margin = ' '.repeat(6);
|
|
113
|
+
// For each weight:
|
|
114
|
+
[4, 3, 2, 1].forEach(weight => {
|
|
115
|
+
// Add a heading to the lines.
|
|
116
|
+
lines.push(`${margin}<h2>${getWeightName(weight)} priority</h2>`);
|
|
117
|
+
const reportedIssues = issuesSummary.issues;
|
|
118
|
+
lines.push(`${margin}<ul>`);
|
|
119
|
+
let existsIssue = false;
|
|
120
|
+
// For each reported issue:
|
|
121
|
+
reportedIssues.forEach(reportedIssue => {
|
|
122
|
+
const {issueID, percentage, reporters} = reportedIssue;
|
|
123
|
+
// If it has the weight and its percentage is at least 2:
|
|
124
|
+
if (reportedIssue.weight === weight && percentage >= 2) {
|
|
125
|
+
existsIssue = true;
|
|
126
|
+
// Get the data on it from the issue classification.
|
|
127
|
+
const issue = issues[issueID];
|
|
128
|
+
const {summary, wcag, why} = issue;
|
|
129
|
+
const wcagLink = `<a href="${getWCAGLink(wcag)}">${wcag}</a>`;
|
|
130
|
+
// Add a description of it to the lines.
|
|
131
|
+
lines.push(`${margin} <li>${summary}`);
|
|
132
|
+
lines.push(`${margin} <ul>`);
|
|
133
|
+
lines.push(`${margin} <li>Why it matters: ${why}`);
|
|
134
|
+
lines.push(`${margin} <li>Related WCAG standard: ${wcagLink}`);
|
|
135
|
+
lines.push(`${margin} <li>Share of violations: ${percentage}%</li>`);
|
|
136
|
+
lines.push(`${margin} <li>Violations reported by ${reporters}</li>`);
|
|
137
|
+
lines.push(`${margin} </ul>`);
|
|
138
|
+
lines.push(`${margin} <ul class="nav">`);
|
|
139
|
+
const linkText = 'What rules belong to this issue?';
|
|
140
|
+
const label = `What rules belong to the <q>${summary}</q> issue?`;
|
|
141
|
+
const href = `/rules.html/${issueID}`;
|
|
142
|
+
lines.push(
|
|
143
|
+
`${margin} <li><a href="${href}" aria-label="${label}">${linkText}</a></li>`
|
|
144
|
+
);
|
|
145
|
+
lines.push(`${margin} </ul>`);
|
|
146
|
+
lines.push(`${margin} </li>`);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
lines.push(`${margin}</ul>`);
|
|
150
|
+
if (! existsIssue) {
|
|
151
|
+
lines.push(`${margin}<p>No issues with this priority.</p>`);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
// Add the lines to the query.
|
|
155
|
+
query.issues = lines.join('\n');
|
|
156
|
+
};
|
|
157
|
+
// Returns a page answering the issues question.
|
|
158
|
+
exports.answer = async () => {
|
|
159
|
+
const query = {};
|
|
160
|
+
// Create a query to replace placeholders.
|
|
161
|
+
await populateQuery(query);
|
|
162
|
+
// Get the template.
|
|
163
|
+
let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
|
|
164
|
+
// Replace its placeholders.
|
|
165
|
+
Object.keys(query).forEach(param => {
|
|
166
|
+
answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
|
|
167
|
+
});
|
|
168
|
+
// Return the populated page.
|
|
169
|
+
return {
|
|
170
|
+
status: 'ok',
|
|
171
|
+
answerPage
|
|
172
|
+
};
|
|
173
|
+
};
|
package/job.json
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "",
|
|
3
|
+
"what": "Kilotest job",
|
|
4
|
+
"strict": false,
|
|
5
|
+
"standard": "only",
|
|
6
|
+
"observe": false,
|
|
7
|
+
"device": {
|
|
8
|
+
"id": "default",
|
|
9
|
+
"windowOptions": {
|
|
10
|
+
"reducedMotion": "no-preference"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"browserID": "chromium",
|
|
14
|
+
"creationTimeStamp": "",
|
|
15
|
+
"executionTimeStamp": "",
|
|
16
|
+
"target": {
|
|
17
|
+
"what": "",
|
|
18
|
+
"url": ""
|
|
19
|
+
},
|
|
20
|
+
"sources": {
|
|
21
|
+
"application": "Kilotest"
|
|
22
|
+
},
|
|
23
|
+
"acts": [
|
|
24
|
+
{
|
|
25
|
+
"type": "test",
|
|
26
|
+
"launch": {},
|
|
27
|
+
"which": "alfa",
|
|
28
|
+
"what": "Alfa"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"type": "test",
|
|
32
|
+
"launch": {},
|
|
33
|
+
"which": "aslint",
|
|
34
|
+
"what": "ASLint"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"type": "test",
|
|
38
|
+
"launch": {},
|
|
39
|
+
"which": "axe",
|
|
40
|
+
"what": "Axe by Deque",
|
|
41
|
+
"detailLevel": 2
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"type": "test",
|
|
45
|
+
"launch": {},
|
|
46
|
+
"which": "ed11y",
|
|
47
|
+
"what": "Editoria11y by Princeton University"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"type": "test",
|
|
51
|
+
"launch": {},
|
|
52
|
+
"which": "htmlcs",
|
|
53
|
+
"what": "HTML CodeSniffer by Squiz Labs"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"type": "test",
|
|
57
|
+
"launch": {},
|
|
58
|
+
"which": "ibm",
|
|
59
|
+
"what": "Accessibility Checker by IBM",
|
|
60
|
+
"withItems": true,
|
|
61
|
+
"withNewContent": false
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"type": "test",
|
|
65
|
+
"launch": {},
|
|
66
|
+
"which": "nuVal",
|
|
67
|
+
"what": "Nu Html Checker API by World Wide Web Consortium",
|
|
68
|
+
"withSource": false
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"type": "test",
|
|
72
|
+
"launch": {},
|
|
73
|
+
"which": "nuVnu",
|
|
74
|
+
"what": "Nu Html Checker by World Wide Web Consortium",
|
|
75
|
+
"withSource": false
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"type": "test",
|
|
79
|
+
"launch": {},
|
|
80
|
+
"which": "qualWeb",
|
|
81
|
+
"what": "QualWeb by University of Lisbon",
|
|
82
|
+
"withNewContent": false
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"type": "test",
|
|
86
|
+
"launch": {},
|
|
87
|
+
"which": "testaro",
|
|
88
|
+
"what": "Testaro by CVS Health",
|
|
89
|
+
"withItems": true,
|
|
90
|
+
"stopOnFail": false
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"type": "test",
|
|
94
|
+
"launch": {},
|
|
95
|
+
"which": "wave",
|
|
96
|
+
"what": "WAVE by Utah State University",
|
|
97
|
+
"reportType": 4
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
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>Management | 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> management</h1>
|
|
16
|
+
<p>Kilotest managers can:</p>
|
|
17
|
+
<ul class="nav">
|
|
18
|
+
<li><a href="/recActionForm.html">Approve or reject a test recommendation</a></li>
|
|
19
|
+
<li><a href="/reannotateForm.html">
|
|
20
|
+
Reannotate reports to incorporate newly classified and reclassified rules
|
|
21
|
+
</a></li>
|
|
22
|
+
<li><a href="/reportsPruneForm.html">Delete superseded reports</a></li>
|
|
23
|
+
<li><a href="/reportsRewindForm.html">Delete latest superseding reports</a></li>
|
|
24
|
+
<li><a href="/reportsExpungeForm.html">Delete sole reports</a></li>
|
|
25
|
+
<li><a href="/reportHideForm.html">Classify a report as experimental</a></li>
|
|
26
|
+
<li><a href="/reportUnhideForm.html">Declassify a report as experimental</a></li>
|
|
27
|
+
<li><a href="/ai0BalanceForm.html">Record the current balance of AI service 0</a></li>
|
|
28
|
+
<li><a href="/wcagRenewForm.html">Renew the map of WCAG numeric to text identifiers</a></li>
|
|
29
|
+
</ul>
|
|
30
|
+
</main>
|
|
31
|
+
</body>
|
|
32
|
+
</html>
|
package/manage/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*
|
|
2
|
+
index.js
|
|
3
|
+
Answers the managers question.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
const fs = require('fs/promises');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
// FUNCTIONS
|
|
12
|
+
|
|
13
|
+
// Returns the answer page.
|
|
14
|
+
exports.answer = async () => {
|
|
15
|
+
// Get the answer page.
|
|
16
|
+
let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
|
|
17
|
+
// Return it.
|
|
18
|
+
return {
|
|
19
|
+
status: 'ok',
|
|
20
|
+
answerPage
|
|
21
|
+
};
|
|
22
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jrpool/kilotest",
|
|
3
|
+
"version": "24.0.4",
|
|
4
|
+
"description": "An ensemble testing service with a focus on accessibility",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "node index",
|
|
8
|
+
"lint": "npx eslint",
|
|
9
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/jrpool/kilotest"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"accessibility",
|
|
17
|
+
"a11y",
|
|
18
|
+
"testing"
|
|
19
|
+
],
|
|
20
|
+
"author": "Jonathan Robert Pool <pool@jpdev.pro>",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/jrpool/kilotest/issues"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/jrpool/kilotest",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"dotenv": "*",
|
|
28
|
+
"testilo": "*"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@eslint/css": "^1.0.0",
|
|
32
|
+
"@eslint/js": "^10.0.1",
|
|
33
|
+
"@eslint/json": "^1.1.0",
|
|
34
|
+
"@eslint/markdown": "^7.5.1",
|
|
35
|
+
"eslint": "^10.0.3",
|
|
36
|
+
"globals": "^17.4.0"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/pm2.config.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
apps: [{
|
|
3
|
+
name: 'kilotest',
|
|
4
|
+
script: 'index.js',
|
|
5
|
+
instances: 1,
|
|
6
|
+
autorestart: true,
|
|
7
|
+
watch: false,
|
|
8
|
+
max_memory_restart: '500M',
|
|
9
|
+
env: {
|
|
10
|
+
NODE_ENV: 'production',
|
|
11
|
+
BASE_PATH: '/',
|
|
12
|
+
DEMO_SSE_DELAY_MS: '100'
|
|
13
|
+
}
|
|
14
|
+
}]
|
|
15
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
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>Reannotation order | 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>: Reannotation order</h1>
|
|
16
|
+
<p>The latest report on each target has been reannotated.</p>
|
|
17
|
+
</main>
|
|
18
|
+
</body>
|
|
19
|
+
</html>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/*
|
|
2
|
+
index.js
|
|
3
|
+
Implements a reannotation order, i.e. an order to update the issue IDs of the standard instances of the latest reports on all tested targets.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
const {annotateReport, getLogs, ruleIDs} = require('../util');
|
|
9
|
+
const fs = require('fs/promises');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
// FUNCTIONS
|
|
13
|
+
|
|
14
|
+
// Implements a reannotation order and returns an acknowledgement page.
|
|
15
|
+
exports.answer = async authCode => {
|
|
16
|
+
// If the authorization code is valid:
|
|
17
|
+
if (authCode === process.env.AUTH_CODE) {
|
|
18
|
+
// Get the logs of the latest reports per target.
|
|
19
|
+
const targetsData = (await getLogs()).filter(log => ! log.superseded);
|
|
20
|
+
// For each report:
|
|
21
|
+
for (const targetData of targetsData) {
|
|
22
|
+
const [timeStamp, jobID] = targetData.jobName.split('-');
|
|
23
|
+
// Reannotate it.
|
|
24
|
+
await annotateReport(ruleIDs, timeStamp, jobID);
|
|
25
|
+
}
|
|
26
|
+
// Get the answer page.
|
|
27
|
+
let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
|
|
28
|
+
// Return it.
|
|
29
|
+
return {
|
|
30
|
+
status: 'ok',
|
|
31
|
+
answerPage
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// Otherwise, i.e. if the authorization code is invalid, return an error page.
|
|
35
|
+
return {
|
|
36
|
+
status: 'error',
|
|
37
|
+
error: 'Invalid authorization code'
|
|
38
|
+
};
|
|
39
|
+
};
|
|
@@ -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>Reannotation order | 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>: Reannotation order</h1>
|
|
16
|
+
<p>These are the unclassified rules whose violations have been reported. Only the most recent report on each target is inspected.</p>
|
|
17
|
+
<h2>Still unclassified</h2>
|
|
18
|
+
<ul>
|
|
19
|
+
__stillUnclassified__
|
|
20
|
+
</ul>
|
|
21
|
+
<h2>Reclassified</h2>
|
|
22
|
+
<ul>
|
|
23
|
+
__reClassified__
|
|
24
|
+
</ul>
|
|
25
|
+
<p>__how__</p>
|
|
26
|
+
__reannotateForm__
|
|
27
|
+
</main>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/*
|
|
2
|
+
index.js
|
|
3
|
+
Discloses unclassified rules.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
const {getIssue, getReport, getLogs, ruleIDs} = require('../util');
|
|
9
|
+
const fs = require('fs/promises');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
// FUNCTIONS
|
|
13
|
+
|
|
14
|
+
// Adds parameters to a query for the answer page.
|
|
15
|
+
const populateQuery = async query => {
|
|
16
|
+
const targetLogs = (await getLogs()).filter(log => ! log.superseded);
|
|
17
|
+
const stillUnclassified = {};
|
|
18
|
+
const reClassified = {};
|
|
19
|
+
// For each target:
|
|
20
|
+
for (const targetLog of targetLogs) {
|
|
21
|
+
const {jobName} = targetLog;
|
|
22
|
+
// Get the latest report on it.
|
|
23
|
+
const report = await getReport(... jobName.split('-'));
|
|
24
|
+
const {acts} = report;
|
|
25
|
+
// For each act in the report:
|
|
26
|
+
acts.forEach(act => {
|
|
27
|
+
const {result, type, which} = act;
|
|
28
|
+
// If it is a test act with standard instances:
|
|
29
|
+
if (type === 'test' && result?.standardResult?.instances?.length) {
|
|
30
|
+
// For each standard instance:
|
|
31
|
+
result.standardResult.instances.forEach(instance => {
|
|
32
|
+
const {ruleID} = instance;
|
|
33
|
+
// Get the issue ID of the rule, or null if none.
|
|
34
|
+
const issueID = getIssue(ruleIDs, which, ruleID);
|
|
35
|
+
// If the issue ID of the instance differs from that of the rule:
|
|
36
|
+
if ((instance.issueID || null) !== issueID) {
|
|
37
|
+
// Add the rule and the report to the rules with changed issue IDs.
|
|
38
|
+
reClassified[which] ??= {};
|
|
39
|
+
reClassified[which][ruleID] ??= new Set();
|
|
40
|
+
reClassified[which][ruleID].add(jobName);
|
|
41
|
+
}
|
|
42
|
+
// Otherwise, if the instance and the rule both have no issue ID:
|
|
43
|
+
else if (! issueID){
|
|
44
|
+
// Add the rule and the report to the rules that are still unclassified.
|
|
45
|
+
stillUnclassified[which] ??= {};
|
|
46
|
+
stillUnclassified[which][ruleID] ??= new Set();
|
|
47
|
+
stillUnclassified[which][ruleID].add(jobName);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
const stillUnclassifiedLines = [];
|
|
54
|
+
const reClassifiedLines = [];
|
|
55
|
+
const margin = ' '.repeat(6);
|
|
56
|
+
// For each tool reporting any violations of still unclassified rules:
|
|
57
|
+
Object.keys(stillUnclassified).forEach(toolID => {
|
|
58
|
+
// For each such rule:
|
|
59
|
+
Object.keys(stillUnclassified[toolID]).forEach(ruleID => {
|
|
60
|
+
const reportIDs = Array.from(stillUnclassified[toolID][ruleID]);
|
|
61
|
+
// Add a line to the lines on the rule.
|
|
62
|
+
stillUnclassifiedLines.push(
|
|
63
|
+
`${margin}<li>${toolID}: ${ruleID} (${reportIDs.join(', ')})</li>`
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
// Add the lines to the query.
|
|
68
|
+
query.stillUnclassified = stillUnclassifiedLines.join('\n');
|
|
69
|
+
// For each tool reporting any discrepancies in rule classification:
|
|
70
|
+
Object.keys(reClassified).forEach(toolID => {
|
|
71
|
+
// For each such rule:
|
|
72
|
+
Object.keys(reClassified[toolID]).forEach(ruleID => {
|
|
73
|
+
const reportIDs = Array.from(reClassified[toolID][ruleID]);
|
|
74
|
+
// Add a line to the lines on the rule.
|
|
75
|
+
reClassifiedLines.push(
|
|
76
|
+
`${margin}<li>${toolID}: ${ruleID} (${reportIDs.join(', ')})</li>`
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
// Add the lines to the query.
|
|
81
|
+
query.reClassified = reClassifiedLines.join('\n');
|
|
82
|
+
if (reClassifiedLines.length) {
|
|
83
|
+
query.how = 'Each <q>reclassified</q> rule indicates that report annotations are out of date. To update them, submit your authorization code.';
|
|
84
|
+
const formLines = [];
|
|
85
|
+
formLines.push(`${margin}<form action="/reannotate.html" method="post">`);
|
|
86
|
+
formLines.push(
|
|
87
|
+
`${margin} <p><label>Authorization code: <input size="3" minLength="3" maxlength="3" name="authCode" required></label></p>`
|
|
88
|
+
);
|
|
89
|
+
formLines.push(`${margin} <p><button type="submit">Reannotate</button></p>`);
|
|
90
|
+
formLines.push(`${margin}</form>`);
|
|
91
|
+
query.reannotateForm = formLines.join('\n');
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
query.how = 'No violated rules have been classified or reclassified after being reported, so reannotation of the reports is not necessary.';
|
|
95
|
+
query.reannotateForm = '';
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
// Returns a page disclosing newly classified rules and a form to reannotate reports.
|
|
99
|
+
exports.answer = async () => {
|
|
100
|
+
const query = {};
|
|
101
|
+
// Create a query to replace placeholders.
|
|
102
|
+
await populateQuery(query);
|
|
103
|
+
// Get the template.
|
|
104
|
+
let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
|
|
105
|
+
// Replace its placeholders.
|
|
106
|
+
Object.keys(query).forEach(param => {
|
|
107
|
+
answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
|
|
108
|
+
});
|
|
109
|
+
// Return the populated page.
|
|
110
|
+
return {
|
|
111
|
+
status: 'ok',
|
|
112
|
+
answerPage
|
|
113
|
+
};
|
|
114
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
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>Recommendation approval or rejection | 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>: Recommendation approval or rejection</h1>
|
|
16
|
+
<p>To approve a test or retest recommendation, check its recommended page name.</p>
|
|
17
|
+
<p>To approve testing a page that is not yet recommended, first <a href="targets.html">recommend it</a>.</p>
|
|
18
|
+
<p>To reject all recommendations for a page, check the radio button of its URL.</p>
|
|
19
|
+
<form action="/recAction.html" method="post">
|
|
20
|
+
<fieldset>
|
|
21
|
+
<legend>Recommendations</legend>
|
|
22
|
+
__recs__
|
|
23
|
+
<p>__noRecs__</p>
|
|
24
|
+
</fieldset>
|
|
25
|
+
<p><label>
|
|
26
|
+
Authorization code:
|
|
27
|
+
<input size="3" minLength="3" maxlength="3" name="authCode" required__disabled__>
|
|
28
|
+
</label></p>
|
|
29
|
+
<p><button type="submit"__disabled__>Submit</button></p>
|
|
30
|
+
</form>
|
|
31
|
+
</main>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/*
|
|
2
|
+
index.js
|
|
3
|
+
Serves a form for ordering a recommended test.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
const {getRecs} = require('../util');
|
|
9
|
+
const fs = require('fs/promises');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
// FUNCTIONS
|
|
13
|
+
|
|
14
|
+
// Returns a test order form.
|
|
15
|
+
exports.answer = async () => {
|
|
16
|
+
const recs = await getRecs();
|
|
17
|
+
const urls = Object.keys(recs);
|
|
18
|
+
const margin = ' '.repeat(12);
|
|
19
|
+
const lines = [];
|
|
20
|
+
// For each page with any recommendations:
|
|
21
|
+
urls.forEach(url => {
|
|
22
|
+
// Add a radio button and its URL to the lines.
|
|
23
|
+
lines.push(`${margin}<h2><input type="radio" name="target" value="${url}" required> ${url}</h2>`);
|
|
24
|
+
// Get the recommended target names for the page.
|
|
25
|
+
const targetNames = new Set(recs[url].map(rec => rec.what));
|
|
26
|
+
// For each recommended target name:
|
|
27
|
+
targetNames.forEach(what => {
|
|
28
|
+
const radio = `<input type="radio" name="target" value="${url}\t${what}" required>`;
|
|
29
|
+
// Add a radio button and the recommended page name to the lines.
|
|
30
|
+
lines.push(`${margin} <p>${radio} ${what}</p>`);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
const query = {
|
|
34
|
+
recs: lines.join('\n'),
|
|
35
|
+
noRecs: urls.length ? '' : 'No recommendations exist now.',
|
|
36
|
+
disabled: urls.length ? '' : ' disabled'
|
|
37
|
+
};
|
|
38
|
+
// Get the order form template.
|
|
39
|
+
let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
|
|
40
|
+
// Replace its placeholders.
|
|
41
|
+
Object.keys(query).forEach(param => {
|
|
42
|
+
answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
|
|
43
|
+
});
|
|
44
|
+
// Return the populated page.
|
|
45
|
+
return {
|
|
46
|
+
status: 'ok',
|
|
47
|
+
answerPage
|
|
48
|
+
};
|
|
49
|
+
};
|