testaro 60.17.0 → 60.18.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/CONTRIBUTING.md CHANGED
@@ -6,7 +6,7 @@ Testaro can benefit from contributions of various types, such as:
6
6
 
7
7
  - Adding other tools to the tools that it integrates.
8
8
  - Improving its execution speed.
9
- - Improving its own rule implementations.
9
+ - Improving the rule implementations of the `testaro` tool.
10
10
  - Implementing new rules.
11
11
 
12
12
  ## Adding tools
@@ -17,21 +17,23 @@ Tools that may merit consideration include:
17
17
 
18
18
  ## Improving execution speed
19
19
 
20
- To come.
20
+ Major improvements in execution speed were made in 2025 with efficiency improvements in the `testaro` tool, which previously consumed more time than any other tool. The `testaro` tests were refactored to perform their computations within the browser environment. This increased execution speed by about 2 orders of magnitude, permitting the elimination of element sampling.
21
+
22
+ Further improvements in execution speed are hypothesized to require parallelization, so that multiple tools perform their tests simultaneously.
21
23
 
22
24
  ## Improving rule implementations
23
25
 
24
- To come.
26
+ Testaro relies mainly on the integrated tools for rule implementation. The rules of the `testaro` tool are intended to fill gaps not covered by the integrated tools. The tests for those rules are typically more crude than the tests of the integrated tools, so improvements in implementation quality are possible.
25
27
 
26
28
  ## Implementing new rules
27
29
 
28
- Testaro has about 50 of its own rules, in addition to the approximately 900 rules of the other tools that it integrates. According to the issue classifications in the [Testilo](https://www.npmjs.com/package/testilo) package, these 950 or so rules can be classified into about 290 accessibility _issues_, because some rules of some tools at least approximately duplicate some rules of other tools.
30
+ Testaro has about 50 of its own rules, in addition to the approximately 950 rules of the other tools that it integrates. According to the issue classifications in the [Testilo](https://www.npmjs.com/package/testilo) package, these 1000 or so rules can be classified into about 300 accessibility _issues_, because some rules of some tools at least approximately duplicate some rules of other tools.
29
31
 
30
32
  However, many other significant accessibility issues exist that are not covered by any of the existing rules. Thus, Testaro welcomes contributions of new rules for such issues.
31
33
 
32
34
  ### Step 1
33
35
 
34
- The first step in contributing a new rule to Testaro is to satisfy yourself that it will not duplicate existing rules. The latest `procs/score/tic….js` file in the Testilo package should be helpful here.
36
+ The first step in contributing a new rule to Testaro is to satisfy yourself that it will not duplicate existing rules. The `procs/score/tic.js` file in the Testilo package should be helpful here.
35
37
 
36
38
  ### Step 2
37
39
 
@@ -46,57 +48,19 @@ Inspecting some of the jobs and targets in the `validation/tests` directory can
46
48
 
47
49
  ### Step 3
48
50
 
49
- The third step is to add an entry to the `evalRules` or `etcRules` object in the `tests/testaro.js` file.
51
+ The third step is to add an entry to the `allRules` object in the `tests/testaro.js` file.
50
52
 
51
53
  ### Step 4
52
54
 
53
- The fourth step is to implement the new rule by creating a JavaScript or JSON file and saving it in the `testaro` directory.
55
+ The fourth step is to implement the new rule by creating a JavaScript file and saving it in the `testaro` directory.
54
56
 
55
57
  To optimize quality, it may be wise for one person to perform steps 1, 2, and 3, and then for a second person independently to perform step 4 (“clean-room” development).
56
58
 
57
59
  At any time after an implementation is attempted or revised, the developer can run the validation on it, simply by executing the statement `npm test xyz` (replacing `xyz` with the name of the new rule). When the implementation fails validation, diagnosis may find fault either with the implementation or with the validator.
58
60
 
59
- Whether a new rule should be implemented in JSON or JavaScript depends on the complexity of the rule. The JSON format is effective for simple rules, and JavaScript is needed for more complex rules.
60
-
61
- ### Simple rules
62
-
63
- You can create a JSON-defined rule if a single CSS selector can identify all and only the elements on a page that violate the rule.
64
-
65
- Suppose, for example, that you want a rule prohibiting `i` elements (because `i` represents confusingly many different semantic properties). A single CSS selector, namely `"i"`, will identify all the `i` elements on the page, so this rule can be defined with JSON.
66
-
67
- Substantially more complex rules, too, can satisfy this criterion. An example is the `titledEl` rule, which prohibits an element from having a `title` attribute unless the element type is `input`, `button`, `textarea`, `select`, or `iframe`. Its CSS selector is `"[title]:not(input, button, textarea, select, iframe)"`. The CSS selector in a JSON-defined rule may include [custom Playwright pseudo-classes](https://playwright.dev/docs/other-locators#css-locator).
68
-
69
- You can copy and revise any of the existing JSON files in the `testaro` directory to implement a new rule. If, for example, you start with a copy of the `titledEl` file, you can change its properties to fit your new rule. In particular:
70
-
71
- ```json
72
- {
73
- "ruleID": "titledEl",
74
- "selector": "[title]:not(input, button, textarea, select, iframe):visible",
75
- "complaints": {
76
- "instance": "Ineligible element has a title attribute",
77
- "summary": "Ineligible elements have title attributes"
78
- },
79
- "ordinalSeverity": 2,
80
- "summaryTagName": ""
81
- }
82
- ```
83
-
84
- - Assign a violation description for a single instance to `complaints.instance`.
85
- - Assign a violation description for a summary instance (when itemization has been turned off) to `complaints.summary`.
86
- - Assign an integer from 0 through 3 to `ordinalSeverity`.
87
- - If all instances of violations of the rule necessarily involve elements of the same type, assign its tag name (such as `"BUTTON"`) to `summaryTagName`.
88
-
89
- ### Simplifiable rules
90
-
91
- More complex Testaro rules are implemented in JavaScript. Some rules are _simplifiable_. These can be implemented with JavaScript modules like the one for the `allSlanted` rule. To implement such a rule, you can copy an existing module and replace the values of the 6 properties of the`ruleData` object. The significant decisions here are about the values of the `selector` and `pruner` properties.
92
-
93
- The `selector` value is a CSS selector that identifies candidate elements for violation reporting. What makes this rule simplifiable, instead of simple, is that these elements may or may not be determined to violate the rule. Each of the elements identified by the selector must be further analyzed by the pruner. The pruner takes a Playwright locator as its argument and returns `true` if it finds that the element located by the locator violates the rule, or `false` if not.
94
-
95
- ### Complex rules
96
-
97
- Even more complex Testaro rules require analysis that cannot fit into the simple or simplifiable category. You can begin with existing JavaScript rules, or the `data/template.js` file, as an example.
61
+ Whether a new rule should be implemented with support from the `doTest` function or the `getBasicResult` function depends on the requirements of the rule. If operations on each element suffice to determine whether and how that element violates the rule, the `doTest` function is appropriate. If, however, the verdict on an element requires features offered by Playwright or other dependencies that cannot be replicated easily in the browser environment, or if the verdict on one element depends on the state or properties of other elements, then the `getBasicResult` function is appropriate.
98
62
 
99
- Some utility functions in modules in the `procs` directory are available for support of new rules. Among these modules are `testaro` (used in many tests), `isInlineLink`, `operable`, and `visChange`.
63
+ The existing `testaro` tests can serve as templates for new ones. At present, only the `hover` and `role` tests make use of the `getBasicResult` function.
100
64
 
101
65
  ## License agreement
102
66
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "60.17.0",
3
+ "version": "60.18.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": {
package/procs/testaro.js CHANGED
@@ -15,8 +15,6 @@
15
15
 
16
16
  // ########## IMPORTS
17
17
 
18
- // Module to sample a population.
19
- const {getSample} = require('../procs/sample');
20
18
  // Module to get locator data.
21
19
  const {getLocatorData} = require('../procs/getLocatorData');
22
20
  // Module to get element IDs.
@@ -26,122 +24,6 @@ const {xPath} = require('playwright-dompath');
26
24
 
27
25
  // ########## FUNCTIONS
28
26
 
29
- // Initializes violation locators and a result and returns them in an object.
30
- const init = exports.init = async (sampleMax, page, locAllSelector, options = {}) => {
31
- // Get locators for the specified elements.
32
- const locPop = page.locator(locAllSelector, options);
33
- const locPops = await locPop.all();
34
- const populationSize = locPops.length;
35
- const sampleSize = Math.min(sampleMax, populationSize);
36
- const locIndexes = getSample(locPops, sampleSize);
37
- const allLocs = locIndexes.map(index => locPops[index]);
38
- const result = {
39
- data: {
40
- populationSize,
41
- sampleSize,
42
- populationRatio: sampleSize ? populationSize / sampleSize : null
43
- },
44
- totals: [0, 0, 0, 0],
45
- standardInstances: []
46
- };
47
- // Return the result.
48
- return {
49
- allLocs,
50
- locs: [],
51
- result
52
- };
53
- };
54
-
55
- // Populates and returns a result.
56
- const getRuleResult = exports.getRuleResult = async (
57
- withItems, all, ruleID, whats, ordinalSeverity, tagName = ''
58
- ) => {
59
- const {locs, result} = all;
60
- const {data, totals, standardInstances} = result;
61
- // For each violation locator:
62
- for (const locItem of locs) {
63
- // Get data on its element.
64
- let loc, whatParam;
65
- if (Array.isArray(locItem)) {
66
- loc = locItem[0];
67
- whatParam = locItem[1];
68
- }
69
- else {
70
- loc = locItem;
71
- }
72
- const elData = await getLocatorData(loc);
73
- // Increment the totals.
74
- totals[ordinalSeverity] += data.populationRatio;
75
- // If itemization is required:
76
- if (withItems) {
77
- // Get the bounding box of the element.
78
- const {tagName, id, location, excerpt} = elData;
79
- const box = location.type === 'box' ? location.spec : await boxOf(loc);
80
- // Add a standard instance to the result.
81
- standardInstances.push({
82
- ruleID,
83
- what: whatParam ? whats[0].replace('__param__', whatParam) : whats[0],
84
- ordinalSeverity,
85
- tagName,
86
- id,
87
- location,
88
- excerpt,
89
- boxID: boxToString(box),
90
- pathID: tagName === 'HTML' ? '/html' : await xPath(loc)
91
- });
92
- }
93
- }
94
- // If itemization is not required and any instances exist:
95
- if (! withItems && locs.length) {
96
- // Add a summary standard instance to the result.
97
- standardInstances.push({
98
- ruleID,
99
- what: whats[1],
100
- ordinalSeverity,
101
- count: Math.round(totals[ordinalSeverity]),
102
- tagName,
103
- id: '',
104
- location: {
105
- doc: '',
106
- type: '',
107
- spec: ''
108
- },
109
- excerpt: '',
110
- boxID: '',
111
- pathID: ''
112
- });
113
- }
114
- // Return the result.
115
- return result;
116
- };
117
- // Performs a simplifiable test.
118
- exports.simplify = async (page, withItems, ruleData) => {
119
- const {
120
- ruleID, selector, pruner, complaints, ordinalSeverity, summaryTagName
121
- } = ruleData;
122
- // Get an object with initialized violation locators and result as properties.
123
- const all = await init(100, page, selector);
124
- // For each locator:
125
- for (const loc of all.allLocs) {
126
- // Get whether its element violates the rule.
127
- const isBad = await pruner(loc);
128
- // If it does:
129
- if (isBad) {
130
- // Add the locator of the element to the array of violation locators.
131
- all.locs.push(loc);
132
- }
133
- }
134
- // Populate and return the result.
135
- const whats = [
136
- complaints.instance,
137
- complaints.summary
138
- ];
139
- const result = await getRuleResult(
140
- withItems, all, ruleID, whats, ordinalSeverity, summaryTagName
141
- );
142
- // Return the result.
143
- return result;
144
- };
145
27
  // Performs a standard test.
146
28
  exports.doTest = async (
147
29
  page,
@@ -14,48 +14,44 @@
14
14
  This test reports elements with native or transformed upper-case text at least 8 characters long. Blocks of upper-case text are difficult to read.
15
15
  */
16
16
 
17
- // ########## IMPORTS
17
+ // IMPORTS
18
18
 
19
- // Module to perform common operations.
20
- const {simplify} = require('../procs/testaro');
19
+ const {doTest} = require('../procs/testaro');
21
20
 
22
- // ########## FUNCTIONS
21
+ // FUNCTIONS
23
22
 
24
23
  // Runs the test and returns the result.
25
24
  exports.reporter = async (page, withItems) => {
26
- // Specify the rule.
27
- const ruleData = {
28
- ruleID: 'allCaps',
29
- selector: 'body *:not(style, script, svg)',
30
- pruner: async loc => await loc.evaluate(el => {
31
- // Get the concatenated and debloated text content of the element and its child text nodes.
32
- const elText = Array
33
- .from(el.childNodes)
34
- .filter(node => node.nodeType === Node.TEXT_NODE)
35
- .map(textNode => textNode.nodeValue)
36
- .join(' ')
37
- .replace(/\s{2,}/g, ' ')
38
- .replace(/-{2,}/g, '-');
39
- // If the element text includes 8 sequential upper-case letters, spaces, or hyphen-minuses:
40
- if (/[- A-Z]{8}/.test(elText)) {
41
- // Report this.
42
- return true;
25
+ const getBadWhat = element => {
26
+ // Get the child text nodes of the element.
27
+ const childTextNodes = Array.from(element.childNodes).filter(
28
+ node => node.nodeType === Node.TEXT_NODE
29
+ );
30
+ // Get the concatenation of their texts that contain 8 or more consecutive letters.
31
+ let longText = childTextNodes
32
+ .map(node => node.nodeValue.trim())
33
+ .filter(text => /[A-Z]{8,}/i.test(text))
34
+ .join(' ');
35
+ // If there is any:
36
+ if (longText) {
37
+ // Get the style declaration of the element.
38
+ const styleDec = window.getComputedStyle(element);
39
+ const {textTransform} = styleDec;
40
+ // If the style declaration transforms the text to upper case:
41
+ if (textTransform === 'uppercase') {
42
+ // Return a violation description.
43
+ return 'Element text is rendered as all-capital';
43
44
  }
44
- // Otherwise:
45
- else {
46
- // Report whether its text is at least 8 characters long and transformed to upper case.
47
- const elStyleDec = window.getComputedStyle(el);
48
- const transformStyle = elStyleDec.textTransform;
49
- return transformStyle === 'uppercase' && elText.length > 7;
45
+ // Otherwise, if the text contains 8 or more consecutive upper-case letters:
46
+ if (/[A-Z]{8,}/.test(longText)) {
47
+ // Return a violation description.
48
+ return 'Element contains all-capital text';
50
49
  }
51
- }),
52
- complaints: {
53
- instance: 'Element contains all-capital text',
54
- summary: 'Elements contain all-capital text'
55
- },
56
- ordinalSeverity: 0,
57
- summaryTagName: ''
50
+ }
58
51
  };
59
- // Run the test and return the result.
60
- return await simplify(page, withItems, ruleData);
52
+ const selector = 'body *:not(style, script, svg)';
53
+ const whats = 'Elements have all-capital text';
54
+ return await doTest(
55
+ page, withItems, 'allCaps', selector, whats, 0, null, getBadWhat.toString()
56
+ );
61
57
  };
@@ -14,31 +14,26 @@
14
14
  This test reports elements with italic or oblique text at least 40 characters long. Blocks of slanted text are difficult to read.
15
15
  */
16
16
 
17
- // ########## IMPORTS
17
+ // IMPORTS
18
18
 
19
- // Module to perform common operations.
20
- const {simplify} = require('../procs/testaro');
19
+ const {doTest} = require('../procs/testaro');
21
20
 
22
- // ########## FUNCTIONS
21
+ // FUNCTIONS
23
22
 
24
23
  // Runs the test and returns the result.
25
24
  exports.reporter = async (page, withItems) => {
26
- // Specify the rule.
27
- const ruleData = {
28
- ruleID: 'allSlanted',
29
- selector: 'body *:not(style, script, svg)',
30
- pruner: async loc => await loc.evaluate(el => {
31
- const elStyleDec = window.getComputedStyle(el);
32
- const elText = el.textContent;
33
- return ['italic', 'oblique'].includes(elStyleDec.fontStyle) && elText.length > 39;
34
- }),
35
- complaints: {
36
- instance: 'Element contains all-italic or all-oblique text',
37
- summary: 'Elements contain all-italic or all-oblique text'
38
- },
39
- ordinalSeverity: 0,
40
- summaryTagName: ''
25
+ const getBadWhat = element => {
26
+ const styleDec = window.getComputedStyle(element);
27
+ const {textContent} = element;
28
+ // If the element contains 40 or more characters of slanted text:
29
+ if (['italic', 'oblique'].includes(styleDec.fontStyle) && textContent.length > 39) {
30
+ // Return a violation description.
31
+ return 'Element contains all-slanted text';
32
+ }
41
33
  };
42
- // Run the test and return the result.
43
- return await simplify(page, withItems, ruleData);
34
+ const selector = 'body *:not(style, script, svg)';
35
+ const whats = 'Elements contain all-slanted text';
36
+ return await doTest(
37
+ page, withItems, 'allSlanted', selector, whats, 0, null, getBadWhat.toString()
38
+ );
44
39
  };
@@ -14,32 +14,30 @@
14
14
  This test reports elements whose transform style properties distort the content. Distortion makes text difficult to read.
15
15
  */
16
16
 
17
- // ########## IMPORTS
17
+ // IMPORTS
18
18
 
19
- // Module to perform common operations.
20
- const {simplify} = require('../procs/testaro');
19
+ const {doTest} = require('../procs/testaro');
21
20
 
22
- // ########## FUNCTIONS
21
+ // FUNCTIONS
23
22
 
24
23
  // Runs the test and returns the result.
25
24
  exports.reporter = async (page, withItems) => {
26
- // Specify the rule.
27
- const ruleData = {
28
- ruleID: 'distortion',
29
- selector: 'body *',
30
- pruner: async loc => await loc.evaluate(el => {
31
- const styleDec = window.getComputedStyle(el);
32
- const {transform} = styleDec;
33
- return transform
34
- && ['matrix', 'perspective', 'rotate', 'scale', 'skew'].some(key => transform.includes(key));
35
- }),
36
- complaints: {
37
- instance: 'Element distorts its text',
38
- summary: 'Elements distort their texts'
39
- },
40
- ordinalSeverity: 1,
41
- summaryTagName: ''
25
+ const getBadWhat = element => {
26
+ const styleDec = window.getComputedStyle(element);
27
+ const {transform} = styleDec;
28
+ const badTransformTypes = ['matrix', 'perspective', 'rotate', 'scale', 'skew'];
29
+ // If the element style transforms the text:
30
+ if (transform) {
31
+ const transformType = badTransformTypes.find(key => transform.includes(key));
32
+ // If the transformation is distortive:
33
+ if (transformType) {
34
+ // Return a violation description.
35
+ return `Element distorts its text with ${transformType} transformation`;
36
+ }
37
+ }
42
38
  };
43
- // Run the test and return the result.
44
- return await simplify(page, withItems, ruleData);
39
+ const whats = 'Elements distort their texts';
40
+ return await doTest(
41
+ page, withItems, 'distortion', 'body *', whats, 0, null, getBadWhat.toString()
42
+ );
45
43
  };
@@ -70,19 +70,18 @@ exports.reporter = async (page, withItems) => {
70
70
  ]);
71
71
  // Initialize the operabilities of the element.
72
72
  const opHow = [];
73
- let hasPointer = false;
74
73
  // If the element is not a label:
75
74
  if (element.tagName !== 'LABEL') {
76
- const styleDec = window.getComputedStyle(element);
77
- hasPointer = styleDec.cursor === 'pointer';
75
+ const liveStyleDec = window.getComputedStyle(element);
78
76
  // If it has a pointer cursor:
79
- if (hasPointer) {
77
+ if (liveStyleDec.cursor === 'pointer') {
80
78
  // Neutralize the cursor style of the parent element of the element.
81
79
  element.parentElement.style.cursor = 'default';
82
- // Get whether, after this, the element still has a pointer cursor.
83
- hasPointer = styleDec.cursor === 'pointer';
84
- // Add this to the operabilities of the element.
85
- opHow.push('pointer cursor');
80
+ // If, after this, the element still has a pointer cursor:
81
+ if (liveStyleDec.cursor === 'pointer') {
82
+ // Add this to the operabilities of the element.
83
+ opHow.push('pointer cursor');
84
+ }
86
85
  }
87
86
  }
88
87
  // If the element has a click event listener:
package/testaro/hover.js CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  /*
12
12
  hover
13
- This test reports unexpected impacts of hovering. The elements that are subjected to hovering (called “triggers”) include all the elements that have attributes associated with control over the visibility of other elements. If hovering over an element results in an increase or decrease in the total count of visible elements in the tree rooted in the grandparent of the trigger, the rule is considered violated.
13
+ This test reports unexpected impacts of hovering. The elements that are subjected to hovering (called “triggers”) include all the elements that have attributes associated with control over the visibility of other elements. If hovering over an element results in an increase or decrease in the total count of visible elements in the tree rooted in the grandparent of the trigger, the rule is considered violated. This test uses the getBasicResult function in order to use Playwright for the most realistic hover simulation.
14
14
  */
15
15
 
16
16
  // IMPORTS
@@ -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.
4
+ © 2025 Jonathan Robert Pool
4
5
 
5
6
  Licensed under the MIT License. See LICENSE file at the project root or
6
7
  https://opensource.org/license/mit/ for details.
@@ -11,25 +12,27 @@
11
12
  /*
12
13
  imageLink
13
14
  Clean-room rule.
14
- This test reports anchor elements whose href attributes point to image files.
15
+ This test reports links whose destinations are image files.
15
16
  */
16
17
 
17
- const {simplify} = require('../procs/testaro');
18
+ // IMPORTS
18
19
 
20
+ const {doTest} = require('../procs/testaro');
21
+
22
+ // FUNCTIONS
23
+
24
+ // Runs the test and returns the result.
19
25
  exports.reporter = async (page, withItems) => {
20
- const ruleData = {
21
- ruleID: 'imageLink',
22
- selector: 'a[href]',
23
- pruner: async loc => await loc.evaluate(el => {
24
- const href = el.getAttribute('href') || '';
25
- return /\.(?:png|jpe?g|gif|svg|webp|ico)(?:$|[?#])/i.test(href);
26
- }),
27
- complaints: {
28
- instance: 'Link destination is an image file',
29
- summary: 'Links have image files as their destinations'
30
- },
31
- ordinalSeverity: 0,
32
- summaryTagName: 'A'
26
+ const getBadWhat = element => {
27
+ const href = element.getAttribute('href') || '';
28
+ // If the destination of the element is an image file:
29
+ if (/\.(?:png|jpe?g|gif|svg|webp|ico)(?:$|[?#])/i.test(href)) {
30
+ // Return a violation description.
31
+ return 'Link destination is an image file';
32
+ }
33
33
  };
34
- return await simplify(page, withItems, ruleData);
34
+ const whats = 'Links have image files as their destinations';
35
+ return await doTest(
36
+ page, withItems, 'imageLink', 'a[href]', whats, 0, 'A', getBadWhat.toString()
37
+ );
35
38
  };
@@ -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.
4
+ © 2025 Jonathan Robert Pool.
4
5
 
5
6
  Licensed under the MIT License. See LICENSE file at the project root or
6
7
  https://opensource.org/license/mit/ for details.
@@ -14,30 +15,24 @@
14
15
  This test reports legend elements that are not the first children of fieldset elements.
15
16
  */
16
17
 
17
- const {simplify} = require('../procs/testaro');
18
+ // IMPORTS
18
19
 
20
+ const {doTest} = require('../procs/testaro');
21
+
22
+ // FUNCTIONS
23
+
24
+ // Runs the test and returns the result.
19
25
  exports.reporter = async (page, withItems) => {
20
- const ruleData = {
21
- ruleID: 'legendLoc',
22
- selector: 'legend',
23
- pruner: async (loc) => await loc.evaluate(el => {
24
- const parent = el.parentElement;
25
- if (!parent) return true;
26
- if (parent.tagName.toUpperCase() !== 'FIELDSET') return true;
27
- // Check if this legend is the first element child of the fieldset
28
- for (const child of parent.children) {
29
- if (child.nodeType === 1) {
30
- return child !== el; // true if not first child
31
- }
32
- }
33
- return true;
34
- }),
35
- complaints: {
36
- instance: 'Element is not the first child of a fieldset element',
37
- summary: 'legend elements are not the first children of fieldset elements'
38
- },
39
- ordinalSeverity: 3,
40
- summaryTagName: 'LEGEND'
26
+ const getBadWhat = element => {
27
+ const parent = element.parentElement;
28
+ // If the element violates the rule:
29
+ if (! (parent && parent.tagName === 'FIELDSET' && parent.firstElementChild === element)) {
30
+ // Return a violation description.
31
+ return 'Element is not the first child of a fieldset element';
32
+ }
41
33
  };
42
- return await simplify(page, withItems, ruleData);
34
+ const whats = 'Legend elements are not the first children of fieldset elements';
35
+ return await doTest(
36
+ page, withItems, 'legendLoc', 'legend', whats, 3, 'LEGEND', getBadWhat.toString()
37
+ );
43
38
  };
package/testaro/linkUl.js CHANGED
@@ -10,43 +10,37 @@
10
10
 
11
11
  /*
12
12
  linkUl
13
- This test reports failures to underline inline links. Underlining and color are the traditional style properties that identify links. Lists of links containing only links can be recognized without underlines, but other links are difficult or impossible to distinguish visually from surrounding text if not underlined. Underlining adjacent links only on hover provides an indicator valuable only to mouse users, and even they must traverse the text with a mouse merely to discover which passages are links.
13
+ This test reports failures to underline inline links. Underlining and color are the traditional style properties that identify links. Lists of links containing only links may be recognizable without underlines, but other links are difficult or impossible to distinguish visually from surrounding text if not underlined. Underlining adjacent links only on hover provides an indicator valuable only to mouse users, and even they must traverse the text with a mouse merely to discover which passages are links.
14
14
  */
15
15
 
16
- // ########## IMPORTS
16
+ // IMPORTS
17
17
 
18
- // Module to perform common operations.
19
- const {simplify} = require('../procs/testaro');
20
- // Module to classify links.
21
- const {isInlineLink} = require('../procs/isInlineLink');
18
+ const {doTest} = require('../procs/testaro');
22
19
 
23
- // ########## FUNCTIONS
20
+ // FUNCTIONS
24
21
 
25
22
  // Runs the test and returns the result.
26
23
  exports.reporter = async (page, withItems) => {
27
- // Specify the rule.
28
- const ruleData = {
29
- ruleID: 'linkUl',
30
- selector: 'a',
31
- pruner: async loc => {
32
- // Get whether each link is underlined.
33
- const isUnderlined = await loc.evaluate(el => {
34
- const styleDec = window.getComputedStyle(el);
35
- return styleDec.textDecorationLine === 'underline';
36
- });
37
- // If it is not:
38
- if (! isUnderlined) {
39
- // Return whether it is a violator.
40
- return await isInlineLink(loc);
24
+ const getBadWhat = element => {
25
+ const liAncestor = element.closest('li');
26
+ // If the element is not the only link inside a list item:
27
+ if (! (liAncestor && liAncestor.getElementsByTagName('a').length === 1)) {
28
+ const styleDec = window.getComputedStyle(element);
29
+ const {textDecoration} = styleDec;
30
+ // If the element text is not underlined:
31
+ if (! textDecoration.includes('underline')) {
32
+ const styleDec = window.getComputedStyle(element);
33
+ const {display} = styleDec;
34
+ // If the element has does not have a block display style:
35
+ if (display !== 'block') {
36
+ // Return a violation description.
37
+ return 'Element is not a list item but is not underlined';
38
+ }
41
39
  }
42
- },
43
- complaints: {
44
- instance: 'Link is inline but has no underline',
45
- summary: 'Inline links are missing underlines'
46
- },
47
- ordinalSeverity: 1,
48
- summaryTagName: 'A'
40
+ }
49
41
  };
50
- // Run the test and return the result.
51
- return await simplify(page, withItems, ruleData);
42
+ const whats = 'Links that are not list items are not underlined';
43
+ return await doTest(
44
+ page, withItems, 'linkUl', 'a', whats, 1, 'A', getBadWhat.toString()
45
+ );
52
46
  };
@@ -14,65 +14,49 @@
14
14
  This test reports tables used for layout.
15
15
  */
16
16
 
17
- // ########## IMPORTS
17
+ // IMPORTS
18
18
 
19
- // Module to perform common operations.
20
- const {simplify} = require('../procs/testaro');
19
+ const {doTest} = require('../procs/testaro');
21
20
 
22
- // ########## FUNCTIONS
21
+ // FUNCTIONS
23
22
 
24
23
  // Runs the test and returns the result.
25
24
  exports.reporter = async (page, withItems) => {
26
- // Specify the rule.
27
- const ruleData = {
28
- ruleID: 'nonTable',
29
- selector: 'table',
30
- pruner: async loc => await loc.evaluate(el => {
31
- const role = el.getAttribute('role');
32
- // If it contains another table:
33
- if (el.querySelector('table')) {
34
- // Return misuse.
35
- return true;
36
- }
37
- // Otherwise, if it has only 1 column or 1 row:
38
- else if (
39
- el.querySelectorAll('tr').length === 1
40
- || Math.max(
41
- ... Array
42
- .from(el.querySelectorAll('tr'))
43
- .map(row => Array.from(row.querySelectorAll('th, td')).length)
44
- ) === 1
45
- ) {
46
- // Return misuse.
47
- return true;
48
- }
49
- // Otherwise, if it contains an object or player:
50
- else if (el.querySelector('object, embed, applet, audio, video')) {
51
- // Return misuse.
52
- return true;
53
- }
54
- // Otherwise, if it contains a table-compatible element:
55
- else if (
56
- el.caption
57
- || ['grid', 'treegrid'].includes(role)
58
- || el.querySelector('col, colgroup, tfoot, thead, th')
59
- ) {
60
- // Return validity.
61
- return false;
62
- }
63
- // Otherwise:
64
- else {
65
- // Return misuse.
66
- return true;
67
- }
68
- }),
69
- complaints: {
70
- instance: 'Table is misused to arrange content',
71
- summary: 'Tables are misused to arrange content'
72
- },
73
- ordinalSeverity: 2,
74
- summaryTagName: 'TABLE'
25
+ const getBadWhat = element => {
26
+ // If the element contains another table:
27
+ if (element.querySelector('table')) {
28
+ // Return a violation description.
29
+ return 'Element contains another table';
30
+ }
31
+ const rowCount = element.querySelectorAll('tr').length;
32
+ const columnCount = Math.max(
33
+ ... Array
34
+ .from(element.querySelectorAll('tr'))
35
+ .map(row => Array.from(row.querySelectorAll('th, td')).length)
36
+ );
37
+ // Otherwise, if it has only 1 column or 1 row:
38
+ if (rowCount === 1 || columnCount === 1) {
39
+ // Return a violation description.
40
+ return 'Element has only one row or one column';
41
+ }
42
+ // Otherwise, if it contains an object or player:
43
+ if (element.querySelector('object, embed, applet, audio, video')) {
44
+ // Return a violation description.
45
+ return 'Element contains an object or player';
46
+ }
47
+ const role = element.getAttribute('role');
48
+ // Otherwise, if it has no table-compatible explicit role or descendant element:
49
+ if (! (
50
+ ['grid', 'treegrid'].includes(role)
51
+ || element.caption
52
+ || element.querySelector('col, colgroup, tfoot, th, thead')
53
+ )) {
54
+ // Return a violation description.
55
+ return 'Element has no table-compatible explicit role or descendant element';
56
+ }
75
57
  };
76
- // Run the test and return the result.
77
- return await simplify(page, withItems, ruleData);
58
+ const whats = 'table elements are misused for non-table content';
59
+ return await doTest(
60
+ page, withItems, 'nonTable', 'table', whats, 2, 'TABLE', getBadWhat.toString()
61
+ );
78
62
  };
@@ -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.
4
+ © 2025 Jonathan Robert Pool.
4
5
 
5
6
  Licensed under the MIT License. See LICENSE file at the project root or
6
7
  https://opensource.org/license/mit/ for details.
@@ -11,24 +12,26 @@
11
12
  /*
12
13
  optRoleSel
13
14
  Clean-room rule.
14
- This test reports elements with role="option" that are missing aria-selected attributes.
15
+ This test reports elements with role=option that are missing aria-selected attributes.
15
16
  */
16
17
 
17
- const {simplify} = require('../procs/testaro');
18
+ // IMPORTS
18
19
 
20
+ const {doTest} = require('../procs/testaro');
21
+
22
+ // FUNCTIONS
23
+
24
+ // Runs the test and returns the result.
19
25
  exports.reporter = async (page, withItems) => {
20
- const ruleData = {
21
- ruleID: 'optRoleSel',
22
- selector: '[role="option"]',
23
- pruner: async (loc) => await loc.evaluate(el => {
24
- return ! el.hasAttribute('aria-selected');
25
- }),
26
- complaints: {
27
- instance: 'Element has an explicit option role but no aria-selected attribute',
28
- summary: 'Elements with explicit option roles have no aria-selected attributes'
29
- },
30
- ordinalSeverity: 1,
31
- summaryTagName: ''
26
+ const getBadWhat = element => {
27
+ // If the element has no aria-selected attribute:
28
+ if (! element.hasAttribute('aria-selected')) {
29
+ // Return a violation description.
30
+ return 'Element has role=option but no aria-selected attribute';
31
+ }
32
32
  };
33
- return await simplify(page, withItems, ruleData);
33
+ const whats = 'Elements with role=option have no aria-selected attributes';
34
+ return await doTest(
35
+ page, withItems, 'optRoleSel', '[role="option"]', whats, 1, null, getBadWhat.toString()
36
+ );
34
37
  };
package/testaro/role.js CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  /*
12
12
  role
13
- This test reports elements with native-replacing explicit role attributes.
13
+ This test reports elements with native-replacing explicit role attributes. This test uses the getBasicResult function in order to have access to the aria-query dependency.
14
14
  */
15
15
 
16
16
  // IMPORTS
package/tests/testaro.js CHANGED
@@ -15,12 +15,8 @@
15
15
 
16
16
  // IMPORTS
17
17
 
18
- // Module to perform common operations.
19
- const {init, getRuleResult} = require('../procs/testaro');
20
18
  // Function to launch a browser.
21
19
  const {launch} = require('../run');
22
- // Module to handle files.
23
- const fs = require('fs/promises');
24
20
 
25
21
  // CONSTANTS
26
22
 
@@ -44,7 +40,7 @@ const allRules = [
44
40
  id: 'allCaps',
45
41
  what: 'leaf elements with entirely upper-case text longer than 7 characters',
46
42
  launchRole: 'sharer',
47
- timeOut: 10,
43
+ timeOut: 5,
48
44
  defaultOn: true
49
45
  },
50
46
  {
@@ -114,14 +110,14 @@ const allRules = [
114
110
  id: 'distortion',
115
111
  what: 'distorted text',
116
112
  launchRole: 'sharer',
117
- timeOut: 10,
113
+ timeOut: 5,
118
114
  defaultOn: true
119
115
  },
120
116
  {
121
117
  id: 'docType',
122
118
  what: 'document without a doctype property',
123
119
  launchRole: 'sharer',
124
- timeOut: 10,
120
+ timeOut: 5,
125
121
  defaultOn: true
126
122
  },
127
123
  {
@@ -170,7 +166,7 @@ const allRules = [
170
166
  id: 'labClash',
171
167
  what: 'labeling inconsistencies',
172
168
  launchRole: 'sharer',
173
- timeOut: 10,
169
+ timeOut: 5,
174
170
  defaultOn: true
175
171
  },
176
172
  {
@@ -184,14 +180,14 @@ const allRules = [
184
180
  id: 'lineHeight',
185
181
  what: 'text with a line height less than 1.5 times its font size',
186
182
  launchRole: 'sharer',
187
- timeOut: 10,
183
+ timeOut: 5,
188
184
  defaultOn: true
189
185
  },
190
186
  {
191
187
  id: 'linkAmb',
192
188
  what: 'links with identical texts but different destinations',
193
189
  launchRole: 'sharer',
194
- timeOut: 50,
190
+ timeOut: 20,
195
191
  defaultOn: true
196
192
  },
197
193
  {
@@ -208,13 +204,6 @@ const allRules = [
208
204
  timeOut: 5,
209
205
  defaultOn: true
210
206
  },
211
- {
212
- id: 'linkTitle',
213
- what: 'links with title attributes repeating text content',
214
- launchRole: 'sharer',
215
- timeOut: 10,
216
- defaultOn: true
217
- },
218
207
  {
219
208
  id: 'linkTo',
220
209
  what: 'links without destinations',
@@ -226,7 +215,7 @@ const allRules = [
226
215
  id: 'linkUl',
227
216
  what: 'missing underlines on inline links',
228
217
  launchRole: 'sharer',
229
- timeOut: 10,
218
+ timeOut: 5,
230
219
  defaultOn: true
231
220
  },
232
221
  {
@@ -275,7 +264,7 @@ const allRules = [
275
264
  id: 'role',
276
265
  what: 'native-replacing explicit roles',
277
266
  launchRole: 'sharer',
278
- timeOut: 5,
267
+ timeOut: 20,
279
268
  defaultOn: true
280
269
  },
281
270
  {
@@ -303,7 +292,7 @@ const allRules = [
303
292
  id: 'textSem',
304
293
  what: 'semantically vague elements i, b, and/or small',
305
294
  launchRole: 'sharer',
306
- timeOut: 10,
295
+ timeOut: 5,
307
296
  defaultOn: true
308
297
  },
309
298
  {
@@ -387,7 +376,7 @@ const allRules = [
387
376
  id: 'hover',
388
377
  what: 'hover-caused content changes',
389
378
  launchRole: 'waster',
390
- timeOut: 300,
379
+ timeOut: 20,
391
380
  defaultOn: true
392
381
  },
393
382
  {
@@ -428,24 +417,6 @@ process.on('unhandledRejection', reason => {
428
417
 
429
418
  // FUNCTIONS
430
419
 
431
- // Conducts a JSON-defined test.
432
- const jsonTest = async (ruleID, ruleArgs) => {
433
- const [page, withItems] = ruleArgs;
434
- // Get the rule definition.
435
- const ruleJSON = await fs.readFile(`${__dirname}/../testaro/${ruleID}.json`, 'utf8');
436
- const ruleObj = JSON.parse(ruleJSON);
437
- // Initialize the locators and result.
438
- const all = await init(100, page, ruleObj.selector);
439
- all.locs = all.allLocs;
440
- // Populate and return the result.
441
- const whats = [
442
- ruleObj.complaints.instance,
443
- ruleObj.complaints.summary
444
- ];
445
- return await getRuleResult(
446
- withItems, all, ruleObj.ruleID, whats, ruleObj.ordinalSeverity, ruleObj.summaryTagName
447
- );
448
- };
449
420
  // Waits.
450
421
  const wait = ms => {
451
422
  return new Promise(resolve => {
package/data/template.js DELETED
@@ -1,39 +0,0 @@
1
- /*
2
- © 2023 CVS Health and/or one of its affiliates. All rights reserved.
3
-
4
- Licensed under the MIT License. See LICENSE file at the project root or
5
- https://opensource.org/license/mit/ for details.
6
-
7
- SPDX-License-Identifier: MIT
8
- */
9
-
10
- /*
11
- template
12
- This test reports ….
13
- */
14
-
15
- // ########## IMPORTS
16
-
17
- // Module to perform common operations.
18
- const {init, report} = require('../procs/testaro');
19
-
20
- // ########## FUNCTIONS
21
-
22
- // Runs the test and returns the result.
23
- exports.reporter = async (page, withItems) => {
24
- // Initialize the locators and result.
25
- const all = await init(100, page, 'body a');
26
- // For each locator:
27
- for (const loc of all.allLocs) {
28
- // Get whether its element violates the rule.
29
- const isBad = await loc.evaluate(el => el.tabIndex !== 0);
30
- // If it does:
31
- if (isBad) {
32
- // Add the locator to the array of violators.
33
- all.locs.push(loc);
34
- }
35
- }
36
- // Populate and return the result.
37
- const whats = ['Itemized description', 'Summary description'];
38
- return await report(withItems, all, 'ruleID', whats, 0);
39
- };
@@ -1,46 +0,0 @@
1
- /*
2
- © 2023 CVS Health and/or one of its affiliates. All rights reserved.
3
- © 2025 Jonathan Robert Pool.
4
-
5
- Licensed under the MIT License. See LICENSE file at the project root or
6
- https://opensource.org/license/mit/ for details.
7
-
8
- SPDX-License-Identifier: MIT
9
- */
10
-
11
- /*
12
- linkTitle
13
- Related to Tenon rule 79.
14
- This test reports links with title attributes whose values the link text contains.
15
- */
16
-
17
- // ########## IMPORTS
18
-
19
- // Module to perform common operations.
20
- const {simplify} = require('../procs/testaro');
21
- // Module to get locator data.
22
- const {getLocatorData} = require('../procs/getLocatorData');
23
-
24
- // ########## FUNCTIONS
25
-
26
- // Runs the test and returns the result.
27
- exports.reporter = async (page, withItems) => {
28
- // Specify the rule.
29
- const ruleData = {
30
- ruleID: 'linkTitle',
31
- selector: 'a[title]',
32
- pruner: async loc => {
33
- const elData = await getLocatorData(loc);
34
- const title = await loc.getAttribute('title');
35
- return elData.excerpt.toLowerCase().includes(title.toLowerCase());
36
- },
37
- complaints: {
38
- instance: 'Link has a title attribute that repeats link text content',
39
- summary: 'Links have title attributes that repeat link text contents'
40
- },
41
- ordinalSeverity: 0,
42
- summaryTagName: 'A'
43
- };
44
- // Run the test and return the result.
45
- return await simplify(page, withItems, ruleData);
46
- };