@jrpool/kilotest 25.0.0 → 26.0.0

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/index.js CHANGED
@@ -75,9 +75,6 @@ const WAVE_THRESHOLD = Number(process.env.WAVE_BALANCE_THRESHOLD);
75
75
  const AI_SERVICE0_THRESHOLD = Number(process.env.AI_SERVICE0_BALANCE_THRESHOLD);
76
76
  const AI_MODEL0_INPUT_PRICE = Number(process.env.AI_MODEL0_INPUT_PRICE);
77
77
  const AI_MODEL0_OUTPUT_PRICE = Number(process.env.AI_MODEL0_OUTPUT_PRICE);
78
- const researchAgents = {
79
- 'research-agent': 'Internal Research Agent'
80
- }
81
78
 
82
79
  // FUNCTIONS
83
80
 
@@ -585,12 +582,20 @@ const requestHandler = async (request, response) => {
585
582
  }
586
583
  // Otherwise, if the agent is the authorized research agent and it is authenticated:
587
584
  else if (agentID === researchAgent && postData.agentPW === researchAgentPW) {
588
- // If the service is provision of facts about issues in a report:
589
- if (service === 'reportIssues') {
590
- // Get the report identifiers from the path.
585
+ // If the service is provision of facts about the available reports:
586
+ if (service === 'targets') {
587
+ // Get the agent ID from the path.
588
+ const args = [agentID];
589
+ // Get the response (potentially error) data.
590
+ const responseData = await require(path.join(__dirname, 'targets', 'api')).response(args);
591
+ // Send them.
592
+ response.end(JSON.stringify(responseData));
593
+ }
594
+ // Otherwise, if the service is provision of facts about issues in a report:
595
+ else if (service === 'reportIssues') {
596
+ // Get the agent ID and report identifiers from the path.
591
597
  const [timeStamp, jobID] = specs;
592
- const agentName = researchAgents[agentID];
593
- const args = [agentName, timeStamp, jobID];
598
+ const args = [agentID, timeStamp, jobID];
594
599
  const reportSpecsBad = await isHidden(timeStamp, jobID);
595
600
  // If the report is nonexistent or hidden:
596
601
  if (reportSpecsBad) {
@@ -613,7 +618,7 @@ const requestHandler = async (request, response) => {
613
618
  // Otherwise, i.e. if the service is invalid:
614
619
  else {
615
620
  // Report this.
616
- await serveError({message: 'ERROR: Invalid service request'}, response, false);
621
+ await serveError({message: 'ERROR: Invalid service request from research agent'}, response, false);
617
622
  }
618
623
  }
619
624
  // Otherwise, i.e. if the agent is not authorized or not authenticated:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jrpool/kilotest",
3
- "version": "25.0.0",
3
+ "version": "26.0.0",
4
4
  "description": "An ensemble testing service with a focus on accessibility",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -5,49 +5,42 @@
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {getData, getToolData} = require('./util');
8
+ const {getData} = require('./util');
9
9
  const {
10
10
  getDateTime,
11
11
  getNowStamp,
12
12
  getRandomString,
13
- getToolNamesString,
13
+ getToolsFacts,
14
14
  isHidden,
15
+ researchAgents,
15
16
  tools
16
17
  } = require('../util');
17
18
 
18
19
  // FUNCTIONS
19
20
 
20
- // Gets facts about tools.
21
- const getToolFacts = toolIDs => {
22
- const crypticData = getToolData(toolIDs);
23
- return crypticData.map(tool => {
24
- const {toolID, toolName, toolMaker} = tool;
25
- return {
26
- identifier: toolID,
27
- name: toolName,
28
- sponsor: toolMaker
29
- };
30
- });
31
- };
32
21
  // Gets facts about an issue.
33
- const getIssueFacts = issue => {
34
- const {issueID, reporterCount, reporterList, reporters, summary, violatorCount, wcag, why} = issue;
22
+ const getIssueFacts = (thisHost, agentID, timeStamp, jobID, issue) => {
23
+ const {issueID, reporterCount, reporters, summary, violatorCount, wcag, why} = issue;
24
+ const wcagType = wcag.length === 3 ? 'principle' : 'success criterion';
35
25
  return {
36
26
  identifier: issueID,
37
27
  summary,
38
- 'related WCAG principle (n.n) or success criterion (n.n.n)': wcag,
28
+ [`related WCAG ${wcagType}`]: wcag,
39
29
  'impact on a user': why,
40
- 'tools reporting violations of rules belonging to the issue': {
41
- 'number of the tools': reporterCount,
42
- 'alphabetized list of names of the tools': reporterList,
43
- 'facts about the tools alphabetized by name': getToolFacts(reporters.map(tool => tool.toolID))
30
+ 'tools reporting the issue': {
31
+ 'number': reporterCount,
32
+ 'names': reporters.map(tool => tool.toolName)
44
33
  },
45
- 'number of HTML elements reported as exhibiting the issue': violatorCount
34
+ 'number of HTML elements reported as exhibiting the issue': violatorCount,
35
+ 'URLs for details about the issue on the page': {
36
+ 'for you': `${thisHost}/api/${agentID}/reportIssue/${timeStamp}/${jobID}/${issueID}`,
37
+ 'for humans': `${thisHost}/reportIssue/${timeStamp}/${jobID}/${issueID}`
38
+ }
46
39
  };
47
40
  };
48
41
  // Returns a response to a target-issues request.
49
42
  exports.response = async args => {
50
- const [agentName, timeStamp, jobID] = args;
43
+ const [agentID, timeStamp, jobID] = args;
51
44
  const reportIsHidden = await isHidden(timeStamp, jobID);
52
45
  // If the report is not available:
53
46
  if (reportIsHidden) {
@@ -61,44 +54,52 @@ exports.response = async args => {
61
54
  const {pageData, issuesData} = data;
62
55
  const {what, url, daysAgo} = pageData;
63
56
  const {issueCount, issues, preventions, reporterCount, reporters, violatorCount} = issuesData;
64
- const preventedTools = getToolFacts(Object.keys(preventions));
65
- preventedTools.forEach(preventedTool => {
66
- preventedTool['reason for failure'] = preventions[preventedTool.identifier];
67
- });
57
+ const preventedTools = Object.entries(preventions).map(prevention => ({
58
+ name: tools[prevention[0]][0],
59
+ 'reason for failure': prevention[1]
60
+ }));
68
61
  const thisHost = process.env.THIS_KILOTEST_HOST;
69
62
  // Get a response.
70
63
  const response = {
71
- summary: `This document fulfills a request made by an agent to Kilotest. The agent requested data from a report produced by Kilotest. The report contains results of tests performed on a web page by an ensemble of tools. The tools use a combination of rule-based and machine-learning-based methods to identify accessibility, usability, and standard-conformity issues. More detailed information about Kilotest is available from its deployed instance, ${process.env.DEPLOYED_KILOTEST_HOST}. which contains an introduction on its home page and a tutorial.`,
64
+ summary: `This document fulfills a request made by an agent to the Kilotest service. The agent requested data from a Kilotest report about the accessibility, usability, and standard-conformity of a web page. Kilotest, with the help of Testaro, Testilo, and an ensemble of ten testing tools, performs tests on web pages, using a combination of rule- and machine-learning-based methods, and produces reports. Kilotest exposes several API endpoints for agents and several web UI URLs for humans to obtain information from Kilotest reports. To learn more about Kilotest and the advangages of testing with an ensemble of tools, visit the deployed instance of Kilotest (${process.env.DEPLOYED_KILOTEST_HOST}), which contains an introduction on its home page and a tutorial.`,
72
65
  'tool name': 'Kilotest',
73
66
  request: {
74
- 'name of requesting agent': agentName,
67
+ 'requesting agent': {
68
+ identifier: agentID,
69
+ name: researchAgents[agentID]
70
+ },
75
71
  'type of request': {
76
72
  identifier: 'reportIssues',
77
- description: 'What issues did Kilotest report in the specified report?'
73
+ description: 'What issues does the specified report describe?'
74
+ },
75
+ 'closest ancestor request': {
76
+ description: 'Which web pages are reports available about, and what are the statistics about the issues reported for each page?',
77
+ 'URL for you': `${thisHost}/api/${agentID}/targets.html`,
78
+ 'URL for humans': `${thisHost}/targets.html`
78
79
  }
79
80
  },
80
81
  'response metadata': {
82
+ identifier: `${getNowStamp()}-${getRandomString(3)}`,
81
83
  'date and time': new Date().toISOString(),
82
- 'identifier': `${getNowStamp()}-${getRandomString(3)}`
84
+ 'URL of the human-oriented equivalent of this response': `${thisHost}/reportIssues.html/${timeStamp}/${jobID}`
83
85
  },
84
86
  report: {
85
87
  identifier: `${timeStamp}-${jobID}`,
86
88
  'creation date': getDateTime(timeStamp),
87
- 'days since the creation date': daysAgo,
88
- 'URL for human inspection': `${thisHost}/reportIssues.html/${timeStamp}/${jobID}`
89
+ 'days since the creation date': daysAgo
89
90
  },
90
91
  'tested web page': {
91
92
  description: what,
92
93
  URL: url
93
94
  },
94
- 'names of tools that tried to test the page': getToolNamesString(Object.keys(tools)),
95
+ 'tools that tried to test the page': getToolsFacts(Object.keys(tools)),
95
96
  'tools that were unable to test the page': preventedTools,
96
97
  'tools that reported issues': {
97
- 'number of tools': reporterCount,
98
- 'facts about the tools': getToolFacts(reporters.map(tool => tool.toolID))
98
+ number: reporterCount,
99
+ names: reporters.map(tool => tool.toolName)
99
100
  },
100
101
  'number of issues reported': {
101
- 'total': issueCount,
102
+ total: issueCount,
102
103
  'by priority': {
103
104
  'highest priority': issues[4].length,
104
105
  'high priority': issues[3].length,
@@ -108,10 +109,14 @@ exports.response = async args => {
108
109
  },
109
110
  'number of HTML elements reported as exhibiting issues': violatorCount,
110
111
  'issues reported': {
111
- 'highest priority': issues[4].map(issue => getIssueFacts(issue)),
112
- 'high priority': issues[3].map(issue => getIssueFacts(issue)),
113
- 'low priority': issues[2].map(issue => getIssueFacts(issue)),
114
- 'lowest priority': issues[1].map(issue => getIssueFacts(issue))
112
+ 'highest priority': issues[4]
113
+ .map(issue => getIssueFacts(thisHost, agentID, timeStamp, jobID, issue)),
114
+ 'high priority': issues[3]
115
+ .map(issue => getIssueFacts(thisHost, agentID, timeStamp, jobID, issue)),
116
+ 'low priority': issues[2]
117
+ .map(issue => getIssueFacts(thisHost, agentID, timeStamp, jobID, issue)),
118
+ 'lowest priority': issues[1]
119
+ .map(issue => getIssueFacts(thisHost, agentID, timeStamp, jobID, issue))
115
120
  }
116
121
  };
117
122
  return response;
@@ -5,29 +5,13 @@
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {getPageData, getReport, isValidReport, objectSort, tools} = require('../util');
8
+ const {
9
+ getPageData, getReport, getToolsData, getToolList, isValidReport, objectSort
10
+ } = require('../util');
9
11
  const issuesClassification = require('testilo/procs/score/tic').issues;
10
12
 
11
13
  // FUNCTIONS
12
14
 
13
- // Converts tool IDs to tool data sorted by tool name.
14
- const getToolData = exports.getToolData = toolIDs => objectSort(
15
- Array.from(toolIDs).map(toolID => {
16
- const toolData = tools[toolID];
17
- return {
18
- toolID,
19
- toolName: toolData[0],
20
- toolMaker: toolData[1]
21
- }
22
- }),
23
- 'toolName',
24
- 'alpha'
25
- );
26
- // Returns a +-delimited list of sorted tool names.
27
- const getToolList = toolIDs => Array.from(toolIDs)
28
- .map(toolID => tools[toolID][0])
29
- .sort((a, b) => a.localeCompare(b, 'en', {sensitivity: 'base'}))
30
- .join(' + ');
31
15
  // Returns data on the issues reported by a report.
32
16
  const getIssuesData = async (timeStamp, jobID) => {
33
17
  // Get the report.
@@ -96,7 +80,7 @@ const getIssuesData = async (timeStamp, jobID) => {
96
80
  }
97
81
  });
98
82
  // Finish populating the final data.
99
- final.reporters = getToolData(temp.reporters);
83
+ final.reporters = getToolsData(temp.reporters);
100
84
  final.reporterList = getToolList(temp.reporters);
101
85
  final.reporterCount = final.reporters.length;
102
86
  final.violatorCount = temp.violators.size;
@@ -109,7 +93,7 @@ const getIssuesData = async (timeStamp, jobID) => {
109
93
  why,
110
94
  weight
111
95
  };
112
- finalIssue.reporters = getToolData(issue.reporters);
96
+ finalIssue.reporters = getToolsData(issue.reporters);
113
97
  finalIssue.reporterList = getToolList(issue.reporters);
114
98
  finalIssue.reporterCount = finalIssue.reporters.length;
115
99
  finalIssue.violatorCount = issue.violators.size;
@@ -5,7 +5,7 @@
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {getTargetData, logsPath, reportsPath} = require('../util');
8
+ const {getReportData, logsPath, reportsPath} = require('../util');
9
9
  const fs = require('fs/promises');
10
10
  const path = require('path');
11
11
 
@@ -43,13 +43,13 @@ exports.answer = async (_, search) => {
43
43
  for (const reportName of reportNames) {
44
44
  const [timeStamp, jobID] = reportName.slice(0, -5).split('-');
45
45
  // Get a summary of it.
46
- const reportSummary = await getTargetData(timeStamp, jobID);
47
- const {issueSet, preventedTools, url} = reportSummary;
46
+ const reportSummary = await getReportData(timeStamp, jobID);
47
+ const {issueCount, preventedToolCount, url} = reportSummary;
48
48
  reportSpecs.push({
49
49
  timeStamp,
50
50
  jobID,
51
- issueCount: issueSet.size,
52
- preventionCount: preventedTools?.length ?? 0,
51
+ issueCount,
52
+ preventedToolCount,
53
53
  url
54
54
  });
55
55
  }
@@ -65,9 +65,9 @@ exports.answer = async (_, search) => {
65
65
  let anyDeletable = false;
66
66
  // For each summary:
67
67
  reportSpecs.forEach((spec, index) => {
68
- const {timeStamp, jobID, issueCount, preventionCount, url} = spec;
68
+ const {timeStamp, jobID, issueCount, preventedToolCount, url} = spec;
69
69
  const jobName = `${timeStamp}-${jobID}`;
70
- const specString = `<code>${url}</code> (<code>${jobName}</code>): preventions ${preventionCount}, issues ${issueCount}`;
70
+ const specString = `<code>${url}</code> (<code>${jobName}</code>): preventions ${preventedToolCount}, issues ${issueCount}`;
71
71
  // If its report is the sole report on a target:
72
72
  if (reportSpecs[index - 1]?.url !== url && reportSpecs[index + 1]?.url !== url) {
73
73
  // Add a line with a deletion checkbox.
@@ -5,7 +5,7 @@
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {getTargetData, logsPath, reportsPath} = require('../util');
8
+ const {getReportData, logsPath, reportsPath} = require('../util');
9
9
  const fs = require('fs/promises');
10
10
  const path = require('path');
11
11
 
@@ -43,13 +43,13 @@ exports.answer = async (_, search) => {
43
43
  for (const reportName of reportNames) {
44
44
  const [timeStamp, jobID] = reportName.slice(0, -5).split('-');
45
45
  // Get a summary of it.
46
- const reportSummary = await getTargetData(timeStamp, jobID);
47
- const {issueSet, preventedTools, url} = reportSummary;
46
+ const reportSummary = await getReportData(timeStamp, jobID);
47
+ const {issueCount, preventedToolCount, url} = reportSummary;
48
48
  reportSpecs.push({
49
49
  timeStamp,
50
50
  jobID,
51
- issueCount: issueSet.size,
52
- preventionCount: preventedTools?.length ?? 0,
51
+ issueCount,
52
+ preventedToolCount,
53
53
  url
54
54
  });
55
55
  }
@@ -65,9 +65,9 @@ exports.answer = async (_, search) => {
65
65
  let anyDeletable = false;
66
66
  // For each summary:
67
67
  reportSpecs.forEach((spec, index) => {
68
- const {timeStamp, jobID, issueCount, preventionCount, url} = spec;
68
+ const {timeStamp, jobID, issueCount, preventedToolCount, url} = spec;
69
69
  const jobName = `${timeStamp}-${jobID}`;
70
- const specString = `<code>${url}</code> (<code>${jobName}</code>): preventions ${preventionCount}, issues ${issueCount}`;
70
+ const specString = `<code>${url}</code> (<code>${jobName}</code>): preventions ${preventedToolCount}, issues ${issueCount}`;
71
71
  // If its report is a non-latest report:
72
72
  if (reportSpecs[index + 1]?.url === url) {
73
73
  // Add a line with a deletion checkbox.
@@ -5,7 +5,7 @@
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {getTargetData, logsPath, reportsPath} = require('../util');
8
+ const {getReportData, logsPath, reportsPath} = require('../util');
9
9
  const fs = require('fs/promises');
10
10
  const path = require('path');
11
11
 
@@ -43,13 +43,13 @@ exports.answer = async (_, search) => {
43
43
  for (const reportName of reportNames) {
44
44
  const [timeStamp, jobID] = reportName.slice(0, -5).split('-');
45
45
  // Get a summary of it.
46
- const reportSummary = await getTargetData(timeStamp, jobID);
47
- const {issueSet, preventedTools, url} = reportSummary;
46
+ const reportSummary = await getReportData(timeStamp, jobID);
47
+ const {issueCount, preventedToolCount, url} = reportSummary;
48
48
  reportSpecs.push({
49
49
  timeStamp,
50
50
  jobID,
51
- issueCount: issueSet.size,
52
- preventionCount: preventedTools?.length ?? 0,
51
+ issueCount,
52
+ preventedToolCount,
53
53
  url
54
54
  });
55
55
  }
@@ -65,9 +65,9 @@ exports.answer = async (_, search) => {
65
65
  let anyDeletable = false;
66
66
  // For each summary:
67
67
  reportSpecs.forEach((spec, index) => {
68
- const {timeStamp, jobID, issueCount, preventionCount, url} = spec;
68
+ const {timeStamp, jobID, issueCount, preventedToolCount, url} = spec;
69
69
  const jobName = `${timeStamp}-${jobID}`;
70
- const specString = `<code>${url}</code> (<code>${jobName}</code>): preventions ${preventionCount}, issues ${issueCount}`;
70
+ const specString = `<code>${url}</code> (<code>${jobName}</code>): preventions ${preventedToolCount}, issues ${issueCount}`;
71
71
  // If its report is the latest report on a target with at least 2 reports:
72
72
  if (reportSpecs[index - 1]?.url === url && reportSpecs[index + 1]?.url !== url) {
73
73
  // Add a line with a deletion checkbox.
package/researchAgent.js CHANGED
@@ -9,12 +9,11 @@
9
9
 
10
10
  /*
11
11
  researchAgent.js
12
- Module for simulating a research agent.
12
+ Simulates a research agent.
13
13
  */
14
14
 
15
15
  // IMPORTS
16
16
 
17
- const {getLogs} = require('./util');
18
17
  require('dotenv').config();
19
18
  const httpClient = require('http');
20
19
  const httpsClient = require('https');
@@ -30,22 +29,13 @@ const hostParts = kilotestHost.split(/:\/*/);
30
29
  const scheme = hostParts[0];
31
30
  const host = hostParts[1];
32
31
  const port = hostParts[2] || (scheme === 'https' ? 443 : 80);
33
- const services = ['reportIssues'];
34
- // Randomly chosen service.
35
- const service = services[Math.floor(services.length * Math.random())];
36
32
 
37
33
  // FUNCTIONS
38
34
 
39
- // Submits a random research request to a random Kilotest host.
35
+ // Submits two request to a random Kilotest host.
40
36
  const requestService = async () => {
41
- const logs = await getLogs();
42
- // Randomly chosen log.
43
- const log = logs[Math.floor(logs.length * Math.random())];
44
- const specs = log.jobName.split('-').join('/');
45
- const path = `/api/${agent}/${service}/${specs}`;
46
37
  const client = scheme === 'https' ? httpsClient : httpClient;
47
- // Use its job name in the request path.
48
- const requestOptions = {
38
+ const getRequestOptions = path => ({
49
39
  method: 'POST',
50
40
  host,
51
41
  port,
@@ -53,10 +43,11 @@ const requestService = async () => {
53
43
  headers: {
54
44
  'content-type': 'application/json; charset=utf-8'
55
45
  }
56
- };
46
+ });
47
+ const path = `/api/${agent}/targets`;
57
48
  console.log(`About to submit ${scheme} request as JSON on port ${port} to ${host}${path}`);
58
- // Submit a request.
59
- client.request(requestOptions, response => {
49
+ // Submit a targets request.
50
+ client.request(getRequestOptions(path), response => {
60
51
  // Initialize a collection of data from the response.
61
52
  const chunks = [];
62
53
  response
@@ -73,17 +64,62 @@ const requestService = async () => {
73
64
  // When the response is completed:
74
65
  .on('end', async () => {
75
66
  const content = chunks.join('');
76
- // Output it.
77
67
  try {
78
- console.log(JSON.stringify(JSON.parse(content), null, 2));
68
+ // Output it.
69
+ const contentObj = JSON.parse(content);
70
+ console.log('======================');
71
+ console.log(JSON.stringify(contentObj, null, 2));
72
+ // Get the IDs of the available reports.
73
+ const reportIDs = contentObj['available reports'].map(report => report.identifier);
74
+ // Choose one at random.
75
+ const [timeStamp, jobID] = reportIDs[Math.floor(Math.random() * reportIDs.length)]
76
+ .split('-');
77
+ console.log('======================');
78
+ const path = `/api/${agent}/reportIssues/${timeStamp}/${jobID}`;
79
+ console.log(`About to submit ${scheme} request as JSON on port ${port} to ${host}${path}`);
80
+ const requestOptions = getRequestOptions(path);
81
+ // Submit an issues request for it.
82
+ client.request(requestOptions, response => {
83
+ // Initialize a collection of data from the response.
84
+ const chunks = [];
85
+ response
86
+ // If the response throws an error:
87
+ .on('error', async error => {
88
+ // Report it.
89
+ console.log(error.message);
90
+ })
91
+ // If the response delivers data:
92
+ .on('data', chunk => {
93
+ // Add them to the collection.
94
+ chunks.push(chunk);
95
+ })
96
+ // When the response is completed:
97
+ .on('end', async () => {
98
+ const content = chunks.join('');
99
+ try {
100
+ // Output it.
101
+ const contentObj = JSON.parse(content);
102
+ console.log(JSON.stringify(contentObj, null, 2));
103
+ console.log('======================');
104
+ }
105
+ catch (error) {
106
+ console.log(error.message);
107
+ console.log(`Issues response content: ${content || 'No content'}`);
108
+ }
109
+ });
110
+ })
111
+ // Finish sending the issues request.
112
+ .end(JSON.stringify({
113
+ agentPW
114
+ }));
79
115
  }
80
116
  catch (error) {
81
117
  console.log(error.message);
82
- console.log(`Response content: ${content || 'No content'}`);
118
+ console.log(`Targets response content: ${content || 'No content'}`);
83
119
  }
84
120
  });
85
121
  })
86
- // Finish sending the job request.
122
+ // Finish sending the targets request.
87
123
  .end(JSON.stringify({
88
124
  agentPW
89
125
  }));
package/targets/api.js ADDED
@@ -0,0 +1,101 @@
1
+ /*
2
+ api.js
3
+ Responds to the targets request.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ const {
9
+ getLogs,
10
+ getNowStamp,
11
+ getRandomString,
12
+ getReportData,
13
+ researchAgents
14
+ } = require('../util');
15
+
16
+ // CONSTANTS
17
+
18
+ const thisHost = process.env.THIS_KILOTEST_HOST;
19
+
20
+ // FUNCTIONS
21
+
22
+ // Returns a response to a targets request.
23
+ exports.response = async agentID => {
24
+ const availableReports = [];
25
+ // Get the non-hidden logs.
26
+ const targetLogs = await getLogs();
27
+ // For each log:
28
+ for (const targetLog of targetLogs) {
29
+ const {jobName} = targetLog;
30
+ const [timeStamp, jobID] = jobName.split('-');
31
+ const {superseded, url, what} = targetLog;
32
+ // Get data about its report.
33
+ const data = await getReportData(timeStamp, jobID);
34
+ const {
35
+ creationDate,
36
+ daysAgo,
37
+ issueCount,
38
+ toolNames,
39
+ toolCount,
40
+ reporterNames,
41
+ reporterCount,
42
+ violatorCount,
43
+ preventedToolNames,
44
+ preventedToolCount
45
+ } = data;
46
+ availableReports.push({
47
+ identifier: jobName,
48
+ 'creation date': creationDate,
49
+ 'days since the creation date': daysAgo,
50
+ 'tested web page': {
51
+ description: what,
52
+ URL: url
53
+ },
54
+ 'whether a later report about the same page exists': !! superseded,
55
+ 'number of issues reported': issueCount,
56
+ 'number of HTML elements reported as exhibiting issues': violatorCount,
57
+ 'tools that tried to test the page': {
58
+ number: toolCount,
59
+ names: toolNames
60
+ },
61
+ 'tools that were unable to test the page': {
62
+ number: preventedToolCount,
63
+ names: preventedToolNames
64
+ },
65
+ 'tools that reported issues': {
66
+ number: reporterCount,
67
+ names: reporterNames
68
+ },
69
+ 'URLs for getting data on the reported issues': {
70
+ 'for you': `${thisHost}/api/${agentID}/reportIssues/${timeStamp}/${jobID}`,
71
+ 'for humans': `${thisHost}/reportIssues/${timeStamp}/${jobID}`
72
+ },
73
+ 'URL for getting the full technical report as JSON': `${thisHost}/fullReport.html/${timeStamp}/${jobID}`
74
+ });
75
+ }
76
+ // Get a response.
77
+ const response = {
78
+ summary: `This document fulfills a request made by an agent to the Kilotest service. The agent requested data about the web pages that Kilotest had tested for accessibility, usability, and standard-conformity and statistics for each page on the results of the tests. Kilotest, with the help of Testaro, Testilo, and an ensemble of ten testing tools, performs tests on web pages, using a combination of rule- and machine-learning-based methods, and produces reports. Kilotest exposes API endpoints for agents and web UI URLs for humans to recommend web pages for testing and obtain information from Kilotest reports. To learn more about Kilotest and the advangages of testing with an ensemble of tools, visit the deployed instance of Kilotest (${process.env.DEPLOYED_KILOTEST_HOST}), whose home page contains an introduction and a link to a tutorial.`,
79
+ 'tool name': 'Kilotest',
80
+ request: {
81
+ 'requesting agent': {
82
+ identifier: agentID,
83
+ name: researchAgents[agentID]
84
+ },
85
+ 'type of request': {
86
+ identifier: 'targets',
87
+ description: 'Give me summary data about each available report.'
88
+ },
89
+ URLs: {
90
+ 'URL of this request': `${thisHost}/api/${agentID}/targets`,
91
+ 'equivalent URL for humans': `${thisHost}/targets.html`
92
+ }
93
+ },
94
+ 'response metadata': {
95
+ identifier: `${getNowStamp()}-${getRandomString(3)}`,
96
+ 'date and time': new Date().toISOString(),
97
+ },
98
+ 'available reports': availableReports
99
+ };
100
+ return response;
101
+ };
package/targets/index.js CHANGED
@@ -7,13 +7,14 @@
7
7
 
8
8
  const {
9
9
  getAgoDays,
10
+ getCountString,
10
11
  getJobNames,
11
12
  getObject,
12
13
  getPageDataStrings,
13
14
  getRecs,
14
15
  getToolNamesString,
15
16
  getLogs,
16
- getTargetData,
17
+ getReportData,
17
18
  isRecommendable,
18
19
  jobsPath
19
20
  } = require('../util');
@@ -22,8 +23,6 @@ const path = require('path');
22
23
 
23
24
  // FUNCTIONS
24
25
 
25
- // Returns a description of a tool count.
26
- const getToolCountString = toolCount => toolCount === 1 ? '1 tool' : `${toolCount} tools`;
27
26
  // Adds parameters to a query for the answer page.
28
27
  const populateQuery = async query => {
29
28
  const margin = ' '.repeat(8);
@@ -71,19 +70,26 @@ const populateQuery = async query => {
71
70
  query.noQueued = lines.queue.length ? '' : 'No pages are queued for testing.';
72
71
  // Add a no-claimed message, if applicable, to the query.
73
72
  query.noClaimed = lines.claimed.length ? '' : 'No pages are being tested now.';
74
- // Get the logs of the reports.
75
- const targetLogs = (await getLogs()).filter(log => ! log.hidden);
73
+ // Get the logs of the non-hidden reports.
74
+ const targetLogs = await getLogs();
76
75
  query.which = targetLogs.length ? 'the following' : 'no';
77
76
  query.some = (targetLogs.length || jobFileNames.queue.length || jobFileNames.claimed.length)
78
77
  ? 'another'
79
78
  : 'a';
80
79
  const multiReportTargets = new Set(targetLogs.filter(log => log.superseded).map(log => log.what));
81
- // For each log that is not hidden:
80
+ // For each log:
82
81
  for (const targetLog of targetLogs) {
83
82
  const {jobName, url, what} = targetLog;
84
83
  const [timeStamp, jobID] = jobName.split('-');
85
- const reportData = await getTargetData(timeStamp, jobID);
86
- const {issueSet, preventedTools, reporterSet, violatorSet} = reportData;
84
+ const reportData = await getReportData(timeStamp, jobID);
85
+ const {
86
+ issueCount,
87
+ preventedToolCount,
88
+ preventedToolNames,
89
+ reporterNames,
90
+ reporterCount,
91
+ violatorCount
92
+ } = reportData;
87
93
  lines.tested.push(`${margin}<details>`);
88
94
  const daysAgo = getAgoDays(timeStamp);
89
95
  const pageDataStrings = await getPageDataStrings(timeStamp, jobID, {what, url, daysAgo});
@@ -96,38 +102,32 @@ const populateQuery = async query => {
96
102
  // Add facts about the report to the lines.
97
103
  lines.tested.push(`${margin} <li>${testInfo}</li>`);
98
104
  // If the page prevented any tool from performing its tests:
99
- if (preventedTools?.length) {
105
+ if (preventedToolCount) {
100
106
  // Add this to the lines.
101
- const preventedToolSet = new Set(preventedTools);
102
- const toolCountString = getToolCountString(preventedToolSet.size);
103
- const toolsString = getToolNamesString(preventedToolSet);
107
+ const toolCountString = getCountString(preventedToolCount, 'tool', 'tools');
104
108
  lines.tested.push(
105
- `${margin} <li>Page not testable by ${toolCountString} (${toolsString})</li>`,
109
+ `${margin} <li>Page not testable by ${toolCountString} (${preventedToolNames.join(' + ')})</li>`,
106
110
  );
107
111
  }
108
112
  // Add facts about the test results to the lines.
109
- const reporterCount = reporterSet.size;
110
- const reporterCountString = getToolCountString(reporterCount);
111
- let reporterString = reporterCountString;
113
+ let reporterString = `${getCountString(reporterCount, 'tool', 'tools')} reported issues`;
112
114
  if (reporterCount) {
113
- const reporterNamesString = getToolNamesString(reporterSet);
114
- reporterString = `${reporterCountString} (${reporterNamesString})`;
115
+ const reporterNamesString = reporterNames.join(' + ');
116
+ reporterString = `${reporterString} (${reporterNamesString})`;
115
117
  }
116
- const issueCountString = issueSet.size === 1 ? '1 issue was' : `${issueSet.size} issues were`;
117
- const violatorString = violatorSet.size === 1
118
- ? '1 violator was'
119
- : `${violatorSet.size} violators were`;
120
- lines.tested.push(`${margin} <li>${reporterString} reported issues</li>`);
118
+ const issueCountString = getCountString(issueCount, 'issue was', 'issues were');
119
+ const violatorString = getCountString(violatorCount, 'violator was', 'violators were');
120
+ lines.tested.push(`${margin} <li>${reporterString}</li>`);
121
121
  lines.tested.push(`${margin} <li>${issueCountString} reported</li>`);
122
122
  lines.tested.push(`${margin} <li>${violatorString} reported</li>`);
123
123
  lines.tested.push(`${margin} </ul>`);
124
124
  lines.tested.push(`${margin}<ul class="nav">`);
125
125
  // If any issues were reported:
126
- if (issueSet.size) {
126
+ if (issueCount) {
127
127
  // Add a question link about the reported issues to the lines.
128
128
  const href = `href="reportIssues.html/${timeStamp}/${jobID}"`;
129
129
  const label = `aria-label="What ${issueCountString} reported for the ${what} page?"`;
130
- const questionString = issueSet.size === 1 ? 'was the issue' : 'were the issues';
130
+ const questionString = issueCount === 1 ? 'was the issue' : 'were the issues';
131
131
  const link = `<a ${href} ${label}>What ${questionString}?</a>`;
132
132
  lines.tested.push(`${margin} <li>${link}</li>`);
133
133
  }
package/util.js CHANGED
@@ -6,7 +6,7 @@
6
6
  // IMPORTS
7
7
 
8
8
  const {sendAlert} = require('./alerts');
9
- const {issues} = require('testilo/procs/score/tic');
9
+ const issuesClassification = require('testilo/procs/score/tic').issues;
10
10
  const fs = require('fs/promises');
11
11
  const path = require('path');
12
12
  const querystring = require('querystring');
@@ -34,9 +34,16 @@ const tools = exports.tools = {
34
34
  nuVnu: ['Html Checker', 'World Wide Web Consortium'],
35
35
  qualWeb: ['QualWeb', 'University of Lisbon'],
36
36
  testaro: ['Testaro', 'CVS Health'],
37
- wax: ['WallyAX', 'Wally'],
38
37
  wave: ['WAVE', 'Utah State University'],
38
+ <<<<<<< HEAD
39
+ wax: ['WallyAX', 'Wally']
40
+ =======
41
+ wax: ['WAX', 'Wally']
42
+ >>>>>>> api3
39
43
  };
44
+ exports.researchAgents = {
45
+ 'research-agent': 'Internal Research Agent'
46
+ }
40
47
 
41
48
  // FUNCTIONS
42
49
 
@@ -144,7 +151,7 @@ const alphaCompare = (a, b) => a.localeCompare(b, 'en', {sensitivity: 'base'});
144
151
  // Sorts strings alphabetically and case-insensitively.
145
152
  const alphaSort = strings => strings.sort((a, b) => alphaCompare(a, b));
146
153
  // Sorts objects by a property value.
147
- exports.objectSort = (objects, property, sortType) => objects
154
+ const objectSort = exports.objectSort = (objects, property, sortType) => objects
148
155
  .sort((a, b) => {
149
156
  // If the property values are numbers to be sorted in increasing order:
150
157
  if (sortType === 'numericUp') {
@@ -172,8 +179,8 @@ const getRuleIDs = exports.getRuleIDs = () => {
172
179
  // Initialize a validity checker.
173
180
  const validityChecker = {};
174
181
  // For each classified issue:
175
- Object.keys(issues).forEach(issueID => {
176
- const {tools, weight} = issues[issueID];
182
+ Object.keys(issuesClassification).forEach(issueID => {
183
+ const {tools, weight} = issuesClassification[issueID];
177
184
  // If the weight is invalid:
178
185
  if (weight < 1 || weight > 4) {
179
186
  // Report this.
@@ -322,9 +329,9 @@ exports.isHidden = async (timeStamp, jobID) => {
322
329
  }
323
330
  return !! log.hidden;
324
331
  };
325
- // Returns summary data on the results in a report.
326
- exports.getTargetData = async (timeStamp, jobID) => {
327
- // Vasidate the report and annotate it if necessary.
332
+ // Returns summary data on a report.
333
+ exports.getReportData = async (timeStamp, jobID) => {
334
+ // Validate the report and annotate it if necessary.
328
335
  const log = await getLog(timeStamp, jobID, true);
329
336
  // If this failed:
330
337
  if (typeof log === 'string') {
@@ -335,12 +342,22 @@ exports.getTargetData = async (timeStamp, jobID) => {
335
342
  const data = {
336
343
  what: log.what,
337
344
  url: log.url,
338
- issueSet: new Set(),
339
- reporterSet: new Set(),
340
- violatorSet: new Set(),
341
- preventedTools: {}
345
+ jobName: `${timeStamp}-${jobID}`,
346
+ creationDate: getDateTime(timeStamp),
347
+ daysAgo: getAgoDays(timeStamp),
348
+ issueCount: 0,
349
+ toolNames: [],
350
+ toolCount: 0,
351
+ reporterNames: [],
352
+ reporterCount: 0,
353
+ violatorCount: 0,
354
+ preventedToolNames: [],
355
+ preventedToolCount: 0
342
356
  };
343
- const {issueSet, reporterSet, violatorSet} = data;
357
+ const issueIDSet = new Set();
358
+ const toolNameSet = new Set();
359
+ const reporterIDSet = new Set();
360
+ const violatorIndexSet = new Set();
344
361
  // Get the report.
345
362
  const report = await getReport(timeStamp, jobID);
346
363
  // If this failed:
@@ -353,27 +370,44 @@ exports.getTargetData = async (timeStamp, jobID) => {
353
370
  // If it is a test act:
354
371
  if (act.type === 'test') {
355
372
  const {result, which} = act;
373
+ // Ensure that the tool is in the temporary data.
374
+ toolNameSet.add(which);
356
375
  const instances = result?.standardResult?.instances ?? [];
357
- // For each standard instance:
376
+ // For each standard instance of the act:
358
377
  instances.forEach(instance => {
359
378
  const {catalogIndex, issueID} = instance;
360
379
  // If it has a non-ignorable classified issue ID:
361
- if (issueID && issues[issueID] && issueID !== 'ignorable') {
362
- // Ensure that the tool is in the data.
363
- reporterSet.add(which);
364
- // Ensure that the issue is in the data.
365
- issueSet.add(issueID);
366
- // If it has a catalog index:
380
+ if (issueID && issuesClassification[issueID] && issueID !== 'ignorable') {
381
+ // Ensure that the tool is in the temporary data.
382
+ reporterIDSet.add(which);
383
+ // Ensure that the issue is in the temporary data.
384
+ issueIDSet.add(issueID);
385
+ // If the violator has a catalog index:
367
386
  if (catalogIndex) {
368
- // Ensure that the violator is in the data.
369
- violatorSet.add(catalogIndex);
387
+ // Ensure that the violator is in the temporary data.
388
+ violatorIndexSet.add(catalogIndex);
370
389
  }
371
390
  }
372
391
  });
373
392
  }
374
393
  });
375
- // Add the IDs of any prevented tools to the data.
376
- data.preventedTools = Object.keys(report.jobData?.preventions || {});
394
+ // Populate the data with the act data.
395
+ data.issueCount = issueIDSet.size;
396
+ data.toolNames = Array
397
+ .from(toolNameSet)
398
+ .sort((a, b) => a.localeCompare(b, 'en', {sensitivity: 'base'}));
399
+ data.toolCount = toolNameSet.size;
400
+ data.reporterNames = Array
401
+ .from(reporterIDSet)
402
+ .map(id => tools[id][0])
403
+ .sort((a, b) => a.localeCompare(b, 'en', {sensitivity: 'base'}));
404
+ data.reporterCount = data.reporterNames.length;
405
+ data.violatorCount = violatorIndexSet.size;
406
+ // Add the names of any prevented tools to the data.
407
+ data.preventedToolNames = Object.keys(report.jobData?.preventions || {})
408
+ .map(toolID => tools[toolID][0])
409
+ .sort((a, b) => a.localeCompare(b, 'en', {sensitivity: 'base'}));
410
+ data.preventedToolCount = data.preventedToolNames.length;
377
411
  // Return the data.
378
412
  return data;
379
413
  }
@@ -478,7 +512,7 @@ const getRecs = exports.getRecs = async () => {
478
512
  };
479
513
  // Returns a string of tool names.
480
514
  exports.getToolNamesString = toolIDSet =>
481
- alphaSort(Array.from(toolIDSet).map(toolID => tools[toolID][0])).join(' + ');
515
+ alphaSort(Array.from(toolIDSet).map(toolID => tools[toolID]?.[0] || toolID)).join(' + ');
482
516
  // Converts a catalog item text to a text-fragment link destination.
483
517
  exports.getTextFragmentHref = (text, url) => {
484
518
  const fragmentList = text
@@ -488,7 +522,7 @@ exports.getTextFragmentHref = (text, url) => {
488
522
  // Return a text-fragment link.
489
523
  return `${url}#:~:text=${fragmentList}`;
490
524
  };
491
- // Returns an array of the logs, with job names added, of the non-hidden reports.
525
+ // Returns a sorted array of the logs, with job names added, of the non-hidden reports.
492
526
  exports.getLogs = async () => {
493
527
  // Initialize data on the tested targets.
494
528
  const logs = [];
@@ -652,7 +686,7 @@ exports.getWCAGLink = numericID => {
652
686
  // Return the link.
653
687
  return `https://www.w3.org/WAI/WCAG22/Understanding/${wcagMap[numericID]}`;
654
688
  };
655
- // Gets page data from a report.
689
+ // Returns page data from a report.
656
690
  const getPageData = exports.getPageData = async (timeStamp, jobID) => {
657
691
  // Get the log of the report.
658
692
  const log = await getLog(timeStamp, jobID, false);
@@ -673,7 +707,7 @@ const getPageData = exports.getPageData = async (timeStamp, jobID) => {
673
707
  };
674
708
  // Gets HTML strings for page data from a report.
675
709
  exports.getPageDataStrings = async (timeStamp, jobID, pageData) => {
676
- // If the paga data were not specified:
710
+ // If the page data were not specified:
677
711
  if (! pageData) {
678
712
  // Get them.
679
713
  pageData = await getPageData(timeStamp, jobID);
@@ -688,3 +722,35 @@ exports.getPageDataStrings = async (timeStamp, jobID, pageData) => {
688
722
  testInfo: `Tested ${daysAgo} days ago by job <code>${jobID}</code> on ${when}`
689
723
  };
690
724
  };
725
+ // Returns tool data sorted by tool name.
726
+ const getToolsData = exports.getToolsData = toolIDs => objectSort(
727
+ Array.from(toolIDs).map(toolID => {
728
+ const toolData = tools[toolID];
729
+ return {
730
+ toolID,
731
+ toolName: toolData[0],
732
+ toolMaker: toolData[1]
733
+ }
734
+ }),
735
+ 'toolName',
736
+ 'alpha'
737
+ );
738
+ // Returns a +-delimited list of sorted tool names.
739
+ exports.getToolList = toolIDs => Array.from(toolIDs)
740
+ .map(toolID => tools[toolID][0])
741
+ .sort((a, b) => a.localeCompare(b, 'en', {sensitivity: 'base'}))
742
+ .join(' + ');
743
+ // Returns facts about tools.
744
+ exports.getToolsFacts = toolIDs => {
745
+ const crypticData = getToolsData(toolIDs);
746
+ return crypticData.map(tool => {
747
+ const {toolID, toolName, toolMaker} = tool;
748
+ return {
749
+ identifier: toolID,
750
+ name: toolName,
751
+ sponsor: toolMaker
752
+ };
753
+ });
754
+ };
755
+ // Returns a string describing a count.
756
+ exports.getCountString = (count, singular, plural) => count === 1 ? `1 ${singular}` : `${count} ${plural}`;