testaro 64.5.0 → 64.6.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/package.json +1 -1
- package/procs/nu.js +6 -44
- package/procs/standardize.js +7 -5
- package/procs/testaro.js +53 -0
- package/run.js +37 -2
- package/tests/htmlcs.js +29 -23
package/package.json
CHANGED
package/procs/nu.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
// ########## IMPORTS
|
|
16
16
|
|
|
17
|
+
// Module to add Testaro IDs to elements.
|
|
18
|
+
const {addTestaroIDs, getLocationData} = require('./testaro');
|
|
17
19
|
// Module to get the document source.
|
|
18
20
|
const {getSource} = require('./getSource');
|
|
19
21
|
|
|
@@ -43,13 +45,8 @@ exports.getContent = async (page, withSource) => {
|
|
|
43
45
|
}
|
|
44
46
|
// Otherwise, i.e. if the specified content type was the Playwright page content:
|
|
45
47
|
else {
|
|
46
|
-
// Annotate all elements
|
|
47
|
-
await page
|
|
48
|
-
let serialID = 0;
|
|
49
|
-
for (const element of Array.from(document.querySelectorAll('*'))) {
|
|
50
|
-
element.setAttribute('data-testaro-id', `${serialID++}#`);
|
|
51
|
-
}
|
|
52
|
-
});
|
|
48
|
+
// Annotate all elements on the page with unique identifiers.
|
|
49
|
+
await addTestaroIDs(page);
|
|
53
50
|
// Add the annotated page content to the data.
|
|
54
51
|
data.testTarget = await page.content();
|
|
55
52
|
}
|
|
@@ -94,43 +91,8 @@ exports.curate = async (page, data, nuData, rules) => {
|
|
|
94
91
|
// For each message:
|
|
95
92
|
for (const message of result.messages) {
|
|
96
93
|
const {extract} = message;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (testaroIDArray) {
|
|
100
|
-
const testaroID = message.testaroID = testaroIDArray[1];
|
|
101
|
-
// Add location data for the element to the message.
|
|
102
|
-
message.elementLocation = await page.evaluate(testaroID => {
|
|
103
|
-
const element = document.querySelector(`[data-testaro-id="${testaroID}#"]`);
|
|
104
|
-
// If any element has that identifier:
|
|
105
|
-
if (element) {
|
|
106
|
-
// Get a box specification and an XPath for the element.
|
|
107
|
-
const box = {};
|
|
108
|
-
const boundingBox = element.getBoundingClientRect() || {};
|
|
109
|
-
if (boundingBox.x) {
|
|
110
|
-
['x', 'y', 'width', 'height'].forEach(coordinate => {
|
|
111
|
-
box[coordinate] = Math.round(boundingBox[coordinate]);
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
const xPath = window.getXPath(element) || '';
|
|
115
|
-
// Treat them as the element location.
|
|
116
|
-
return {
|
|
117
|
-
box,
|
|
118
|
-
xPath
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
// Otherwise, i.e. if no element has it, make the location data empty.
|
|
122
|
-
return {};
|
|
123
|
-
}, testaroID);
|
|
124
|
-
}
|
|
125
|
-
// Otherwise, i.e. if its extract contains no Testaro identifier:
|
|
126
|
-
else {
|
|
127
|
-
// Add a non-DOM location to the message.
|
|
128
|
-
message.elementLocation = {
|
|
129
|
-
notInDOM: true,
|
|
130
|
-
box: {},
|
|
131
|
-
xPath: ''
|
|
132
|
-
};
|
|
133
|
-
}
|
|
94
|
+
// Add location data for the element to the message.
|
|
95
|
+
message.elementLocation = await getLocationData(page, extract);
|
|
134
96
|
}
|
|
135
97
|
}
|
|
136
98
|
// Return the result.
|
package/procs/standardize.js
CHANGED
|
@@ -189,7 +189,7 @@ const doHTMLCS = (result, standardResult, severity) => {
|
|
|
189
189
|
const ruleData = result[severity][ruleID];
|
|
190
190
|
Object.keys(ruleData).forEach(what => {
|
|
191
191
|
ruleData[what].forEach(item => {
|
|
192
|
-
const {tagName, id,
|
|
192
|
+
const {tagName, id, excerpt, notInDOM, boxID, pathID} = item;
|
|
193
193
|
const instance = {
|
|
194
194
|
ruleID,
|
|
195
195
|
what,
|
|
@@ -197,11 +197,13 @@ const doHTMLCS = (result, standardResult, severity) => {
|
|
|
197
197
|
tagName: tagName.toUpperCase(),
|
|
198
198
|
id: isBadID(id.slice(1)) ? '' : id.slice(1),
|
|
199
199
|
location: {
|
|
200
|
-
doc: 'dom',
|
|
200
|
+
doc: notInDOM ? 'notInDOM' : 'dom',
|
|
201
201
|
type: '',
|
|
202
202
|
spec: ''
|
|
203
203
|
},
|
|
204
|
-
excerpt: cap(
|
|
204
|
+
excerpt: cap(excerpt),
|
|
205
|
+
boxID,
|
|
206
|
+
pathID
|
|
205
207
|
};
|
|
206
208
|
standardResult.instances.push(instance);
|
|
207
209
|
});
|
|
@@ -253,8 +255,8 @@ const doNu = (withSource, result, standardResult) => {
|
|
|
253
255
|
spec
|
|
254
256
|
},
|
|
255
257
|
excerpt: cap(extract),
|
|
256
|
-
boxID: elementLocation?.
|
|
257
|
-
pathID: elementLocation?.
|
|
258
|
+
boxID: elementLocation?.boxID || '',
|
|
259
|
+
pathID: elementLocation?.pathID || ''
|
|
258
260
|
};
|
|
259
261
|
if (type === 'info' && subType === 'warning') {
|
|
260
262
|
instance.ordinalSeverity = 0;
|
package/procs/testaro.js
CHANGED
|
@@ -225,3 +225,56 @@ exports.getVisibleCountChange = async (
|
|
|
225
225
|
elapsedTime
|
|
226
226
|
};
|
|
227
227
|
};
|
|
228
|
+
// Annotates every element on a page with a unique identifier.
|
|
229
|
+
exports.addTestaroIDs = async page => {
|
|
230
|
+
await page.evaluate(() => {
|
|
231
|
+
let serialID = 0;
|
|
232
|
+
for (const element of Array.from(document.querySelectorAll('*'))) {
|
|
233
|
+
element.setAttribute('data-testaro-id', `${serialID++}#`);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
// Returns location data from the extract of a standard instance.
|
|
238
|
+
exports.getLocationData = async (page, extract) => {
|
|
239
|
+
const testaroIDArray = extract.match(/data-testaro-id="(\d+)#"/);
|
|
240
|
+
// If the extract contains a Testaro identifier:
|
|
241
|
+
if (testaroIDArray) {
|
|
242
|
+
const testaroID = testaroIDArray[1];
|
|
243
|
+
// Return location data for the element.
|
|
244
|
+
return await page.evaluate(testaroID => {
|
|
245
|
+
const element = document.querySelector(`[data-testaro-id="${testaroID}#"]`);
|
|
246
|
+
// If any element has that identifier:
|
|
247
|
+
if (element) {
|
|
248
|
+
// Get box and path IDs for the element.
|
|
249
|
+
const box = {};
|
|
250
|
+
let boxID = '';
|
|
251
|
+
const boundingBox = element.getBoundingClientRect() || {};
|
|
252
|
+
if (boundingBox.x) {
|
|
253
|
+
['x', 'y', 'width', 'height'].forEach(coordinate => {
|
|
254
|
+
box[coordinate] = Math.round(boundingBox[coordinate]);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
if (typeof box.x === 'number') {
|
|
258
|
+
boxID = Object.values(box).join(':');
|
|
259
|
+
}
|
|
260
|
+
const pathID = window.getXPath(element) || '';
|
|
261
|
+
// Return them.
|
|
262
|
+
return {
|
|
263
|
+
boxID,
|
|
264
|
+
pathID
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
// Otherwise, i.e. if no element has it, return empty location data.
|
|
268
|
+
return {};
|
|
269
|
+
}, testaroID);
|
|
270
|
+
}
|
|
271
|
+
// Otherwise, i.e. if the extract contains no Testaro identifier:
|
|
272
|
+
else {
|
|
273
|
+
// Return a non-DOM location.
|
|
274
|
+
return {
|
|
275
|
+
notInDOM: true,
|
|
276
|
+
boxID: '',
|
|
277
|
+
pathID: ''
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
};
|
package/run.js
CHANGED
|
@@ -414,9 +414,10 @@ const launch = exports.launch = async (
|
|
|
414
414
|
});
|
|
415
415
|
});
|
|
416
416
|
const isTestaroTest = act.type === 'test' && act.which === 'testaro';
|
|
417
|
+
const isHTMLCSTest = act.type === 'test' && act.which === 'htmlcs';
|
|
417
418
|
const isNuTest = act.type === 'test' && (['nuVal', 'nuVnu'].some(id => act.which === id));
|
|
418
|
-
// If the launch is for a testaro or Nu test act:
|
|
419
|
-
if (isTestaroTest || isNuTest) {
|
|
419
|
+
// If the launch is for a testaro,3 htmlcs, or Nu test act:
|
|
420
|
+
if (isTestaroTest || isHTMLCSTest || isNuTest) {
|
|
420
421
|
// Add a script to the page to add a window method to get the XPath of an element.
|
|
421
422
|
await page.addInitScript(() => {
|
|
422
423
|
window.getXPath = element => {
|
|
@@ -1674,6 +1675,40 @@ const doActs = async (report, opts = {}) => {
|
|
|
1674
1675
|
// Close the last browser launched for standardization.
|
|
1675
1676
|
await browserClose();
|
|
1676
1677
|
console.log('Standardization completed');
|
|
1678
|
+
// XXX
|
|
1679
|
+
const {acts} = report;
|
|
1680
|
+
const idData = {};
|
|
1681
|
+
for (const act of acts) {
|
|
1682
|
+
if (act.type === 'test') {
|
|
1683
|
+
const {which} = act;
|
|
1684
|
+
idData[which] ??= {
|
|
1685
|
+
instanceCount: 0,
|
|
1686
|
+
boxIDCount: 0,
|
|
1687
|
+
pathIDCount: 0,
|
|
1688
|
+
boxIDPercent: null,
|
|
1689
|
+
pathIDPercent: null
|
|
1690
|
+
};
|
|
1691
|
+
const actIDData = idData[which];
|
|
1692
|
+
const {standardResult} = act;
|
|
1693
|
+
const {instances} = standardResult;
|
|
1694
|
+
for (const instance of instances) {
|
|
1695
|
+
const {boxID, pathID} = instance;
|
|
1696
|
+
actIDData.instanceCount++;
|
|
1697
|
+
if (boxID) {
|
|
1698
|
+
actIDData.boxIDCount++;
|
|
1699
|
+
}
|
|
1700
|
+
if (pathID) {
|
|
1701
|
+
actIDData.pathIDCount++;
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
const {instanceCount, boxIDCount, pathIDCount} = actIDData;
|
|
1705
|
+
if (instanceCount) {
|
|
1706
|
+
actIDData.boxIDPercent = Math.round(100 * boxIDCount / instanceCount);
|
|
1707
|
+
actIDData.pathIDPercent = Math.round(100 * pathIDCount / instanceCount);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
report.jobData.idData = idData;
|
|
1677
1712
|
}
|
|
1678
1713
|
// Delete the temporary report file.
|
|
1679
1714
|
await fs.rm(reportPath, {force: true});
|
package/tests/htmlcs.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
// IMPORTS
|
|
16
16
|
|
|
17
|
+
// Module to add Testaro IDs to elements.
|
|
18
|
+
const {addTestaroIDs, getLocationData} = require('../procs/testaro');
|
|
17
19
|
// Module to handle files.
|
|
18
20
|
const fs = require('fs/promises');
|
|
19
21
|
|
|
@@ -28,10 +30,12 @@ exports.reporter = async (page, report, actIndex) => {
|
|
|
28
30
|
// Get the HTMLCS script.
|
|
29
31
|
const scriptText = await fs.readFile(`${__dirname}/../htmlcs/HTMLCS.js`, 'utf8');
|
|
30
32
|
const scriptNonce = report.jobData && report.jobData.lastScriptNonce;
|
|
31
|
-
//
|
|
33
|
+
// Annotate all elements on the page with unique identifiers.
|
|
34
|
+
await addTestaroIDs(page);
|
|
32
35
|
let messageStrings = [];
|
|
36
|
+
// Define the rules to be employed as those of WCAG 2 level AAA.
|
|
33
37
|
for (const actStandard of ['WCAG2AAA']) {
|
|
34
|
-
const
|
|
38
|
+
const nextViolations = await page.evaluate(args => {
|
|
35
39
|
// Add the HTMLCS script to the page.
|
|
36
40
|
const scriptText = args[2];
|
|
37
41
|
const scriptNonce = args[3];
|
|
@@ -49,18 +53,18 @@ exports.reporter = async (page, report, actIndex) => {
|
|
|
49
53
|
}
|
|
50
54
|
window.HTMLCS_WCAG2AAA.sniffs = rules;
|
|
51
55
|
}
|
|
56
|
+
let violations = null;
|
|
52
57
|
// Run the tests.
|
|
53
|
-
let issues = null;
|
|
54
58
|
try {
|
|
55
|
-
|
|
59
|
+
violations = window.HTMLCS_RUNNER.run(actStandard);
|
|
56
60
|
}
|
|
57
61
|
catch(error) {
|
|
58
62
|
console.log(`ERROR executing HTMLCS_RUNNER on ${document.URL} (${error.message})`);
|
|
59
63
|
}
|
|
60
|
-
return
|
|
64
|
+
return violations;
|
|
61
65
|
}, [actStandard, rules, scriptText, scriptNonce]);
|
|
62
|
-
if (
|
|
63
|
-
messageStrings.push(...
|
|
66
|
+
if (nextViolations && nextViolations.every(violation => typeof violation === 'string')) {
|
|
67
|
+
messageStrings.push(... nextViolations);
|
|
64
68
|
}
|
|
65
69
|
else {
|
|
66
70
|
data.prevented = true;
|
|
@@ -69,39 +73,41 @@ exports.reporter = async (page, report, actIndex) => {
|
|
|
69
73
|
}
|
|
70
74
|
}
|
|
71
75
|
if (! data.prevented) {
|
|
72
|
-
// Sort the
|
|
76
|
+
// Sort the violations by class and standard.
|
|
73
77
|
messageStrings.sort();
|
|
74
|
-
// Remove any duplicate
|
|
78
|
+
// Remove any duplicate violations.
|
|
75
79
|
messageStrings = [... new Set(messageStrings)];
|
|
76
80
|
// Initialize the result.
|
|
77
81
|
result.Error = {};
|
|
78
82
|
result.Warning = {};
|
|
79
|
-
// For each
|
|
80
|
-
messageStrings.forEach(string => {
|
|
83
|
+
// For each violation:
|
|
84
|
+
messageStrings.forEach(async string => {
|
|
85
|
+
// Split its message into severity class, rule ID, tagname, ID, rule description, and excerpt.
|
|
81
86
|
const parts = string.split(/\|/, 6);
|
|
82
87
|
const partCount = parts.length;
|
|
83
88
|
if (partCount < 6) {
|
|
84
|
-
console.log(`ERROR:
|
|
89
|
+
console.log(`ERROR: Violation string ${string} has too few parts`);
|
|
85
90
|
}
|
|
86
91
|
// If it is an error or a warning (not a notice):
|
|
87
92
|
else if (['Error', 'Warning'].includes(parts[0])) {
|
|
88
93
|
/*
|
|
89
|
-
Add the
|
|
90
|
-
This saves space, because, although some descriptions are
|
|
94
|
+
Add the violation to an violationClass.violationCode.description array in the result.
|
|
95
|
+
This saves space, because, although some descriptions are violation-specific, such as
|
|
91
96
|
descriptions that state the contrast ratio of an element, most descriptions are
|
|
92
97
|
generic, so typically many violations share a description.
|
|
93
98
|
*/
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
result[parts[0]][issueCode][parts[4]].push({
|
|
99
|
+
const ruleID = parts[1].replace(/^WCAG2|\.Principle\d\.Guideline[\d_]+/g, '');
|
|
100
|
+
result[parts[0]][ruleID] ??= {};
|
|
101
|
+
result[parts[0]][ruleID][parts[4]] ??= [];
|
|
102
|
+
const elementLocation = await getLocationData(page, parts[5]);
|
|
103
|
+
const {boxID, notInDOM, pathID} = elementLocation;
|
|
104
|
+
result[parts[0]][ruleID][parts[4]].push({
|
|
102
105
|
tagName: parts[2],
|
|
103
106
|
id: parts[3],
|
|
104
|
-
|
|
107
|
+
notInDOM,
|
|
108
|
+
excerpt: parts[5],
|
|
109
|
+
boxID,
|
|
110
|
+
pathID
|
|
105
111
|
});
|
|
106
112
|
}
|
|
107
113
|
});
|