testaro 60.14.0 → 60.15.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": "60.14.0",
3
+ "version": "60.15.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": {
@@ -39,6 +39,7 @@
39
39
  "@siteimprove/alfa-rules": "*",
40
40
  "@wally-ax/wax-dev": "npm:@jrpool/wax-dev@^1.0.0",
41
41
  "accessibility-checker": "*",
42
+ "aria-query": "*",
42
43
  "aslint-testaro": "*",
43
44
  "axe-playwright": "*",
44
45
  "dotenv": "*",
@@ -14,6 +14,7 @@
14
14
  A link is classified as inline unless its declared or effective display is block.
15
15
  */
16
16
 
17
+ // Returns whether a locator references an inline link.
17
18
  exports.isInlineLink = async loc => await loc.evaluate(element => {
18
19
  // Returns the normalized text content of an element.
19
20
  const realTextOf = element => element ? element.textContent.replace(/\s/g, '') : '';
package/procs/testaro.js CHANGED
@@ -155,6 +155,7 @@ exports.doTest = async (
155
155
  ) => {
156
156
  // Return totals and standard instances for the rule.
157
157
  return await page.evaluate(async args => {
158
+ // Get the arguments (summaryTagName must be upper-case or null).
158
159
  const [
159
160
  withItems,
160
161
  ruleID,
package/run.js CHANGED
@@ -494,7 +494,7 @@ const launch = exports.launch = async (
494
494
  window.getInstance = (
495
495
  element, ruleID, what, count = 1, ordinalSeverity, summaryTagName = ''
496
496
  ) => {
497
- // If the element exists:
497
+ // If an element has been specified:
498
498
  if (element) {
499
499
  // Get its properties.
500
500
  const boxData = element.getBoundingClientRect();
@@ -503,8 +503,9 @@ const launch = exports.launch = async (
503
503
  });
504
504
  const {x, y, width, height} = boxData;
505
505
  const {tagName, id = ''} = element;
506
- const rawExcerpt = element.textContent.trim() || element.outerHTML.trim();
507
- const excerpt = rawExcerpt.replace(/\s+/g, ' ').slice(0, 200);
506
+ const rawExcerpt = (element.textContent.trim() || element.outerHTML.trim())
507
+ .replace(/\s+/g, ' ');
508
+ const excerpt = rawExcerpt.slice(0, 200);
508
509
  // Return an itemized instance.
509
510
  return {
510
511
  ruleID,
@@ -528,7 +529,7 @@ const launch = exports.launch = async (
528
529
  pathID: window.getXPath(element)
529
530
  };
530
531
  }
531
- // Otherwise, i.e. if no element exists, return a summary instance.
532
+ // Otherwise, i.e. if no element has been specified, return a summary instance.
532
533
  return {
533
534
  ruleID,
534
535
  what,
package/testaro/adbID.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  © 2025 CVS Health and/or one of its affiliates. All rights reserved.
3
- © 2025 Juan S. Casado. All rights reserved.
3
+ © 2025 Juan S. Casado.
4
4
  © 2025 Jonathan Robert Pool.
5
5
 
6
6
  Licensed under the MIT License. See LICENSE file at the project root or
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  © 2025 CVS Health and/or one of its affiliates. All rights reserved.
3
- © 2025 Juan S. Casado. All rights reserved.
3
+ © 2025 Juan S. Casado.
4
4
  © 2025 Jonathan Robert Pool.
5
5
 
6
6
  Licensed under the MIT License. See LICENSE file at the project root or
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  © 2025 CVS Health and/or one of its affiliates. All rights reserved.
3
- © 2025 Juan S. Casado. All rights reserved.
3
+ © 2025 Juan S. Casado.
4
4
  © 2025 Jonathan Robert Pool.
5
5
 
6
6
  Licensed under the MIT License. See LICENSE file at the project root or
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  © 2025 CVS Health and/or one of its affiliates. All rights reserved.
3
- © 2025 Juan S. Casado. All rights reserved.
3
+ © 2025 Juan S. Casado.
4
4
  © 2025 Jonathan Robert Pool.
5
5
 
6
6
  Licensed under the MIT License. See LICENSE file at the project root or
package/testaro/hover.js CHANGED
@@ -17,12 +17,12 @@
17
17
  the rule is considered violated.
18
18
  */
19
19
 
20
- // ########## IMPORTS
20
+ // IMPORTS
21
21
 
22
22
  // Module to perform common operations.
23
23
  const {getBasicResult, getVisibleCountChange} = require('../procs/testaro');
24
24
 
25
- // ########## FUNCTIONS
25
+ // FUNCTIONS
26
26
 
27
27
  // Gets a violation description.
28
28
  const getViolationDescription = (change, elapsedTime) =>
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  © 2025 CVS Health and/or one of its affiliates. All rights reserved.
3
- © 2025 Juan S. Casado. All rights reserved.
3
+ © 2025 Juan S. Casado.
4
4
 
5
5
  Licensed under the MIT License. See LICENSE file at the project root or
6
6
  https://opensource.org/license/mit/ for details.
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  © 2025 CVS Health and/or one of its affiliates. All rights reserved.
3
- © 2025 Juan S. Casado. All rights reserved.
3
+ © 2025 Juan S. Casado.
4
4
 
5
5
  Licensed under the MIT License. See LICENSE file at the project root or
6
6
  https://opensource.org/license/mit/ for details.
@@ -15,46 +15,69 @@
15
15
  Text contents are compared case-insensitively.
16
16
  */
17
17
 
18
- // Module to perform common operations.
19
- const {init, getRuleResult} = require('../procs/testaro');
20
- // Module to get locator data.
21
- const {getLocatorData} = require('../procs/getLocatorData');
22
-
23
- // ########## FUNCTIONS
18
+ // FUNCTIONS
24
19
 
25
20
  // Runs the test and returns the result.
26
21
  exports.reporter = async (page, withItems) => {
27
- // Initialize the locators and result.
28
- const all = await init(100, page, 'a[href]:visible');
29
- const linksData = [];
30
- // For each locator:
31
- for (const loc of all.allLocs) {
32
- // Get its text.
33
- const elData = await getLocatorData(loc);
34
- const linkText = elData.excerpt.toLowerCase();
35
- // Get its destination.
36
- const linkTo = await loc.getAttribute('href');
37
- // If the text and destination exist:
38
- if (linkText && linkTo) {
39
- // If a previous link has the same text but a different destination:
40
- if (linksData.some(linkData => linkData.text === linkText && linkData.to !== linkTo)) {
41
- // Add the locator to the array of violators.
42
- all.locs.push(loc);
43
- }
44
- // Otherwise, i.e. if no previous link has the same taxt but a different destination:
45
- else {
46
- // Record its text and destination.
47
- linksData.push({
48
- text: linkText,
49
- to: linkTo
50
- });
22
+ // Return totals and standard instances for the rule.
23
+ return await page.evaluate(withItems => {
24
+ // Get all links.
25
+ const allLinks = Array.from(document.body.getElementsByTagName('a'));
26
+ // Get the visible ones.
27
+ const visibleLinks = allLinks.filter(link => link.checkVisibility({
28
+ contentVisibilityAuto: true,
29
+ opacityProperty: true,
30
+ visibilityProperty: true
31
+ }));
32
+ // Initialize the data.
33
+ const linksData = {
34
+ elementData: [],
35
+ textTotals: {}
36
+ };
37
+ // For each visible link:
38
+ visibleLinks.forEach(element => {
39
+ // Get its trimmed and lowercased text.
40
+ const text = element.textContent.trim().toLowerCase();
41
+ // Get its destination.
42
+ const href = element.getAttribute('href');
43
+ // Add to the data.
44
+ linksData.elementData.push([text, href]);
45
+ linksData.textTotals[text] ??= {
46
+ linkCount: 0,
47
+ hrefs: new Set()
48
+ };
49
+ const linkData = linksData.textTotals[text];
50
+ linkData.linkCount++;
51
+ linkData.hrefs.add(href);
52
+ });
53
+ let violationCount = 0;
54
+ const instances = [];
55
+ // For each visible link:
56
+ visibleLinks.forEach((element, index) => {
57
+ const text = linksData.elementData[index][0];
58
+ const {linkCount, hrefs} = linksData.textTotals[text];
59
+ // If it violates the rule:
60
+ if (hrefs.size > 1) {
61
+ // Increment the violation count.
62
+ violationCount++;
63
+ // If itemization is required:
64
+ if (withItems) {
65
+ const what = `${linkCount} links with this text have ${hrefs.size} different destinations`;
66
+ // Add an instance to the instances.
67
+ instances.push(window.getInstance(element, 'linkAmb', what, 1, 2));
68
+ }
51
69
  }
70
+ });
71
+ // If there were any violations and itemization is not required:
72
+ if (violationCount && ! withItems) {
73
+ const what = 'Links have the same text but different destinations';
74
+ // Add a summary instance to the instances.
75
+ instances.push(window.getInstance(null, 'linkAmb', what, violationCount, 2, 'A'));
52
76
  }
53
- }
54
- // Populate and return the result.
55
- const whats = [
56
- 'Link has the same text as, but a different destination from, another',
57
- 'Links have the same texts but different destinations'
58
- ];
59
- return await getRuleResult(withItems, all, 'linkAmb', whats, 2);
77
+ return {
78
+ data: {},
79
+ totals: [0, 0, violationCount, 0],
80
+ standardInstances: instances
81
+ };
82
+ }, withItems);
60
83
  };
@@ -47,6 +47,6 @@ exports.reporter = async (page, withItems) => {
47
47
  };
48
48
  const whats = 'Visible elements have font sizes smaller than 11 pixels';
49
49
  return await doTest(
50
- page, withItems, 'miniText', 'body *:not(script, style)', whats, 2, '', getBadWhat.toString()
50
+ page, withItems, 'miniText', 'body *:not(script, style)', whats, 2, null, getBadWhat.toString()
51
51
  );
52
52
  };
package/testaro/motion.js CHANGED
@@ -45,7 +45,7 @@ exports.reporter = async page => {
45
45
  // Get the screenshot PNG buffers made by the shoot0 and shoot1 tests.
46
46
  let shoot0PNGBuffer = await fs.readFile(`${tmpDir}/testaro-shoot-0.png`);
47
47
  let shoot1PNGBuffer = await fs.readFile(`${tmpDir}/testaro-shoot-1.png`);
48
- // Delete the buffers files.
48
+ // Delete the buffer files.
49
49
  await fs.unlink(`${tmpDir}/testaro-shoot-0.png`);
50
50
  await fs.unlink(`${tmpDir}/testaro-shoot-1.png`);
51
51
  // If both buffers exist:
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  © 2025 CVS Health and/or one of its affiliates. All rights reserved.
3
- © 2025 Juan S. Casado. All rights reserved.
3
+ © 2025 Juan S. Casado.
4
4
 
5
5
  Licensed under the MIT License. See LICENSE file at the project root or
6
6
  https://opensource.org/license/mit/ for details.
package/testaro/phOnly.js CHANGED
@@ -1,6 +1,7 @@
1
1
  /*
2
2
  © 2025 CVS Health and/or one of its affiliates. All rights reserved.
3
- © 2025 Juan S. Casado. All rights reserved.
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.
@@ -10,42 +11,31 @@
10
11
 
11
12
  /*
12
13
  phOnly
13
- Clean-room rule: input elements that have a placeholder but no accessible name (no label/title/aria-label/aria-labelledby)
14
+ Clean-room rule: This test reports input elements that have placeholders but no accessible names.
15
+ The standard for accessible name computation is employed; it accepts title attributes as
16
+ sources for accessible names. Thus, this test does not report reliance on title attributes for
17
+ accessible names, although such reliance is generally considered a poor practice.
14
18
  */
15
19
 
16
- const {init, getRuleResult} = require('../procs/testaro');
20
+ // IMPORTS
17
21
 
18
- const hasAccessibleName = async (loc) => {
19
- return await loc.evaluate(el => {
20
- // Quick accessible name checks: aria-label, aria-labelledby, associated <label>
21
- // NOTE: `title` is intentionally NOT considered a reliable accessible name here
22
- // because the validation targets expect placeholders with title attributes to be flagged.
23
- if (el.hasAttribute('aria-label')) return true;
24
- if (el.hasAttribute('aria-labelledby')) return true;
25
- // check for label[for]
26
- const id = el.getAttribute('id');
27
- if (id) {
28
- if (document.querySelector(`label[for="${CSS.escape(id)}"]`)) return true;
29
- }
30
- // check implicit ancestor label
31
- let parent = el.parentElement;
32
- while (parent) {
33
- if (parent.tagName && parent.tagName.toUpperCase() === 'LABEL') return true;
34
- parent = parent.parentElement;
35
- }
36
- return false;
37
- });
38
- };
22
+ const {doTest} = require('../procs/testaro');
39
23
 
24
+ // FUNCTIONS
25
+
26
+ // Runs the test and returns the result.
40
27
  exports.reporter = async (page, withItems) => {
41
- const all = await init(200, page, 'input[placeholder]');
42
- for (const loc of all.allLocs) {
43
- const isBad = await hasAccessibleName(loc).then(has => !has);
44
- if (isBad) all.locs.push(loc);
45
- }
46
- const whats = [
47
- 'Element has a placeholder but no accessible name',
48
- 'input elements have placeholders but no accessible names'
49
- ];
50
- return await getRuleResult(withItems, all, 'phOnly', whats, 2, 'INPUT');
28
+ const getBadWhat = element => {
29
+ // Get the accessible name of the element.
30
+ const accessibleName = window.getAccessibleName(element);
31
+ // If there is none:
32
+ if (! accessibleName) {
33
+ // Return a violation description.
34
+ return 'Element has a placeholder but no accessible name';
35
+ }
36
+ };
37
+ const whats = 'input elements have placeholders but no accessible names';
38
+ return await doTest(
39
+ page, withItems, 'phOnly', 'input[placeholder]', whats, 2, 'INPUT', getBadWhat.toString()
40
+ );
51
41
  };
@@ -0,0 +1,10 @@
1
+ {
2
+ "ruleID": "pseudoP",
3
+ "selector": "body br + br",
4
+ "complaints": {
5
+ "instance": "br element follows another br element, likely acting as a pseudo-paragraph",
6
+ "summary": "br elements follow other br elements, likely acting as pseudo-paragraphs"
7
+ },
8
+ "ordinalSeverity": 0,
9
+ "summaryTagName": "BR"
10
+ }
@@ -1,5 +1,6 @@
1
1
  /*
2
2
  © 2021–2023 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.
@@ -9,101 +10,68 @@
9
10
 
10
11
  /*
11
12
  radioSet
12
- This test reports nonstandard grouping of radio buttons. It defines standard grouping to require
13
- that two or more radio buttons with the same name, and no other radio buttons, be grouped in a
14
- 'fieldset' element with a valid 'legend' element.
13
+ This test reports nonstandard groupings of radio buttons. It defines a standard grouping to
14
+ require that two or more radio buttons with the same name, and no other radio buttons, be grouped
15
+ in a fieldset element with a valid legend element.
15
16
  */
16
17
 
17
- // ########## IMPORTS
18
+ // IMPORTS
18
19
 
19
- // Module to perform common operations.
20
- const {init, getRuleResult} = require('../procs/testaro');
20
+ const {doTest} = require('../procs/testaro');
21
21
 
22
- // ########## FUNCTIONS
22
+ // FUNCTIONS
23
23
 
24
24
  // Runs the test and returns the result.
25
25
  exports.reporter = async (page, withItems) => {
26
- // Initialize the locators and result.
27
- const all = await init(100, page, 'input[type=radio]');
28
- const params = {
29
- nameLeak: 'shares a name with others outside its field set',
30
- fsMixed: 'shares a field set with others having different names',
31
- only1RB: 'is the only one with its name in its field set',
32
- legendBad: 'is in a field set without a valid legend',
33
- noFS: 'is not in a field set',
34
- noName: 'has no name attribute'
35
- };
36
- // For each locator:
37
- for (const loc of all.allLocs) {
38
- // Get whether and, if so, how it violates the rule.
39
- const howBad = await loc.evaluate(el => {
40
- // Get its name.
41
- const elName = el.name;
42
- // If it has one:
43
- if (elName) {
44
- // Identify the field set of the element.
45
- const elFS = el.closest('fieldset');
46
- // If it has one:
47
- if (elFS) {
48
- // Get the first child element of the field set.
49
- const fsChild1 = elFS.firstElementChild;
50
- // Get whether the child is a legend with text content.
51
- const legendOK = fsChild1.tagName === 'LEGEND'
52
- && fsChild1.textContent.replace(/\s/g, '').length;
53
- // If it is:
54
- if (legendOK) {
55
- // Get the count of radio buttons with the same name in the field set.
56
- const nameGroupSize = elFS
57
- .querySelectorAll(`input[type=radio][name=${elName}]`)
58
- .length;
59
- // If the count is at least 2:
60
- if (nameGroupSize > 1) {
61
- // Get the count of radio buttons in the field set.
62
- const groupSize = elFS.querySelectorAll('input[type=radio]').length;
63
- // If it is the same:
64
- if (groupSize === nameGroupSize) {
65
- // Get the count of radio buttons with the same name in the document.
66
- const nameDocSize = document
67
- .querySelectorAll(`input[type=radio][name=${elName}]`)
68
- .length;
69
- // If none of them is outside the field set:
70
- if (nameDocSize === nameGroupSize) {
71
- // Return rule conformance.
72
- return false;
73
- }
74
- else {
75
- return 'nameLeak';
76
- }
77
- }
78
- else {
79
- return 'fsMixed';
80
- }
81
- }
82
- else {
83
- return 'only1RB';
84
- }
85
- }
86
- else {
87
- return 'legendBad';
88
- }
89
- }
90
- else {
91
- return 'noFS';
92
- }
93
- }
94
- else {
95
- return 'noName';
96
- }
97
- });
98
- // If it does:
99
- if (howBad) {
100
- // Add the locator to the array of violators.
101
- all.locs.push([loc, params[howBad]]);
26
+ const getBadWhat = element => {
27
+ // Get the name of the element.
28
+ const elName = element.name;
29
+ // If it has none:
30
+ if (! elName) {
31
+ // Return a violation description.
32
+ return 'radio button has no name attribute';
33
+ }
34
+ // Identify the field set of the element.
35
+ const elFS = element.closest('fieldset');
36
+ // If it has none:
37
+ if (! elFS) {
38
+ // Return a violation description.
39
+ return 'radio button is not in a field set';
40
+ }
41
+ // Get the first child element of the field set.
42
+ const fsChild1 = elFS.firstElementChild;
43
+ // Get whether the child is a legend with text content.
44
+ const legendOK = fsChild1.tagName === 'LEGEND'
45
+ && fsChild1.textContent.replace(/\s/g, '').length;
46
+ // If it is not:
47
+ if (! legendOK) {
48
+ // Return a violation description.
49
+ return 'radio button is in a field set without a valid legend';
50
+ }
51
+ // Get the count of radio buttons with the same name in the field set.
52
+ const nameGroupSize = elFS.querySelectorAll(`input[type=radio][name=${elName}]`).length;
53
+ // If the count is only 1:
54
+ if (nameGroupSize === 1) {
55
+ // Return a violation description.
56
+ return 'radio button is the only one with its name in its field set';
102
57
  }
103
- }
104
- // Populate and return the result.
105
- const whats = [
106
- 'Radio button __param__', 'Radio buttons are not validly grouped in fieldsets with legends'
107
- ];
108
- return await getRuleResult(withItems, all, 'radioSet', whats, 2, 'INPUT');
58
+ // Get the count of radio buttons in the field set.
59
+ const groupSize = elFS.querySelectorAll('input[type=radio]').length;
60
+ // If it is greater than the count of radio buttons with the same name in the field set:
61
+ if (groupSize > nameGroupSize) {
62
+ // Return a violation description.
63
+ return 'radio button shares a field set with differently named others';
64
+ }
65
+ // Get the count of radio buttons with the same name in the document.
66
+ const nameDocSize = document.querySelectorAll(`input[type=radio][name=${elName}]`).length;
67
+ // If it is greater, and thus some such radio button is outside the field set:
68
+ if (nameDocSize > nameGroupSize) {
69
+ // Return a violation description.
70
+ return 'radio button shares a name with others outside its field set';
71
+ }
72
+ };
73
+ const whats = 'Radio buttons are not validly grouped in fieldsets with legends';
74
+ return await doTest(
75
+ page, withItems, 'radioSet', 'input[type=radio]', whats, 2, 'INPUT', getBadWhat.toString()
76
+ );
109
77
  };
package/testaro/role.js CHANGED
@@ -15,75 +15,35 @@
15
15
 
16
16
  // IMPORTS
17
17
 
18
- // Module to perform common operations.
19
- const {init, getRuleResult} = require('../procs/testaro');
18
+ const {elementRoles} = require('aria-query');
19
+ const {getBasicResult} = require('../procs/testaro');
20
20
 
21
21
  // CONSTANTS
22
22
 
23
- // Implicit roles
24
- const roleImplications = {
25
- article: 'article',
26
- aside: 'complementary',
27
- button: 'button',
28
- datalist: 'listbox',
29
- dd: 'definition',
30
- details: 'group',
31
- dfn: 'term',
32
- dialog: 'dialog',
33
- dt: 'term',
34
- fieldset: 'group',
35
- figure: 'figure',
36
- h1: 'heading',
37
- h2: 'heading',
38
- h3: 'heading',
39
- h4: 'heading',
40
- h5: 'heading',
41
- h6: 'heading',
42
- hr: 'separator',
43
- html: 'document',
44
- 'input[type=number]': 'spinbutton',
45
- 'input[type=text]': 'textbox',
46
- 'input[type=text, list]': 'combobox',
47
- li: 'listitem',
48
- main: 'main',
49
- math: 'math',
50
- menu: 'list',
51
- nav: 'navigation',
52
- ol: 'list',
53
- output: 'status',
54
- progress: 'progressbar',
55
- summary: 'button',
56
- SVG: 'graphics-document',
57
- table: 'table',
58
- tbody: 'rowgroup',
59
- textarea: 'textbox',
60
- tfoot: 'rowgroup',
61
- thead: 'rowgroup',
62
- tr: 'row',
63
- ul: 'list'
64
- };
65
- const implicitRoles = new Set(Object.values(roleImplications));
23
+ // Implicit roles
24
+ const implicitRoles = new Set(Array.from(elementRoles.values()).flat());
66
25
 
67
26
  // FUNCTIONS
68
27
 
69
28
  // Runs the test and returns the result.
70
29
  exports.reporter = async (page, withItems) => {
71
- // Get locators for all elements with explicit roles.
72
- const all = await init(100, page, '[role]');
73
- // For each locator:
74
- for (const loc of all.allLocs) {
75
- // Get the explicit role of the element.
76
- const role = await loc.getAttribute('role');
77
- // If it is also implicit:
78
- if (implicitRoles.has(role)) {
79
- // Add the locator to the array of violators.
80
- all.locs.push([loc, role]);
30
+ // Get locators for the elements with explicit roles.
31
+ const loc = page.locator('[role]');
32
+ const locs = await loc.all();
33
+ const violations = [];
34
+ // Get data on those with roles that are also implicit.
35
+ for (const loc of locs) {
36
+ const roleSpec = await loc.getAttribute('role');
37
+ const roles = roleSpec.split(/\s+/);
38
+ const badRole = roles.find(role => implicitRoles.has(role));
39
+ if (badRole) {
40
+ violations.push({
41
+ loc,
42
+ what: `Explicit ${badRole} role of the element is also an implicit HTML element role`
43
+ });
81
44
  }
82
45
  }
83
- // Populate and return the result.
84
- const whats = [
85
- 'Element has an explicit __param__ role, which is also an implicit HTML element role',
86
- 'Elements have roles assigned that are also implicit roles of HTML elements'
87
- ];
88
- return await getRuleResult(withItems, all, 'role', whats, 0);
46
+ // Get and return a result.
47
+ const whats = 'Elements have roles assigned that are also implicit HTML element roles';
48
+ return await getBasicResult(page, withItems, 'role', 0, null, whats, {}, violations);
89
49
  };
@@ -1,6 +1,7 @@
1
1
  /*
2
2
  © 2025 CVS Health and/or one of its affiliates. All rights reserved.
3
- © 2025 Juan S. Casado. All rights reserved.
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.
@@ -10,35 +11,44 @@
10
11
 
11
12
  /*
12
13
  secHeading
13
- Flag headings that are a lower-numbered heading (e.g., H2 after H3) than the
14
- immediately preceding heading within the same sectioning container.
14
+ This test reports sectioning containers that have child headings of which the first has a depth
15
+ (i.e. level) lower than at least one of the others. An example is a section element whose first
16
+ heading child is an h3 element and whose subsequent heading children include an h2 element. The
17
+ first child heading is presumed the principal heading of the container, so this pattern merits
18
+ scrutiny.
15
19
  */
16
20
 
17
- const {init, getRuleResult} = require('../procs/testaro');
21
+ // IMPORTS
18
22
 
23
+ const {doTest} = require('../procs/testaro');
24
+
25
+ // FUNCTIONS
26
+
27
+ // Runs the test and returns the result.
19
28
  exports.reporter = async (page, withItems) => {
20
- const all = await init(200, page, 'h1,h2,h3,h4,h5,h6');
21
- for (const loc of all.allLocs) {
22
- const isBad = await loc.evaluate(el => {
23
- // find nearest sectioning ancestor
24
- let ancestor = el.parentElement;
25
- while (ancestor && !['SECTION','ARTICLE','NAV','ASIDE','MAIN','BODY','HTML'].includes(ancestor.tagName)) {
26
- ancestor = ancestor.parentElement;
29
+ const getBadWhat = element => {
30
+ // Get the children of the element.
31
+ const children = Array.from(element.children);
32
+ // Get the headings among them.
33
+ const headingChildren = children.filter(child => /^H[1-6]$/.test(child.tagName));
34
+ // Get their depths.
35
+ const headingChildDepths = headingChildren.map(child => Number(child.tagName.slice(1)));
36
+ // If there are 2 or more heading children:
37
+ if (headingChildren.length > 1) {
38
+ // Get the depth of the first of them.
39
+ const firstHeadingDepth = headingChildDepths[0];
40
+ // Get the minimum of their depths.
41
+ const minHeadingDepth = Math.min(...headingChildDepths);
42
+ // If any heading is less deep than the first:
43
+ if (minHeadingDepth < firstHeadingDepth) {
44
+ // Return a violation description.
45
+ return `First child heading is H${firstHeadingDepth}, but another is H${minHeadingDepth}`;
27
46
  }
28
- if (!ancestor) return false;
29
- const headings = Array.from(ancestor.querySelectorAll('h1,h2,h3,h4,h5,h6'));
30
- const idx = headings.indexOf(el);
31
- if (idx <= 0) return false;
32
- const prev = headings[idx - 1];
33
- const curLevel = Number(el.tagName.substring(1));
34
- const prevLevel = Number(prev.tagName.substring(1));
35
- return curLevel < prevLevel;
36
- });
37
- if (isBad) all.locs.push(loc);
38
- }
39
- const whats = [
40
- 'Element violates the logical level order in its sectioning container',
41
- 'Heading elements violate the logical level order in their sectioning containers'
42
- ];
43
- return await getRuleResult(withItems, all, 'secHeading', whats, 1);
47
+ }
48
+ };
49
+ const selector = 'SECTION, ARTICLE, NAV, ASIDE, MAIN';
50
+ const whats = 'First child headings of sectioning containers are deeper than others';
51
+ return await doTest(
52
+ page, withItems, 'secHeading', selector, whats, 0, null, getBadWhat.toString()
53
+ );
44
54
  };
@@ -10,38 +10,96 @@
10
10
 
11
11
  /*
12
12
  targetSmall
13
- Related to Tenon rule 152, but stricter.
14
- This test reports visible buttons, inputs, and non-inline links with widths or heights smaller
15
- than 44 pixels.
13
+ Related to Tenon rule 152.
14
+ This test reports visible pointer targets, i.e. labels, buttons, inputs, and links, that are
15
+ small enough or near enough to other targets to make pointer interaction difficult. This test
16
+ relates to WCAG 2.2 Success Criteria 2.5.5 and 2.5.8, but does not attempt to implement either
17
+ of them precisely. For example, the test reports a small pointer target that is far from all
18
+ other targets, although it conforms to the Success Criteria.
16
19
  */
17
20
 
18
- // ########## IMPORTS
19
-
20
- // Module to perform common operations.
21
- const {init, getRuleResult} = require('../procs/testaro');
22
- // Module to classify links.
23
- const {isTooSmall} = require('../procs/target');
24
-
25
- // ########## FUNCTIONS
21
+ // FUNCTIONS
26
22
 
27
23
  // Runs the test and returns the result.
28
24
  exports.reporter = async (page, withItems) => {
29
- // Initialize the locators and result.
30
- const all = await init(100, page, 'a:visible, button:visible, input:visible');
31
- // For each locator:
32
- for (const loc of all.allLocs) {
33
- // Get data on it if illicitly small.
34
- const sizeData = await isTooSmall(loc, 44);
35
- // If it is:
36
- if (sizeData) {
37
- // Add the locator to the array of violators.
38
- all.locs.push([loc, `${sizeData.width} wide by ${sizeData.height} high`]);
25
+ // Return totals and standard instances for the rule.
26
+ return await page.evaluate(withItems => {
27
+ // Get all pointer targets.
28
+ const allPTs = Array.from(document.body.querySelectorAll('label, button, input, a'));
29
+ // Get the visible ones.
30
+ const visiblePTs = allPTs.filter(pt => pt.checkVisibility({
31
+ contentVisibilityAuto: true,
32
+ opacityProperty: true,
33
+ visibilityProperty: true
34
+ }));
35
+ // Initialize the data.
36
+ const ptsData = [];
37
+ // For each visible pointer target:
38
+ visiblePTs.forEach(element => {
39
+ // Get the coordinates of its centerpoint.
40
+ const rect = element.getBoundingClientRect();
41
+ const centerX = rect.left + rect.width / 2;
42
+ const centerY = rect.top + rect.height / 2;
43
+ // Add them to the data.
44
+ ptsData.push([centerX, centerY]);
45
+ });
46
+ // Initialize the counts of minor and major violations.
47
+ let violationCounts = [0, 0];
48
+ const instances = [];
49
+ // For each visible pointer target:
50
+ visiblePTs.forEach((element, index) => {
51
+ // Get the minimum of the vertical distances of its centerpoint from those of the others.
52
+ const minYDiff = ptsData[index][1] - Math.abs(
53
+ Math.min(...ptsData.splice(index, 1).map(ptData => ptData[1]))
54
+ );
55
+ // If it is close enough to make a violation possible:
56
+ if (minYDiff < 44) {
57
+ // Get the centerpoint coordinates of those within that vertical distance.
58
+ const yNearPTsData = ptsData.splice(index, 1).filter(
59
+ ptData => Math.abs(ptData[1] - ptsData[index][1]) < 44
60
+ );
61
+ // Get the minimum of their planar distances.
62
+ const minPlanarDistance = Math.min(...yNearPTsData.map(ptData => {
63
+ const planarDistance = Math.sqrt(
64
+ Math.pow(centerX - ptData[0], 2) + Math.pow(centerY - ptData[1], 2)
65
+ );
66
+ return planarDistance;
67
+ }));
68
+ // If the minimum planar distance is less than 44px:
69
+ if (minPlanarDistance < 44) {
70
+ const isVeryNear = minPlanarDistance < 24;
71
+ // Get the ordinal severity of the violation.
72
+ const ordinalSeverity = isVeryNear ? 3 : 2;
73
+ // Increment the applicable violation count.
74
+ violationCounts[isVeryNear ? 1 : 0]++;
75
+ // If itemization is required:
76
+ if (withItems) {
77
+ const what = `Pointer-target centerpoint is only ${Math.round(minPlanarDistance)}px from another one`;
78
+ // Add an instance to the instances.
79
+ instances.push(window.getInstance(element, 'targetSmall', what, 1, ordinalSeverity));
80
+ }
81
+ }
82
+ }
83
+ });
84
+ // If itemization is not required:
85
+ if (! withItems) {
86
+ // If there were any major violations:
87
+ if (violationCounts[1]) {
88
+ const what = 'Pointer-target centerpoints are less than 24px from others';
89
+ // Add a summary instance to the instances.
90
+ instances.push(window.getInstance(null, 'targetSmall', what, violationCounts[1], 1));
91
+ }
92
+ // If there were any minor violations:
93
+ if (violationCounts[0]) {
94
+ const what = 'Pointer-target centerpoints are less than 44px from others';
95
+ // Add a summary instance to the instances.
96
+ instances.push(window.getInstance(null, 'targetSmall', what, violationCounts[0], 0));
97
+ }
39
98
  }
40
- };
41
- // Populate and return the result.
42
- const whats = [
43
- 'Interactive element pixel size (__param__) is less than 44 by 44',
44
- 'Interactive elements are smaller than 44 pixels wide and high'
45
- ];
46
- return await getRuleResult(withItems, all, 'targetSmall', whats, 0);
99
+ return {
100
+ data: {},
101
+ totals: [...violationCounts, 0, 0],
102
+ standardInstances: instances
103
+ };
104
+ }, withItems);
47
105
  };
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  © 2025 CVS Health and/or one of its affiliates. All rights reserved.
3
- © 2025 Juan S. Casado. All rights reserved.
3
+ © 2025 Juan S. Casado.
4
4
 
5
5
  Licensed under the MIT License. See LICENSE file at the project root or
6
6
  https://opensource.org/license/mit/ for details.
package/tests/testaro.js CHANGED
@@ -191,7 +191,7 @@ const allRules = [
191
191
  id: 'linkAmb',
192
192
  what: 'links with identical texts but different destinations',
193
193
  launchRole: 'sharer',
194
- timeOut: 5,
194
+ timeOut: 50,
195
195
  defaultOn: true
196
196
  },
197
197
  {
@@ -212,7 +212,7 @@ const allRules = [
212
212
  id: 'linkTitle',
213
213
  what: 'links with title attributes repeating text content',
214
214
  launchRole: 'sharer',
215
- timeOut: 5,
215
+ timeOut: 10,
216
216
  defaultOn: true
217
217
  },
218
218
  {
@@ -294,14 +294,7 @@ const allRules = [
294
294
  },
295
295
  {
296
296
  id: 'targetSmall',
297
- what: 'buttons, inputs, and non-inline links smaller than 44 pixels wide and high',
298
- launchRole: 'sharer',
299
- timeOut: 5,
300
- defaultOn: true
301
- },
302
- {
303
- id: 'targetTiny',
304
- what: 'buttons, inputs, and non-inline links smaller than 24 pixels wide and high',
297
+ what: 'labels, buttons, inputs, and links too near each other',
305
298
  launchRole: 'sharer',
306
299
  timeOut: 5,
307
300
  defaultOn: true
@@ -24,7 +24,7 @@
24
24
  <li><a id="basqueInfoWP0" href="https://eus.wikipedia.org">Basque information</a></li>
25
25
  <li><a id="basqueInfoWP1" href="https://eus.wikipedia.org">Basque information</a></li>
26
26
  </ul>
27
- <p>However, the next two links are ambiguous, because their names are the same and yet they have different destinations:</p>
27
+ <p>However, they are made ambiguous by the second link below. As a result, there are 4 ambiguous links, with 2 different destinations.</p>
28
28
  <ul>
29
29
  <li><a id="basqueInfoWP2" href="https://eus.wikipedia.org">Basque information</a></li>
30
30
  <li><a id="basqueInfoICB" href="https://www.eke.eus/en/kultura/basque-country">Basque information</a></li>
@@ -55,7 +55,7 @@
55
55
  <h3>Input with no label</h3>
56
56
  <p>Enter the name of a company</p>
57
57
  <p><input size="30" maxlength="30" name="company2" placeholder="company"></p>
58
- <p>The last two inputs violate the phOnly rule.</p>
58
+ <p>The last input violates the phOnly rule.</p>
59
59
  </main>
60
60
  </body>
61
61
  </html>
@@ -32,11 +32,11 @@
32
32
  <p>More detailed information about the blip of the blah.</p>
33
33
  <h3>This heads a second part of the same section.</h3>
34
34
  <p>More detailed information about the swax of the blah.</p>
35
- <h2>This heading is illogical</h2>
36
- <p>This should have been its own section.</p>
35
+ <h2>This heading is not quite illogical</h2>
36
+ <p>This would have started its own section if each section started with a uniquely top-level heading.</p>
37
37
  </section>
38
38
  <section>
39
- <h2>This heads another illogical section.</h2>
39
+ <h2>This heads a truly illogical section.</h2>
40
40
  <p>Information about blah.</p>
41
41
  <h1>This heading is fundamentally illogical</h1>
42
42
  <p>It is not obvious what information belongs here.</p>
@@ -56,7 +56,7 @@
56
56
  <p>More detailed information about the blip of the blah.</p>
57
57
  <h3>This heads a second part of the same article.</h3>
58
58
  <p>More detailed information about the swax of the blah.</p>
59
- <h2>This article heading is illogical</h2>
59
+ <h1>This article heading is illogical</h1>
60
60
  <p>This should have been its own article.</p>
61
61
  </article>
62
62
  </main>
@@ -1,51 +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
- pseudoP
12
- Related to Tenon rule 242.
13
- This test reports 2 or more sequential br elements. They may be inferior substitutes for a
14
- p element.
15
- */
16
-
17
- // ########## IMPORTS
18
-
19
- // Module to perform common operations.
20
- const {init, getRuleResult} = require('../procs/testaro');
21
-
22
- // ########## FUNCTIONS
23
-
24
- // Runs the test and returns the result.
25
- exports.reporter = async (page, withItems) => {
26
- // Initialize the locators and result.
27
- const all = await init(100, page, 'body br + br');
28
- // For each locator:
29
- for (const loc of all.allLocs) {
30
- // Return whether the second br element violates the rule.
31
- const parentTagNameIfBad = await loc.evaluate(el => {
32
- el.parentElement.normalize();
33
- const previousSib = el.previousSibling;
34
- return previousSib.nodeType === Node.ELEMENT_NODE
35
- || previousSib.nodeType === Node.TEXT_NODE && /^\s+$/.test(previousSib)
36
- ? el.parentElement.tagName
37
- : false;
38
- });
39
- // If it does:
40
- if (parentTagNameIfBad) {
41
- // Add the locator to the array of violators.
42
- all.locs.push([loc, parentTagNameIfBad]);
43
- }
44
- }
45
- // Populate and return the result.
46
- const whats = [
47
- 'Adjacent BR elements within a __param__ element may be pseudo-paragraphs',
48
- 'Elements contain 2 or more adjacent br elements that may be pseudo-paragraphs'
49
- ];
50
- return await getRuleResult(withItems, all, 'pseudoP', whats, 0, 'br');
51
- };
@@ -1,47 +0,0 @@
1
- /*
2
- © 2023–2025 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
- targetTiny
13
- Related to Tenon rule 152.
14
- This test reports visible buttons, inputs, and non-inline links with widths or heights smaller
15
- than 24 pixels.
16
- */
17
-
18
- // ########## IMPORTS
19
-
20
- // Module to perform common operations.
21
- const {init, getRuleResult} = require('../procs/testaro');
22
- // Module to classify links.
23
- const {isTooSmall} = require('../procs/target');
24
-
25
- // ########## FUNCTIONS
26
-
27
- // Runs the test and returns the result.
28
- exports.reporter = async (page, withItems) => {
29
- // Initialize the locators and result.
30
- const all = await init(100, page, 'a:visible, button:visible, input:visible');
31
- // For each locator:
32
- for (const loc of all.allLocs) {
33
- // Get data on it if illicitly small.
34
- const sizeData = await isTooSmall(loc, 24);
35
- // If it is:
36
- if (sizeData) {
37
- // Add the locator to the array of violators.
38
- all.locs.push([loc, `${sizeData.width} wide by ${sizeData.height} high`]);
39
- }
40
- }
41
- // Populate and return the result.
42
- const whats = [
43
- 'Interactive element pixel size (__param__) is less than 24 by 24',
44
- 'Interactive elements are smaller than 24 pixels wide and high'
45
- ];
46
- return await getRuleResult(withItems, all, 'targetTiny', whats, 1);
47
- };