testaro 64.8.9 → 64.9.1
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/getLocatorData.js +44 -0
- package/procs/identify.js +106 -94
- package/procs/nu.js +3 -1
- package/procs/testaro.js +0 -44
- package/run.js +10 -9
- package/tests/htmlcs.js +5 -2
- package/tests/wax.js +3 -1
package/package.json
CHANGED
package/procs/getLocatorData.js
CHANGED
|
@@ -101,3 +101,47 @@ exports.getLocatorData = async loc => {
|
|
|
101
101
|
return null;
|
|
102
102
|
}
|
|
103
103
|
};
|
|
104
|
+
// Returns location data from the extract of a standard instance.
|
|
105
|
+
exports.getLocationData = async (page, excerpt) => {
|
|
106
|
+
const testaroIDArray = excerpt.match(/data-testaro-id="(\d+)#"/);
|
|
107
|
+
// If the excerpt contains a Testaro identifier:
|
|
108
|
+
if (testaroIDArray) {
|
|
109
|
+
const testaroID = testaroIDArray[1];
|
|
110
|
+
// Return location data for the element.
|
|
111
|
+
return await page.evaluate(testaroID => {
|
|
112
|
+
const element = document.querySelector(`[data-testaro-id="${testaroID}#"]`);
|
|
113
|
+
// If any element has that identifier:
|
|
114
|
+
if (element) {
|
|
115
|
+
// Get box and path IDs for the element.
|
|
116
|
+
const box = {};
|
|
117
|
+
let boxID = '';
|
|
118
|
+
const boundingBox = element.getBoundingClientRect() || {};
|
|
119
|
+
if (boundingBox.x) {
|
|
120
|
+
['x', 'y', 'width', 'height'].forEach(coordinate => {
|
|
121
|
+
box[coordinate] = Math.round(boundingBox[coordinate]);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (typeof box.x === 'number') {
|
|
125
|
+
boxID = Object.values(box).join(':');
|
|
126
|
+
}
|
|
127
|
+
const pathID = window.getXPath(element) || '';
|
|
128
|
+
// Return them.
|
|
129
|
+
return {
|
|
130
|
+
boxID,
|
|
131
|
+
pathID
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
// Otherwise, i.e. if no element has it, return empty location data.
|
|
135
|
+
return {};
|
|
136
|
+
}, testaroID);
|
|
137
|
+
}
|
|
138
|
+
// Otherwise, i.e. if the extract contains no Testaro identifier:
|
|
139
|
+
else {
|
|
140
|
+
// Return a non-DOM location.
|
|
141
|
+
return {
|
|
142
|
+
notInDOM: true,
|
|
143
|
+
boxID: '',
|
|
144
|
+
pathID: ''
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
};
|
package/procs/identify.js
CHANGED
|
@@ -83,6 +83,21 @@ const addIDs = async (locator, recipient) => {
|
|
|
83
83
|
recipient.pathID = pathID;
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
+
// Normalize the path ID.
|
|
87
|
+
const xPathSegments = recipient.pathID.split('/');
|
|
88
|
+
const normalizedSegments = xPathSegments.map(segment => {
|
|
89
|
+
if (segment === '') {
|
|
90
|
+
return '';
|
|
91
|
+
}
|
|
92
|
+
if (['html', 'body'].includes(segment)) {
|
|
93
|
+
return segment;
|
|
94
|
+
}
|
|
95
|
+
if (segment.endsWith(']')) {
|
|
96
|
+
return segment;
|
|
97
|
+
}
|
|
98
|
+
return `${segment}[1]`;
|
|
99
|
+
});
|
|
100
|
+
recipient.pathID = normalizedSegments.join('/');
|
|
86
101
|
}
|
|
87
102
|
};
|
|
88
103
|
// Sanitizes a tag name.
|
|
@@ -101,115 +116,112 @@ const tagify = tagName => {
|
|
|
101
116
|
};
|
|
102
117
|
// Returns the XPath and box ID of the element of a standard instance.
|
|
103
118
|
exports.identify = async (instance, page) => {
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (type === 'xpath') {
|
|
119
|
-
specifier = spec.replace(/\/text\(\)\[\d+\]$/, '');
|
|
120
|
-
}
|
|
121
|
-
if (specifier) {
|
|
122
|
-
if (type === 'xpath') {
|
|
123
|
-
specifier = `xpath=${specifier}`;
|
|
124
|
-
}
|
|
125
|
-
try {
|
|
126
|
-
const locators = page.locator(specifier);
|
|
127
|
-
// Get their count, or throw an error if the specifier is invalid.
|
|
128
|
-
const locatorCount = await locators.count();
|
|
129
|
-
// If the specifier is valid and the count is 1:
|
|
130
|
-
if (locatorCount === 1) {
|
|
131
|
-
// Add a box ID and a path ID to the result.
|
|
132
|
-
await addIDs(locators, elementID);
|
|
133
|
-
}
|
|
134
|
-
/*
|
|
135
|
-
Otherwise, if the specifier is valid, the count is not 1, and the instance specifies
|
|
136
|
-
an XPath location:
|
|
137
|
-
*/
|
|
138
|
-
else if (type === 'xpath') {
|
|
139
|
-
// Use the XPath location as the path ID.
|
|
140
|
-
elementID.pathID = spec;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
// If the specifier is invalid:
|
|
144
|
-
catch(error) {
|
|
145
|
-
// Add this to the instance.
|
|
146
|
-
instance.invalidity = {
|
|
147
|
-
badProperty: 'location',
|
|
148
|
-
validityError: error.message
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
}
|
|
119
|
+
// Initialize a result.
|
|
120
|
+
const elementID = {
|
|
121
|
+
boxID: '',
|
|
122
|
+
pathID: ''
|
|
123
|
+
};
|
|
124
|
+
const {excerpt, id, location} = instance;
|
|
125
|
+
const tagName = tagify(instance.tagName);
|
|
126
|
+
const {type, spec} = location;
|
|
127
|
+
// If the instance specifies a CSS selector or XPath location:
|
|
128
|
+
if (['selector', 'xpath'].includes(type)) {
|
|
129
|
+
// Get locators for elements with that specifier.
|
|
130
|
+
let specifier = spec;
|
|
131
|
+
if (type === 'xpath') {
|
|
132
|
+
specifier = spec.replace(/\/text\(\)\[\d+\]$/, '');
|
|
152
133
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
try {
|
|
157
|
-
let locator = page.locator(`#${id.replace(/([-&;/]|^\d)/g, '\\$1')}`).first();
|
|
158
|
-
// Add a box ID and a path ID to the result.
|
|
159
|
-
await addIDs(locator, elementID);
|
|
160
|
-
}
|
|
161
|
-
// If the identifier is invalid:
|
|
162
|
-
catch(error) {
|
|
163
|
-
// Add this to the instance.
|
|
164
|
-
instance.invalidity = {
|
|
165
|
-
badProperty: 'id',
|
|
166
|
-
validityError: error.message
|
|
167
|
-
};
|
|
134
|
+
if (specifier) {
|
|
135
|
+
if (type === 'xpath') {
|
|
136
|
+
specifier = `xpath=${specifier}`;
|
|
168
137
|
}
|
|
169
|
-
}
|
|
170
|
-
// If either ID remains undefined and the instance specifies a tag name:
|
|
171
|
-
if (tagName && ! (elementID.boxID && elementID.pathID)) {
|
|
172
138
|
try {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
139
|
+
const locators = page.locator(specifier);
|
|
140
|
+
// Get their count, or throw an error if the specifier is invalid.
|
|
141
|
+
const locatorCount = await locators.count();
|
|
142
|
+
// If the specifier is valid and the count is 1:
|
|
177
143
|
if (locatorCount === 1) {
|
|
178
144
|
// Add a box ID and a path ID to the result.
|
|
179
145
|
await addIDs(locators, elementID);
|
|
180
146
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const sortedPlainParts = plainParts.sort((a, b) => b.length - a.length);
|
|
189
|
-
const mainPart = sortedPlainParts.length ? sortedPlainParts[0] : '';
|
|
190
|
-
// If there is one:
|
|
191
|
-
if (mainPart.trim().replace(/\s+/g, '').length) {
|
|
192
|
-
// Get locators for elements with the tag name and the text.
|
|
193
|
-
const locators = page.locator(tagName.toLowerCase(), {hasText: mainPart});
|
|
194
|
-
// If there is exactly 1 of them:
|
|
195
|
-
const locatorCount = await locators.count();
|
|
196
|
-
if (locatorCount === 1) {
|
|
197
|
-
// Add a box ID and a path ID to the result.
|
|
198
|
-
await addIDs(locators, elementID);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
147
|
+
/*
|
|
148
|
+
Otherwise, if the specifier is valid, the count is not 1, and the instance specifies
|
|
149
|
+
an XPath location:
|
|
150
|
+
*/
|
|
151
|
+
else if (type === 'xpath') {
|
|
152
|
+
// Use the XPath location as the path ID.
|
|
153
|
+
elementID.pathID = spec;
|
|
201
154
|
}
|
|
202
155
|
}
|
|
203
|
-
// If the
|
|
156
|
+
// If the specifier is invalid:
|
|
204
157
|
catch(error) {
|
|
205
158
|
// Add this to the instance.
|
|
206
159
|
instance.invalidity = {
|
|
207
|
-
badProperty: '
|
|
160
|
+
badProperty: 'location',
|
|
208
161
|
validityError: error.message
|
|
209
162
|
};
|
|
210
163
|
}
|
|
211
164
|
}
|
|
212
|
-
// Return the result.
|
|
213
|
-
return elementID;
|
|
214
165
|
}
|
|
166
|
+
// If either ID remains undefined and the instance specifies an element ID:
|
|
167
|
+
if (id && ! (elementID.boxID && elementID.pathID)) {
|
|
168
|
+
// Get the first locator for an element with the ID.
|
|
169
|
+
try {
|
|
170
|
+
let locator = page.locator(`#${id.replace(/([-&;/]|^\d)/g, '\\$1')}`).first();
|
|
171
|
+
// Add a box ID and a path ID to the result.
|
|
172
|
+
await addIDs(locator, elementID);
|
|
173
|
+
}
|
|
174
|
+
// If the identifier is invalid:
|
|
175
|
+
catch(error) {
|
|
176
|
+
// Add this to the instance.
|
|
177
|
+
instance.invalidity = {
|
|
178
|
+
badProperty: 'id',
|
|
179
|
+
validityError: error.message
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// If either ID remains undefined and the instance specifies a tag name:
|
|
184
|
+
if (tagName && ! (elementID.boxID && elementID.pathID)) {
|
|
185
|
+
try {
|
|
186
|
+
// Get locators for elements with the tag name.
|
|
187
|
+
let locators = page.locator(tagName);
|
|
188
|
+
// If there is exactly 1 of them:
|
|
189
|
+
let locatorCount = await locators.count();
|
|
190
|
+
if (locatorCount === 1) {
|
|
191
|
+
// Add a box ID and a path ID to the result.
|
|
192
|
+
await addIDs(locators, elementID);
|
|
193
|
+
}
|
|
194
|
+
// If either ID remains undefined and the instance also specifies an excerpt:
|
|
195
|
+
if (excerpt && ! (elementID.boxID && elementID.pathID)) {
|
|
196
|
+
// Get the plain text parts of the excerpt, converting ... to an empty string.
|
|
197
|
+
const minTagExcerpt = excerpt.replace(/<[^>]+>/g, '<>');
|
|
198
|
+
const plainParts = (minTagExcerpt.match(/[^<>]+/g) || [])
|
|
199
|
+
.map(part => part === '...' ? '' : part);
|
|
200
|
+
// Get the longest of them.
|
|
201
|
+
const sortedPlainParts = plainParts.sort((a, b) => b.length - a.length);
|
|
202
|
+
const mainPart = sortedPlainParts.length ? sortedPlainParts[0] : '';
|
|
203
|
+
// If there is one:
|
|
204
|
+
if (mainPart.trim().replace(/\s+/g, '').length) {
|
|
205
|
+
// Get locators for elements with the tag name and the text.
|
|
206
|
+
const locators = page.locator(tagName.toLowerCase(), {hasText: mainPart});
|
|
207
|
+
// If there is exactly 1 of them:
|
|
208
|
+
const locatorCount = await locators.count();
|
|
209
|
+
if (locatorCount === 1) {
|
|
210
|
+
// Add a box ID and a path ID to the result.
|
|
211
|
+
await addIDs(locators, elementID);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// If the tag name is invalid:
|
|
217
|
+
catch(error) {
|
|
218
|
+
// Add this to the instance.
|
|
219
|
+
instance.invalidity = {
|
|
220
|
+
badProperty: 'tagName',
|
|
221
|
+
validityError: error.message
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// Return the result.
|
|
226
|
+
return elementID;
|
|
215
227
|
};
|
package/procs/nu.js
CHANGED
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
// ########## IMPORTS
|
|
16
16
|
|
|
17
17
|
// Module to add Testaro IDs to elements.
|
|
18
|
-
const {addTestaroIDs
|
|
18
|
+
const {addTestaroIDs} = require('./testaro');
|
|
19
|
+
// Module to get location data from an element.
|
|
20
|
+
const {getLocationData} = require('./getLocatorData');
|
|
19
21
|
// Module to get the document source.
|
|
20
22
|
const {getSource} = require('./getSource');
|
|
21
23
|
|
package/procs/testaro.js
CHANGED
|
@@ -234,47 +234,3 @@ exports.addTestaroIDs = async page => {
|
|
|
234
234
|
}
|
|
235
235
|
});
|
|
236
236
|
};
|
|
237
|
-
// Returns location data from the extract of a standard instance.
|
|
238
|
-
exports.getLocationData = async (page, excerpt) => {
|
|
239
|
-
const testaroIDArray = excerpt.match(/data-testaro-id="(\d+)#"/);
|
|
240
|
-
// If the excerpt 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
|
@@ -438,22 +438,19 @@ const launch = exports.launch = async (
|
|
|
438
438
|
const parent = element.parentNode;
|
|
439
439
|
// If (abnormally) the parent node is not an element:
|
|
440
440
|
if (!parent || parent.nodeType !== Node.ELEMENT_NODE) {
|
|
441
|
-
// Prepend
|
|
441
|
+
// Prepend the element (not the parent) to the segment array.
|
|
442
442
|
segments.unshift(tag);
|
|
443
443
|
// Stop traversing, leaving the segment array partial.
|
|
444
444
|
break;
|
|
445
445
|
}
|
|
446
|
-
|
|
447
|
-
// Get the subscript of the element.
|
|
446
|
+
// Get the subscript of the element if it is not the body element.
|
|
448
447
|
const cohort = Array
|
|
449
448
|
.from(parent.childNodes)
|
|
450
449
|
.filter(
|
|
451
450
|
childNode => childNode.nodeType === Node.ELEMENT_NODE
|
|
452
451
|
&& childNode.tagName === element.tagName
|
|
453
452
|
);
|
|
454
|
-
|
|
455
|
-
subscript = `[${cohort.indexOf(element) + 1}]`;
|
|
456
|
-
}
|
|
453
|
+
const subscript = tag === 'body' ? '' : `[${cohort.indexOf(element) + 1}]`;
|
|
457
454
|
// Prepend the element identifier to the segment array.
|
|
458
455
|
segments.unshift(`${tag}${subscript}`);
|
|
459
456
|
// Continue the traversal with the parent of the current element.
|
|
@@ -1649,14 +1646,18 @@ const doActs = async (report, opts = {}) => {
|
|
|
1649
1646
|
};
|
|
1650
1647
|
// Populate it.
|
|
1651
1648
|
standardize(act);
|
|
1652
|
-
//
|
|
1649
|
+
// For each of its standard instances:
|
|
1653
1650
|
for (const instance of act.standardResult.instances) {
|
|
1654
1651
|
const elementID = await identify(instance, page);
|
|
1652
|
+
// If a box ID is missing:
|
|
1655
1653
|
if (! instance.boxID) {
|
|
1654
|
+
// Add one.
|
|
1656
1655
|
instance.boxID = elementID ? elementID.boxID : '';
|
|
1657
1656
|
}
|
|
1658
|
-
|
|
1659
|
-
|
|
1657
|
+
// If a path ID is missing or different:
|
|
1658
|
+
if (instance.pathID !== elementID.pathID) {
|
|
1659
|
+
// Add a box ID and a path ID to each of its standard instances if missing.
|
|
1660
|
+
instance.pathID = elementID.pathID;
|
|
1660
1661
|
}
|
|
1661
1662
|
};
|
|
1662
1663
|
// If the original-format result is not to be included in the report:
|
package/tests/htmlcs.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
© 2022–2024 CVS Health and/or one of its affiliates. All rights reserved.
|
|
3
|
+
© 2025 Jonathan Robert Pool.
|
|
3
4
|
|
|
4
5
|
Licensed under the MIT License. See LICENSE file at the project root or
|
|
5
6
|
https://opensource.org/license/mit/ for details.
|
|
@@ -15,7 +16,9 @@
|
|
|
15
16
|
// IMPORTS
|
|
16
17
|
|
|
17
18
|
// Module to add and use unique element IDs.
|
|
18
|
-
const {addTestaroIDs
|
|
19
|
+
const {addTestaroIDs} = require('../procs/testaro');
|
|
20
|
+
// Module to get location data from an element.
|
|
21
|
+
const {getLocationData} = require('../procs/getLocatorData');
|
|
19
22
|
// Module to handle files.
|
|
20
23
|
const fs = require('fs/promises');
|
|
21
24
|
|
|
@@ -91,7 +94,7 @@ exports.reporter = async (page, report, actIndex) => {
|
|
|
91
94
|
// If it is an error or a warning (not a notice):
|
|
92
95
|
else if (['Error', 'Warning'].includes(parts[0])) {
|
|
93
96
|
/*
|
|
94
|
-
Add the violation to
|
|
97
|
+
Add the violation to a violationClass.violationCode.description array in the result.
|
|
95
98
|
This saves space, because, although some descriptions are violation-specific, such as
|
|
96
99
|
descriptions that state the contrast ratio of an element, most descriptions are
|
|
97
100
|
generic, so typically many violations share a description.
|
package/tests/wax.js
CHANGED
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
// IMPORTS
|
|
17
17
|
|
|
18
18
|
// Module to add and use unique element IDs.
|
|
19
|
-
const {addTestaroIDs
|
|
19
|
+
const {addTestaroIDs} = require('../procs/testaro');
|
|
20
|
+
// Module to get location data from an element.
|
|
21
|
+
const {getLocationData} = require('../procs/getLocatorData');
|
|
20
22
|
// Modules to run WAX.
|
|
21
23
|
const runWax = require('@wally-ax/wax-dev');
|
|
22
24
|
const waxDev = {runWax};
|