@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.
Files changed (69) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/DEVELOPMENT.md +5860 -0
  3. package/LICENSE +21 -0
  4. package/README.md +44 -0
  5. package/SERVICE.md +268 -0
  6. package/aceconfig.js +28 -0
  7. package/ai0BalanceForm/index.html +30 -0
  8. package/ai0BalanceForm/index.js +79 -0
  9. package/alerts.js +73 -0
  10. package/diagnoses/index.html +46 -0
  11. package/diagnoses/index.js +140 -0
  12. package/env.example +21 -0
  13. package/env.testaro +17 -0
  14. package/error.html +18 -0
  15. package/eslint.config.mjs +53 -0
  16. package/favicon.ico +0 -0
  17. package/index.html +39 -0
  18. package/index.js +639 -0
  19. package/issues/index.html +20 -0
  20. package/issues/index.js +173 -0
  21. package/job.json +100 -0
  22. package/manage/index.html +32 -0
  23. package/manage/index.js +22 -0
  24. package/package.json +38 -0
  25. package/pm2.config.js +15 -0
  26. package/reannotate/index.html +19 -0
  27. package/reannotate/index.js +39 -0
  28. package/reannotateForm/index.html +29 -0
  29. package/reannotateForm/index.js +114 -0
  30. package/recActionForm/index.html +33 -0
  31. package/recActionForm/index.js +49 -0
  32. package/reportHideForm/index.html +29 -0
  33. package/reportHideForm/index.js +89 -0
  34. package/reportIssue/index.html +38 -0
  35. package/reportIssue/index.js +181 -0
  36. package/reportIssues/index.html +47 -0
  37. package/reportIssues/index.js +259 -0
  38. package/reportUnhideForm/index.html +29 -0
  39. package/reportUnhideForm/index.js +89 -0
  40. package/reportsExpungeForm/index.html +29 -0
  41. package/reportsExpungeForm/index.js +105 -0
  42. package/reportsPruneForm/index.html +29 -0
  43. package/reportsPruneForm/index.js +105 -0
  44. package/reportsRewindForm/index.html +29 -0
  45. package/reportsRewindForm/index.js +105 -0
  46. package/retestRec/index.html +23 -0
  47. package/retestRec/index.js +19 -0
  48. package/retestRecForm/index.html +27 -0
  49. package/retestRecForm/index.js +36 -0
  50. package/rules/index.html +28 -0
  51. package/rules/index.js +71 -0
  52. package/style.css +196 -0
  53. package/targets/index.html +37 -0
  54. package/targets/index.js +170 -0
  55. package/testOrder/index.html +23 -0
  56. package/testOrder/index.js +62 -0
  57. package/testRec/index.html +23 -0
  58. package/testRec/index.js +25 -0
  59. package/testRecForm/index.html +34 -0
  60. package/testRecForm/index.js +22 -0
  61. package/tutorial/images/newsletter-form.png +0 -0
  62. package/tutorial/index.html +796 -0
  63. package/tutorial/index.js +53 -0
  64. package/util.js +686 -0
  65. package/wcagMap.json +102 -0
  66. package/wcagRenew/index.html +19 -0
  67. package/wcagRenew/index.js +70 -0
  68. package/wcagRenewForm/index.html +25 -0
  69. package/wcagRenewForm/index.js +22 -0
@@ -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>
@@ -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
+ };