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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "64.8.9",
3
+ "version": "64.9.1",
4
4
  "description": "Run 1000 web accessibility tests from 11 tools and get a standardized report",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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
- // If the instance does not yet have both boxID and pathID properties:
105
- if (['boxID', 'pathID'].some(key => instance[key] === undefined)) {
106
- // Initialize a result.
107
- const elementID = {
108
- boxID: '',
109
- pathID: ''
110
- };
111
- const {excerpt, id, location} = instance;
112
- const tagName = tagify(instance.tagName);
113
- const {type, spec} = location;
114
- // If the instance specifies a CSS selector or XPath location:
115
- if (['selector', 'xpath'].includes(type)) {
116
- // Get locators for elements with that specifier.
117
- let specifier = spec;
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
- // If either ID remains undefined and the instance specifies an element ID:
154
- if (id && ! (elementID.boxID && elementID.pathID)) {
155
- // Get the first locator for an element with the ID.
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
- // Get locators for elements with the tag name.
174
- let locators = page.locator(tagName);
175
- // If there is exactly 1 of them:
176
- let locatorCount = await locators.count();
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
- // If either ID remains undefined and the instance also specifies an excerpt:
182
- if (excerpt && ! (elementID.boxID && elementID.pathID)) {
183
- // Get the plain text parts of the excerpt, converting ... to an empty string.
184
- const minTagExcerpt = excerpt.replace(/<[^>]+>/g, '<>');
185
- const plainParts = (minTagExcerpt.match(/[^<>]+/g) || [])
186
- .map(part => part === '...' ? '' : part);
187
- // Get the longest of them.
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 tag name is invalid:
156
+ // If the specifier is invalid:
204
157
  catch(error) {
205
158
  // Add this to the instance.
206
159
  instance.invalidity = {
207
- badProperty: 'tagName',
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, getLocationData} = require('./testaro');
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 it to the segment array.
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
- let subscript = '';
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
- if (cohort.length > 1) {
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
- // Add a box ID and a path ID to each of its standard instances if missing.
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
- if (! instance.pathID) {
1659
- instance.pathID = elementID ? elementID.pathID : '';
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, getLocationData} = require('../procs/testaro');
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 an violationClass.violationCode.description array in the result.
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, getLocationData} = require('../procs/testaro');
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};