@jrpool/kilotest 25.0.1 → 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
@@ -582,9 +582,18 @@ const requestHandler = async (request, response) => {
582
582
  }
583
583
  // Otherwise, if the agent is the authorized research agent and it is authenticated:
584
584
  else if (agentID === researchAgent && postData.agentPW === researchAgentPW) {
585
- // If the service is provision of facts about issues in a report:
586
- if (service === 'reportIssues') {
587
- // 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.
588
597
  const [timeStamp, jobID] = specs;
589
598
  const args = [agentID, timeStamp, jobID];
590
599
  const reportSpecsBad = await isHidden(timeStamp, jobID);
@@ -609,7 +618,7 @@ const requestHandler = async (request, response) => {
609
618
  // Otherwise, i.e. if the service is invalid:
610
619
  else {
611
620
  // Report this.
612
- await serveError({message: 'ERROR: Invalid service request'}, response, false);
621
+ await serveError({message: 'ERROR: Invalid service request from research agent'}, response, false);
613
622
  }
614
623
  }
615
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.1",
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,11 +5,12 @@
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
+ getToolsFacts,
13
14
  isHidden,
14
15
  researchAgents,
15
16
  tools
@@ -17,18 +18,6 @@ const {
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
22
  const getIssueFacts = (thisHost, agentID, timeStamp, jobID, issue) => {
34
23
  const {issueID, reporterCount, reporters, summary, violatorCount, wcag, why} = issue;
@@ -103,7 +92,7 @@ exports.response = async args => {
103
92
  description: what,
104
93
  URL: url
105
94
  },
106
- 'tools that tried to test the page': getToolFacts(Object.keys(tools)),
95
+ 'tools that tried to test the page': getToolsFacts(Object.keys(tools)),
107
96
  'tools that were unable to test the page': preventedTools,
108
97
  'tools that reported issues': {
109
98
  number: reporterCount,
@@ -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');
@@ -35,6 +35,11 @@ const tools = exports.tools = {
35
35
  qualWeb: ['QualWeb', 'University of Lisbon'],
36
36
  testaro: ['Testaro', 'CVS Health'],
37
37
  wave: ['WAVE', 'Utah State University'],
38
+ <<<<<<< HEAD
39
+ wax: ['WallyAX', 'Wally']
40
+ =======
41
+ wax: ['WAX', 'Wally']
42
+ >>>>>>> api3
38
43
  };
39
44
  exports.researchAgents = {
40
45
  'research-agent': 'Internal Research Agent'
@@ -146,7 +151,7 @@ const alphaCompare = (a, b) => a.localeCompare(b, 'en', {sensitivity: 'base'});
146
151
  // Sorts strings alphabetically and case-insensitively.
147
152
  const alphaSort = strings => strings.sort((a, b) => alphaCompare(a, b));
148
153
  // Sorts objects by a property value.
149
- exports.objectSort = (objects, property, sortType) => objects
154
+ const objectSort = exports.objectSort = (objects, property, sortType) => objects
150
155
  .sort((a, b) => {
151
156
  // If the property values are numbers to be sorted in increasing order:
152
157
  if (sortType === 'numericUp') {
@@ -174,8 +179,8 @@ const getRuleIDs = exports.getRuleIDs = () => {
174
179
  // Initialize a validity checker.
175
180
  const validityChecker = {};
176
181
  // For each classified issue:
177
- Object.keys(issues).forEach(issueID => {
178
- const {tools, weight} = issues[issueID];
182
+ Object.keys(issuesClassification).forEach(issueID => {
183
+ const {tools, weight} = issuesClassification[issueID];
179
184
  // If the weight is invalid:
180
185
  if (weight < 1 || weight > 4) {
181
186
  // Report this.
@@ -324,9 +329,9 @@ exports.isHidden = async (timeStamp, jobID) => {
324
329
  }
325
330
  return !! log.hidden;
326
331
  };
327
- // Returns summary data on the results in a report.
328
- exports.getTargetData = async (timeStamp, jobID) => {
329
- // 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.
330
335
  const log = await getLog(timeStamp, jobID, true);
331
336
  // If this failed:
332
337
  if (typeof log === 'string') {
@@ -337,12 +342,22 @@ exports.getTargetData = async (timeStamp, jobID) => {
337
342
  const data = {
338
343
  what: log.what,
339
344
  url: log.url,
340
- issueSet: new Set(),
341
- reporterSet: new Set(),
342
- violatorSet: new Set(),
343
- 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
344
356
  };
345
- 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();
346
361
  // Get the report.
347
362
  const report = await getReport(timeStamp, jobID);
348
363
  // If this failed:
@@ -355,27 +370,44 @@ exports.getTargetData = async (timeStamp, jobID) => {
355
370
  // If it is a test act:
356
371
  if (act.type === 'test') {
357
372
  const {result, which} = act;
373
+ // Ensure that the tool is in the temporary data.
374
+ toolNameSet.add(which);
358
375
  const instances = result?.standardResult?.instances ?? [];
359
- // For each standard instance:
376
+ // For each standard instance of the act:
360
377
  instances.forEach(instance => {
361
378
  const {catalogIndex, issueID} = instance;
362
379
  // If it has a non-ignorable classified issue ID:
363
- if (issueID && issues[issueID] && issueID !== 'ignorable') {
364
- // Ensure that the tool is in the data.
365
- reporterSet.add(which);
366
- // Ensure that the issue is in the data.
367
- issueSet.add(issueID);
368
- // 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:
369
386
  if (catalogIndex) {
370
- // Ensure that the violator is in the data.
371
- violatorSet.add(catalogIndex);
387
+ // Ensure that the violator is in the temporary data.
388
+ violatorIndexSet.add(catalogIndex);
372
389
  }
373
390
  }
374
391
  });
375
392
  }
376
393
  });
377
- // Add the IDs of any prevented tools to the data.
378
- 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;
379
411
  // Return the data.
380
412
  return data;
381
413
  }
@@ -480,7 +512,7 @@ const getRecs = exports.getRecs = async () => {
480
512
  };
481
513
  // Returns a string of tool names.
482
514
  exports.getToolNamesString = toolIDSet =>
483
- alphaSort(Array.from(toolIDSet).map(toolID => tools[toolID][0])).join(' + ');
515
+ alphaSort(Array.from(toolIDSet).map(toolID => tools[toolID]?.[0] || toolID)).join(' + ');
484
516
  // Converts a catalog item text to a text-fragment link destination.
485
517
  exports.getTextFragmentHref = (text, url) => {
486
518
  const fragmentList = text
@@ -490,7 +522,7 @@ exports.getTextFragmentHref = (text, url) => {
490
522
  // Return a text-fragment link.
491
523
  return `${url}#:~:text=${fragmentList}`;
492
524
  };
493
- // 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.
494
526
  exports.getLogs = async () => {
495
527
  // Initialize data on the tested targets.
496
528
  const logs = [];
@@ -654,7 +686,7 @@ exports.getWCAGLink = numericID => {
654
686
  // Return the link.
655
687
  return `https://www.w3.org/WAI/WCAG22/Understanding/${wcagMap[numericID]}`;
656
688
  };
657
- // Gets page data from a report.
689
+ // Returns page data from a report.
658
690
  const getPageData = exports.getPageData = async (timeStamp, jobID) => {
659
691
  // Get the log of the report.
660
692
  const log = await getLog(timeStamp, jobID, false);
@@ -675,7 +707,7 @@ const getPageData = exports.getPageData = async (timeStamp, jobID) => {
675
707
  };
676
708
  // Gets HTML strings for page data from a report.
677
709
  exports.getPageDataStrings = async (timeStamp, jobID, pageData) => {
678
- // If the paga data were not specified:
710
+ // If the page data were not specified:
679
711
  if (! pageData) {
680
712
  // Get them.
681
713
  pageData = await getPageData(timeStamp, jobID);
@@ -690,3 +722,35 @@ exports.getPageDataStrings = async (timeStamp, jobID, pageData) => {
690
722
  testInfo: `Tested ${daysAgo} days ago by job <code>${jobID}</code> on ${when}`
691
723
  };
692
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}`;