testaro 60.10.3 → 60.12.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/UPGRADES.md CHANGED
@@ -3239,3 +3239,69 @@ Given your goal to move on with [testaro](cci:7://file:///Users/pool/Users/pool/
3239
3239
  - Plan separately for a future **element-matching module** that consumes all tools’ outputs and decides “these instances refer to the same DOM element”.
3240
3240
 
3241
3241
  That way, you get immediate performance and implementation wins in [testaro](cci:7://file:///Users/pool/Users/pool/Documents/Topics/work/testaro:0:0-0:0), without locking yourself into the fragile assumption that “XPath strings must match exactly across all tools.
3242
+
3243
+ ## Sampling and performance
3244
+
3245
+ Refactoring Testaro tests to eliminate sampling of elements began in December 2025. Initial results suggest that refactoring decreases elapsed test times despite the fact that all applicable elements are examined rather than only a sample.
3246
+
3247
+ In a run by the Kilotest server on the [home page of the Open Source Collective](https://opencollective.com/opensource), with about 2700 visible elements,the elapsed times of Testaro tests were:
3248
+
3249
+ ```json
3250
+ "ruleTestTimes": {
3251
+ "allCaps": 11,
3252
+ "opFoc": 10,
3253
+ "allSlanted": 9,
3254
+ "hovInd": 9,
3255
+ "focOp": 8,
3256
+ "targetSmall": 7,
3257
+ "focAll": 7,
3258
+ "focVis": 6,
3259
+ "distortion": 5,
3260
+ "linkAmb": 5,
3261
+ "titledEl": 5,
3262
+ "zIndex": 5,
3263
+ "targetTiny": 4,
3264
+ "shoot1": 4,
3265
+ "linkTitle": 3,
3266
+ "hover": 3,
3267
+ "shoot0": 2,
3268
+ "adbID": 2,
3269
+ "linkUl": 2,
3270
+ "buttonMenu": 2,
3271
+ "focInd": 2,
3272
+ "tabNav": 2,
3273
+ "dupAtt": 0,
3274
+ "imageLink": 1,
3275
+ "labClash": 1,
3276
+ "allHidden": 0,
3277
+ "altScheme": 0,
3278
+ "autocomplete": 0,
3279
+ "bulk": 0,
3280
+ "captionLoc": 0,
3281
+ "datalistRef": 0,
3282
+ "docType": 0,
3283
+ "embAc": 0,
3284
+ "headEl": 0,
3285
+ "headingAmb": 0,
3286
+ "hr": 0,
3287
+ "legendLoc": 0,
3288
+ "lineHeight": 0,
3289
+ "linkExt": 0,
3290
+ "linkOldAtt": 0,
3291
+ "linkTo": 0,
3292
+ "miniText": 0,
3293
+ "nonTable": 0,
3294
+ "optRoleSel": 0,
3295
+ "phOnly": 0,
3296
+ "pseudoP": 0,
3297
+ "radioSet": 0,
3298
+ "role": 0,
3299
+ "secHeading": 0,
3300
+ "styleDiff": 0,
3301
+ "textSem": 0
3302
+ }
3303
+ ```
3304
+
3305
+ All of the tests with elapsed times longer than 2 seconds were not yet refactored. Some of the refactored tests applied `checkVisibility` to all `body` descendant elements.
3306
+
3307
+ Credit for the speed improvement in refactored tests is apparently owed to the encapsulation of the entire test logic in a browser function, versus the repeated element-by-element execution of the same logic in Node.js with Playwright methods.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "60.10.3",
3
+ "version": "60.12.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": {
@@ -18,7 +18,7 @@ exports.screenShot = async (page, exclusion = null) => {
18
18
  const options = {
19
19
  fullPage: true,
20
20
  omitBackground: true,
21
- timeout: 2000
21
+ timeout: 4000
22
22
  };
23
23
  if (exclusion) {
24
24
  options.mask = [exclusion];
package/procs/testaro.js CHANGED
@@ -157,3 +157,79 @@ exports.simplify = async (page, withItems, ruleData) => {
157
157
  // Return the result.
158
158
  return result;
159
159
  };
160
+ // Performs a standard test.
161
+ exports.doTest = async (
162
+ page,
163
+ withItems,
164
+ ruleID,
165
+ candidateSelector,
166
+ whats,
167
+ severity,
168
+ summaryTagName,
169
+ getBadWhatString
170
+ ) => {
171
+ // Return totals and standard instances for the rule.
172
+ return await page.evaluate(args => {
173
+ const [
174
+ withItems,
175
+ ruleID,
176
+ candidateSelector,
177
+ whats,
178
+ severity,
179
+ summaryTagName,
180
+ getBadWhatString
181
+ ] = args;
182
+ // Get all candidates.
183
+ const candidates = document.body.querySelectorAll(candidateSelector);
184
+ let violationCount = 0;
185
+ const instances = [];
186
+ // Get a violation function.
187
+ const getBadWhat = eval(`(${getBadWhatString})`);
188
+ // For each candidate:
189
+ candidates.forEach(element => {
190
+ const violationWhat = getBadWhat(element);
191
+ // If it violates the rule:
192
+ if (violationWhat) {
193
+ // Increment the violation count.
194
+ violationCount++;
195
+ // If itemization is required:
196
+ if (withItems) {
197
+ const violationWhatStart = violationWhat.slice(0, 2);
198
+ let ruleSeverity = severity;
199
+ let ruleWhat = violationWhat
200
+ // If this violation has a custom severity:
201
+ if (/[0-3]:/.test(violationWhatStart)) {
202
+ // Get it and remove it from the violation description.
203
+ ruleSeverity = Number(violationWhat[0]);
204
+ ruleWhat = violationWhat.slice(2);
205
+ }
206
+ // Add an instance to the instances.
207
+ instances.push(
208
+ window.getInstance(element, ruleID, ruleWhat, 1, ruleSeverity)
209
+ );
210
+ }
211
+ }
212
+ });
213
+ // If there are any violations and itemization is not required:
214
+ if (violationCount && ! withItems) {
215
+ // Add a summary instance to the instances.
216
+ instances.push(
217
+ window.getInstance(null, ruleID, whats, violationCount, severity, summaryTagName)
218
+ );
219
+ }
220
+ return {
221
+ data: {},
222
+ totals: [0, 0, 0, violationCount],
223
+ standardInstances: instances
224
+ }
225
+ }, [
226
+ withItems,
227
+ ruleID,
228
+ candidateSelector,
229
+ whats,
230
+ severity,
231
+ summaryTagName,
232
+ getBadWhatString
233
+ ]
234
+ );
235
+ };
package/testaro/adbID.js CHANGED
@@ -32,81 +32,50 @@
32
32
  the implementation of a test for a similar rule in the Tenon tool.
33
33
  */
34
34
 
35
+ // IMPORTS
36
+
37
+ const {doTest} = require('../procs/testaro');
38
+
35
39
  // FUNCTIONS
36
40
 
37
41
  // Runs the test and returns the result.
38
42
  exports.reporter = async (page, withItems) => {
39
- // Return totals and standard instances for the rule.
40
- return await page.evaluate(withItems => {
41
- // Get all candidates, i.e. elements with aria-describedby attributes.
42
- const candidates = document.body.querySelectorAll('[aria-describedby]');
43
- let violationCount = 0;
44
- const instances = [];
45
- // For each candidate:
46
- candidates.forEach(element => {
47
- // Get the IDs in its aria-describedby attribute.
48
- const IDs = element.getAttribute('aria-describedby').trim().split(/\s+/).filter(Boolean);
49
- // If there are none:
50
- if (! IDs.length) {
51
- // Increment the violation count.
52
- violationCount++;
53
- // If itemization is required:
54
- if (withItems) {
55
- const what = 'Element has an aria-describedby attribute with no value';
56
- // Add an instance to the instances.
57
- instances.push(window.getInstance(element, 'adbID', what, 1, 3));
43
+ // Define a violation function for execution in the browser.
44
+ const getBadWhat = element => {
45
+ // Get the IDs in the aria-describedby attribute of the element.
46
+ const IDs = element.getAttribute('aria-describedby').trim().split(/\s+/).filter(Boolean);
47
+ // If there are none:
48
+ if (! IDs.length) {
49
+ // Return a violation description.
50
+ return 'Element has an aria-describedby attribute with no value';
51
+ }
52
+ // Otherwise, i.e. if there is at least 1 ID:
53
+ else {
54
+ // For each ID:
55
+ for (const id of IDs) {
56
+ // Get the element with that ID.
57
+ const describer = document.getElementById(id);
58
+ // If it doesn't exist:
59
+ if (! describer) {
60
+ // Return a violation description.
61
+ return `No element has the aria-describedby ID ${id}`;
58
62
  }
59
- }
60
- // Otherwise, i.e. if there is at least 1 ID:
61
- else {
62
- // For each ID:
63
- for (const id of IDs) {
64
- // Get the element with that ID.
65
- const describer = document.getElementById(id);
66
- // If it doesn't exist:
67
- if (! describer) {
68
- // Increment the violation count.
69
- violationCount++;
70
- // If itemization is required:
71
- if (withItems) {
72
- const what = `No element has the aria-describedby ID ${id}`;
73
- // Add an instance to the instances.
74
- instances.push(window.getInstance(element, 'adbID', what, 1, 3));
75
- }
76
- // Stop checking the element.
77
- break;
78
- }
79
- // Otherwise, i.e. if it exists:
80
- else {
81
- // Get the elements with that ID.
82
- const sameIDElements = document.querySelectorAll(`#${id}`);
83
- // If there is more than one:
84
- if (sameIDElements.length > 1) {
85
- // Increment the violation count.
86
- violationCount++;
87
- // If itemization is required:
88
- if (withItems) {
89
- const what = `Multiple elements share the aria-describedby ID ${id}`;
90
- // Add an instance to the instances.
91
- instances.push(window.getInstance(element, 'adbID', what, 1, 2));
92
- }
93
- // Stop checking the element.
94
- break;
95
- }
63
+ // Otherwise, i.e. if it exists:
64
+ else {
65
+ // Get the elements with that ID.
66
+ const sameIDElements = document.querySelectorAll(`#${id}`);
67
+ // If there is more than one:
68
+ if (sameIDElements.length > 1) {
69
+ // Return a violation description.
70
+ return `Multiple elements share the aria-describedby ID ${id}`;
96
71
  }
97
72
  }
98
73
  }
99
- });
100
- // If there were any violations and itemization is not required:
101
- if (violationCount && ! withItems) {
102
- const what = 'Elements have aria-describedby attributes with missing or invalid id values';
103
- // Add a summary instance to the instances.
104
- instances.push(window.getInstance(null, 'adbID', what, violationCount, 3));
105
74
  }
106
- return {
107
- data: {},
108
- totals: [0, violationCount, 0, 0],
109
- standardInstances: instances
110
- };
111
- }, withItems);
75
+ };
76
+ const whats = 'Elements have aria-describedby attributes with missing or invalid id values';
77
+ // Perform the test and return the result.
78
+ return doTest(
79
+ page, withItems, 'adbID', '[aria-describedby]', whats, 3, null, getBadWhat.toString()
80
+ );
112
81
  };
@@ -29,49 +29,35 @@
29
29
  Identify img elements whose alt attribute is a URL or file name.
30
30
  */
31
31
 
32
+ // IMPORTS
33
+
34
+ const {doTest} = require('../procs/testaro');
35
+
32
36
  // FUNCTIONS
33
37
 
34
38
  // Runs the test and returns the result.
35
39
  exports.reporter = async (page, withItems) => {
36
- // Return totals and standard instances for the rule.
37
- return await page.evaluate(withItems => {
38
- // Get all candidates, i.e. img elements with alt attributes.
39
- const candidates = document.body.querySelectorAll('img[alt]');
40
- let violationCount = 0;
41
- const instances = [];
42
- // For each candidate:
43
- candidates.forEach(element => {
44
- const alt = (element.getAttribute('alt') || '').trim();
45
- // If it is non-empty:
46
- if (alt) {
47
- const isURL = /^(?:https?:|file:|ftp:)\S+$/i.test(alt);
48
- const isFileName = /favicon|^\S+\.(?:png|jpe?g|gif|svg|webp|ico)$/i.test(alt);
49
- // If it is a URL or file name:
50
- if (isURL || isFileName) {
51
- // Increment the violation count.
52
- violationCount++;
53
- // If itemization is required:
54
- if (withItems) {
55
- const valueType = isURL && isFileName
56
- ? 'the URL of an image file'
57
- : (isURL ? 'a URL' : 'a file name');
58
- const what = `img element has an alt attribute with ${valueType} as its value`;
59
- // Add an instance to the instances.
60
- instances.push(window.getInstance(element, 'altScheme', what, 1, 2));
61
- }
62
- }
40
+ // Define a violation function for execution in the browser.
41
+ const getBadWhat = element => {
42
+ // Get the value of the alt attribute of the element.
43
+ const alt = (element.getAttribute('alt') || '').trim();
44
+ // If it is non-empty:
45
+ if (alt) {
46
+ const isURL = /^(?:https?:|file:|ftp:)\S+$/i.test(alt);
47
+ const isFileName = /favicon|^\S+\.(?:png|jpe?g|gif|svg|webp|ico)$/i.test(alt);
48
+ // If it is a URL or file name:
49
+ if (isURL || isFileName) {
50
+ const valueType = isURL && isFileName
51
+ ? 'the URL of an image file'
52
+ : (isURL ? 'a URL' : 'a file name');
53
+ // Return a violation description.
54
+ return `img element has an alt attribute with ${valueType} as its value`;
63
55
  }
64
- });
65
- // If there were any violations and itemization is not required:
66
- if (violationCount && ! withItems) {
67
- const what = 'img elements have alt attributes with URL or filename values';
68
- // Add a summary instance to the instances.
69
- instances.push(window.getInstance(null, 'altScheme', what, violationCount, 2, 'IMG'));
70
56
  }
71
- return {
72
- data: {},
73
- totals: [0, violationCount, 0, 0],
74
- standardInstances: instances
75
- };
76
- }, withItems);
57
+ };
58
+ const whats = 'img elements have alt attributes with URL or filename values';
59
+ // Perform the test and return the result.
60
+ return doTest(
61
+ page, withItems, 'altScheme', 'img[alt]', whats, 1, 'IMG', getBadWhat.toString()
62
+ );
77
63
  };
@@ -1,6 +1,7 @@
1
1
  /*
2
2
  © 2025 CVS Health and/or one of its affiliates. All rights reserved.
3
3
  © 2025 Juan S. Casado. All rights reserved.
4
+ © 2025 Jonathan Robert Pool. All rights reserved.
4
5
 
5
6
  MIT License
6
7
 
@@ -25,43 +26,28 @@
25
26
 
26
27
  /*
27
28
  captionLoc
28
- Report caption elements that are not the first child of their table element.
29
+ Report caption elements that are not the first children of table elements.
29
30
  */
30
31
 
32
+ // IMPORTS
33
+
34
+ const {doTest} = require('../procs/testaro');
35
+
31
36
  // FUNCTIONS
32
37
 
33
38
  exports.reporter = async (page, withItems) => {
34
- // Return totals and standard instances for the rule.
35
- return await page.evaluate(withItems => {
36
- // Get all candidates, i.e. caption elements.
37
- const candidates = document.body.querySelectorAll('caption');
38
- let violationCount = 0;
39
- const instances = [];
40
- // For each candidate:
41
- candidates.forEach(element => {
42
- const parent = element.parentElement;
43
- // If the element is not the first child of a table element:
44
- if (! parent || parent.tagName !== 'TABLE' || parent.firstElementChild !== el) {
45
- // Increment the violation count.
46
- violationCount++;
47
- // If itemization is required:
48
- if (withItems) {
49
- const what = 'caption element is not the first child of a table element';
50
- // Add an instance to the instances.
51
- instances.push(window.getInstance(element, 'captionLoc', what, 1, 3));
52
- }
53
- }
54
- });
55
- // If there are any violations and itemization is not required:
56
- if (violationCount && ! withItems) {
57
- const what = 'caption elements are not the first children of table elements';
58
- // Add a summary instance to the instances.
59
- instances.push(window.getInstance(null, 'captionLoc', what, violationCount, 3, 'caption'));
60
- }
61
- return {
62
- data: {},
63
- totals: [0, 0, 0, violationCount],
64
- standardInstances: instances
39
+ // Define a violation function for execution in the browser.
40
+ const getBadWhat = element => {
41
+ const parent = element.parentElement;
42
+ // If the element is not the first child of a table element:
43
+ if (! parent || parent.tagName !== 'TABLE' || parent.firstElementChild !== element) {
44
+ // Return a violation description.
45
+ return 'caption element is not the first child of a table element';
65
46
  }
66
- }, withItems);
47
+ };
48
+ const whats = 'caption elements are not the first children of table elements';
49
+ // Perform the test and return the result.
50
+ return doTest(
51
+ page, withItems, 'captionLoc', 'caption', whats, 3, 'CAPTION', getBadWhat.toString()
52
+ );
67
53
  };
@@ -1,6 +1,7 @@
1
1
  /*
2
2
  © 2025 CVS Health and/or one of its affiliates. All rights reserved.
3
3
  © 2025 Juan S. Casado. All rights reserved.
4
+ © 2025 Jonathan Robert Pool. All rights reserved.
4
5
 
5
6
  MIT License
6
7
 
@@ -28,22 +29,39 @@
28
29
  Report inputs whose list attribute references a missing or ambiguous datalist
29
30
  */
30
31
 
31
- const {init, getRuleResult} = require('../procs/testaro');
32
+ // IMPORTS
33
+
34
+ const {doTest} = require('../procs/testaro');
35
+
36
+ // FUNCTIONS
32
37
 
33
38
  exports.reporter = async (page, withItems) => {
34
- const all = await init(100, page, 'input[list]');
35
- for (const loc of all.allLocs) {
36
- const isBad = await loc.evaluate(el => {
37
- const list = el.getAttribute('list');
38
- if (!list) return false;
39
- const matches = Array.from(document.querySelectorAll('datalist')).filter(d => d.id === list);
40
- return matches.length !== 1;
41
- });
42
- if (isBad) all.locs.push(loc);
43
- }
44
- const whats = [
45
- 'list attribute of the element references an ambiguous or missing datalist element',
46
- 'list attributes of elements reference ambiguous or missing datalist elements'
47
- ];
48
- return await getRuleResult(withItems, all, 'datalistRef', whats, 3, 'INPUT');
39
+ const getBadWhat = element => {
40
+ // Get the ID of the datalist element referenced by the list attribute of the element.
41
+ const listID = element.getAttribute('list');
42
+ // If the element has a list attribute with a non-empty value:
43
+ if (listID) {
44
+ // Get the element it references.
45
+ const listElement = document.getElementById(listID);
46
+ // If no such element exists:
47
+ if (! listElement) {
48
+ // Return a violation description.
49
+ return 'input element list attribute references a missing element';
50
+ }
51
+ // Otherwise, if the element it references is not a datalist:
52
+ if (listElement.tagName.toLowerCase() !== 'datalist') {
53
+ // Return a violation description.
54
+ return 'input element list attribute references a non-datalist element';
55
+ }
56
+ }
57
+ // Otherwise, i.e. if it has no list attribute with a non-empty value:
58
+ else {
59
+ // Return a violation description.
60
+ return 'input element list attribute is empty';
61
+ }
62
+ };
63
+ const whats = 'list attributes of input elements are empty or IDs of no or non-datalist elements';
64
+ return doTest(
65
+ page, withItems, 'datalistRef', 'input[list]', whats, 3, 'INPUT', getBadWhat.toString()
66
+ );
49
67
  };
package/testaro/embAc.js CHANGED
@@ -30,41 +30,23 @@
30
30
  non-obvious what a user will activate with a click.
31
31
  */
32
32
 
33
- // ########## IMPORTS
33
+ // IMPORTS
34
34
 
35
- // Module to perform common operations.
36
- const {init, getRuleResult} = require('../procs/testaro');
35
+ const {doTest} = require('../procs/testaro');
37
36
 
38
- // ########## FUNCTIONS
37
+ // FUNCTIONS
39
38
 
40
- // Runs the test and returns the result.
41
39
  exports.reporter = async (page, withItems) => {
42
- // Initialize the locators and result.
43
- const all = await init(
44
- 100,
45
- page,
46
- 'a a, a button, a input, a select, button a, button button, button input, button select'
47
- );
48
- // For each locator:
49
- for (const loc of all.allLocs) {
50
- // Get whether its embedder is a link or a button.
51
- const embedderTagName = await loc.evaluate(element => {
52
- const embedder = element.parentElement.closest('a, button');
53
- return embedder ? embedder.tagName : '';
54
- });
55
- let param = 'a link or button';
56
- if (embedderTagName === 'A') {
57
- param = 'a link';
58
- }
59
- else if (embedderTagName === 'BUTTON') {
60
- param = 'a button';
61
- }
62
- all.locs.push([loc, param]);
63
- }
64
- // Populate and return the result.
65
- const whats = [
66
- 'Interactive element is embedded in __param__',
67
- 'Interactive elements are contained by links or buttons'
68
- ];
69
- return await getRuleResult(withItems, all, 'embAc', whats, 2);
40
+ const getBadWhat = element => {
41
+ // Get whether the embedding element is a link or a button.
42
+ const embedder = element.parentElement.closest('a, button');
43
+ const embedderWhat = embedder.tagName.toLowerCase() === 'a' ? 'a link' : 'a button';
44
+ // Return a violation description.
45
+ return `interactive element is embedded in ${embedderWhat}`;
46
+ };
47
+ const selector = ['a', 'button', 'input', 'select']
48
+ .map(tag => `a ${tag}, button ${tag}`)
49
+ .join(', ');
50
+ const whats = 'interactive elements are embedded in links or buttons';
51
+ return doTest(page, withItems, 'embAc', selector, whats, 2, null, getBadWhat.toString());
70
52
  };