testaro 66.0.0 → 67.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/README.md +1 -1
- package/UPGRADES.md +1 -1
- package/package.json +2 -2
- package/procs/getLocatorData.js +63 -34
- package/procs/identify.js +29 -24
- package/procs/nu.js +2 -2
- package/procs/standardize.js +2 -181
- package/run.js +2 -2
- package/testaro/motion.js +2 -2
- package/tests/alfa.js +11 -2
- package/tests/aslint.js +218 -42
- package/tests/htmlcs.js +2 -2
- package/tests/qualWeb.js +2 -2
- package/tests/wax.js +2 -2
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ Testaro uses:
|
|
|
48
48
|
- [Playwright](https://playwright.dev/) to launch browsers, perform user actions in them, and perform tests
|
|
49
49
|
- [playwright-extra](https://www.npmjs.com/package/playwright-extra) and [puppeteer-extra-plugin-stealth](https://www.npmjs.com/package/puppeteer-extra-plugin-stealth) to make a Playwright-controlled browser more indistinguishable from a human-operated browser and thus make their requests more likely to succeed
|
|
50
50
|
- [playwright-dompath](https://www.npmjs.com/package/playwright-dompath) to retrieve XPaths of elements
|
|
51
|
-
- [
|
|
51
|
+
- [BlazeDiff](https://blazediff.dev/) to measure motion
|
|
52
52
|
|
|
53
53
|
Testaro performs tests of these _tools_:
|
|
54
54
|
|
package/UPGRADES.md
CHANGED
|
@@ -54,7 +54,7 @@ I'll quickly scan for module patterns and JSON imports across the repo to tailor
|
|
|
54
54
|
- Strategy for optional properties and progressive enrichment during execution.
|
|
55
55
|
|
|
56
56
|
- **3rd‑party types and augmentations**
|
|
57
|
-
- Verify/choose type packages for `playwright`, `@siteimprove/alfa-*`, `axe-playwright`, `@qualweb/*`, `accessibility-checker`, `playwright-dompath
|
|
57
|
+
- Verify/choose type packages for `playwright`, `@siteimprove/alfa-*`, `axe-playwright`, `@qualweb/*`, `accessibility-checker`, `playwright-dompath`. Plan fallbacks if defs are missing.
|
|
58
58
|
- Decide on module augmentation vs casting for custom fields (e.g., `page.browserID` assignment requires augmenting `playwright.Page` or casting).
|
|
59
59
|
|
|
60
60
|
- **Dynamic loading patterns**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testaro",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "67.0.0",
|
|
4
4
|
"description": "Run 1000 web accessibility tests from 11 tools and get a standardized report",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"homepage": "https://github.com/jrpool/testaro#readme",
|
|
32
32
|
"dependencies": {
|
|
33
|
+
"@blazediff/core": "*",
|
|
33
34
|
"@qualweb/core": "*",
|
|
34
35
|
"@qualweb/act-rules": "*",
|
|
35
36
|
"@qualweb/wcag-techniques": "*",
|
|
@@ -43,7 +44,6 @@
|
|
|
43
44
|
"aslint-testaro": "*",
|
|
44
45
|
"axe-playwright": "*",
|
|
45
46
|
"dotenv": "*",
|
|
46
|
-
"pixelmatch": "*",
|
|
47
47
|
"playwright": "*",
|
|
48
48
|
"playwright-dompath": "*",
|
|
49
49
|
"playwright-extra": "*",
|
package/procs/getLocatorData.js
CHANGED
|
@@ -101,20 +101,52 @@ exports.getLocatorData = async loc => {
|
|
|
101
101
|
return null;
|
|
102
102
|
}
|
|
103
103
|
};
|
|
104
|
-
// Returns
|
|
105
|
-
exports.
|
|
104
|
+
// Returns properties of an element with an excerpt when Testaro identifiers have been added.
|
|
105
|
+
exports.getElementData = async (page, excerpt) => {
|
|
106
|
+
// Initialize the properties.
|
|
107
|
+
let data = {
|
|
108
|
+
tagName: '',
|
|
109
|
+
id: '',
|
|
110
|
+
text: '',
|
|
111
|
+
notInDOM: true,
|
|
112
|
+
boxID: '',
|
|
113
|
+
pathID: '',
|
|
114
|
+
originalExcerpt: excerpt,
|
|
115
|
+
};
|
|
116
|
+
let elementProperties = {};
|
|
106
117
|
const testaroIDArray = excerpt.match(/data-testaro-id="(\d+)#([^"]*)"/);
|
|
107
|
-
// If the
|
|
118
|
+
// If the excerpt contains a Testaro identifier:
|
|
108
119
|
if (testaroIDArray) {
|
|
109
|
-
|
|
120
|
+
// Remove the identifier.
|
|
121
|
+
originalExcerpt = excerpt.replace(/ data-testaro-id="\d+#[^" ]+"?/, '');
|
|
122
|
+
elementProperties = await page.evaluate(testaroIDArray => {
|
|
123
|
+
let tagName = '';
|
|
124
|
+
let id = '';
|
|
125
|
+
const text = [];
|
|
126
|
+
let notInDOM = false;
|
|
127
|
+
let boxID = '';
|
|
128
|
+
let pathID = '';
|
|
110
129
|
const testaroID = `${testaroIDArray[1]}#${testaroIDArray[2]}`;
|
|
111
130
|
const element = document.querySelector(`[data-testaro-id="${testaroID}"]`);
|
|
112
|
-
// If any element has
|
|
131
|
+
// If any element has the identifier:
|
|
113
132
|
if (element) {
|
|
114
|
-
// Get
|
|
133
|
+
// Get properties of the element.
|
|
134
|
+
({tagName, id} = element);
|
|
135
|
+
const segments = element
|
|
136
|
+
.innerText
|
|
137
|
+
?.trim()
|
|
138
|
+
.split(/[\t\n]+/)
|
|
139
|
+
.filter(segment => segment.length);
|
|
140
|
+
if (segments?.length) {
|
|
141
|
+
if (segments.length > 1) {
|
|
142
|
+
text.push(segments[0], segments[segments.length - 1]);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
text.push(segments[0]);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const boundingBox = element.getBoundingClientRect() ?? {};
|
|
115
149
|
const box = {};
|
|
116
|
-
let boxID = '';
|
|
117
|
-
const boundingBox = element.getBoundingClientRect() || {};
|
|
118
150
|
if (boundingBox.x) {
|
|
119
151
|
['x', 'y', 'width', 'height'].forEach(coordinate => {
|
|
120
152
|
box[coordinate] = Math.round(boundingBox[coordinate]);
|
|
@@ -123,41 +155,38 @@ exports.getLocationData = async (page, excerpt) => {
|
|
|
123
155
|
if (typeof box.x === 'number') {
|
|
124
156
|
boxID = Object.values(box).join(':');
|
|
125
157
|
}
|
|
126
|
-
// Get a path ID from the
|
|
127
|
-
|
|
158
|
+
// Get a path ID from the identifier or, if necessary, the element.
|
|
159
|
+
pathID = testaroIDArray[2];
|
|
128
160
|
if (! pathID) {
|
|
129
161
|
pathID = window.getXPath(element);
|
|
130
162
|
}
|
|
131
|
-
// Return the box and path IDs.
|
|
132
|
-
return {
|
|
133
|
-
boxID,
|
|
134
|
-
pathID
|
|
135
|
-
};
|
|
136
163
|
}
|
|
137
|
-
// Otherwise, if no element has
|
|
138
|
-
else
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
notInDOM: true,
|
|
142
|
-
boxID: '',
|
|
143
|
-
pathID: testaroIDArray[2]
|
|
144
|
-
};
|
|
164
|
+
// Otherwise, i.e. if no element has the identifier:
|
|
165
|
+
else {
|
|
166
|
+
// Report this.
|
|
167
|
+
notInDOM = true;
|
|
145
168
|
}
|
|
146
|
-
//
|
|
169
|
+
// Report the properties.
|
|
147
170
|
return {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
171
|
+
tagName,
|
|
172
|
+
id,
|
|
173
|
+
text,
|
|
174
|
+
notInDOM,
|
|
175
|
+
boxID,
|
|
176
|
+
pathID
|
|
151
177
|
};
|
|
152
178
|
}, testaroIDArray);
|
|
179
|
+
// Populate the data with any properties obtained from the element.
|
|
180
|
+
Object.assign(data, elementProperties);
|
|
153
181
|
}
|
|
154
|
-
// Otherwise, i.e. if the
|
|
182
|
+
// Otherwise, i.e. if the excerpt contains no Testaro identifier:
|
|
155
183
|
else {
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
};
|
|
184
|
+
// Get properties of the element that are gettable from the excerpt.
|
|
185
|
+
const tagNameArray = excerpt.match(/<([-a-z]+)/);
|
|
186
|
+
data.tagName = tagNameArray?.[1] ?? '';
|
|
187
|
+
const idArray = excerpt.match(/ id="([^"]*)"/);
|
|
188
|
+
data.id = idArray?.[1] ?? '';
|
|
162
189
|
}
|
|
190
|
+
// Return the properties.
|
|
191
|
+
return data;
|
|
163
192
|
};
|
package/procs/identify.js
CHANGED
|
@@ -56,30 +56,35 @@ const boxToString = exports.boxToString = box => {
|
|
|
56
56
|
};
|
|
57
57
|
// Normalizes an XPath.
|
|
58
58
|
const getNormalizedXPath = exports.getNormalizedXPath = xPath => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
59
|
+
if (xPath) {
|
|
60
|
+
xPath = xPath.replace(/^\.\/\//, '/');
|
|
61
|
+
const segments = xPath.split('/');
|
|
62
|
+
// Initialize an array of normalized segments.
|
|
63
|
+
const normalizedSegments = [];
|
|
64
|
+
// For each segment of the XPath:
|
|
65
|
+
segments.forEach(segment => {
|
|
66
|
+
// If the segment is html[1] or body[1]:
|
|
67
|
+
if (/html\[1\]|body\[1\]/.test(segment)) {
|
|
68
|
+
// Add it without its subscript to the array.
|
|
69
|
+
normalizedSegments.push(segment.replace(/\[1\]/, ''));
|
|
70
|
+
}
|
|
71
|
+
// Otherwise, if the segment is empty or html or body or ends with a subscript:
|
|
72
|
+
else if (segment === '' || ['html', 'body'].includes(segment) || segment.endsWith(']')) {
|
|
73
|
+
// Add it to the array.
|
|
74
|
+
normalizedSegments.push(segment);
|
|
75
|
+
}
|
|
76
|
+
// Otherwise, i.e. if the segment is a tag name with no subscript:
|
|
77
|
+
else {
|
|
78
|
+
// Add it with a subscript 1 to the array.
|
|
79
|
+
normalizedSegments.push(`${segment}[1]`);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
// Return the concatenated segments as the normalized XPath.
|
|
83
|
+
return normalizedSegments.join('/');
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
return '';
|
|
87
|
+
}
|
|
83
88
|
};
|
|
84
89
|
// Adds a box ID and a path ID to an object.
|
|
85
90
|
const addIDs = async (locator, recipient) => {
|
package/procs/nu.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// Module to add Testaro IDs to elements.
|
|
18
18
|
const {addTestaroIDs} = require('./testaro');
|
|
19
19
|
// Module to get location data from an element.
|
|
20
|
-
const {
|
|
20
|
+
const {getElementData} = require('./getLocatorData');
|
|
21
21
|
// Module to get the document source.
|
|
22
22
|
const {getSource} = require('./getSource');
|
|
23
23
|
|
|
@@ -94,7 +94,7 @@ exports.curate = async (page, data, nuData, rules) => {
|
|
|
94
94
|
for (const message of result.messages) {
|
|
95
95
|
const {extract} = message;
|
|
96
96
|
// Add location data for the element to the message.
|
|
97
|
-
message.elementLocation = await
|
|
97
|
+
message.elementLocation = await getElementData(page, extract);
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
// Return the result.
|
package/procs/standardize.js
CHANGED
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
// IMPORTS
|
|
17
17
|
|
|
18
18
|
const {cap} = require('./job');
|
|
19
|
-
const {getNormalizedXPath} = require('./identify');
|
|
20
19
|
|
|
21
20
|
// FUNCTIONS
|
|
22
21
|
|
|
@@ -41,103 +40,6 @@ const getIdentifiers = exports.getIdentifiers = code => {
|
|
|
41
40
|
}
|
|
42
41
|
return ['', ''];
|
|
43
42
|
};
|
|
44
|
-
/*
|
|
45
|
-
Differentiates some rule IDs of aslint.
|
|
46
|
-
If the purported rule ID is a key and the what property contains all of the strings except the
|
|
47
|
-
last of any array item of the value of that key, then the final rule ID is the last item of that
|
|
48
|
-
array item.
|
|
49
|
-
*/
|
|
50
|
-
const aslintData = {
|
|
51
|
-
'misused_required_attribute': [
|
|
52
|
-
['not needed', 'misused_required_attributeR']
|
|
53
|
-
],
|
|
54
|
-
'accessible_svg': [
|
|
55
|
-
['associated', 'accessible_svgI'],
|
|
56
|
-
['tabindex', 'accessible_svgT']
|
|
57
|
-
],
|
|
58
|
-
'audio_alternative': [
|
|
59
|
-
['track', 'audio_alternativeT'],
|
|
60
|
-
['alternative', 'audio_alternativeA'],
|
|
61
|
-
['bgsound', 'audio_alternativeB']
|
|
62
|
-
],
|
|
63
|
-
'table_missing_description': [
|
|
64
|
-
['describedby', 'associated', 'table_missing_descriptionDM'],
|
|
65
|
-
['labeledby', 'associated', 'table_missing_descriptionLM'],
|
|
66
|
-
['caption', 'not been defined', 'table_missing_descriptionC'],
|
|
67
|
-
['summary', 'empty', 'table_missing_descriptionS'],
|
|
68
|
-
['describedby', 'empty', 'table_missing_descriptionDE'],
|
|
69
|
-
['labeledby', 'empty', 'table_missing_descriptionLE'],
|
|
70
|
-
['caption', 'no content', 'table_missing_descriptionE']
|
|
71
|
-
],
|
|
72
|
-
'label_implicitly_associated': [
|
|
73
|
-
['only whice spaces', 'label_implicitly_associatedW'],
|
|
74
|
-
['more than one', 'label_implicitly_associatedM']
|
|
75
|
-
],
|
|
76
|
-
'label_inappropriate_association': [
|
|
77
|
-
['Missing', 'label_inappropriate_associationM'],
|
|
78
|
-
['non-form', 'label_inappropriate_associationN']
|
|
79
|
-
],
|
|
80
|
-
'table_row_and_column_headers': [
|
|
81
|
-
['headers', 'table_row_and_column_headersRC'],
|
|
82
|
-
['Content', 'table_row_and_column_headersB'],
|
|
83
|
-
['head of the columns', 'table_row_and_column_headersH']
|
|
84
|
-
],
|
|
85
|
-
'color_contrast_state_pseudo_classes_abstract': [
|
|
86
|
-
['position: fixed', 'color_contrast_state_pseudo_classes_abstractF'],
|
|
87
|
-
['transparent', 'color_contrast_state_pseudo_classes_abstractB'],
|
|
88
|
-
['least 3:1', 'color_contrast_state_pseudo_classes_abstract3'],
|
|
89
|
-
['least 4.5:1', 'color_contrast_state_pseudo_classes_abstract4']
|
|
90
|
-
],
|
|
91
|
-
'color_contrast_state_pseudo_classes_active': [
|
|
92
|
-
['position: fixed', 'color_contrast_state_pseudo_classes_abstractF'],
|
|
93
|
-
['transparent', 'color_contrast_state_pseudo_classes_abstractB'],
|
|
94
|
-
['least 3:1', 'color_contrast_state_pseudo_classes_abstract3'],
|
|
95
|
-
['least 4.5:1', 'color_contrast_state_pseudo_classes_abstract4']
|
|
96
|
-
],
|
|
97
|
-
'color_contrast_state_pseudo_classes_focus': [
|
|
98
|
-
['position: fixed', 'color_contrast_state_pseudo_classes_abstractF'],
|
|
99
|
-
['transparent', 'color_contrast_state_pseudo_classes_abstractB'],
|
|
100
|
-
['least 3:1', 'color_contrast_state_pseudo_classes_abstract3'],
|
|
101
|
-
['least 4.5:1', 'color_contrast_state_pseudo_classes_abstract4']
|
|
102
|
-
],
|
|
103
|
-
'color_contrast_state_pseudo_classes_hover': [
|
|
104
|
-
['position: fixed', 'color_contrast_state_pseudo_classes_abstractF'],
|
|
105
|
-
['transparent', 'color_contrast_state_pseudo_classes_abstractB'],
|
|
106
|
-
['least 3:1', 'color_contrast_state_pseudo_classes_abstract3'],
|
|
107
|
-
['least 4.5:1', 'color_contrast_state_pseudo_classes_abstract4']
|
|
108
|
-
],
|
|
109
|
-
'color_contrast_aaa': [
|
|
110
|
-
['transparent', 'color_contrast_aaaB'],
|
|
111
|
-
['least 4.5:1', 'color_contrast_aaa4'],
|
|
112
|
-
['least 7:1', 'color_contrast_aaa7']
|
|
113
|
-
],
|
|
114
|
-
'animation': [
|
|
115
|
-
['duration', 'animationD'],
|
|
116
|
-
['iteration', 'animationI'],
|
|
117
|
-
['mechanism', 'animationM']
|
|
118
|
-
],
|
|
119
|
-
'page_title': [
|
|
120
|
-
['empty', 'page_titleN'],
|
|
121
|
-
['not identify', 'page_titleU']
|
|
122
|
-
],
|
|
123
|
-
'aria_labelledby_association': [
|
|
124
|
-
['exist', 'aria_labelledby_associationN'],
|
|
125
|
-
['empty', 'aria_labelledby_associationE']
|
|
126
|
-
],
|
|
127
|
-
'html_lang_attr': [
|
|
128
|
-
['parameters', 'html_lang_attrP'],
|
|
129
|
-
['nothing', 'html_lang_attrN'],
|
|
130
|
-
['empty', 'html_lang_attrE']
|
|
131
|
-
],
|
|
132
|
-
'missing_label': [
|
|
133
|
-
['associated', 'missing_labelI'],
|
|
134
|
-
['defined', 'missing_labelN'],
|
|
135
|
-
['multiple labels', 'missing_labelM']
|
|
136
|
-
],
|
|
137
|
-
'orientation': [
|
|
138
|
-
['loaded', 'orientationT']
|
|
139
|
-
]
|
|
140
|
-
};
|
|
141
43
|
// Converts issue instances at an axe certainty level.
|
|
142
44
|
const doAxe = (result, standardResult, certainty) => {
|
|
143
45
|
if (result.details && result.details[certainty]) {
|
|
@@ -379,95 +281,14 @@ const convert = (toolName, data, result, standardResult) => {
|
|
|
379
281
|
// Add that to the standard result and disregard tool-specific conversions.
|
|
380
282
|
standardResult.prevented = true;
|
|
381
283
|
}
|
|
382
|
-
// alfa
|
|
383
|
-
else if (
|
|
284
|
+
// alfa, aslint
|
|
285
|
+
else if (['alfa', 'aslint'].includes(toolName) && result.standardResult) {
|
|
384
286
|
// Move the results to standard locations.
|
|
385
287
|
Object.assign(result, result.nativeResult);
|
|
386
288
|
Object.assign(standardResult, result.standardResult);
|
|
387
289
|
delete result.nativeResult;
|
|
388
290
|
delete result.standardResult;
|
|
389
291
|
}
|
|
390
|
-
// aslint
|
|
391
|
-
else if (toolName === 'aslint' && result.summary && result.summary.byIssueType) {
|
|
392
|
-
// For each rule:
|
|
393
|
-
Object.keys(result.rules).forEach(ruleID => {
|
|
394
|
-
// If it has a valid issue type:
|
|
395
|
-
const {issueType} = result.rules[ruleID];
|
|
396
|
-
if (issueType && ['warning', 'error'].includes(issueType)) {
|
|
397
|
-
// If there are any violations:
|
|
398
|
-
const ruleResults = result.rules[ruleID].results;
|
|
399
|
-
if (ruleResults && ruleResults.length) {
|
|
400
|
-
// For each violation:
|
|
401
|
-
ruleResults.forEach(ruleResult => {
|
|
402
|
-
// If it has a description:
|
|
403
|
-
if (
|
|
404
|
-
ruleResult.message
|
|
405
|
-
&& ruleResult.message.actual
|
|
406
|
-
&& ruleResult.message.actual.description
|
|
407
|
-
) {
|
|
408
|
-
const what = ruleResult.message.actual.description;
|
|
409
|
-
// Get the differentiated ID of the rule if any.
|
|
410
|
-
const ruleData = aslintData[ruleID];
|
|
411
|
-
let finalRuleID = ruleID;
|
|
412
|
-
if (ruleData) {
|
|
413
|
-
const changer = ruleData.find(
|
|
414
|
-
specs => specs.slice(0, -1).every(matcher => what.includes(matcher))
|
|
415
|
-
);
|
|
416
|
-
if (changer) {
|
|
417
|
-
finalRuleID = changer[changer.length - 1];
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
// Initialize the path ID of the violating element as any normalized reported XPath.
|
|
421
|
-
let pathID = getNormalizedXPath(ruleResult.element && ruleResult.element.xpath) || '';
|
|
422
|
-
const {locationData} = ruleResult;
|
|
423
|
-
// If an XPath was obtained from the excerpt:
|
|
424
|
-
if (locationData && locationData.pathID) {
|
|
425
|
-
// Replace the path ID with it, because some ASLint-reported XPaths are abbreviated.
|
|
426
|
-
({pathID} = locationData);
|
|
427
|
-
}
|
|
428
|
-
// Get and normalize the reported excerpt.
|
|
429
|
-
const excerpt = ruleResult.element
|
|
430
|
-
&& ruleResult.element.html
|
|
431
|
-
&& ruleResult.element.html.replace(/\s+/g, ' ')
|
|
432
|
-
|| '';
|
|
433
|
-
// Get the tag name from the XPath, if possible.
|
|
434
|
-
let tagName = pathID && pathID.replace(/[^-\w].*$/, '').toUpperCase() || '';
|
|
435
|
-
if (! tagName && finalRuleID.endsWith('_svg')) {
|
|
436
|
-
tagName = 'SVG';
|
|
437
|
-
}
|
|
438
|
-
// If that was impossible but there is a tag name in the excerpt:
|
|
439
|
-
if (! tagName && /^<[a-z]+[ >]/.test(excerpt)) {
|
|
440
|
-
// Get it.
|
|
441
|
-
tagName = excerpt.slice(1).replace(/[ >].+/, '').toUpperCase();
|
|
442
|
-
}
|
|
443
|
-
// Get the ID, if any.
|
|
444
|
-
const idDraft = excerpt && excerpt.replace(/^[^[>]+id="/, 'id=').replace(/".*$/, '');
|
|
445
|
-
const idFinal = idDraft && idDraft.length > 3 && idDraft.startsWith('id=')
|
|
446
|
-
? idDraft.slice(3)
|
|
447
|
-
: '';
|
|
448
|
-
const id = idFinal === '' || isBadID(idFinal) ? '' : idFinal;
|
|
449
|
-
const instance = {
|
|
450
|
-
ruleID: finalRuleID,
|
|
451
|
-
what,
|
|
452
|
-
ordinalSeverity: ['warning', 0, 0, 'error'].indexOf(issueType),
|
|
453
|
-
tagName,
|
|
454
|
-
id,
|
|
455
|
-
location: {
|
|
456
|
-
doc: 'dom',
|
|
457
|
-
type: 'xpath',
|
|
458
|
-
spec: pathID
|
|
459
|
-
},
|
|
460
|
-
excerpt,
|
|
461
|
-
boxID: '',
|
|
462
|
-
pathID
|
|
463
|
-
};
|
|
464
|
-
standardResult.instances.push(instance);
|
|
465
|
-
}
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
});
|
|
470
|
-
}
|
|
471
292
|
// axe
|
|
472
293
|
else if (
|
|
473
294
|
toolName === 'axe'
|
package/run.js
CHANGED
|
@@ -1871,8 +1871,8 @@ const doActs = async (report, opts = {}) => {
|
|
|
1871
1871
|
// Increment the path ID count.
|
|
1872
1872
|
actIDData.pathIDCount++;
|
|
1873
1873
|
}
|
|
1874
|
-
// If the instance has
|
|
1875
|
-
if (text) {
|
|
1874
|
+
// If the instance has any text segments:
|
|
1875
|
+
if (text?.[0]?.length) {
|
|
1876
1876
|
// Increment the text count.
|
|
1877
1877
|
actIDData.textCount++;
|
|
1878
1878
|
}
|
package/testaro/motion.js
CHANGED
|
@@ -19,7 +19,7 @@ const fs = require('fs/promises');
|
|
|
19
19
|
// Module to get operating-system properties.
|
|
20
20
|
const os = require('os');
|
|
21
21
|
// Module to compare screenshots.
|
|
22
|
-
const
|
|
22
|
+
const blazediff = require('@blazediff/core').diff;
|
|
23
23
|
// Module to parse PNGs.
|
|
24
24
|
const {PNG} = require('pngjs');
|
|
25
25
|
|
|
@@ -60,7 +60,7 @@ exports.reporter = async page => {
|
|
|
60
60
|
// Otherwise, i.e. if their dimensions are identical:
|
|
61
61
|
else {
|
|
62
62
|
// Get the count of differing pixels between the shots.
|
|
63
|
-
const pixelChanges =
|
|
63
|
+
const pixelChanges = blazediff(shoot0PNG.data, shoot1PNG.data, null, width, height);
|
|
64
64
|
// Get the ratio of differing to all pixels as a percentage.
|
|
65
65
|
const changePercent = Math.round(100 * pixelChanges / (width * height));
|
|
66
66
|
// Free the memory used by screenshots.
|
package/tests/alfa.js
CHANGED
|
@@ -121,9 +121,18 @@ exports.reporter = async (page, report, actIndex) => {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
catch(error) {}
|
|
124
|
-
|
|
124
|
+
const text = [];
|
|
125
125
|
try {
|
|
126
|
-
|
|
126
|
+
const textRaw = await targetLoc.innerText({timeout: 50});
|
|
127
|
+
const segments = textRaw?.trim().split(/[\t\n]+/).filter(segment => segment.length);
|
|
128
|
+
if (segments?.length) {
|
|
129
|
+
if (segments.length > 1) {
|
|
130
|
+
text.push(segments[0], segments[segments.length - 1]);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
text.push(segments[0]);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
127
136
|
}
|
|
128
137
|
catch(error) {}
|
|
129
138
|
// Get rule-specific properties of a standard instance.
|
package/tests/aslint.js
CHANGED
|
@@ -17,20 +17,138 @@
|
|
|
17
17
|
|
|
18
18
|
// Function to add unique identifiers to the elements in the page.
|
|
19
19
|
const {addTestaroIDs} = require('../procs/testaro');
|
|
20
|
+
// Module to simplify strings.
|
|
21
|
+
const {cap, tidy} = require('../procs/job');
|
|
20
22
|
// Module to handle files.
|
|
21
23
|
const fs = require('fs/promises');
|
|
22
|
-
// Function to get location data
|
|
23
|
-
const {
|
|
24
|
+
// Function to get location data with a Testaro identifier.
|
|
25
|
+
const {getElementData} = require('../procs/getLocatorData');
|
|
26
|
+
// Function to normalize an XPath.
|
|
27
|
+
const {getNormalizedXPath} = require('../procs/identify');
|
|
28
|
+
|
|
29
|
+
// CONSTANTS
|
|
30
|
+
|
|
31
|
+
/*
|
|
32
|
+
Differentiates some rule IDs of aslint.
|
|
33
|
+
If the purported rule ID is a key and the what property contains all of the strings except the
|
|
34
|
+
last of any array item of the value of that key, then the final rule ID is the last item of that
|
|
35
|
+
array item.
|
|
36
|
+
*/
|
|
37
|
+
const aslintData = {
|
|
38
|
+
'misused_required_attribute': [
|
|
39
|
+
['not needed', 'misused_required_attributeR']
|
|
40
|
+
],
|
|
41
|
+
'accessible_svg': [
|
|
42
|
+
['associated', 'accessible_svgI'],
|
|
43
|
+
['tabindex', 'accessible_svgT']
|
|
44
|
+
],
|
|
45
|
+
'audio_alternative': [
|
|
46
|
+
['track', 'audio_alternativeT'],
|
|
47
|
+
['alternative', 'audio_alternativeA'],
|
|
48
|
+
['bgsound', 'audio_alternativeB']
|
|
49
|
+
],
|
|
50
|
+
'table_missing_description': [
|
|
51
|
+
['describedby', 'associated', 'table_missing_descriptionDM'],
|
|
52
|
+
['labeledby', 'associated', 'table_missing_descriptionLM'],
|
|
53
|
+
['caption', 'not been defined', 'table_missing_descriptionC'],
|
|
54
|
+
['summary', 'empty', 'table_missing_descriptionS'],
|
|
55
|
+
['describedby', 'empty', 'table_missing_descriptionDE'],
|
|
56
|
+
['labeledby', 'empty', 'table_missing_descriptionLE'],
|
|
57
|
+
['caption', 'no content', 'table_missing_descriptionE']
|
|
58
|
+
],
|
|
59
|
+
'label_implicitly_associated': [
|
|
60
|
+
['only whice spaces', 'label_implicitly_associatedW'],
|
|
61
|
+
['more than one', 'label_implicitly_associatedM']
|
|
62
|
+
],
|
|
63
|
+
'label_inappropriate_association': [
|
|
64
|
+
['Missing', 'label_inappropriate_associationM'],
|
|
65
|
+
['non-form', 'label_inappropriate_associationN']
|
|
66
|
+
],
|
|
67
|
+
'table_row_and_column_headers': [
|
|
68
|
+
['headers', 'table_row_and_column_headersRC'],
|
|
69
|
+
['Content', 'table_row_and_column_headersB'],
|
|
70
|
+
['head of the columns', 'table_row_and_column_headersH']
|
|
71
|
+
],
|
|
72
|
+
'color_contrast_state_pseudo_classes_abstract': [
|
|
73
|
+
['position: fixed', 'color_contrast_state_pseudo_classes_abstractF'],
|
|
74
|
+
['transparent', 'color_contrast_state_pseudo_classes_abstractB'],
|
|
75
|
+
['least 3:1', 'color_contrast_state_pseudo_classes_abstract3'],
|
|
76
|
+
['least 4.5:1', 'color_contrast_state_pseudo_classes_abstract4']
|
|
77
|
+
],
|
|
78
|
+
'color_contrast_state_pseudo_classes_active': [
|
|
79
|
+
['position: fixed', 'color_contrast_state_pseudo_classes_abstractF'],
|
|
80
|
+
['transparent', 'color_contrast_state_pseudo_classes_abstractB'],
|
|
81
|
+
['least 3:1', 'color_contrast_state_pseudo_classes_abstract3'],
|
|
82
|
+
['least 4.5:1', 'color_contrast_state_pseudo_classes_abstract4']
|
|
83
|
+
],
|
|
84
|
+
'color_contrast_state_pseudo_classes_focus': [
|
|
85
|
+
['position: fixed', 'color_contrast_state_pseudo_classes_abstractF'],
|
|
86
|
+
['transparent', 'color_contrast_state_pseudo_classes_abstractB'],
|
|
87
|
+
['least 3:1', 'color_contrast_state_pseudo_classes_abstract3'],
|
|
88
|
+
['least 4.5:1', 'color_contrast_state_pseudo_classes_abstract4']
|
|
89
|
+
],
|
|
90
|
+
'color_contrast_state_pseudo_classes_hover': [
|
|
91
|
+
['position: fixed', 'color_contrast_state_pseudo_classes_abstractF'],
|
|
92
|
+
['transparent', 'color_contrast_state_pseudo_classes_abstractB'],
|
|
93
|
+
['least 3:1', 'color_contrast_state_pseudo_classes_abstract3'],
|
|
94
|
+
['least 4.5:1', 'color_contrast_state_pseudo_classes_abstract4']
|
|
95
|
+
],
|
|
96
|
+
'color_contrast_aaa': [
|
|
97
|
+
['transparent', 'color_contrast_aaaB'],
|
|
98
|
+
['least 4.5:1', 'color_contrast_aaa4'],
|
|
99
|
+
['least 7:1', 'color_contrast_aaa7']
|
|
100
|
+
],
|
|
101
|
+
'animation': [
|
|
102
|
+
['duration', 'animationD'],
|
|
103
|
+
['iteration', 'animationI'],
|
|
104
|
+
['mechanism', 'animationM']
|
|
105
|
+
],
|
|
106
|
+
'page_title': [
|
|
107
|
+
['empty', 'page_titleN'],
|
|
108
|
+
['not identify', 'page_titleU']
|
|
109
|
+
],
|
|
110
|
+
'aria_labelledby_association': [
|
|
111
|
+
['exist', 'aria_labelledby_associationN'],
|
|
112
|
+
['empty', 'aria_labelledby_associationE']
|
|
113
|
+
],
|
|
114
|
+
'html_lang_attr': [
|
|
115
|
+
['parameters', 'html_lang_attrP'],
|
|
116
|
+
['nothing', 'html_lang_attrN'],
|
|
117
|
+
['empty', 'html_lang_attrE']
|
|
118
|
+
],
|
|
119
|
+
'missing_label': [
|
|
120
|
+
['associated', 'missing_labelI'],
|
|
121
|
+
['defined', 'missing_labelN'],
|
|
122
|
+
['multiple labels', 'missing_labelM']
|
|
123
|
+
],
|
|
124
|
+
'orientation': [
|
|
125
|
+
['loaded', 'orientationT']
|
|
126
|
+
]
|
|
127
|
+
};
|
|
24
128
|
|
|
25
129
|
// FUNCTIONS
|
|
26
130
|
|
|
27
131
|
// Conducts and reports the ASLint tests.
|
|
28
|
-
exports.reporter = async (page, report, actIndex
|
|
29
|
-
// Add unique identifiers to the elements in the page.
|
|
132
|
+
exports.reporter = async (page, report, actIndex) => {
|
|
133
|
+
// Add unique Testaro identifiers to the elements in the page.
|
|
30
134
|
await addTestaroIDs(page);
|
|
31
135
|
// Initialize the act report.
|
|
32
136
|
let data = {};
|
|
33
|
-
|
|
137
|
+
const result = {
|
|
138
|
+
nativeResult: {},
|
|
139
|
+
standardResult: {}
|
|
140
|
+
};
|
|
141
|
+
const standard = report.standard !== 'no';
|
|
142
|
+
// If standard results are to be reported:
|
|
143
|
+
if (standard) {
|
|
144
|
+
// Initialize the standard result.
|
|
145
|
+
result.standardResult = {
|
|
146
|
+
prevented: false,
|
|
147
|
+
totals: [0, 0, 0, 0],
|
|
148
|
+
instances: []
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const {standardResult} = result;
|
|
34
152
|
// Get the ASLint runner and bundle scripts.
|
|
35
153
|
const aslintRunner = await fs.readFile(`${__dirname}/../procs/aslint.js`, 'utf8');
|
|
36
154
|
const aslintBundlePath = require.resolve('aslint-testaro/aslint.bundle.js');
|
|
@@ -65,6 +183,9 @@ exports.reporter = async (page, report, actIndex, timeLimit) => {
|
|
|
65
183
|
console.log(message);
|
|
66
184
|
data.prevented = true;
|
|
67
185
|
data.error = message;
|
|
186
|
+
if (standard) {
|
|
187
|
+
standardResult.prevented = true;
|
|
188
|
+
}
|
|
68
189
|
});
|
|
69
190
|
const reportLoc = page.locator('#aslintResult');
|
|
70
191
|
// If the injection succeeded:
|
|
@@ -73,7 +194,7 @@ exports.reporter = async (page, report, actIndex, timeLimit) => {
|
|
|
73
194
|
// Wait for the test results to be attached to the page.
|
|
74
195
|
const waitOptions = {
|
|
75
196
|
state: 'attached',
|
|
76
|
-
timeout:
|
|
197
|
+
timeout: 20000
|
|
77
198
|
};
|
|
78
199
|
await reportLoc.waitFor(waitOptions);
|
|
79
200
|
}
|
|
@@ -88,47 +209,102 @@ exports.reporter = async (page, report, actIndex, timeLimit) => {
|
|
|
88
209
|
if (! data.prevented) {
|
|
89
210
|
// Get their text.
|
|
90
211
|
const actReport = await reportLoc.textContent();
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
212
|
+
try {
|
|
213
|
+
const nativeResult = result.nativeResult = JSON.parse(actReport);
|
|
214
|
+
// If any rules were reported violated:
|
|
215
|
+
if (nativeResult.rules) {
|
|
216
|
+
const {rules} = nativeResult;
|
|
217
|
+
// For each such rule:
|
|
218
|
+
for (const ruleID of Object.keys(rules)) {
|
|
219
|
+
const ruleData = rules[ruleID];
|
|
220
|
+
const {issueType, status} = ruleData;
|
|
221
|
+
const excluded = act.rules && ! act.rules.includes(ruleID);
|
|
222
|
+
const {type} = status;
|
|
223
|
+
// If rule is not an error or warning or is not to be tested:
|
|
224
|
+
if (
|
|
225
|
+
excluded
|
|
226
|
+
|| ['passed', 'skipped'].includes(type)
|
|
227
|
+
|| ! ['error', 'warning'].includes(issueType)
|
|
228
|
+
) {
|
|
229
|
+
// Delete the rule report.
|
|
230
|
+
delete rules[ruleID];
|
|
231
|
+
}
|
|
102
232
|
}
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
// For each
|
|
107
|
-
for (
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
233
|
+
// If standard results are to be reported:
|
|
234
|
+
if (standard) {
|
|
235
|
+
const ruleIDs = Object.keys(rules);
|
|
236
|
+
// For each violated rule:
|
|
237
|
+
for (let ruleID of ruleIDs) {
|
|
238
|
+
const ruleData = rules[ruleID];
|
|
239
|
+
const {issueType, results} = ruleData;
|
|
240
|
+
// For each violation:
|
|
241
|
+
for (const result of results) {
|
|
242
|
+
const {message, element} = result;
|
|
243
|
+
const what = message?.actual?.description ?? '';
|
|
244
|
+
// Get the values of the properties required for a standard result.
|
|
245
|
+
if (ruleID) {
|
|
246
|
+
const changer = aslintData[ruleID]?.find(
|
|
247
|
+
specs => specs.slice(0, -1).every(matcher => what.includes(matcher))
|
|
248
|
+
);
|
|
249
|
+
if (changer) {
|
|
250
|
+
ruleID = changer[changer.length - 1];
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const ordinalSeverity = issueType === 'warning' ? 1 : 2;
|
|
254
|
+
const {html, xpath} = element;
|
|
255
|
+
const excerpt = html?.replace(/\s+/g, ' ') ?? '';
|
|
256
|
+
let tagName = '';
|
|
257
|
+
let id = '';
|
|
258
|
+
let text = [];
|
|
259
|
+
let notInDOM = false;
|
|
260
|
+
let boxID = '';
|
|
261
|
+
let pathID = '';
|
|
262
|
+
if (excerpt) {
|
|
263
|
+
const elementData = await getElementData(page, excerpt);
|
|
264
|
+
({tagName, id, text, notInDOM, boxID, pathID, originalExcerpt} = elementData);
|
|
265
|
+
}
|
|
266
|
+
if (! pathID) {
|
|
267
|
+
pathID = getNormalizedXPath(xpath);
|
|
268
|
+
}
|
|
269
|
+
if (pathID && ! tagName) {
|
|
270
|
+
tagName = pathID?.replace(/[^-\w].*$/, '').toUpperCase() ?? '';
|
|
271
|
+
}
|
|
272
|
+
if (ruleID.endsWith('_svg') && ! tagName) {
|
|
273
|
+
tagName = 'SVG';
|
|
274
|
+
}
|
|
275
|
+
// Add an instance to the standard result.
|
|
276
|
+
standardResult.instances.push({
|
|
277
|
+
ruleID,
|
|
278
|
+
what,
|
|
279
|
+
ordinalSeverity,
|
|
280
|
+
count: 1,
|
|
281
|
+
tagName,
|
|
282
|
+
id,
|
|
283
|
+
location: {
|
|
284
|
+
notInDOM,
|
|
285
|
+
doc: 'dom',
|
|
286
|
+
type: 'xpath',
|
|
287
|
+
spec: pathID
|
|
288
|
+
},
|
|
289
|
+
excerpt: cap(tidy(originalExcerpt)),
|
|
290
|
+
text,
|
|
291
|
+
boxID,
|
|
292
|
+
pathID
|
|
293
|
+
});
|
|
115
294
|
}
|
|
116
|
-
}
|
|
295
|
+
}
|
|
117
296
|
}
|
|
118
|
-
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// If the results are not JSON:
|
|
300
|
+
catch(error) {
|
|
301
|
+
const message = `Result processing failed (${error.message})`;
|
|
302
|
+
console.log(`ERROR: ${message}`);
|
|
303
|
+
// Report this.
|
|
304
|
+
data.prevented = true;
|
|
305
|
+
data.error = message;
|
|
119
306
|
}
|
|
120
307
|
}
|
|
121
|
-
// Return the act report.
|
|
122
|
-
try {
|
|
123
|
-
JSON.stringify(data);
|
|
124
|
-
}
|
|
125
|
-
catch(error) {
|
|
126
|
-
const message = `ERROR: ASLint result cannot be made JSON (${error.message.slice(0, 200)})`;
|
|
127
|
-
data = {
|
|
128
|
-
prevented: true,
|
|
129
|
-
error: message
|
|
130
|
-
};
|
|
131
|
-
};
|
|
132
308
|
return {
|
|
133
309
|
data,
|
|
134
310
|
result
|
package/tests/htmlcs.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// Module to add and use unique element IDs.
|
|
19
19
|
const {addTestaroIDs} = require('../procs/testaro');
|
|
20
20
|
// Module to get location data from an element.
|
|
21
|
-
const {
|
|
21
|
+
const {getElementData} = require('../procs/getLocatorData');
|
|
22
22
|
// Module to handle files.
|
|
23
23
|
const fs = require('fs/promises');
|
|
24
24
|
|
|
@@ -102,7 +102,7 @@ exports.reporter = async (page, report, actIndex) => {
|
|
|
102
102
|
const ruleID = parts[1].replace(/^WCAG2|\.Principle\d\.Guideline[\d_]+/g, '');
|
|
103
103
|
result[parts[0]][ruleID] ??= {};
|
|
104
104
|
result[parts[0]][ruleID][parts[4]] ??= [];
|
|
105
|
-
const elementLocation = await
|
|
105
|
+
const elementLocation = await getElementData(page, parts[5]);
|
|
106
106
|
const {boxID, notInDOM, pathID} = elementLocation;
|
|
107
107
|
result[parts[0]][ruleID][parts[4]].push({
|
|
108
108
|
tagName: parts[2],
|
package/tests/qualWeb.js
CHANGED
|
@@ -20,7 +20,7 @@ const {ACTRules} = require('@qualweb/act-rules');
|
|
|
20
20
|
const {WCAGTechniques} = require('@qualweb/wcag-techniques');
|
|
21
21
|
const {BestPractices} = require('@qualweb/best-practices');
|
|
22
22
|
const {addTestaroIDs} = require('../procs/testaro');
|
|
23
|
-
const {
|
|
23
|
+
const {getElementData} = require('../procs/getLocatorData');
|
|
24
24
|
|
|
25
25
|
// CONSTANTS
|
|
26
26
|
|
|
@@ -211,7 +211,7 @@ exports.reporter = async (page, report, actIndex, timeLimit) => {
|
|
|
211
211
|
// For each violating element:
|
|
212
212
|
for (const element of elements) {
|
|
213
213
|
// Add location data from its excerpt to the element data.
|
|
214
|
-
element.locationData = await
|
|
214
|
+
element.locationData = await getElementData(page, element.htmlCode);
|
|
215
215
|
// Limit the size of its reported excerpt.
|
|
216
216
|
if (element.htmlCode && element.htmlCode.length > 700) {
|
|
217
217
|
element.htmlCode = `${element.htmlCode.slice(0, 700)} …`;
|
package/tests/wax.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// Function to add and use unique element IDs.
|
|
19
19
|
const {addTestaroIDs} = require('../procs/testaro');
|
|
20
20
|
// Function to get location data from an element.
|
|
21
|
-
const {
|
|
21
|
+
const {getElementData} = require('../procs/getLocatorData');
|
|
22
22
|
// Modules to run WAX.
|
|
23
23
|
const runWax = require('@wally-ax/wax-dev');
|
|
24
24
|
const waxDev = {runWax};
|
|
@@ -63,7 +63,7 @@ exports.reporter = async (page, report, actIndex) => {
|
|
|
63
63
|
// Add location data to its reported violations.
|
|
64
64
|
for (const violation of actReport) {
|
|
65
65
|
const {element} = violation;
|
|
66
|
-
const elementLocation = await
|
|
66
|
+
const elementLocation = await getElementData(page, element);
|
|
67
67
|
Object.assign(violation, elementLocation);
|
|
68
68
|
}
|
|
69
69
|
// Populate the act report.
|