testaro 67.0.0 → 68.0.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.
Files changed (95) hide show
  1. package/LICENSE +4 -16
  2. package/README.md +10 -2
  3. package/UPGRADES.md +1 -1
  4. package/dirWatch.js +2 -3
  5. package/ed11y/editoria11y.min.js +109 -690
  6. package/ed11y/editoria11y210.min.js +747 -0
  7. package/netWatch.js +6 -6
  8. package/package.json +1 -1
  9. package/procs/aslint.js +2 -2
  10. package/procs/catalog.js +190 -0
  11. package/procs/{dateOf.js → dateTime.js} +6 -4
  12. package/procs/doActs.js +1227 -0
  13. package/procs/doTestAct.js +63 -29
  14. package/procs/error.js +53 -0
  15. package/procs/job.js +64 -38
  16. package/procs/launch.js +596 -0
  17. package/procs/nu.js +3 -18
  18. package/procs/shoot.js +18 -2
  19. package/procs/testaro.js +102 -125
  20. package/procs/xPath.js +62 -0
  21. package/run.js +42 -1938
  22. package/scratch/README.md +9 -0
  23. package/testaro/adbID.js +3 -3
  24. package/testaro/allCaps.js +4 -5
  25. package/testaro/allHidden.js +19 -18
  26. package/testaro/allSlanted.js +4 -5
  27. package/testaro/altScheme.js +3 -3
  28. package/testaro/attVal.js +19 -35
  29. package/testaro/autocomplete.js +65 -62
  30. package/testaro/bulk.js +21 -20
  31. package/testaro/buttonMenu.js +112 -33
  32. package/testaro/captionLoc.js +3 -3
  33. package/testaro/datalistRef.js +4 -5
  34. package/testaro/distortion.js +3 -3
  35. package/testaro/docType.js +6 -9
  36. package/testaro/dupAtt.js +12 -25
  37. package/testaro/elements.js +4 -3
  38. package/testaro/embAc.js +4 -2
  39. package/testaro/focAll.js +6 -13
  40. package/testaro/focAndOp.js +3 -3
  41. package/testaro/focInd.js +3 -3
  42. package/testaro/focVis.js +4 -3
  43. package/testaro/headEl.js +5 -12
  44. package/testaro/headingAmb.js +45 -88
  45. package/testaro/hovInd.js +5 -5
  46. package/testaro/hover.js +44 -8
  47. package/testaro/hr.js +4 -4
  48. package/testaro/imageLink.js +3 -3
  49. package/testaro/labClash.js +3 -3
  50. package/testaro/legendLoc.js +3 -3
  51. package/testaro/lineHeight.js +3 -3
  52. package/testaro/linkAmb.js +25 -17
  53. package/testaro/linkExt.js +5 -5
  54. package/testaro/linkOldAtt.js +4 -3
  55. package/testaro/linkTo.js +4 -3
  56. package/testaro/linkUl.js +4 -5
  57. package/testaro/miniText.js +4 -3
  58. package/testaro/motion.js +3 -22
  59. package/testaro/nonTable.js +4 -5
  60. package/testaro/optRoleSel.js +3 -3
  61. package/testaro/phOnly.js +3 -3
  62. package/testaro/pseudoP.js +5 -5
  63. package/testaro/radioSet.js +4 -5
  64. package/testaro/role.js +4 -5
  65. package/testaro/secHeading.js +4 -5
  66. package/testaro/shoot0.js +3 -2
  67. package/testaro/shoot1.js +3 -2
  68. package/testaro/styleDiff.js +5 -12
  69. package/testaro/tabNav.js +30 -118
  70. package/testaro/targetSmall.js +30 -15
  71. package/testaro/textNodes.js +3 -1
  72. package/testaro/textSem.js +4 -5
  73. package/testaro/title.js +4 -2
  74. package/testaro/titledEl.js +3 -3
  75. package/testaro/zIndex.js +3 -3
  76. package/tests/alfa.js +28 -54
  77. package/tests/aslint.js +20 -53
  78. package/tests/axe.js +76 -13
  79. package/tests/ed11y.js +69 -141
  80. package/tests/htmlcs.js +69 -38
  81. package/tests/ibm.js +54 -9
  82. package/tests/nuVal.js +65 -12
  83. package/tests/nuVnu.js +76 -26
  84. package/tests/qualWeb.js +89 -44
  85. package/tests/testaro.js +288 -273
  86. package/tests/wave.js +142 -117
  87. package/tests/wax.js +61 -42
  88. package/procs/getLocatorData.js +0 -192
  89. package/procs/identify.js +0 -250
  90. package/procs/isInlineLink.js +0 -42
  91. package/procs/screenShot.js +0 -32
  92. package/procs/standardize.js +0 -524
  93. package/procs/target.js +0 -90
  94. package/procs/tellServer.js +0 -43
  95. package/scripts/dumpAlts.js +0 -28
package/scratch/README.md CHANGED
@@ -1,3 +1,12 @@
1
1
  # scratch
2
2
 
3
3
  This directory is used for temporary files produced during execution of jobs.
4
+
5
+ ## License
6
+
7
+ © 2021–2025 CVS Health and/or one of its affiliates. All rights reserved.
8
+ © 2025–2026 Jonathan Robert Pool.
9
+
10
+ Licensed under the [MIT License](https://opensource.org/license/mit/). See [LICENSE](../../LICENSE) file at the project root for details.
11
+
12
+ SPDX-License-Identifier: MIT
package/testaro/adbID.js CHANGED
@@ -1,7 +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
+ © 2025–2026 Jonathan Robert Pool.
5
5
 
6
6
  Licensed under the MIT License. See LICENSE file at the project root or
7
7
  https://opensource.org/license/mit/ for details.
@@ -22,7 +22,7 @@ const {doTest} = require('../procs/testaro');
22
22
  // FUNCTIONS
23
23
 
24
24
  // Runs the test and returns the result.
25
- exports.reporter = async (page, withItems) => {
25
+ exports.reporter = async (page, catalog, withItems) => {
26
26
  const getBadWhat = element => {
27
27
  // Get the IDs in the aria-describedby attribute of the element.
28
28
  const IDs = element.getAttribute('aria-describedby').trim().split(/\s+/).filter(Boolean);
@@ -57,6 +57,6 @@ exports.reporter = async (page, withItems) => {
57
57
  };
58
58
  const whats = 'Elements have aria-describedby attributes with missing or invalid id values';
59
59
  return await doTest(
60
- page, withItems, 'adbID', '[aria-describedby]', whats, 3, null, getBadWhat.toString()
60
+ page, catalog, withItems, 'adbID', '[aria-describedby]', whats, 3, getBadWhat.toString()
61
61
  );
62
62
  };
@@ -1,9 +1,8 @@
1
1
  /*
2
2
  © 2023 CVS Health and/or one of its affiliates. All rights reserved.
3
- © 2025 Jonathan Robert Pool.
3
+ © 2025–2026 Jonathan Robert Pool.
4
4
 
5
- Licensed under the MIT License. See LICENSE file at the project root or
6
- https://opensource.org/license/mit/ for details.
5
+ Licensed under the MIT License. See LICENSE file at the project root or https://opensource.org/license/mit/ for details.
7
6
 
8
7
  SPDX-License-Identifier: MIT
9
8
  */
@@ -21,7 +20,7 @@ const {doTest} = require('../procs/testaro');
21
20
  // FUNCTIONS
22
21
 
23
22
  // Runs the test and returns the result.
24
- exports.reporter = async (page, withItems) => {
23
+ exports.reporter = async (page, catalog, withItems) => {
25
24
  const getBadWhat = element => {
26
25
  // Get the child text nodes of the element.
27
26
  const childTextNodes = Array.from(element.childNodes).filter(
@@ -52,6 +51,6 @@ exports.reporter = async (page, withItems) => {
52
51
  const selector = 'body *:not(style, script, svg)';
53
52
  const whats = 'Elements have all-capital text';
54
53
  return await doTest(
55
- page, withItems, 'allCaps', selector, whats, 0, null, getBadWhat.toString()
54
+ page, catalog, withItems, 'allCaps', selector, whats, 0, getBadWhat.toString()
56
55
  );
57
56
  };
@@ -1,9 +1,8 @@
1
1
  /*
2
2
  © 2023 CVS Health and/or one of its affiliates. All rights reserved.
3
- © 2025 Jonathan Robert Pool.
3
+ © 2025–2026 Jonathan Robert Pool.
4
4
 
5
- Licensed under the MIT License. See LICENSE file at the project root or
6
- https://opensource.org/license/mit/ for details.
5
+ Licensed under the MIT License. See LICENSE file at the project root or https://opensource.org/license/mit/ for details.
7
6
 
8
7
  SPDX-License-Identifier: MIT
9
8
  */
@@ -19,22 +18,24 @@
19
18
  exports.reporter = async page => {
20
19
  // Get a count of elements deemed visible by Playwright.
21
20
  const visibleElementCount = await page.locator('body :visible').count();
22
- // Return totals and standard instances for the rule.
23
- return await page.evaluate(visibleElementCount => {
24
- let violationCount = 0;
25
- const instances = [];
26
- // If no element is visible:
27
- if (! visibleElementCount) {
28
- // Increment the violation count.
29
- violationCount = 1;
30
- const what = `The entire page body is hidden or empty`;
31
- // Add a summary instance to the instances.
32
- instances.push(window.getInstance(null, 'allHidden', what, 1, 3));
33
- }
21
+ // If no element is visible:
22
+ if (! visibleElementCount) {
23
+ // Return data, totals, and a summary standard instance.
34
24
  return {
35
25
  data: {},
36
- totals: [0, 0, 0, violationCount],
37
- standardInstances: instances
26
+ totals: [0, 0, 0, 1],
27
+ standardInstances: [{
28
+ ruleID: 'allHidden',
29
+ what: 'The entire page body is hidden or empty',
30
+ ordinalSeverity: 3,
31
+ count: 1
32
+ }]
38
33
  };
39
- }, visibleElementCount);
34
+ }
35
+ // Otherwise, return data, totals, and an empty array of standard instances.
36
+ return {
37
+ data: {},
38
+ totals: [0, 0, 0, 0],
39
+ standardInstances: []
40
+ };
40
41
  };
@@ -1,9 +1,8 @@
1
1
  /*
2
2
  © 2023 CVS Health and/or one of its affiliates. All rights reserved.
3
- © 2025 Jonathan Robert Pool.
3
+ © 2025–2026 Jonathan Robert Pool.
4
4
 
5
- Licensed under the MIT License. See LICENSE file at the project root or
6
- https://opensource.org/license/mit/ for details.
5
+ Licensed under the MIT License. See LICENSE file at the project root or https://opensource.org/license/mit/ for details.
7
6
 
8
7
  SPDX-License-Identifier: MIT
9
8
  */
@@ -21,7 +20,7 @@ const {doTest} = require('../procs/testaro');
21
20
  // FUNCTIONS
22
21
 
23
22
  // Runs the test and returns the result.
24
- exports.reporter = async (page, withItems) => {
23
+ exports.reporter = async (page, catalog, withItems) => {
25
24
  const getBadWhat = element => {
26
25
  const styleDec = window.getComputedStyle(element);
27
26
  const {textContent} = element;
@@ -34,6 +33,6 @@ exports.reporter = async (page, withItems) => {
34
33
  const selector = 'body *:not(style, script, svg)';
35
34
  const whats = 'Elements contain all-slanted text';
36
35
  return await doTest(
37
- page, withItems, 'allSlanted', selector, whats, 0, null, getBadWhat.toString()
36
+ page, catalog, withItems, 'allSlanted', selector, whats, 0, getBadWhat.toString()
38
37
  );
39
38
  };
@@ -1,7 +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
+ © 2025–2026 Jonathan Robert Pool.
5
5
 
6
6
  Licensed under the MIT License. See LICENSE file at the project root or
7
7
  https://opensource.org/license/mit/ for details.
@@ -21,7 +21,7 @@ const {doTest} = require('../procs/testaro');
21
21
  // FUNCTIONS
22
22
 
23
23
  // Runs the test and returns the result.
24
- exports.reporter = async (page, withItems) => {
24
+ exports.reporter = async (page, catalog, withItems) => {
25
25
  const getBadWhat = element => {
26
26
  // Get the value of the alt attribute of the element.
27
27
  const alt = (element.getAttribute('alt') || '').trim();
@@ -41,6 +41,6 @@ exports.reporter = async (page, withItems) => {
41
41
  };
42
42
  const whats = 'img elements have alt attributes with URL or filename values';
43
43
  return await doTest(
44
- page, withItems, 'altScheme', 'img[alt]', whats, 1, 'IMG', getBadWhat.toString()
44
+ page, catalog, withItems, 'altScheme', 'img[alt]', whats, 1, getBadWhat.toString()
45
45
  );
46
46
  };
package/testaro/attVal.js CHANGED
@@ -1,8 +1,8 @@
1
1
  /*
2
2
  © 2023 CVS Health and/or one of its affiliates. All rights reserved.
3
+ © 2026 Jonathan Robert Pool.
3
4
 
4
- Licensed under the MIT License. See LICENSE file at the project root or
5
- https://opensource.org/license/mit/ for details.
5
+ Licensed under the MIT License. See LICENSE file at the project root or https://opensource.org/license/mit/ for details.
6
6
 
7
7
  SPDX-License-Identifier: MIT
8
8
  */
@@ -12,41 +12,25 @@
12
12
  This test reports elements with illicit values of an attribute.
13
13
  */
14
14
 
15
+ // IMPORTS
16
+
17
+ const {doTest} = require('../procs/testaro');
18
+
15
19
  // FUNCTIONS
16
20
 
17
21
  // Runs the test and returns the result.
18
- exports.reporter = async (page, withItems, attributeName, areLicit, values) => {
19
- // Return totals and standard instances for the rule.
20
- return await page.evaluate(args => {
21
- const [withItems, attributeName, areLicit, values] = args;
22
- // Get all candidates, i.e. elements with the attribute.
23
- const candidates = document.body.querySelectorAll(`[${attributeName}]`);
24
- let violationCount = 0;
25
- const instances = [];
26
- // For each candidate:
27
- candidates.forEach(element => {
28
- const value = element.getAttribute(attributeName);
29
- // If it violates the rule:
30
- if (areLicit !== values.includes(value)) {
31
- // Increment the violation count.
32
- violationCount++;
33
- // If itemization is required:
34
- if (withItems) {
35
- const what = `Element has attribute ${attributeName} with illicit value ${value}`;
36
- instances.push(window.getInstance(element, 'attVal', what, 1, 2));
37
- }
38
- }
39
- });
40
- // If there were any violations and itemization is not required:
41
- if (violationCount && ! withItems) {
42
- const what = `Elements have attribute ${attributeName} with illicit values`;
43
- // Add a summary instance to the instances.
44
- instances.push(window.getInstance(null, 'attVal', what, violationCount, 2));
22
+ exports.reporter = async (page, catalog, withItems, attributeName, areLicit, values) => {
23
+ const getBadWhat = element => {
24
+ // Get the value of the attribute.
25
+ const value = element.getAttribute(attributeName);
26
+ // If it violates the rule:
27
+ if (areLicit !== values.includes(value)) {
28
+ // Return a violation description.
29
+ return `Element has attribute ${attributeName} with illicit value ${value}`;
45
30
  }
46
- return {
47
- data: {},
48
- totals: [0, 0, violationCount, 0],
49
- standardInstances: instances
50
- };
51
- }, [withItems, attributeName, areLicit, values]);
31
+ };
32
+ const whats = `Elements have attribute ${attributeName} with illicit values`;
33
+ return await doTest(
34
+ page, catalog, withItems, 'attVal', `[${attributeName}]`, whats, 2, getBadWhat.toString()
35
+ );
52
36
  };
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  © 2023 CVS Health and/or one of its affiliates. All rights reserved.
3
- © 2025 Jonathan Robert Pool.
3
+ © 2025–2026 Jonathan Robert Pool.
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.
@@ -13,74 +13,77 @@
13
13
  This test reports failures to equip name and email inputs with correct autocomplete attributes.
14
14
  */
15
15
 
16
+ // IMPORTS
17
+
18
+ const {doTest} = require('../procs/testaro');
19
+
16
20
  // FUNCTIONS
17
21
 
18
22
  // Runs the test and returns the result.
19
23
  exports.reporter = async (
20
24
  page,
25
+ catalog,
21
26
  withItems,
22
- nameLabels = ['your name', 'full name', 'first and last name'],
23
- emailLabels = ['email'],
24
- givenLabels = ['first name', 'forename', 'given name'],
25
- familyLabels = ['last name', 'surname', 'family name']
27
+ labels = {
28
+ name: ['your name', 'full name', 'first and last name'],
29
+ email: ['email'],
30
+ given: ['first name', 'forename', 'given name'],
31
+ family: ['last name', 'surname', 'family name']
32
+ }
26
33
  ) => {
27
- // Return totals and standard instances for the rule.
28
- return await page.evaluate(args => {
29
- const [withItems, nameLabels,givenLabels, familyLabels, emailLabels] = args;
30
- // Get all candidates, i.e. text and email input elements.
31
- const candidates = document.body.querySelectorAll(
32
- 'input[type=text], input[type=email], input:not([type])'
33
- );
34
- let violationCount = 0;
35
- const instances = [];
36
- // For each candidate:
37
- candidates.forEach(candidate => {
38
- // Get its lower-cased accessible name.
39
- const name = window.getAccessibleName(candidate).toLowerCase();
40
- // Get its required autocomplete value.
41
- let requiredAuto = '';
42
- if (candidate.type === 'email' || name && emailLabels.some(label => name.includes(label))) {
43
- requiredAuto = 'email';
44
- }
45
- else if (
46
- name && candidate.type === 'text' && nameLabels.some(label => name.includes(label))
47
- ) {
48
- requiredAuto = 'name';
49
- }
50
- else if (
51
- name && candidate.type === 'text' && givenLabels.some(label => name.includes(label))
52
- ) {
53
- requiredAuto = 'given-name';
54
- }
55
- else if (
56
- name && candidate.type === 'text' && familyLabels.some(label => name.includes(label))
57
- ) {
58
- requiredAuto = 'family-name';
59
- }
60
- // Get its actual autocomplete value.
61
- const actualAuto = candidate.getAttribute('autocomplete');
62
- // If an autocomplete value is required but not present:
63
- if (requiredAuto && ! (actualAuto && actualAuto.includes(requiredAuto))) {
64
- // Increment the violation count.
65
- violationCount++;
66
- // If itemization is required:
67
- if (withItems) {
68
- const what = `input has no autocomplete="${requiredAuto}" attribute`;
69
- // Add an instance to the instances.
70
- instances.push(window.getInstance(candidate, 'autocomplete', what, 1, 2));
71
- }
72
- }
73
- });
74
- // If there were any violations and itemization is not required:
75
- if (violationCount && ! withItems) {
76
- const what = 'Inputs are missing required autocomplete attributes';
77
- // Add a summary instance to the instances.
78
- instances.push(window.getInstance(null, 'autocomplete', what, violationCount, 2));
34
+ const getBadWhat = element => {
35
+ // Get the lower-cased accessible name of the element.
36
+ const accessibleName = window.getAccessibleName(element).toLowerCase();
37
+ const {type} = element;
38
+ let requiredAuto = '';
39
+ const labels = {
40
+ name: ['__nameLabels__'],
41
+ email: ['__emailLabels__'],
42
+ given: ['__givenLabels__'],
43
+ family: ['__familyLabels__']
44
+ };
45
+ // Get its required autocomplete value.
46
+ if (
47
+ type === 'email'
48
+ || accessibleName && labels.email.some(label => accessibleName.includes(label))
49
+ ) {
50
+ requiredAuto = 'email';
51
+ }
52
+ else if (
53
+ accessibleName && type === 'text' && labels.name.some(label => accessibleName.includes(label))
54
+ ) {
55
+ requiredAuto = 'name';
56
+ }
57
+ else if (
58
+ accessibleName
59
+ && type === 'text'
60
+ && labels.given.some(label => accessibleName.includes(label))
61
+ ) {
62
+ requiredAuto = 'given-name';
63
+ }
64
+ else if (
65
+ accessibleName
66
+ && type === 'text'
67
+ && labels.family.some(label => accessibleName.includes(label))
68
+ ) {
69
+ requiredAuto = 'family-name';
79
70
  }
80
- return {
81
- data: {},
82
- totals: [0, 0, violationCount, 0],
83
- standardInstances: instances
71
+ // Get its actual autocomplete value.
72
+ const actualAuto = element.getAttribute('autocomplete');
73
+ // If an autocomplete value is required but not present:
74
+ if (requiredAuto && ! (actualAuto && actualAuto.includes(requiredAuto))) {
75
+ // Return a violation description.
76
+ return `input has no autocomplete="${requiredAuto}" attribute`;
84
77
  }
85
- }, [withItems, nameLabels, givenLabels, familyLabels, emailLabels]);
78
+ };
79
+ const selector = 'input[type=text], input[type=email], input:not([type])';
80
+ const whats = 'Inputs are missing required autocomplete attributes';
81
+ const placeHolders = Object.keys(labels).map(key => `__${key}Labels__`);
82
+ const replacers = Object.values(labels).map(value => JSON.stringify(value));
83
+ // Create a stringified getBadWhat, with placeholders replaced with the specified label arrays.
84
+ let getBadWhatString = getBadWhat.toString();
85
+ [0, 1, 2, 3].forEach(index => {
86
+ getBadWhatString = getBadWhatString.replace(placeHolders[index], replacers[index]);
87
+ });
88
+ return doTest(page, catalog, withItems, 'autocomplete', selector, whats, 2, getBadWhatString);
86
89
  };
package/testaro/bulk.js CHANGED
@@ -1,6 +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
+ © 2025–2026 Jonathan Robert Pool.
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.
@@ -12,32 +12,33 @@
12
12
  bulk
13
13
  This test reports the count of visible elements. The test assumes that simplicity and compactness, with one page having one purpose, is an accessibility virtue. Users with visual, motor, and cognitive disabilities often have trouble finding what they want or understanding the purpose of a page if the page is cluttered with content.
14
14
  */
15
+
16
+ // Runs the test and returns the result.
15
17
  exports.reporter = async page => {
16
18
  // Get a count of elements deemed visible by Playwright.
17
- const visibleElementCount = await page.locator(':visible').count();
18
- // Get totals and an instance.
19
- const violationData = await page.evaluate(visibleElementCount => {
20
- // Convert the count to a severity level, treating up to 400 as non-reportable.
21
- const severity = Math.min(4, Math.round(visibleElementCount / 400)) - 1;
22
- const totals = [0, 0, 0, 0];
23
- const instances = [];
24
- // If the severity is reportable:
25
- if (severity > -1) {
26
- totals[severity] = 1;
27
- const what = `Page contains ${visibleElementCount} visible elements`;
28
- // Create an instance reporting it.
29
- instances.push(window.getInstance(document.documentElement, 'bulk', what, 1, severity));
30
- }
19
+ const visibleElementCount = await page.locator('body :visible').count();
20
+ // Convert the count to a severity level, treating up to 400 as non-reportable.
21
+ const severity = Math.min(4, Math.round(visibleElementCount / 400)) - 1;
22
+ const totals = [0, 0, 0, 0];
23
+ // If the severity is reportable:
24
+ if (severity > -1) {
25
+ totals[severity] = 1;
26
+ // Return data, totals, and a summary standard instance.
31
27
  return {
28
+ data: {},
32
29
  totals,
33
- instances
30
+ standardInstances: [{
31
+ ruleID: 'bulk',
32
+ what: `Page contains ${visibleElementCount} visible elements`,
33
+ ordinalSeverity: severity,
34
+ count: 1
35
+ }]
34
36
  };
35
- }, visibleElementCount);
36
- const {totals, instances} = violationData;
37
- // Return the result.
37
+ }
38
+ // Otherwise, return data, totals, and an empty array of standard instances.
38
39
  return {
39
40
  data: {},
40
41
  totals,
41
- standardInstances: instances
42
+ standardInstances: []
42
43
  };
43
44
  };
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  © 2023–2024 CVS Health and/or one of its affiliates. All rights reserved.
3
- © 2025 Jonathan Robert Pool.
3
+ © 2025–2026 Jonathan Robert Pool.
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.
@@ -13,13 +13,99 @@
13
13
  This test reports nonstandard navigation among menu items of button-controlled menus. Standards are based on https://www.w3.org/TR/wai-aria-practices-1.1/#menu. The trialKeys argument is an array of strings, each of which may be 'Home', 'End', '+', or '-'. The '+' string represents the ArrowDown or ArrowRight key, and the '-' string represents the ArrowUp or ArrowLeft key, depending on the orientation of the current menu. When the trialKeys argument is missing or is an empty array, 12 keys are selected at random.
14
14
  */
15
15
 
16
- // ########## IMPORTS
17
-
18
- // Module to get locator data.
19
- const {getLocatorData} = require('../procs/getLocatorData');
20
-
21
16
  // ########## FUNCTIONS
22
17
 
18
+ // Returns data about the element referenced by a locator.
19
+ const getLocatorData = async loc => {
20
+ const locCount = await loc.count();
21
+ // If the locator identifies exactly 1 element:
22
+ if (locCount === 1) {
23
+ // Get the facts obtainable from the browser.
24
+ const data = await loc.evaluate(element => {
25
+ // Tag name.
26
+ const tagName = element.tagName;
27
+ // ID.
28
+ const id = element.id || '';
29
+ // Texts.
30
+ const {textContent} = element;
31
+ const alts = Array.from(element.querySelectorAll('img[alt]:not([alt=""])'));
32
+ const altTexts = alts.map(alt => alt.getAttribute('alt'));
33
+ const altsText = altTexts.join(' ');
34
+ const ariaLabelText = element.ariaLabel || '';
35
+ const refLabelID = element.getAttribute('aria-labelledby');
36
+ const refLabel = refLabelID ? document.getElementById(refLabelID) : '';
37
+ const refLabelText = refLabel ? refLabel.textContent : '';
38
+ let labelsText = '';
39
+ if (tagName === 'INPUT') {
40
+ const labels = element.labels || [];
41
+ const labelTexts = [];
42
+ labels.forEach(label => {
43
+ labelTexts.push(label.textContent);
44
+ });
45
+ labelsText = labelTexts.join(' ');
46
+ }
47
+ let text = [textContent, altsText, ariaLabelText, refLabelText, labelsText]
48
+ .join(' ')
49
+ .replace(/\s+/g, ' ')
50
+ .trim();
51
+ if (! text) {
52
+ text = element.outerHTML.replace(/\s+/g, ' ').trim();
53
+ }
54
+ if (/^<[^<>]+>$/.test(text)) {
55
+ text = element.parentElement.outerHTML.replace(/\s+/g, ' ').trim();
56
+ }
57
+ // Location.
58
+ let location = {
59
+ doc: 'dom',
60
+ type: 'box',
61
+ spec: {}
62
+ };
63
+ if (id) {
64
+ location.type = 'selector';
65
+ location.spec = `#${id}`;
66
+ }
67
+ // Return the data.
68
+ return {
69
+ tagName,
70
+ id,
71
+ location,
72
+ excerpt: text
73
+ };
74
+ });
75
+ // If an ID-based selector could not be defined:
76
+ if (data.location.type === 'box') {
77
+ // Define a bounding-box-based location.
78
+ const rawSpec = await loc.boundingBox();
79
+ // If there is a bounding box (i.e. the element is visible):
80
+ if (rawSpec) {
81
+ // Populate the location.
82
+ Object.keys(rawSpec).forEach(specName => {
83
+ data.location.spec[specName] = Math.round(rawSpec[specName]);
84
+ });
85
+ }
86
+ // Otherwise, i.e. if there is no bounding box:
87
+ else {
88
+ // Empty the location.
89
+ data.location.doc = '';
90
+ data.location.type = '';
91
+ data.location.spec = '';
92
+ }
93
+ }
94
+ // If the text is long:
95
+ if (data.excerpt.length > 400) {
96
+ // Truncate its middle.
97
+ data.excerpt = `${data.excerpt.slice(0, 200)} … ${data.excerpt.slice(-200)}`;
98
+ }
99
+ // Return the data.
100
+ return data;
101
+ }
102
+ // Otherwise, i.e. if it does not identify exactly 1 element:
103
+ else {
104
+ // Report this.
105
+ console.log(`ERROR: Locator count to get data from is ${locCount} instead of 1`);
106
+ return null;
107
+ }
108
+ };
23
109
  // Returns an adjacent index, with wrapping.
24
110
  const getAdjacentIndexWithWrap = (groupSize, startIndex, increment) => {
25
111
  let newIndex = startIndex + increment;
@@ -90,7 +176,7 @@ const focusSuccess = async (miLocsDir, priorIndex, key, isPseudo) => {
90
176
  return result;
91
177
  };
92
178
  // Performs the test and reports the result.
93
- exports.reporter = async (page, withItems, trialKeySpecs = []) => {
179
+ exports.reporter = async (page, catalog, withItems, trialKeySpecs = []) => {
94
180
  // Initialize the result.
95
181
  const data = {};
96
182
  const totals = [0, 0, 0, 0];
@@ -99,8 +185,8 @@ exports.reporter = async (page, withItems, trialKeySpecs = []) => {
99
185
  const mbLocAll = page.locator(
100
186
  'button[aria-controls][aria-expanded][aria-haspopup=true], button[aria-controls][aria-expanded][aria-haspopup=menu]'
101
187
  );
102
- // For each menu button:
103
188
  const mbLocsAll = await mbLocAll.all();
189
+ // For each menu button:
104
190
  for (const mbLoc of mbLocsAll) {
105
191
  // Get a locator for its menu.
106
192
  const menuID = await mbLoc.getAttribute('aria-controls');
@@ -206,17 +292,19 @@ exports.reporter = async (page, withItems, trialKeySpecs = []) => {
206
292
  totals[2]++;
207
293
  // If itemization is required:
208
294
  if (withItems) {
209
- // Add an instance to the result.
210
- standardInstances.push({
295
+ // Create a proto-instance.
296
+ const protoInstance = {
211
297
  ruleID: 'buttonMenu',
212
298
  what: `Menu responds nonstandardly to the ${key} key`,
213
299
  ordinalSeverity: 2,
214
- tagName: elData.tagName,
215
- id: elData.id,
216
- location: elData.location,
217
- excerpt: elData.excerpt
218
- });
300
+ count: 1
301
+ };
302
+ // Add a catalog index or XPath to it if possible.
303
+ addCatalogIndex(protoInstance, mbLoc, catalog);
304
+ // Add the proto-instance to the standard instances.
305
+ standardInstances.push(protoInstance);
219
306
  }
307
+ // Stop testing the menu button.
220
308
  break;
221
309
  }
222
310
  }
@@ -240,18 +328,17 @@ exports.reporter = async (page, withItems, trialKeySpecs = []) => {
240
328
  totals[2]++;
241
329
  // If itemization is required:
242
330
  if (withItems) {
243
- // Get data on the menu button.
244
- const mbData = await getLocatorData(mbLoc);
245
- // Add an instance to the result.
246
- standardInstances.push({
331
+ // Create a proto-instance.
332
+ const protoInstance = {
247
333
  ruleID: 'buttonMenu',
248
- what: `Menu button does not control exactly 1 menu`,
334
+ what: 'Menu button does not control exactly 1 menu',
249
335
  ordinalSeverity: 2,
250
- tagName: 'BUTTON',
251
- id: await mbData.id,
252
- location: mbData.location,
253
- excerpt: mbData.excerpt
254
- });
336
+ count: 1
337
+ };
338
+ // Add a catalog index or XPath to it if possible.
339
+ addCatalogIndex(protoInstance, mbLoc, catalog);
340
+ // Add the proto-instance to the standard instances.
341
+ standardInstances.push(protoInstance);
255
342
  }
256
343
  }
257
344
  }
@@ -263,14 +350,6 @@ exports.reporter = async (page, withItems, trialKeySpecs = []) => {
263
350
  what: 'Menu buttons and menus behave nonstandardly',
264
351
  count: totals[2],
265
352
  ordinalSeverity: 2,
266
- tagName: '',
267
- id: '',
268
- location: {
269
- doc: '',
270
- type: '',
271
- spec: ''
272
- },
273
- excerpt: ''
274
353
  });
275
354
  }
276
355
  return {