@jrpool/kilotest 38.1.0 → 39.0.1
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/.markdownlint-cli2.jsonc +12 -0
- package/.vscode/settings.json +3 -0
- package/CLAUDE.md +5 -0
- package/README.md +6 -4
- package/ai0BalanceForm/index.html +1 -1
- package/alerts.js +3 -1
- package/api/getReport.js +66 -0
- package/api/listDiagnoses.js +157 -0
- package/api/listIssues.js +225 -0
- package/api/listReports.js +87 -0
- package/api/listViolators.js +164 -0
- package/api/requestFeature.js +56 -0
- package/api/requestRetest.js +102 -0
- package/api/requestTest.js +106 -0
- package/api/routes.js +97 -0
- package/api/schemas.js +297 -0
- package/api/testSuite.js +294 -0
- package/api/util.js +149 -0
- package/diagnoses/index.js +2 -2
- package/docs/{AI-TOOL.md → AI-TOOLS.md} +54 -26
- package/docs/SERVICE.md +4 -1
- package/docs/decisions/001-api-design.md +59 -49
- package/eslint.config.mjs +13 -3
- package/index.html +1 -1
- package/index.js +202 -188
- package/issues/index.js +13 -21
- package/jsconfig.json +8 -0
- package/llms-full.txt +2 -2
- package/llms.txt +16 -11
- package/mcp.js +129 -65
- package/openapi.yaml +1234 -572
- package/package.json +18 -11
- package/reannotate/index.js +26 -15
- package/reannotateForm/index.js +15 -13
- package/redocly.yaml +4 -0
- package/reportHideForm/index.html +1 -1
- package/reportHideForm/index.js +24 -38
- package/reportIssue/index.js +3 -3
- package/reportIssues/index.js +128 -1
- package/reportUnhideForm/index.js +36 -44
- package/reportsExpungeForm/index.js +22 -16
- package/reportsPruneForm/index.js +22 -12
- package/reportsRewindForm/index.js +17 -9
- package/retestRec/index.js +8 -4
- package/retestRecForm/index.js +18 -6
- package/scripts/generate-openapi.js +83 -0
- package/targets/index.js +18 -13
- package/testOrder/index.js +11 -8
- package/tutorial/index.html +12 -12
- package/util.js +581 -539
- package/api/reportList.js +0 -83
- package/api/reportSummary.js +0 -181
- package/reportIssue/api.js +0 -183
- package/reportIssues/util.js +0 -138
- package/researchAgent.js +0 -206
- package/target/api.js +0 -126
- package/testRecForm/api.js +0 -55
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Cascade Rules
|
|
2
|
+
|
|
3
|
+
- Ask mode: If the current mode is Ask, answer questions and provide code, diffs, or edit suggestions in code blocks or as plain text. Do not invite the user to switch to Normal or Code mode or any mode that permits writing files.
|
|
4
|
+
- Comply with the current convention to omit line breaks in long lines in `.md` files and in code comments.
|
|
5
|
+
- In conversation responses (not files), when a Markdown table cell would contain a line longer than 120 characters divided by the number of columns, insert `<br>` tags to keep displayed line lengths shorter than that maximum, since raw newlines are not valid inside table cells. If this rule makes any table cumbersome, use a non-tabular format, such as a list, instead.
|
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Kilotest
|
|
2
2
|
|
|
3
|
-
An ensemble testing service
|
|
3
|
+
An ensemble testing and reporting service for front-end web quality
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
This application uses an ensemble of 10 rule engines to test public web pages for front-end quality (i.e. accessibility, usability, and conformity
|
|
7
|
+
This application uses an ensemble of 10 rule engines to test public web pages for front-end quality (i.e. accessibility, usability, and standards conformity).
|
|
8
8
|
|
|
9
9
|
The testing paradigm employed by Kilotest is discussed in these papers:
|
|
10
10
|
|
|
@@ -12,7 +12,9 @@ The testing paradigm employed by Kilotest is discussed in these papers:
|
|
|
12
12
|
- [Testaro: Efficient Ensemble Testing for Web Accessibility](https://arxiv.org/abs/2309.10167)
|
|
13
13
|
- [Accessibility Metatesting: Comparing Nine Testing Tools](https://arxiv.org/abs/2304.07591)
|
|
14
14
|
|
|
15
|
-
Kilotest acts as a server
|
|
15
|
+
Kilotest acts as a web server for human users, an MCP server for AI platforms, and an API for programmatic access.
|
|
16
|
+
|
|
17
|
+
Testing for Kilotest is performed by one or more testing agents that obtain jobs from Kilotest, perform them, and send reports of the results back to Kilotest. Those agents are instances of the [Testaro](https://www.npmjs.com/package/testaro) package.
|
|
16
18
|
|
|
17
19
|
An active production instance of Kilotest may require multiple testing agents to handle the load, because testing one web page typically takes about 3 minutes and agents test only one page at a time.
|
|
18
20
|
|
|
@@ -45,4 +47,4 @@ See the `SERVICE.md` file for instructions on how to make Kilotest a service.
|
|
|
45
47
|
|
|
46
48
|
## Using Kilotest as an AI tool
|
|
47
49
|
|
|
48
|
-
See the `AI-
|
|
50
|
+
See the `AI-TOOLS.md` file for information about the configuration of Kilotest as a collection of AI tools.
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
<input type="number" size="5" min="0" max="99.99" step="0.01" name="newBalance" required>
|
|
22
22
|
</label></p>
|
|
23
23
|
<p><label>
|
|
24
|
-
Authorization code: <input size="3"
|
|
24
|
+
Authorization code: <input size="3" minlength="3" maxlength="3" name="authCode" required>
|
|
25
25
|
</label></p>
|
|
26
26
|
<p><button type="submit">Submit</button></p>
|
|
27
27
|
</form>
|
package/alerts.js
CHANGED
|
@@ -39,7 +39,9 @@ exports.sendAlert = (subject, body) => new Promise(resolve => {
|
|
|
39
39
|
}
|
|
40
40
|
}, res => {
|
|
41
41
|
let data = '';
|
|
42
|
-
res.on('data', chunk => {
|
|
42
|
+
res.on('data', chunk => {
|
|
43
|
+
data += chunk;
|
|
44
|
+
});
|
|
43
45
|
res.on('end', () => {
|
|
44
46
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
45
47
|
console.log(`Alert sent (${subject})`);
|
package/api/getReport.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/*
|
|
2
|
+
getReport.js
|
|
3
|
+
Returns one report.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
getResponseMetadata,
|
|
10
|
+
getToolsFacts,
|
|
11
|
+
thisHost
|
|
12
|
+
} = require('./util');
|
|
13
|
+
const {getReport, getReportStats} = require('../util');
|
|
14
|
+
|
|
15
|
+
// FUNCTIONS
|
|
16
|
+
|
|
17
|
+
// Returns the response body.
|
|
18
|
+
exports.response = async args => {
|
|
19
|
+
const [timeStamp = '', jobID = ''] = args;
|
|
20
|
+
// Initialize the response content.
|
|
21
|
+
const responseContent = {
|
|
22
|
+
'size of the report in bytes': null,
|
|
23
|
+
'full report': null
|
|
24
|
+
};
|
|
25
|
+
// Get the report size.
|
|
26
|
+
const reportStats = await getReportStats(timeStamp, jobID);
|
|
27
|
+
// If this failed:
|
|
28
|
+
if (!reportStats) {
|
|
29
|
+
// Add this to the response content.
|
|
30
|
+
responseContent['size of the report in bytes'] = 'Error: The report could not be accessed for an unknown reason.';
|
|
31
|
+
}
|
|
32
|
+
// Otherwise, i.e. if it succeeded:
|
|
33
|
+
else {
|
|
34
|
+
const {reportSize} = reportStats;
|
|
35
|
+
// Add the size to the response content.
|
|
36
|
+
responseContent['size of the report in bytes'] = reportSize;
|
|
37
|
+
// Get the report (which may be only an error message).
|
|
38
|
+
const report = await getReport(timeStamp, jobID);
|
|
39
|
+
// Add it to the response content.
|
|
40
|
+
responseContent['full report'] = report;
|
|
41
|
+
}
|
|
42
|
+
// Create a response body.
|
|
43
|
+
const body = {
|
|
44
|
+
'tool collection': getToolsFacts(),
|
|
45
|
+
'tool name': 'getReport',
|
|
46
|
+
'this request': {
|
|
47
|
+
description: 'Provide one full report in JSON format. The timeStamp and jobID parameters identify the report that I want details about. Those parameters were in the response to my earlier listIssues request.',
|
|
48
|
+
method: 'GET',
|
|
49
|
+
URL: `${thisHost}/api/getReport/${timeStamp}/${jobID}`,
|
|
50
|
+
'closest ancestor request': {
|
|
51
|
+
'tool name': 'listIssues',
|
|
52
|
+
description: 'Provide details about one report, including basics about the issues reported in it.',
|
|
53
|
+
method: 'GET',
|
|
54
|
+
URL: `${thisHost}/api/listIssues/${timeStamp}/${jobID}`
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
'URLs of similar requests for web users': {
|
|
58
|
+
'this request': `${thisHost}/fullReport.json/${timeStamp}/${jobID}`,
|
|
59
|
+
'closest ancestor request': `${thisHost}/reportIssues.html/${timeStamp}/${jobID}`
|
|
60
|
+
},
|
|
61
|
+
'response metadata': getResponseMetadata(),
|
|
62
|
+
'response content': responseContent
|
|
63
|
+
};
|
|
64
|
+
// Return it.
|
|
65
|
+
return body;
|
|
66
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/*
|
|
2
|
+
listDiagnoses.js
|
|
3
|
+
Returns details about one violator of one issue in one report and facts about all its diagnoses.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
getIssueClassification,
|
|
10
|
+
getReportBasics,
|
|
11
|
+
getResponseMetadata,
|
|
12
|
+
getToolsFacts,
|
|
13
|
+
thisHost
|
|
14
|
+
} = require('./util');
|
|
15
|
+
const {getReport} = require('../util');
|
|
16
|
+
|
|
17
|
+
// FUNCTIONS
|
|
18
|
+
|
|
19
|
+
// Returns the response body.
|
|
20
|
+
exports.response = async args => {
|
|
21
|
+
const [catalogIndex = '', issueID = '', timeStamp = '', jobID = ''] = args;
|
|
22
|
+
// Initialize the response content.
|
|
23
|
+
const responseContent = {
|
|
24
|
+
'basics about the report': null,
|
|
25
|
+
'basics about the issue': null,
|
|
26
|
+
'basics about the element': null,
|
|
27
|
+
'details about the element': null,
|
|
28
|
+
'diagnoses of how the element exhibited the issue': null
|
|
29
|
+
};
|
|
30
|
+
// Get the report.
|
|
31
|
+
const report = await getReport(timeStamp, jobID);
|
|
32
|
+
// If this failed:
|
|
33
|
+
if (report.error) {
|
|
34
|
+
// Add this to the response content.
|
|
35
|
+
responseContent['basics about the report'] = report;
|
|
36
|
+
}
|
|
37
|
+
// Otherwise, i.e. if it succeeded:
|
|
38
|
+
else {
|
|
39
|
+
// Get the basics about the report (which may be only an error message).
|
|
40
|
+
const reportBasics = await getReportBasics(timeStamp, jobID);
|
|
41
|
+
// Add them to the response content.
|
|
42
|
+
responseContent['basics about the report'] = reportBasics;
|
|
43
|
+
}
|
|
44
|
+
// Get the classification of the issue.
|
|
45
|
+
const issueClassification = issueID ? getIssueClassification(issueID) : null;
|
|
46
|
+
// If the issue is ignorable or not fully classified:
|
|
47
|
+
if (!issueClassification) {
|
|
48
|
+
// Add this to the response content.
|
|
49
|
+
responseContent['basics about the issue'] = {
|
|
50
|
+
'error': 'No information about the specified issue is available'
|
|
51
|
+
};
|
|
52
|
+
responseContent['diagnoses of how the element exhibited the issue'] = {
|
|
53
|
+
'error': 'No diagnoses of how the element exhibited the issue is available'
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Otherwise, i.e. if it is non-ignorable and fully classified:
|
|
57
|
+
else {
|
|
58
|
+
const {summary, wcag, weight, why} = issueClassification;
|
|
59
|
+
// Initialize the basics about it.
|
|
60
|
+
const issueBasics = {
|
|
61
|
+
'identifier': issueID,
|
|
62
|
+
summary,
|
|
63
|
+
'impact on a user': why,
|
|
64
|
+
'related WCAG standard': {
|
|
65
|
+
'layer': (wcag.length > 4 ? 'success criterion' : 'guideline'),
|
|
66
|
+
'identifier': wcag
|
|
67
|
+
},
|
|
68
|
+
priority: ['lowest', 'low', 'high', 'highest'][weight - 1]
|
|
69
|
+
};
|
|
70
|
+
// Add them to the response content.
|
|
71
|
+
responseContent['basics about the issue'] = issueBasics;
|
|
72
|
+
}
|
|
73
|
+
// If the report and its catalog exist:
|
|
74
|
+
if (report.catalog) {
|
|
75
|
+
const catalogItem = report.catalog[catalogIndex];
|
|
76
|
+
// If the violator is not in it:
|
|
77
|
+
if (!catalogItem) {
|
|
78
|
+
// Add this to the response content.
|
|
79
|
+
responseContent['basics about the element'] = {
|
|
80
|
+
'error': 'No information about the specified element is available'
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// Otherwise, i.e. if the violator is in the catalog:
|
|
84
|
+
else {
|
|
85
|
+
// Get basics about the violator.
|
|
86
|
+
const violatorBasics = {
|
|
87
|
+
identifier: catalogIndex,
|
|
88
|
+
'tag name': catalogItem.tagName || null,
|
|
89
|
+
'inner text': catalogItem.text ?? null
|
|
90
|
+
};
|
|
91
|
+
// Add them to the response content.
|
|
92
|
+
responseContent['basics about the element'] = violatorBasics;
|
|
93
|
+
// Get details about the violator.
|
|
94
|
+
const violatorDetails = {
|
|
95
|
+
'start tag': catalogItem.startTag ?? null,
|
|
96
|
+
'XPath': catalogItem.pathID ?? null,
|
|
97
|
+
'x, y, width, and height of bounding box': catalogItem.boxID ?? null
|
|
98
|
+
};
|
|
99
|
+
// Add them to the response content.
|
|
100
|
+
responseContent['details about the element'] = violatorDetails;
|
|
101
|
+
// If the issue is non-ignorable and fully classified:
|
|
102
|
+
if (issueClassification) {
|
|
103
|
+
// Initialize data about the diagnoses of the violation of the issue.
|
|
104
|
+
const diagnoses = [];
|
|
105
|
+
// For each act in the report:
|
|
106
|
+
report.acts.forEach(act => {
|
|
107
|
+
const {result, type, which} = act;
|
|
108
|
+
const instances = result?.standardResult?.instances || [];
|
|
109
|
+
// If the act is a test act with a specified rule engine:
|
|
110
|
+
if (type === 'test' && which) {
|
|
111
|
+
// For each standard instance of the act:
|
|
112
|
+
instances.forEach(instance => {
|
|
113
|
+
const {count, ordinalSeverity, ruleID, what} = instance;
|
|
114
|
+
// If the instance has the issue ID and the catalog index of the violator:
|
|
115
|
+
if (instance.issueID === issueID && instance.catalogIndex === catalogIndex) {
|
|
116
|
+
// Get the diagnosis.
|
|
117
|
+
const diagnosis = {
|
|
118
|
+
'identifier of the violated rule': ruleID !== what ? ruleID : null,
|
|
119
|
+
'description of the violation': what,
|
|
120
|
+
'severity of the violation on a 0-to-3 scale': ordinalSeverity,
|
|
121
|
+
'count of violations of the rule by the element': count ?? 1
|
|
122
|
+
}
|
|
123
|
+
diagnoses.push(diagnosis);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
// Add the diagnoses to the response content.
|
|
129
|
+
responseContent['diagnoses of how the element exhibited the issue'] = diagnoses;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Create a response body.
|
|
134
|
+
const body = {
|
|
135
|
+
'tool collection': getToolsFacts(),
|
|
136
|
+
'tool name': 'listDiagnoses',
|
|
137
|
+
'this request': {
|
|
138
|
+
description: 'Provide details about one element reported as exhibiting one issue in one report, including the diagnoses provided by rule engines about how the element exhibited the issue. The catalogIndex, issueID, timeStamp, and jobID parameters identify the element, issue, and report that I want details about. Those parameters were in the response to my earlier listViolators request.',
|
|
139
|
+
method: 'GET',
|
|
140
|
+
URL: `${thisHost}/api/listDiagnoses/${catalogIndex}/${issueID}/${timeStamp}/${jobID}`,
|
|
141
|
+
'closest ancestor request': {
|
|
142
|
+
'tool name': 'listViolators',
|
|
143
|
+
description: 'Provide details about one issue in one report, including basics about the elements of the tested page that were reported as exhibiting the issue.',
|
|
144
|
+
method: 'GET',
|
|
145
|
+
URL: `${thisHost}/api/listViolators/${issueID}/${timeStamp}/${jobID}`
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
'URLs of similar requests for web users': {
|
|
149
|
+
'this request': `${thisHost}/diagnoses.html/${issueID}/${timeStamp}/${jobID}/${catalogIndex}`,
|
|
150
|
+
'closest ancestor request': `${thisHost}/reportIssue.html/${issueID}/${timeStamp}/${jobID}`
|
|
151
|
+
},
|
|
152
|
+
'response metadata': getResponseMetadata(),
|
|
153
|
+
'response content': responseContent
|
|
154
|
+
}
|
|
155
|
+
// Return it.
|
|
156
|
+
return body;
|
|
157
|
+
};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/*
|
|
2
|
+
listIssues.js
|
|
3
|
+
Returns details about one report and basics about all the issues in it.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
getIssueClassification,
|
|
10
|
+
getReportBasics,
|
|
11
|
+
getResponseMetadata,
|
|
12
|
+
getRuleEngineFacts,
|
|
13
|
+
getRuleEnginesFacts,
|
|
14
|
+
getToolsFacts,
|
|
15
|
+
thisHost
|
|
16
|
+
} = require('./util');
|
|
17
|
+
const {getReport, getReportStats, objectSort} = require('../util');
|
|
18
|
+
|
|
19
|
+
// FUNCTIONS
|
|
20
|
+
|
|
21
|
+
// Returns the response body.
|
|
22
|
+
exports.response = async args => {
|
|
23
|
+
const [timeStamp = '', jobID = ''] = args;
|
|
24
|
+
// Initialize the response content.
|
|
25
|
+
const responseContent = {
|
|
26
|
+
'basics about the report': null,
|
|
27
|
+
'details about the report': null,
|
|
28
|
+
'how to request that the page be retested': null,
|
|
29
|
+
'how a web user can request that the page be retested': null,
|
|
30
|
+
'how to get the full report in JSON': null,
|
|
31
|
+
'how a web user can get the full report in JSON': null,
|
|
32
|
+
'basics about all issues reported in the report': null
|
|
33
|
+
};
|
|
34
|
+
// Get the report.
|
|
35
|
+
const report = await getReport(timeStamp, jobID);
|
|
36
|
+
// If this failed:
|
|
37
|
+
if (report.error) {
|
|
38
|
+
// Add this to the response content.
|
|
39
|
+
responseContent['basics about the report'] = report;
|
|
40
|
+
}
|
|
41
|
+
// Otherwise, i.e. if it succeeded:
|
|
42
|
+
else {
|
|
43
|
+
// Get the basics about the report (which may be only an error message).
|
|
44
|
+
const reportBasics = await getReportBasics(timeStamp, jobID);
|
|
45
|
+
// Add them to the response content.
|
|
46
|
+
responseContent['basics about the report'] = reportBasics;
|
|
47
|
+
// If the basics about the report were obtained:
|
|
48
|
+
if (!reportBasics.error) {
|
|
49
|
+
const {
|
|
50
|
+
strict = null,
|
|
51
|
+
standard = null,
|
|
52
|
+
device = {id: 'default'},
|
|
53
|
+
browserID = null
|
|
54
|
+
} = report;
|
|
55
|
+
// Get details about the job definition.
|
|
56
|
+
const jobDefinitionDetails = {
|
|
57
|
+
'whether the job prohibited redirection': strict,
|
|
58
|
+
'whether the native results of rule engines are reported': ['also', 'no'].includes(standard),
|
|
59
|
+
'whether standardized results are reported': ['also', 'only'].includes(standard),
|
|
60
|
+
'device emulated by the job': device,
|
|
61
|
+
'browser type used by the job': browserID
|
|
62
|
+
};
|
|
63
|
+
// Initialize data about the test results.
|
|
64
|
+
const ruleEngineIDs = new Set();
|
|
65
|
+
const reporterIDs = new Set();
|
|
66
|
+
const violatorIndexes = new Set();
|
|
67
|
+
const issuesData = {};
|
|
68
|
+
// For each act in the report:
|
|
69
|
+
report.acts.forEach(act => {
|
|
70
|
+
const {result, type, which} = act;
|
|
71
|
+
// If the act is a test act with a specified rule engine:
|
|
72
|
+
if (type === 'test' && which) {
|
|
73
|
+
// Ensure its rule engine is in the results data.
|
|
74
|
+
ruleEngineIDs.add(which);
|
|
75
|
+
const instances = result?.standardResult?.instances ?? [];
|
|
76
|
+
// For each standard instance of the act:
|
|
77
|
+
instances.forEach(instance => {
|
|
78
|
+
const {catalogIndex, issueID} = instance;
|
|
79
|
+
// Get the classification of its issue.
|
|
80
|
+
const issueClassification = issueID ? getIssueClassification(issueID) : null;
|
|
81
|
+
// If the instance has a non-ignorable and fully classified issue:
|
|
82
|
+
if (issueClassification) {
|
|
83
|
+
const {summary, weight, why} = issueClassification;
|
|
84
|
+
// Ensure the rule-engine ID is in the reporters data.
|
|
85
|
+
reporterIDs.add(which);
|
|
86
|
+
// If the instance has a catalog index:
|
|
87
|
+
if (catalogIndex) {
|
|
88
|
+
// Ensure the index of the violator is in the results data.
|
|
89
|
+
violatorIndexes.add(catalogIndex);
|
|
90
|
+
}
|
|
91
|
+
// Ensure the data about the issue are in the results data.
|
|
92
|
+
issuesData[issueID] ??= {
|
|
93
|
+
id: issueID,
|
|
94
|
+
summary,
|
|
95
|
+
weight,
|
|
96
|
+
why,
|
|
97
|
+
reporterIDs: new Set()
|
|
98
|
+
};
|
|
99
|
+
// Ensure the reporter ID is in the data about the issue.
|
|
100
|
+
issuesData[issueID].reporterIDs.add(which);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
const {preventions} = report.jobData;
|
|
106
|
+
// Get the details about the rule engines that could not test the page.
|
|
107
|
+
const preventionFacts = Object.entries(preventions ?? {}).map(([ruleEngineID, reason]) => ({
|
|
108
|
+
'name': getRuleEngineFacts(ruleEngineID).name,
|
|
109
|
+
'reason for failure': reason
|
|
110
|
+
}));
|
|
111
|
+
const sortedPreventionFacts = objectSort(preventionFacts ?? [], 'name', 'alpha');
|
|
112
|
+
// Initialize the counts of issues by weight.
|
|
113
|
+
const weightCounts = [0, 0, 0, 0];
|
|
114
|
+
// For each issue:
|
|
115
|
+
Object.values(issuesData).forEach(issueData => {
|
|
116
|
+
// Increment the count of issues with its weight.
|
|
117
|
+
weightCounts[issueData.weight - 1]++;
|
|
118
|
+
});
|
|
119
|
+
// Get details about the test results.
|
|
120
|
+
const resultDetails = {
|
|
121
|
+
'rule engines that tried to test the page': getRuleEnginesFacts(ruleEngineIDs),
|
|
122
|
+
'rule engines that could not test the page': sortedPreventionFacts,
|
|
123
|
+
'names of rule engines that reported rule violations': getRuleEnginesFacts(reporterIDs)
|
|
124
|
+
.map(facts => facts.name),
|
|
125
|
+
'counts of issues by priority': {
|
|
126
|
+
'highest': weightCounts[3],
|
|
127
|
+
'high': weightCounts[2],
|
|
128
|
+
'low': weightCounts[1],
|
|
129
|
+
'lowest': weightCounts[0]
|
|
130
|
+
},
|
|
131
|
+
'number of elements reported as violators': violatorIndexes.size
|
|
132
|
+
};
|
|
133
|
+
// Get the size of the report.
|
|
134
|
+
const reportStats = await getReportStats(timeStamp, jobID);
|
|
135
|
+
// If this succeeded:
|
|
136
|
+
if (reportStats) {
|
|
137
|
+
const {reportSize} = reportStats;
|
|
138
|
+
// Add instructions for getting the full report to the response content.
|
|
139
|
+
responseContent['how to get the full report in JSON'] = {
|
|
140
|
+
'size of the report in bytes': reportSize,
|
|
141
|
+
method: 'GET',
|
|
142
|
+
URL: `${thisHost}/api/getReport/${timeStamp}/${jobID}`
|
|
143
|
+
};
|
|
144
|
+
// Add instructions for web users to get the full report to the response content.
|
|
145
|
+
responseContent['how a web user can get the full report in JSON'] = {
|
|
146
|
+
URL: `${thisHost}/fullReport.json/${timeStamp}/${jobID}`
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// Add the details about the job and test results to the response content.
|
|
150
|
+
responseContent['details about the report'] = {
|
|
151
|
+
'job definition': jobDefinitionDetails,
|
|
152
|
+
'test results': resultDetails
|
|
153
|
+
};
|
|
154
|
+
// If the report has been superseded:
|
|
155
|
+
if (reportBasics['whether a later report about the same page exists']) {
|
|
156
|
+
// Add information about requesting a retest to the response content.
|
|
157
|
+
responseContent['how to request that the page be retested'] = {
|
|
158
|
+
notice: 'A later report about the same page already exists. The listIssues output about that report includes instructions for requesting a retest.'
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
// Otherwise, i.e. if the report has not been superseded:
|
|
162
|
+
else {
|
|
163
|
+
// Add instructions for a retest request to the response content.
|
|
164
|
+
responseContent['how to request that the page be retested'] = {
|
|
165
|
+
method: 'POST',
|
|
166
|
+
URL: `${thisHost}/api/requestRetest/${timeStamp}/${jobID}`,
|
|
167
|
+
'request body': {
|
|
168
|
+
reason: '20- to 100-character reason why the page should be retested'
|
|
169
|
+
},
|
|
170
|
+
'how to check whether the request has been fulfilled':
|
|
171
|
+
'use the listReports tool to determine whether a report about the page has become available (typical wait time: 1 hour to 1 day)'
|
|
172
|
+
};
|
|
173
|
+
// Add instructions for a web user to request a retest.
|
|
174
|
+
responseContent['how a web user can request that the page be retested'] = {
|
|
175
|
+
URL: `${thisHost}/retestRecForm.html/${timeStamp}/${jobID}`
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
// Sort the data about issues by summary.
|
|
179
|
+
const sortedIssuesData = objectSort(Object.values(issuesData), 'summary', 'alpha');
|
|
180
|
+
// Get the basics about the issues.
|
|
181
|
+
const issuesBasics = sortedIssuesData.map(issueData => {
|
|
182
|
+
const {id, summary, weight, why, reporterIDs} = issueData;
|
|
183
|
+
return {
|
|
184
|
+
identifier: id,
|
|
185
|
+
summary,
|
|
186
|
+
priority: ['lowest', 'low', 'high', 'highest'][weight - 1],
|
|
187
|
+
'impact on a user': why,
|
|
188
|
+
'rule engines with any violations belonging to the issue': getRuleEnginesFacts(reporterIDs)
|
|
189
|
+
.map(ruleEnginesFact => ruleEnginesFact.name),
|
|
190
|
+
'how to get details about the issue': {
|
|
191
|
+
method: 'GET',
|
|
192
|
+
URL: `${thisHost}/api/listViolators/${id}/${timeStamp}/${jobID}`
|
|
193
|
+
},
|
|
194
|
+
'web users can get details about the issue at': `${thisHost}/reportIssue.html/${id}/${timeStamp}/${jobID}`
|
|
195
|
+
};
|
|
196
|
+
});
|
|
197
|
+
// Add the basics about the issues to the response content.
|
|
198
|
+
responseContent['basics about all issues reported in the report'] = issuesBasics;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Create a response body.
|
|
202
|
+
const body = {
|
|
203
|
+
'tool collection': getToolsFacts(),
|
|
204
|
+
'tool name': 'listIssues',
|
|
205
|
+
'this request': {
|
|
206
|
+
description: 'Provide details about one report, including basics about the issues reported in it. The timeStamp and jobID parameters identify the report that I want details about. Those parameters were in the response to my earlier listReports request.',
|
|
207
|
+
method: 'GET',
|
|
208
|
+
URL: `${thisHost}/api/listIssues/${timeStamp}/${jobID}`,
|
|
209
|
+
'closest ancestor request': {
|
|
210
|
+
'tool name': 'listReports',
|
|
211
|
+
description: 'Provide basics about all available reports.',
|
|
212
|
+
method: 'GET',
|
|
213
|
+
URL: `${thisHost}/api/listReports`,
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
'URLs of similar requests for web users': {
|
|
217
|
+
'this request': `${thisHost}/reportIssues.html/${timeStamp}/${jobID}`,
|
|
218
|
+
'closest ancestor request': `${thisHost}/targets.html`
|
|
219
|
+
},
|
|
220
|
+
'response metadata': getResponseMetadata(),
|
|
221
|
+
'response content': responseContent
|
|
222
|
+
};
|
|
223
|
+
// Return it.
|
|
224
|
+
return body;
|
|
225
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/*
|
|
2
|
+
listReports.js
|
|
3
|
+
Returns basics about all the available reports.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
const {getReportBasics, getResponseMetadata, getToolsFacts, thisHost} = require('./util');
|
|
9
|
+
const {getReportExtracts} = require('../util');
|
|
10
|
+
|
|
11
|
+
// FUNCTIONS
|
|
12
|
+
|
|
13
|
+
// Returns the response body.
|
|
14
|
+
exports.response = async () => {
|
|
15
|
+
// Initialize the response content.
|
|
16
|
+
const responseContent = {
|
|
17
|
+
'basics about all available reports': null,
|
|
18
|
+
'how to request that a page with no report be tested': null,
|
|
19
|
+
'how a web user can request that the page be tested': null
|
|
20
|
+
};
|
|
21
|
+
// Initialize an array of basics about the reports.
|
|
22
|
+
const reportsBasics = [];
|
|
23
|
+
// Get extracts of all available reports.
|
|
24
|
+
const reportExtracts = await getReportExtracts();
|
|
25
|
+
// For each report:
|
|
26
|
+
for (const extract of reportExtracts) {
|
|
27
|
+
const {error, jobID, timeStamp} = extract;
|
|
28
|
+
// Get the basics about it (which may be only an error message).
|
|
29
|
+
const reportBasics = await getReportBasics(timeStamp, jobID);
|
|
30
|
+
// If this succeeded:
|
|
31
|
+
if (!error) {
|
|
32
|
+
// Add instructions for getting details to the basics.
|
|
33
|
+
reportBasics['how to get details about the report'] = {
|
|
34
|
+
method: 'GET',
|
|
35
|
+
URL: `${thisHost}/api/listIssues/${timeStamp}/${jobID}`
|
|
36
|
+
};
|
|
37
|
+
reportBasics['web users can get details about the report at'] = `${thisHost}/reportIssues.html/${timeStamp}/${jobID}`;
|
|
38
|
+
// Add the basics to the array.
|
|
39
|
+
reportsBasics.push(reportBasics);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Sort the array by page description and secondarily by completion recency.
|
|
43
|
+
reportsBasics.sort((a, b) => {
|
|
44
|
+
if (a['tested web page'].description !== b['tested web page'].description) {
|
|
45
|
+
return a['tested web page']
|
|
46
|
+
.description
|
|
47
|
+
.localeCompare(b['tested web page'].description, 'en', {sensitivity: 'base'});
|
|
48
|
+
}
|
|
49
|
+
return a['completion date and time'].localeCompare(b['completion date and time']);
|
|
50
|
+
});
|
|
51
|
+
// Add the sorted basics about the reports to the response content.
|
|
52
|
+
responseContent['basics about all available reports'] = reportsBasics;
|
|
53
|
+
// Add instructions for requesting a test to the response content.
|
|
54
|
+
responseContent['how to request that a page with no report be tested'] = {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
URL: `${thisHost}/api/requestTest`,
|
|
57
|
+
'request body': {
|
|
58
|
+
description: '10- to 100-character description of the page conforming to the naming convention used in this list of reports',
|
|
59
|
+
URL: '12- to 300-character URL of the page, including the https:// scheme and any query',
|
|
60
|
+
reason: '20- to 100-character reason why the page should be tested'
|
|
61
|
+
},
|
|
62
|
+
'how to check whether the request has been fulfilled': 'use this listReports tool to determine whether a report about the page has become available (typical wait time: 1 hour to 1 day)'
|
|
63
|
+
};
|
|
64
|
+
// Add instructions for a web user to request a test to the response content.
|
|
65
|
+
responseContent['how a web user can request that the page be tested'] = {
|
|
66
|
+
URL: `${thisHost}/testRecForm.html`
|
|
67
|
+
};
|
|
68
|
+
// Create a response body.
|
|
69
|
+
const body = {
|
|
70
|
+
'tool collection': getToolsFacts(),
|
|
71
|
+
'tool name': 'listReports',
|
|
72
|
+
'this request': {
|
|
73
|
+
description: 'Provide basics about all available reports.',
|
|
74
|
+
method: 'GET',
|
|
75
|
+
URL: `${thisHost}/api/listReports`,
|
|
76
|
+
'closest ancestor request': null
|
|
77
|
+
},
|
|
78
|
+
'URLs of similar requests for web users': {
|
|
79
|
+
'this request': `${thisHost}/targets.html`,
|
|
80
|
+
'closest ancestor request': null
|
|
81
|
+
},
|
|
82
|
+
'response metadata': getResponseMetadata(),
|
|
83
|
+
'response content': responseContent
|
|
84
|
+
};
|
|
85
|
+
// Return it.
|
|
86
|
+
return body;
|
|
87
|
+
};
|