@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,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>Classify report as experimental | 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>: Classify report as experimental</h1>
|
|
16
|
+
<p>Choose a report to classify as experimental.</p>
|
|
17
|
+
<form action="/reportHideForm.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>
|
|
24
|
+
</label></p>
|
|
25
|
+
<p><button type="submit">Submit</button></p>
|
|
26
|
+
</form>
|
|
27
|
+
</main>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/*
|
|
2
|
+
index.js
|
|
3
|
+
Serves a form for hiding 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 hiding 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 hidden:
|
|
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
|
+
// Add a hiddenness property to the log.
|
|
26
|
+
log.hidden = true;
|
|
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 not already hidden:
|
|
67
|
+
if (! hidden) {
|
|
68
|
+
const specString = `${what} (job <code>${jobID}</code> at ${timeStamp})`;
|
|
69
|
+
// Add a line with a radio button to hide 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 hiding 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,38 @@
|
|
|
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>Violations of __issue__ for __target__ | 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>: Violations of <q>__issue__</q> issue for __target__ page</h1>
|
|
16
|
+
<h2>Basics</h2>
|
|
17
|
+
<h3>About the __target__ page</h3>
|
|
18
|
+
<ul>
|
|
19
|
+
<li>URL: __urlLink__</li>
|
|
20
|
+
<li>__testInfo__</li>
|
|
21
|
+
</ul>
|
|
22
|
+
<h3>About the <q>__issue__</q> issue</h3>
|
|
23
|
+
<ul>
|
|
24
|
+
<li>Why it matters: __why__</li>
|
|
25
|
+
<li>Priority: __priority__</li>
|
|
26
|
+
<li>Related WCAG standard: __wcag__</li>
|
|
27
|
+
<li>Reported by __reporterCount__ (__reporters__)</li>
|
|
28
|
+
<li>__violatorCount__ reported</li>
|
|
29
|
+
</ul>
|
|
30
|
+
<h2>Violators</h2>
|
|
31
|
+
<p>Here are the HTML elements reported to violate rules belonging to the <q>__issue__</q> issue.</p>
|
|
32
|
+
__takeMeThere__
|
|
33
|
+
<ul class="headed">
|
|
34
|
+
__violators__
|
|
35
|
+
</ul>
|
|
36
|
+
</main>
|
|
37
|
+
</body>
|
|
38
|
+
</html>
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/*
|
|
2
|
+
index.js
|
|
3
|
+
Answers the diagnoses question.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
getPageDataStrings,
|
|
10
|
+
getPathID,
|
|
11
|
+
getReport,
|
|
12
|
+
getToolNamesString,
|
|
13
|
+
getTextFragmentHref,
|
|
14
|
+
getWCAGLink,
|
|
15
|
+
getWeightName,
|
|
16
|
+
htmlSafe,
|
|
17
|
+
isHidden,
|
|
18
|
+
makeBreakable,
|
|
19
|
+
} = require('../util');
|
|
20
|
+
const {issues} = require('testilo/procs/score/tic');
|
|
21
|
+
const fs = require('fs/promises');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
// FUNCTIONS
|
|
25
|
+
|
|
26
|
+
// Adds parameters to a query for the answer page.
|
|
27
|
+
const populateQuery = async (issueID, timeStamp, jobID, query) => {
|
|
28
|
+
// Add facts about the issue to the query.
|
|
29
|
+
query.issue = issues[issueID].summary;
|
|
30
|
+
const pageDataStrings = await getPageDataStrings(timeStamp, jobID);
|
|
31
|
+
const {what, url, urlLink, testInfo} = pageDataStrings;
|
|
32
|
+
query.target = what;
|
|
33
|
+
query.urlLink = urlLink;
|
|
34
|
+
query.testInfo = testInfo;
|
|
35
|
+
const issue = issues[issueID];
|
|
36
|
+
const {wcag, weight, why} = issue;
|
|
37
|
+
query.why = why;
|
|
38
|
+
query.priority = getWeightName(weight);
|
|
39
|
+
query.wcag = `<a href="${getWCAGLink(wcag)}">${wcag}</a>`;
|
|
40
|
+
// Initialize those whose values depend on instance inspection.
|
|
41
|
+
query.count = 0;
|
|
42
|
+
query.reporters = new Set();
|
|
43
|
+
let violators = {};
|
|
44
|
+
// Get the report.
|
|
45
|
+
const report = await getReport(timeStamp, jobID);
|
|
46
|
+
const {acts, catalog} = report;
|
|
47
|
+
const testActs = acts.filter(act => act.type === 'test');
|
|
48
|
+
// For each test act:
|
|
49
|
+
testActs.forEach(act => {
|
|
50
|
+
const {result, which} = act;
|
|
51
|
+
const issueInstances = result?.standardResult?.instances?.filter(
|
|
52
|
+
instance => instance.issueID === issueID
|
|
53
|
+
) ?? [];
|
|
54
|
+
// If the rule of any of its standard instances belongs to the issue:
|
|
55
|
+
if (issueInstances.length) {
|
|
56
|
+
query.reporters.add(which);
|
|
57
|
+
}
|
|
58
|
+
// For each standard instance whose rule belongs to the issue:
|
|
59
|
+
issueInstances.forEach(instance => {
|
|
60
|
+
const pathID = instance.pathID || '/html';
|
|
61
|
+
const catalogIndex = instance.catalogIndex || '0';
|
|
62
|
+
const tagName = catalog[catalogIndex]?.tagName
|
|
63
|
+
?? pathID?.split('/').pop().replace(/\[.+$/, '').toUpperCase()
|
|
64
|
+
?? 'HTML';
|
|
65
|
+
violators[catalogIndex] ??= {
|
|
66
|
+
pathID: getPathID(catalog, catalogIndex, pathID),
|
|
67
|
+
tagName,
|
|
68
|
+
text: catalog[catalogIndex]?.text ?? '',
|
|
69
|
+
reporters: new Set()
|
|
70
|
+
};
|
|
71
|
+
// Ensure that the tool is in the sets of reporters of the violator and the issue.
|
|
72
|
+
violators[catalogIndex].reporters.add(which);
|
|
73
|
+
query.reporters.add(which);
|
|
74
|
+
});
|
|
75
|
+
// Populate the violator count.
|
|
76
|
+
const violatorCount = Object.keys(violators).length;
|
|
77
|
+
query.violatorCount = violatorCount === 1 ? '1 violator was' : `${violatorCount} violators were`;
|
|
78
|
+
});
|
|
79
|
+
// For each violator:
|
|
80
|
+
Object.values(violators).forEach(violatorData => {
|
|
81
|
+
// Convert the set of its reporters to a string.
|
|
82
|
+
violatorData.reporters = getToolNamesString(violatorData.reporters);
|
|
83
|
+
});
|
|
84
|
+
const reporterCount = query.reporters.size;
|
|
85
|
+
query.reporterCount = reporterCount === 1 ? '1 tool' : `${reporterCount} tools`;
|
|
86
|
+
// Convert the set of issue reporters to a string.
|
|
87
|
+
query.reporters = getToolNamesString(query.reporters);
|
|
88
|
+
// Convert the violator data to an array.
|
|
89
|
+
violators = Object.entries(violators).map(entry => ({
|
|
90
|
+
catalogIndex: entry[0],
|
|
91
|
+
... entry[1]
|
|
92
|
+
}));
|
|
93
|
+
// Sort the violators in XPath order.
|
|
94
|
+
violators.sort((a, b) => a.pathID.localeCompare(b.pathID));
|
|
95
|
+
// Initialize the lines.
|
|
96
|
+
const lines = [];
|
|
97
|
+
const margin = ' '.repeat(6);
|
|
98
|
+
let takeMeAdviceNeeded = false;
|
|
99
|
+
// For each violator:
|
|
100
|
+
violators.forEach((violator, index) => {
|
|
101
|
+
const {catalogIndex, pathID, reporters, tagName, text} = violator;
|
|
102
|
+
// Add a heading to the lines.
|
|
103
|
+
lines.push(`${margin}<li><h3>Element ${catalogIndex}</h3>`);
|
|
104
|
+
lines.push(`${margin} <ul class="pseudoTopLevel">`);
|
|
105
|
+
// Add properties of the violator to the lines.
|
|
106
|
+
if (pathID) {
|
|
107
|
+
lines.push(`${margin} <li>XPath: <code>${makeBreakable(pathID)}</code></li>`);
|
|
108
|
+
}
|
|
109
|
+
if (tagName) {
|
|
110
|
+
lines.push(`${margin} <li>Tag name: <code>${tagName}</code></li>`);
|
|
111
|
+
}
|
|
112
|
+
if (text && ! ['HTML', 'HEAD', 'BODY', 'MAIN', 'NOSCRIPT'].includes(tagName)) {
|
|
113
|
+
const textString = text.split('\n').join(' … ');
|
|
114
|
+
lines.push(`${margin} <li>Text: <q>${htmlSafe(textString)}</q></li>`);
|
|
115
|
+
}
|
|
116
|
+
lines.push(`${margin} <li>Reported by ${reporters}</li>`);
|
|
117
|
+
lines.push(`${margin} </ul>`);
|
|
118
|
+
lines.push(`${margin} <ul class="nav">`);
|
|
119
|
+
if (catalogIndex) {
|
|
120
|
+
const catalogItem = catalog[catalogIndex] || {};
|
|
121
|
+
if (catalogItem.textLinkable) {
|
|
122
|
+
takeMeAdviceNeeded = true;
|
|
123
|
+
const href = getTextFragmentHref(catalogItem.text, url);
|
|
124
|
+
const label = `Take me to element ${catalogIndex} on the page (in a new tab)`;
|
|
125
|
+
const takeMeLink = `<a href="${href}" target="_blank" aria-label="${label}">Take me there</a>`;
|
|
126
|
+
lines.push(`${margin} <li>${takeMeLink}</li>`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const href
|
|
130
|
+
= `/diagnoses.html/${issueID}/${timeStamp}/${jobID}/${catalogIndex}?pathID=${pathID}`;
|
|
131
|
+
const questionString = 'What diagnoses were reported';
|
|
132
|
+
const labelString = `${questionString} for violator ${index + 1}?`;
|
|
133
|
+
lines.push(
|
|
134
|
+
`${margin} <li><a href="${href}" aria-label="${labelString}">${questionString}?</a></li>`
|
|
135
|
+
);
|
|
136
|
+
lines.push(`${margin} </ul>`);
|
|
137
|
+
lines.push(`${margin}</li>`);
|
|
138
|
+
});
|
|
139
|
+
// Add the lines to the query.
|
|
140
|
+
query.violators = lines.join('\n');
|
|
141
|
+
query.takeMeThere = '';
|
|
142
|
+
// If any lines contain text-fragment links:
|
|
143
|
+
if (takeMeAdviceNeeded) {
|
|
144
|
+
// Include advice about them in the answer.
|
|
145
|
+
query.takeMeThere = '<p><q>Take me there</q> links will open the page in a new tab and try to scroll to the element and highlight it. This does not always succeed. You can return here by closing the new tab.</p>';
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
// Returns a page answering the violators question.
|
|
149
|
+
exports.answer = async pageArgs => {
|
|
150
|
+
const [issueID, timeStamp, jobID] = pageArgs.split('/');
|
|
151
|
+
const reportIsHidden = await isHidden(timeStamp, jobID);
|
|
152
|
+
// If the report is not available:
|
|
153
|
+
if (reportIsHidden) {
|
|
154
|
+
return {
|
|
155
|
+
status: 'error',
|
|
156
|
+
message: 'Report not available'
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const query = {};
|
|
160
|
+
// Create a query to replace the placeholders.
|
|
161
|
+
await populateQuery(issueID, timeStamp, jobID, query);
|
|
162
|
+
// If the test specifications are valid:
|
|
163
|
+
if (query.testInfo) {
|
|
164
|
+
// Get the template.
|
|
165
|
+
let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
|
|
166
|
+
// Replace its placeholders.
|
|
167
|
+
Object.keys(query).forEach(param => {
|
|
168
|
+
answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
|
|
169
|
+
});
|
|
170
|
+
// Return the populated page.
|
|
171
|
+
return {
|
|
172
|
+
status: 'ok',
|
|
173
|
+
answerPage
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
// Otherwise, report this.
|
|
177
|
+
return {
|
|
178
|
+
status: 'error',
|
|
179
|
+
error: 'Invalid report specification'
|
|
180
|
+
};
|
|
181
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
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>Issues reported for __target__ | 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>: Issues reported for __target__</h1>
|
|
16
|
+
<h2>About the __target__ page</h2>
|
|
17
|
+
<ul>
|
|
18
|
+
<li>URL: __urlLink__</li>
|
|
19
|
+
<li>__testInfo__</li>
|
|
20
|
+
__preventions__
|
|
21
|
+
</ul>
|
|
22
|
+
<h2>Issues reported</h2>
|
|
23
|
+
<h3>Summary</h3>
|
|
24
|
+
<p>__reporterCount__ reported issues (__reporters__)</p>
|
|
25
|
+
<p>__issueCount__ reported (__highestCount__ highest, __highCount__ high, __lowCount__ low, __lowestCount__ lowest priority)</p>
|
|
26
|
+
<p>__violatorCount__ reported</p>
|
|
27
|
+
<p>Download the <a href="/fullReport.html/__timeStamp__/__jobID__">full technical report</a></p>
|
|
28
|
+
<h3>Details</h3>
|
|
29
|
+
<details>
|
|
30
|
+
<summary>Highest priority: __highestCount__</summary>
|
|
31
|
+
__highestDetails__
|
|
32
|
+
</details>
|
|
33
|
+
<details>
|
|
34
|
+
<summary>High priority: __highCount__</summary>
|
|
35
|
+
__highDetails__
|
|
36
|
+
</details>
|
|
37
|
+
<details>
|
|
38
|
+
<summary>Low priority: __lowCount__</summary>
|
|
39
|
+
__lowDetails__
|
|
40
|
+
</details>
|
|
41
|
+
<details>
|
|
42
|
+
<summary>Lowest priority: __lowestCount__</summary>
|
|
43
|
+
__lowestDetails__
|
|
44
|
+
</details>
|
|
45
|
+
</main>
|
|
46
|
+
</body>
|
|
47
|
+
</html>
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/*
|
|
2
|
+
index.js
|
|
3
|
+
Answers the report-issues question.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
getPageDataStrings,
|
|
10
|
+
getReport,
|
|
11
|
+
getToolNamesString,
|
|
12
|
+
getWCAGLink,
|
|
13
|
+
getWeightName,
|
|
14
|
+
htmlSafe,
|
|
15
|
+
isHidden,
|
|
16
|
+
isValidReport,
|
|
17
|
+
objectSort,
|
|
18
|
+
tools
|
|
19
|
+
} = require('../util');
|
|
20
|
+
const {issues} = require('testilo/procs/score/tic');
|
|
21
|
+
const fs = require('fs/promises');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
// FUNCTIONS
|
|
25
|
+
|
|
26
|
+
// Gets data on the issues reported in a report.
|
|
27
|
+
const getIssuesData = async (timeStamp, jobID) => {
|
|
28
|
+
// Get the report.
|
|
29
|
+
const report = await getReport(timeStamp, jobID);
|
|
30
|
+
// If it is valid:
|
|
31
|
+
if (typeof report === 'object' && isValidReport(report)) {
|
|
32
|
+
const issuesData = {
|
|
33
|
+
reporters: new Set(),
|
|
34
|
+
reporterCount: 0,
|
|
35
|
+
reportersString: '',
|
|
36
|
+
violators: new Set(),
|
|
37
|
+
violatorCount: 0,
|
|
38
|
+
preventions: {},
|
|
39
|
+
issuesObject: {},
|
|
40
|
+
issueCount: 0,
|
|
41
|
+
issues: []
|
|
42
|
+
};
|
|
43
|
+
const {issuesObject, reporters, violators} = issuesData;
|
|
44
|
+
// For each act in it:
|
|
45
|
+
report.acts.forEach(act => {
|
|
46
|
+
// If it is a test act:
|
|
47
|
+
if (act.type === 'test') {
|
|
48
|
+
const {result, which} = act;
|
|
49
|
+
const instances = result?.standardResult?.instances ?? [];
|
|
50
|
+
// For each of its standard instances:
|
|
51
|
+
instances.forEach(instance => {
|
|
52
|
+
const {issueID} = instance;
|
|
53
|
+
// If the instance has a non-ignorable classified issue:
|
|
54
|
+
if (issueID && issues[issueID] && issueID !== 'ignorable') {
|
|
55
|
+
// Ensure that the issues data include data on the issue.
|
|
56
|
+
issuesObject[issueID] ??= {
|
|
57
|
+
issueID,
|
|
58
|
+
weight: issues[issueID].weight ?? 0,
|
|
59
|
+
reporters: new Set(),
|
|
60
|
+
reporterCount: 0,
|
|
61
|
+
reportersString: '',
|
|
62
|
+
violators: new Set(),
|
|
63
|
+
violatorCount: 0
|
|
64
|
+
};
|
|
65
|
+
// Ensure that the tool is in the issues data.
|
|
66
|
+
reporters.add(which);
|
|
67
|
+
// Ensure that it is in the issue data.
|
|
68
|
+
issuesObject[issueID].reporters.add(which);
|
|
69
|
+
const {catalogIndex} = instance;
|
|
70
|
+
// If the instance has a catalog index:
|
|
71
|
+
if (catalogIndex) {
|
|
72
|
+
// Ensure that the violator is in the issues data.
|
|
73
|
+
violators.add(catalogIndex);
|
|
74
|
+
// Ensure that it is in the issue data.
|
|
75
|
+
issuesObject[issueID].violators.add(catalogIndex);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
// Populate the unpopulated subproperties of the issues data.
|
|
82
|
+
issuesData.reporterCount = issuesData.reporters.size;
|
|
83
|
+
issuesData.reportersString = getToolNamesString(issuesData.reporters);
|
|
84
|
+
issuesData.violatorCount = issuesData.violators.size;
|
|
85
|
+
issuesData.preventions = report.jobData.preventions;
|
|
86
|
+
issuesData.issueCount = Object.keys(issuesData.issuesObject).length;
|
|
87
|
+
issuesData.issues = Object.values(issuesData.issuesObject);
|
|
88
|
+
// For each issue in the issues data:
|
|
89
|
+
issuesData.issues.forEach(issue => {
|
|
90
|
+
// Populate its unpopulated properties.
|
|
91
|
+
issue.reporterCount = issue.reporters.size;
|
|
92
|
+
issue.reportersString = getToolNamesString(issue.reporters);
|
|
93
|
+
issue.violatorCount = issue.violators.size;
|
|
94
|
+
});
|
|
95
|
+
// Sort the issues alphabetically by reporters string.
|
|
96
|
+
objectSort(issuesData.issues, 'reportersString', 'alpha');
|
|
97
|
+
// Sort the issues again in descending reporter-count order, making this the primary order.
|
|
98
|
+
objectSort(issuesData.issues, 'reporterCount', 'numericDown');
|
|
99
|
+
// Return the issues data.
|
|
100
|
+
return issuesData;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
// Adds parameters to a query for the answer page.
|
|
104
|
+
const populateQuery = async (timeStamp, jobID, query) => {
|
|
105
|
+
// Get fact descriptions for the report.
|
|
106
|
+
const pageDataStrings = await getPageDataStrings(timeStamp, jobID);
|
|
107
|
+
const {what, urlLink, testInfo} = pageDataStrings;
|
|
108
|
+
const issuesData = await getIssuesData(timeStamp, jobID);
|
|
109
|
+
// If this failed:
|
|
110
|
+
if (typeof issuesData === 'string') {
|
|
111
|
+
// Return this.
|
|
112
|
+
return issuesData;
|
|
113
|
+
}
|
|
114
|
+
const {
|
|
115
|
+
issueCount,
|
|
116
|
+
preventions,
|
|
117
|
+
reporterCount,
|
|
118
|
+
reportersString,
|
|
119
|
+
violatorCount
|
|
120
|
+
} = issuesData;
|
|
121
|
+
// Add an issue count description to the query.
|
|
122
|
+
query.issueCount = issueCount === 1 ? '1 issue was' : `${issueCount} issues were`;
|
|
123
|
+
query.reporterCount = reporterCount === 1 ? '1 tool' : `${reporterCount} tools`;
|
|
124
|
+
// Add a reporter count and list to the query.
|
|
125
|
+
query.reporters = reportersString;
|
|
126
|
+
// Add a violator count to the query.
|
|
127
|
+
query.violatorCount = violatorCount === 1 ? '1 violator was' : `${violatorCount} violators were`;
|
|
128
|
+
// Add page data to the query.
|
|
129
|
+
query.target = what;
|
|
130
|
+
query.urlLink = urlLink;
|
|
131
|
+
query.testInfo = testInfo;
|
|
132
|
+
query.timeStamp = timeStamp;
|
|
133
|
+
query.jobID = jobID;
|
|
134
|
+
const preventionStrings = [];
|
|
135
|
+
const margin = ' '.repeat(6);
|
|
136
|
+
Object.keys(preventions).forEach(preventedToolID => {
|
|
137
|
+
const toolName = tools[preventedToolID];
|
|
138
|
+
const toolNameString = `${toolName[0]} (${toolName[1]})`;
|
|
139
|
+
const causeString = htmlSafe(preventions[preventedToolID]);
|
|
140
|
+
const preventionString = `${margin}<li>Page not testable by ${toolNameString}: ${causeString}</li>`;
|
|
141
|
+
preventionStrings.push(preventionString);
|
|
142
|
+
});
|
|
143
|
+
query.preventions = preventionStrings.join('\n');
|
|
144
|
+
// For each weight:
|
|
145
|
+
[4, 3, 2, 1].forEach(weight => {
|
|
146
|
+
// Initialize data on issues having the weight.
|
|
147
|
+
const weightData = [];
|
|
148
|
+
// Initialize the lines for the weight.
|
|
149
|
+
const weightLines = [];
|
|
150
|
+
// For each issue:
|
|
151
|
+
issuesData.issues.forEach(issueData => {
|
|
152
|
+
const {
|
|
153
|
+
issueID, reporterCount, reportersString, violatorCount, weight: issueWeight
|
|
154
|
+
} = issueData;
|
|
155
|
+
// If it has the weight:
|
|
156
|
+
if (issueWeight === weight) {
|
|
157
|
+
const issue = issues[issueID];
|
|
158
|
+
const {wcag, why} = issue;
|
|
159
|
+
const wcagLink = `<a href="${getWCAGLink(wcag)}">${wcag}</a>`;
|
|
160
|
+
// Add data on it to the weight data.
|
|
161
|
+
weightData.push({
|
|
162
|
+
issueID,
|
|
163
|
+
summary: issue.summary,
|
|
164
|
+
why,
|
|
165
|
+
wcag: wcagLink,
|
|
166
|
+
reporterCount,
|
|
167
|
+
reportersString,
|
|
168
|
+
violatorCount
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
const weightName = getWeightName(weight);
|
|
173
|
+
// Add the issue count to the query.
|
|
174
|
+
query[`${weightName}Count`] = weightData.length;
|
|
175
|
+
// If any reported issues have the weight:
|
|
176
|
+
if (weightData.length) {
|
|
177
|
+
// Add the start of a list of the issues with the weight to the lines.
|
|
178
|
+
weightLines.push(`${margin}<ul class="headed">`);
|
|
179
|
+
// For each issue with the weight:
|
|
180
|
+
weightData.forEach(weightIssue => {
|
|
181
|
+
const {issueID, reporterCount, reportersString, violatorCount, wcag, why} = weightIssue;
|
|
182
|
+
// Add the start of a list item to the lines.
|
|
183
|
+
weightLines.push(`${margin} <li>`);
|
|
184
|
+
// Add a heading summarizing the issue to the lines.
|
|
185
|
+
weightLines.push(`${margin} <h5>${weightIssue.summary}</h5>`);
|
|
186
|
+
// Add the start of alist of facts about the issue to the lines.
|
|
187
|
+
weightLines.push(`${margin} <ul class="pseudoTopLevel">`);
|
|
188
|
+
// Add the issue facts to the lines.
|
|
189
|
+
weightLines.push(`${margin} <li>Why it matters: ${why}`);
|
|
190
|
+
weightLines.push(`${margin} <li>Related WCAG standard: ${wcag}`);
|
|
191
|
+
const reporterCountString = reporterCount === 1 ? '1 tool' : `${reporterCount} tools`;
|
|
192
|
+
weightLines.push(
|
|
193
|
+
`${margin} <li>Reported by ${reporterCountString} (${reportersString})</li>`
|
|
194
|
+
);
|
|
195
|
+
const violatorCountString = violatorCount === 1
|
|
196
|
+
? '1 violator was'
|
|
197
|
+
: `${violatorCount} violators were`;
|
|
198
|
+
weightLines.push(`${margin} <li>${violatorCountString} reported</li>`);
|
|
199
|
+
// Add the end of the fact list to the lines.
|
|
200
|
+
weightLines.push(`${margin} </ul>`);
|
|
201
|
+
// Add the start of a link list to the lines.
|
|
202
|
+
weightLines.push(`${margin} <ul class="nav">`);
|
|
203
|
+
const whereQuestionString = 'Where was the issue found?';
|
|
204
|
+
const labelString = `Where was the ${weightIssue.summary} issue found on the ${what} page?`;
|
|
205
|
+
const href = `href="/reportIssue.html/${issueID}/${timeStamp}/${jobID}"`;
|
|
206
|
+
const label = `aria-label="${labelString}"`;
|
|
207
|
+
const whereLink = `<a ${href} ${label}>${whereQuestionString}</a>`;
|
|
208
|
+
// Add a violations link to the lines.
|
|
209
|
+
weightLines.push(`${margin} <li>${whereLink}</li>`);
|
|
210
|
+
// Add the end of the link list to the lines.
|
|
211
|
+
weightLines.push(`${margin} </ul>`);
|
|
212
|
+
// Add the end of the list item to the lines.
|
|
213
|
+
weightLines.push(`${margin} </li>`);
|
|
214
|
+
});
|
|
215
|
+
// Add the end of the list of issues with the weight to the lines.
|
|
216
|
+
weightLines.push(`${margin}</ul>`);
|
|
217
|
+
// Add the lines documenting the issues with the weight to the query.
|
|
218
|
+
query[`${weightName}Details`] = weightLines.join('\n');
|
|
219
|
+
}
|
|
220
|
+
// Otherwise, i.e. if no reported issues have the weight:
|
|
221
|
+
else {
|
|
222
|
+
query[`${weightName}Details`] = `${margin} <p>None</p>`;
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
};
|
|
226
|
+
// Returns a page answering the target-issues question.
|
|
227
|
+
exports.answer = async pageArgs => {
|
|
228
|
+
const [timeStamp, jobID] = pageArgs.split('/');
|
|
229
|
+
const reportIsHidden = await isHidden(timeStamp, jobID);
|
|
230
|
+
// If the report is not available:
|
|
231
|
+
if (reportIsHidden) {
|
|
232
|
+
return {
|
|
233
|
+
status: 'error',
|
|
234
|
+
message: 'Report not available'
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const query = {};
|
|
238
|
+
// Create a query to replace the placeholders.
|
|
239
|
+
await populateQuery(timeStamp, jobID, query);
|
|
240
|
+
// If the report facts were obtained:
|
|
241
|
+
if (query.testInfo) {
|
|
242
|
+
// Get the template.
|
|
243
|
+
let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
|
|
244
|
+
// Replace its placeholders.
|
|
245
|
+
Object.keys(query).forEach(param => {
|
|
246
|
+
answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
|
|
247
|
+
});
|
|
248
|
+
// Return the populated page.
|
|
249
|
+
return {
|
|
250
|
+
status: 'ok',
|
|
251
|
+
answerPage
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
// Otherwise, i.e. if they were not obtained, report this.
|
|
255
|
+
return {
|
|
256
|
+
status: 'error',
|
|
257
|
+
error: 'Report processing failed'
|
|
258
|
+
};
|
|
259
|
+
};
|
|
@@ -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>Declassify report as experimental | 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>: Declassify report as experimental</h1>
|
|
16
|
+
<p>Choose a report to declassify as experimental.</p>
|
|
17
|
+
<form action="/reportUnhideForm.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>
|
|
24
|
+
</label></p>
|
|
25
|
+
<p><button type="submit">Submit</button></p>
|
|
26
|
+
</form>
|
|
27
|
+
</main>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|