@jrpool/kilotest 57.2.0 → 57.2.3
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/package.json +1 -1
- package/util.test.ts +0 -205
- package/util.ts +2 -240
- package/web/deleteNotesForm/index.ts +8 -1
- package/web/listDiagnoses/index.ts +6 -4
- package/web/listDiagnoses/listDiagnoses.test.ts +8 -2
- package/web/listIssues/index.ts +7 -5
- package/web/listIssues/listIssues.test.ts +4 -3
- package/web/listReports/index.ts +6 -4
- package/web/listRules/index.ts +3 -1
- package/web/listTopIssues/index.ts +6 -4
- package/web/listViolators/index.ts +9 -7
- package/web/listViolators/listViolators.test.ts +8 -2
- package/web/metrics/index.ts +9 -2
- package/web/reportDeletion.ts +8 -1
- package/web/requestRetestForm/index.ts +2 -1
- package/web/tutorialAI/index.html +25 -22
- package/web/tutorialAI/index.ts +2 -1
- package/web/tutorialWeb/index.ts +2 -1
- package/web/util.test.ts +240 -0
- package/web/util.ts +266 -0
package/web/util.ts
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/*
|
|
2
|
+
util.ts
|
|
3
|
+
Utility functions.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
/* c8 ignore start */
|
|
9
|
+
// c8 intermittently reports import lines as uncovered due to a range-merge
|
|
10
|
+
// artifact in its remapping of Node's type-stripped source.
|
|
11
|
+
import {
|
|
12
|
+
alphaCompare,
|
|
13
|
+
getAgoDays,
|
|
14
|
+
getDateString,
|
|
15
|
+
getDateTime,
|
|
16
|
+
getReport,
|
|
17
|
+
getReportExtracts,
|
|
18
|
+
getTestActInstances,
|
|
19
|
+
getTestActs,
|
|
20
|
+
isReportError,
|
|
21
|
+
ruleEngines
|
|
22
|
+
} from '../util.ts';
|
|
23
|
+
import {issues as issueSpecs} from 'testaro-issues';
|
|
24
|
+
import type {Catalog} from 'testaro';
|
|
25
|
+
/* c8 ignore stop */
|
|
26
|
+
import wcagMap from '../wcagMap.json' with {type: 'json'};
|
|
27
|
+
|
|
28
|
+
// TYPES
|
|
29
|
+
|
|
30
|
+
// Page data from an available report.
|
|
31
|
+
export type PageData = {
|
|
32
|
+
description: string;
|
|
33
|
+
url: string;
|
|
34
|
+
daysAgo: number | null;
|
|
35
|
+
error?: never;
|
|
36
|
+
};
|
|
37
|
+
// HTML strings describing the page data of an available report.
|
|
38
|
+
export type PageDataStrings = {
|
|
39
|
+
description: string;
|
|
40
|
+
url: string;
|
|
41
|
+
urlLink: string;
|
|
42
|
+
testInfo: string;
|
|
43
|
+
error?: never;
|
|
44
|
+
};
|
|
45
|
+
// Basics about an available report.
|
|
46
|
+
export type ReportData = {
|
|
47
|
+
description: string;
|
|
48
|
+
url: string;
|
|
49
|
+
jobName: unknown;
|
|
50
|
+
creationDate: Date | null;
|
|
51
|
+
daysAgo: number | null;
|
|
52
|
+
issueCount: number;
|
|
53
|
+
engineNames: string[];
|
|
54
|
+
engineCount: number;
|
|
55
|
+
reporterNames: string[];
|
|
56
|
+
reporterCount: number;
|
|
57
|
+
violatorCount: number;
|
|
58
|
+
preventedEngineNames: string[];
|
|
59
|
+
preventedEngineCount: number;
|
|
60
|
+
error?: never;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// FUNCTIONS
|
|
64
|
+
|
|
65
|
+
// Sorts strings alphabetically and case-insensitively.
|
|
66
|
+
const alphaSort = (strings: string[]) => strings.sort((a, b) => alphaCompare(a, b));
|
|
67
|
+
// Returns whether a comment's raw length is within the 20-to-1000-character bounds.
|
|
68
|
+
export const checkCommentLength = (
|
|
69
|
+
content: string
|
|
70
|
+
): {status: 'ok'} | {status: 'error'; message: string} => {
|
|
71
|
+
const contentLength = content.length;
|
|
72
|
+
// If the length is invalid:
|
|
73
|
+
if (contentLength < 20 || contentLength > 1000) {
|
|
74
|
+
// Return the applicable error message.
|
|
75
|
+
const messageSpec = contentLength < 20 ? 'shorter than 20' : 'longer than 1000';
|
|
76
|
+
return {
|
|
77
|
+
status: 'error',
|
|
78
|
+
message: `Your comment was ${messageSpec} characters`
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {status: 'ok'};
|
|
82
|
+
};
|
|
83
|
+
// Returns a string encoded for use as a URL fragment.
|
|
84
|
+
const fragmentEncode = (string: string) => {
|
|
85
|
+
return encodeURIComponent(string).replace(/-/g, '%2D');
|
|
86
|
+
};
|
|
87
|
+
// Returns a string describing the time in days since a time stamp.
|
|
88
|
+
export const getAgoString = (timeStamp: string): string => {
|
|
89
|
+
const agoDays = getAgoDays(timeStamp);
|
|
90
|
+
if (agoDays === null) {
|
|
91
|
+
return 'an unknown number of days';
|
|
92
|
+
}
|
|
93
|
+
return agoDays === 1 ? '1 day' : `${agoDays} days`;
|
|
94
|
+
};
|
|
95
|
+
// Returns a string describing a count.
|
|
96
|
+
export const getCountString = (count: number, singular: string, plural: string): string => count === 1 ? `1 ${singular}` : `${count} ${plural}`;
|
|
97
|
+
// Returns a time string from a time stamp.
|
|
98
|
+
const getTimeString = (timeStamp: string) => {
|
|
99
|
+
const timeString = `${timeStamp.slice(7, 9)}:${timeStamp.slice(9, 11)}`;
|
|
100
|
+
// Return the time string if valid, or null if not.
|
|
101
|
+
return (!isNaN(Date.parse(`2000-01-01T${timeString}Z`))) ? timeString : null;
|
|
102
|
+
};
|
|
103
|
+
// Returns a date-and-time string.
|
|
104
|
+
export const getDateTimeString = (timeStamp: string): string => {
|
|
105
|
+
const dateString = getDateString(timeStamp) || 'an unknown date';
|
|
106
|
+
const timeString = getTimeString(timeStamp) || 'an unknown time';
|
|
107
|
+
const dateTimeString = `${dateString} at ${timeString}`;
|
|
108
|
+
return dateTimeString;
|
|
109
|
+
}
|
|
110
|
+
// Converts a catalog item text to a text-fragment link destination.
|
|
111
|
+
export const getTextFragmentHref = (text: string, url: string): string => {
|
|
112
|
+
const fragmentList = text
|
|
113
|
+
.split('\n')
|
|
114
|
+
.map(fragment => fragmentEncode(fragment))
|
|
115
|
+
.join(',');
|
|
116
|
+
// Return a text-fragment link.
|
|
117
|
+
return `${url}#:~:text=${fragmentList}`;
|
|
118
|
+
};
|
|
119
|
+
// Returns a +-delimited list of sorted names of rule engines.
|
|
120
|
+
export const getEngineNamesString = (engineIDSet: Iterable<string>): string => alphaSort(
|
|
121
|
+
Array.from(engineIDSet).map(engineID => ruleEngines[engineID]?.[0] || engineID)
|
|
122
|
+
).join(' + ');
|
|
123
|
+
// Gets the WCAG Understanding link for a numeric WCAG standard identifier.
|
|
124
|
+
export const getWCAGLink = (numericID: string): string => {
|
|
125
|
+
// Return the link.
|
|
126
|
+
return `https://www.w3.org/WAI/WCAG22/Understanding/${(wcagMap as Record<string, string>)[numericID]}`;
|
|
127
|
+
};
|
|
128
|
+
// Gets the name of an issue weight.
|
|
129
|
+
export const getWeightName = (weight: number): string => ['lowest', 'low', 'high', 'highest'][weight - 1] ?? 'unknown';
|
|
130
|
+
// Makes a string breakable before non-initial slashes.
|
|
131
|
+
export const makeBreakable = (string: string): string => string.replace(/\//g, '<wbr>/').replace(/^<wbr>/, '');
|
|
132
|
+
// Returns the path ID of the element of a standard instance.
|
|
133
|
+
export const getPathID = (catalog: Catalog, catalogIndex: string, pathID?: string) => {
|
|
134
|
+
if (catalogIndex) {
|
|
135
|
+
const pathIDFromCatalog = (catalog[catalogIndex] || {}).pathID;
|
|
136
|
+
if (pathIDFromCatalog) {
|
|
137
|
+
return pathIDFromCatalog;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return pathID ?? '/html';
|
|
141
|
+
};
|
|
142
|
+
// Returns basics about an available report.
|
|
143
|
+
export const getReportData = async (timeStamp: string, jobID: string): Promise<ReportData | {error: string}> => {
|
|
144
|
+
// Get the report.
|
|
145
|
+
const report = await getReport(timeStamp, jobID);
|
|
146
|
+
// If this failed:
|
|
147
|
+
if (isReportError(report)) {
|
|
148
|
+
// Return why.
|
|
149
|
+
return {error: report.error};
|
|
150
|
+
}
|
|
151
|
+
// Otherwise, i.e. if it succeeded, initialize the data.
|
|
152
|
+
const data = {
|
|
153
|
+
description: report.target.what,
|
|
154
|
+
url: report.target.url,
|
|
155
|
+
jobName: report.id,
|
|
156
|
+
creationDate: getDateTime(timeStamp),
|
|
157
|
+
daysAgo: getAgoDays(timeStamp),
|
|
158
|
+
issueCount: 0,
|
|
159
|
+
engineNames: [] as string[],
|
|
160
|
+
engineCount: 0,
|
|
161
|
+
reporterNames: [] as string[],
|
|
162
|
+
reporterCount: 0,
|
|
163
|
+
violatorCount: 0,
|
|
164
|
+
preventedEngineNames: [] as string[],
|
|
165
|
+
preventedEngineCount: 0
|
|
166
|
+
};
|
|
167
|
+
const issueIDSet = new Set<string>();
|
|
168
|
+
const engineNameSet = new Set<string>();
|
|
169
|
+
const reporterIDSet = new Set<string>();
|
|
170
|
+
const violatorIndexSet = new Set<string>();
|
|
171
|
+
// For each test act of the report:
|
|
172
|
+
getTestActs(report).forEach(act => {
|
|
173
|
+
// Ensure that the rule engine is in the temporary data.
|
|
174
|
+
engineNameSet.add(ruleEngines[act.which!]![0]);
|
|
175
|
+
});
|
|
176
|
+
// For each violating standard instance of each test act:
|
|
177
|
+
getTestActInstances(report, {violationsOnly: true}).forEach(({act, instance}) => {
|
|
178
|
+
const {catalogIndex, issueID} = instance;
|
|
179
|
+
// If it has a non-ignorable classified issue ID:
|
|
180
|
+
if (issueID && issueSpecs[issueID] && issueID !== 'ignorable') {
|
|
181
|
+
// Ensure that the rule engine is in the temporary data.
|
|
182
|
+
reporterIDSet.add(act.which!);
|
|
183
|
+
// Ensure that the issue is in the temporary data.
|
|
184
|
+
issueIDSet.add(issueID);
|
|
185
|
+
// If the violator has a catalog index:
|
|
186
|
+
if (catalogIndex) {
|
|
187
|
+
// Ensure that the violator is in the temporary data.
|
|
188
|
+
violatorIndexSet.add(String(catalogIndex));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
// Populate the data with the act data.
|
|
193
|
+
data.issueCount = issueIDSet.size;
|
|
194
|
+
data.engineNames = Array
|
|
195
|
+
.from(engineNameSet)
|
|
196
|
+
.sort((a, b) => a.localeCompare(b, 'en', {sensitivity: 'base'}));
|
|
197
|
+
data.engineCount = engineNameSet.size;
|
|
198
|
+
data.reporterNames = Array
|
|
199
|
+
.from(reporterIDSet)
|
|
200
|
+
.map(id => ruleEngines[id]![0])
|
|
201
|
+
.sort((a, b) => a.localeCompare(b, 'en', {sensitivity: 'base'}));
|
|
202
|
+
data.reporterCount = data.reporterNames.length;
|
|
203
|
+
data.violatorCount = violatorIndexSet.size;
|
|
204
|
+
// Add the names of any prevented rule engines to the data.
|
|
205
|
+
data.preventedEngineNames = Object.keys(report.jobData?.preventions || {})
|
|
206
|
+
.map(engineID => ruleEngines[engineID]?.[0] || engineID)
|
|
207
|
+
.sort((a, b) => a.localeCompare(b, 'en', {sensitivity: 'base'}));
|
|
208
|
+
data.preventedEngineCount = data.preventedEngineNames.length;
|
|
209
|
+
// Return the data.
|
|
210
|
+
return data;
|
|
211
|
+
}
|
|
212
|
+
// Returns page data from an available report.
|
|
213
|
+
export const getPageData = async (timeStamp: string, jobID: string): Promise<PageData | {error: string}> => {
|
|
214
|
+
// Get the report.
|
|
215
|
+
const report = await getReport(timeStamp, jobID);
|
|
216
|
+
// If this failed:
|
|
217
|
+
if (isReportError(report)) {
|
|
218
|
+
// Return why.
|
|
219
|
+
return report;
|
|
220
|
+
}
|
|
221
|
+
const {what: description, url} = report.target;
|
|
222
|
+
// Get the elapsed time in days since the report was completed, using the
|
|
223
|
+
// report content rather than the file system birth time.
|
|
224
|
+
const daysAgo = getAgoDays(new Date(`20${report.jobData.endTime}Z`));
|
|
225
|
+
// Return the data.
|
|
226
|
+
return {
|
|
227
|
+
description,
|
|
228
|
+
url,
|
|
229
|
+
daysAgo
|
|
230
|
+
};
|
|
231
|
+
};
|
|
232
|
+
// Gets HTML strings for page data from a report.
|
|
233
|
+
export const getPageDataStrings = async (
|
|
234
|
+
timeStamp: string,
|
|
235
|
+
jobID: string,
|
|
236
|
+
pageData?: PageData | {error: string}
|
|
237
|
+
): Promise<PageDataStrings | {error: string}> => {
|
|
238
|
+
// Get the page data if they were not specified.
|
|
239
|
+
const data = pageData ?? await getPageData(timeStamp, jobID);
|
|
240
|
+
// If the page data are invalid:
|
|
241
|
+
if (data.error !== undefined) {
|
|
242
|
+
// Return why.
|
|
243
|
+
return {
|
|
244
|
+
error: data.error
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const {daysAgo, url, description} = data;
|
|
248
|
+
// Otherwise, i.e. if they are valid, get a description of the timestamp.
|
|
249
|
+
const when = getDateTimeString(timeStamp);
|
|
250
|
+
// Return the HTML strings.
|
|
251
|
+
return {
|
|
252
|
+
description,
|
|
253
|
+
url,
|
|
254
|
+
urlLink: `<a href="${url}">${url}</a>`,
|
|
255
|
+
testInfo: `Tested ${daysAgo === 1 ? '1 day' : `${daysAgo} days`} ago by job <code>${jobID}</code> on ${when}`
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
// Gets the descriptions of multi-report pages.
|
|
259
|
+
export const getMultiReportWhats = async (): Promise<string[]> => {
|
|
260
|
+
const reportExtracts = await getReportExtracts();
|
|
261
|
+
const sortedDescriptions = reportExtracts.map(extract => extract.description).sort();
|
|
262
|
+
const multiReportDescriptions = sortedDescriptions.filter(
|
|
263
|
+
(description, index) => description !== sortedDescriptions[index - 1] && description === sortedDescriptions[index + 1]
|
|
264
|
+
);
|
|
265
|
+
return multiReportDescriptions;
|
|
266
|
+
};
|