@afixt/test-utils 1.1.7 → 1.2.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 (70) hide show
  1. package/.claude/settings.local.json +6 -1
  2. package/.github/workflows/ci.yml +71 -0
  3. package/.github/workflows/pr-check.yml +88 -0
  4. package/.github/workflows/security.yml +139 -0
  5. package/.husky/pre-commit +1 -0
  6. package/.jscpd.json +27 -0
  7. package/.markdownlint.json +9 -0
  8. package/.prettierignore +13 -0
  9. package/.prettierrc +10 -0
  10. package/docs/arrayUtils.js.html +2 -2
  11. package/docs/domUtils.js.html +2 -2
  12. package/docs/getAccessibleName.js.html +2 -2
  13. package/docs/getAccessibleText.js.html +2 -2
  14. package/docs/getAriaAttributesByElement.js.html +2 -2
  15. package/docs/getCSSGeneratedContent.js.html +2 -2
  16. package/docs/getComputedRole.js.html +2 -2
  17. package/docs/getFocusableElements.js.html +2 -2
  18. package/docs/getGeneratedContent.js.html +2 -2
  19. package/docs/getImageText.js.html +2 -2
  20. package/docs/getStyleObject.js.html +2 -2
  21. package/docs/global.html +2 -2
  22. package/docs/hasAccessibleName.js.html +2 -2
  23. package/docs/hasAttribute.js.html +2 -2
  24. package/docs/hasCSSGeneratedContent.js.html +2 -2
  25. package/docs/hasHiddenParent.js.html +2 -2
  26. package/docs/hasParent.js.html +2 -2
  27. package/docs/hasValidAriaAttributes.js.html +2 -2
  28. package/docs/hasValidAriaRole.js.html +2 -2
  29. package/docs/index.html +2 -2
  30. package/docs/index.js.html +2 -2
  31. package/docs/isAriaAttributesValid.js.html +2 -2
  32. package/docs/isComplexTable.js.html +2 -2
  33. package/docs/isDataTable.js.html +2 -2
  34. package/docs/isFocusable.js.html +2 -2
  35. package/docs/isHidden.js.html +2 -2
  36. package/docs/isOffScreen.js.html +2 -2
  37. package/docs/isValidUrl.js.html +2 -2
  38. package/docs/isVisible.js.html +2 -2
  39. package/docs/module-afixt-test-utils.html +2 -2
  40. package/docs/scripts/core.js +726 -726
  41. package/docs/scripts/core.min.js +22 -22
  42. package/docs/scripts/resize.js +90 -90
  43. package/docs/scripts/search.js +265 -265
  44. package/docs/scripts/third-party/Apache-License-2.0.txt +202 -202
  45. package/docs/scripts/third-party/fuse.js +8 -8
  46. package/docs/scripts/third-party/hljs-line-num-original.js +369 -369
  47. package/docs/scripts/third-party/hljs-original.js +5171 -5171
  48. package/docs/scripts/third-party/popper.js +5 -5
  49. package/docs/scripts/third-party/tippy.js +1 -1
  50. package/docs/scripts/third-party/tocbot.js +671 -671
  51. package/docs/styles/clean-jsdoc-theme-base.css +1159 -1159
  52. package/docs/styles/clean-jsdoc-theme-dark.css +412 -412
  53. package/docs/styles/clean-jsdoc-theme-light.css +482 -482
  54. package/docs/styles/clean-jsdoc-theme-scrollbar.css +29 -29
  55. package/docs/testContrast.js.html +2 -2
  56. package/docs/testLang.js.html +2 -2
  57. package/docs/testOrder.js.html +2 -2
  58. package/eslint.config.mjs +84 -0
  59. package/package.json +68 -41
  60. package/scratchpads/issue-6-standardize-repo.md +109 -0
  61. package/src/getAccessibleName.js +156 -112
  62. package/src/getAccessibleText.js +71 -42
  63. package/src/stringUtils.js +19 -21
  64. package/src/testContrast.js +103 -22
  65. package/test/getAccessibleName.test.js +379 -315
  66. package/test/getAccessibleText.test.js +375 -308
  67. package/test/stringUtils.test.js +376 -332
  68. package/test/testContrast.test.js +801 -651
  69. package/.eslintrc +0 -78
  70. package/.github/workflows/test.yml +0 -26
@@ -1,7 +1,9 @@
1
- const { isEmpty } = require("./stringUtils.js");
1
+ const { isEmpty } = require('./stringUtils.js');
2
2
 
3
3
  /**
4
4
  * Get all accessible text for an element, including aria-labels and content from children.
5
+ * Traverses the DOM subtree collecting text from text nodes, img alt attributes,
6
+ * and input[type="image"] alt attributes.
5
7
  * @param {Element} el - The DOM element.
6
8
  * @returns {string} The accessible text.
7
9
  */
@@ -13,55 +15,82 @@ function getAccessibleText(el) {
13
15
  if (!el.isConnected) {
14
16
  return '';
15
17
  }
16
-
17
- let textContent = "";
18
-
19
- // Check for element's own text content
20
- if (el.textContent) {
21
- textContent = el.textContent.trim();
22
- }
23
-
24
- // Check for aria-label
25
- if (el.hasAttribute("aria-label")) {
26
- const ariaLabel = el.getAttribute("aria-label").trim();
18
+
19
+ // Check for aria-label first (highest priority)
20
+ if (el.hasAttribute('aria-label')) {
21
+ const ariaLabel = el.getAttribute('aria-label').trim();
27
22
  if (ariaLabel) {
28
- // Prioritize aria-label if present
29
23
  return ariaLabel;
30
24
  }
31
25
  }
32
-
33
- // Check for img alt text
34
- if (el.tagName.toLowerCase() === "img" && el.hasAttribute("alt")) {
35
- return el.getAttribute("alt").trim();
26
+
27
+ // Check for img alt text when the element itself is an img
28
+ if (el.tagName.toLowerCase() === 'img' && el.hasAttribute('alt')) {
29
+ return el.getAttribute('alt').trim();
30
+ }
31
+
32
+ // Check for input[type="image"] alt text when the element itself is one
33
+ if (
34
+ el.tagName.toLowerCase() === 'input' &&
35
+ el.getAttribute('type') === 'image' &&
36
+ el.hasAttribute('alt')
37
+ ) {
38
+ return el.getAttribute('alt').trim();
36
39
  }
37
-
38
- // Handle other elements by getting visible text
39
- if (!textContent) {
40
- const walker = document.createTreeWalker(
41
- el,
42
- NodeFilter.SHOW_TEXT,
43
- {
44
- acceptNode: function (node) {
45
- // Only accept non-empty text nodes
46
- return node.nodeType === Node.TEXT_NODE && !isEmpty(node.nodeValue)
47
- ? NodeFilter.FILTER_ACCEPT
48
- : NodeFilter.FILTER_REJECT;
49
- },
40
+
41
+ // Collect accessible text from the subtree, including text nodes
42
+ // and alt text from embedded images
43
+ const parts = collectSubtreeText(el);
44
+ return parts.join(' ').replace(/\s+/g, ' ').trim();
45
+ }
46
+
47
+ /**
48
+ * Recursively collect accessible text parts from an element's subtree.
49
+ * Handles text nodes, img alt text, and input[type="image"] alt text.
50
+ * @param {Node} node - The DOM node to traverse.
51
+ * @returns {string[]} Array of text parts found in the subtree.
52
+ */
53
+ function collectSubtreeText(node) {
54
+ const parts = [];
55
+
56
+ for (let child = node.firstChild; child; child = child.nextSibling) {
57
+ if (child.nodeType === Node.TEXT_NODE) {
58
+ const text = child.nodeValue.trim();
59
+ if (!isEmpty(text)) {
60
+ parts.push(text);
61
+ }
62
+ } else if (child.nodeType === Node.ELEMENT_NODE) {
63
+ const tag = child.tagName.toLowerCase();
64
+
65
+ // img with non-empty alt contributes its alt text
66
+ if (tag === 'img' && child.hasAttribute('alt')) {
67
+ const alt = child.getAttribute('alt').trim();
68
+ if (alt) {
69
+ parts.push(alt);
70
+ }
71
+ continue;
50
72
  }
51
- );
52
-
53
- let textNodes = [];
54
- let node;
55
-
56
- while ((node = walker.nextNode())) {
57
- textNodes.push(node.nodeValue.trim());
73
+
74
+ // input[type="image"] with non-empty alt contributes its alt text
75
+ if (
76
+ tag === 'input' &&
77
+ child.getAttribute('type') === 'image' &&
78
+ child.hasAttribute('alt')
79
+ ) {
80
+ const alt = child.getAttribute('alt').trim();
81
+ if (alt) {
82
+ parts.push(alt);
83
+ }
84
+ continue;
85
+ }
86
+
87
+ // Recurse into other element children
88
+ parts.push(...collectSubtreeText(child));
58
89
  }
59
-
60
- textContent = textNodes.join(" ").trim();
61
90
  }
62
-
63
- return textContent;
91
+
92
+ return parts;
64
93
  }
65
94
 
66
95
  // Export for CommonJS module usage
67
- module.exports = { getAccessibleText };
96
+ module.exports = { getAccessibleText };
@@ -14,7 +14,7 @@ const stringUtils = (function () {
14
14
  * @returns {boolean} Whether the value is a string.
15
15
  */
16
16
  function isString(str) {
17
- return typeof str === "string" || str instanceof String;
17
+ return typeof str === 'string' || str instanceof String;
18
18
  }
19
19
 
20
20
  /**
@@ -32,7 +32,7 @@ const stringUtils = (function () {
32
32
  * @returns {boolean}
33
33
  */
34
34
  function isNormalInteger(str) {
35
- const n = ~~Number(str);
35
+ const n = Math.trunc(Number(str));
36
36
  return String(n) === str && n >= 0;
37
37
  }
38
38
 
@@ -49,14 +49,11 @@ const stringUtils = (function () {
49
49
  let char;
50
50
  let isUpper = true;
51
51
 
52
- str = str.replace(/[^\w\s]|_/g, "").replace(/\s+/g, " ");
52
+ str = str.replace(/[^\w\s]|_/g, '').replace(/\s+/g, ' ');
53
53
 
54
54
  while (i <= str.length) {
55
55
  char = str.charAt(i);
56
- if (
57
- char.trim() !== "" &&
58
- false === (!isNaN(parseFloat(char)) && isFinite(char))
59
- ) {
56
+ if (char.trim() !== '' && false === (!isNaN(parseFloat(char)) && isFinite(char))) {
60
57
  if (char === char.toLowerCase()) {
61
58
  isUpper = false;
62
59
  }
@@ -94,14 +91,15 @@ const stringUtils = (function () {
94
91
  * @returns {string} A string containing all concatenated text content from the element.
95
92
  */
96
93
  function getAllText(el) {
97
- const walker = document.createTreeWalker(
98
- el,
99
- NodeFilter.SHOW_ALL,
100
- null,
101
- false
102
- );
94
+ // Check for form element value (input, textarea, select)
95
+ if (el.value !== undefined && el.value !== '') {
96
+ return el.value;
97
+ }
98
+
99
+ const walker = document.createTreeWalker(el, NodeFilter.SHOW_ALL, null, false);
103
100
  const textNodes = [];
104
- let node, text;
101
+ let node;
102
+ let text;
105
103
 
106
104
  while (walker.nextNode()) {
107
105
  node = walker.currentNode;
@@ -113,15 +111,15 @@ const stringUtils = (function () {
113
111
  textNodes.push(node.textContent.trim());
114
112
  }
115
113
  } else if (node.nodeType === Node.ELEMENT_NODE) {
116
- if (node.hasAttribute("aria-label")) {
117
- textNodes.push(node.getAttribute("aria-label"));
118
- } else if (node.tagName === "IMG" && node.hasAttribute("alt")) {
119
- textNodes.push(node.getAttribute("alt"));
114
+ if (node.hasAttribute('aria-label')) {
115
+ textNodes.push(node.getAttribute('aria-label'));
116
+ } else if (node.tagName === 'IMG' && node.hasAttribute('alt')) {
117
+ textNodes.push(node.getAttribute('alt'));
120
118
  }
121
119
  }
122
120
  }
123
121
 
124
- return textNodes.join(" ");
122
+ return textNodes.join(' ');
125
123
  }
126
124
 
127
125
  /**
@@ -131,7 +129,7 @@ const stringUtils = (function () {
131
129
  * @returns {boolean} True if the element contains text, false otherwise.
132
130
  */
133
131
  function hasText(element) {
134
- return getAllText(element).trim() !== "";
132
+ return getAllText(element).trim() !== '';
135
133
  }
136
134
 
137
135
  return {
@@ -143,7 +141,7 @@ const stringUtils = (function () {
143
141
  isAlphaNumeric,
144
142
  getPathFromUrl,
145
143
  getAllText,
146
- hasText
144
+ hasText,
147
145
  };
148
146
  })();
149
147
 
@@ -1,13 +1,21 @@
1
1
  /**
2
2
  * Get the computed background color for an element.
3
3
  * @param {Element} el - the element to be tested
4
+ * @param {Object} [options] - Configuration options
5
+ * @param {string} [options.fallbackColor='rgb(255, 255, 255)'] - Color to use when background can't be determined
6
+ * @param {boolean} [options.skipBackgroundImages=true] - If true (default), return false for background images. If false, use fallbackColor instead.
4
7
  * @returns {string|boolean} background color or false
5
8
  */
6
- function getComputedBackgroundColor(el) {
9
+ function getComputedBackgroundColor(el, options) {
7
10
  if (!window.getComputedStyle || !el || el.nodeType === 9) {
8
11
  return false;
9
12
  }
10
13
 
14
+ const opts = options || {};
15
+ const fallbackColor = opts.fallbackColor || 'rgb(255, 255, 255)';
16
+ const skipBackgroundImages =
17
+ opts.skipBackgroundImages !== undefined ? opts.skipBackgroundImages : true;
18
+
11
19
  const styles = window.getComputedStyle(el);
12
20
  if (!styles) {
13
21
  return false;
@@ -17,12 +25,18 @@ function getComputedBackgroundColor(el) {
17
25
  if (bgImage === 'none') {
18
26
  const bgColor = styles.getPropertyValue('background-color');
19
27
 
20
- if (bgColor === 'rgba(0, 0, 0, 0)' && el.parentElement) {
21
- return getComputedBackgroundColor(el.parentElement);
28
+ if (bgColor === 'rgba(0, 0, 0, 0)') {
29
+ if (el.parentElement) {
30
+ return getComputedBackgroundColor(el.parentElement, options);
31
+ }
32
+ return opts.fallbackColor !== undefined ? fallbackColor : false;
22
33
  }
23
34
 
24
35
  return bgColor;
25
36
  } else {
37
+ if (!skipBackgroundImages) {
38
+ return fallbackColor;
39
+ }
26
40
  return false;
27
41
  }
28
42
  }
@@ -40,9 +54,9 @@ function luminance(R8bit, G8bit, B8bit) {
40
54
  const GsRGB = G8bit / 255;
41
55
  const BsRGB = B8bit / 255;
42
56
 
43
- const R = (RsRGB <= 0.03928) ? RsRGB / 12.92 : Math.pow((RsRGB + 0.055) / 1.055, 2.4);
44
- const G = (GsRGB <= 0.03928) ? GsRGB / 12.92 : Math.pow((GsRGB + 0.055) / 1.055, 2.4);
45
- const B = (BsRGB <= 0.03928) ? BsRGB / 12.92 : Math.pow((BsRGB + 0.055) / 1.055, 2.4);
57
+ const R = RsRGB <= 0.03928 ? RsRGB / 12.92 : Math.pow((RsRGB + 0.055) / 1.055, 2.4);
58
+ const G = GsRGB <= 0.03928 ? GsRGB / 12.92 : Math.pow((GsRGB + 0.055) / 1.055, 2.4);
59
+ const B = BsRGB <= 0.03928 ? BsRGB / 12.92 : Math.pow((BsRGB + 0.055) / 1.055, 2.4);
46
60
 
47
61
  // For the sRGB colorspace, the relative luminance of a color is defined as:
48
62
  const L = 0.2126 * R + 0.7152 * G + 0.0722 * B;
@@ -50,6 +64,24 @@ function luminance(R8bit, G8bit, B8bit) {
50
64
  return L;
51
65
  }
52
66
 
67
+ /**
68
+ * Calculate relative luminance from an RGB object.
69
+ * Wrapper around luminance() that accepts an object instead of three separate arguments.
70
+ * @param {{ r: number, g: number, b: number }} rgb - Object with r, g, b properties (0-255)
71
+ * @returns {number|null} Relative luminance value (0-1) or null if input is invalid
72
+ */
73
+ function getRelativeLuminance(rgb) {
74
+ if (
75
+ !rgb ||
76
+ typeof rgb.r !== 'number' ||
77
+ typeof rgb.g !== 'number' ||
78
+ typeof rgb.b !== 'number'
79
+ ) {
80
+ return null;
81
+ }
82
+ return luminance(rgb.r, rgb.g, rgb.b);
83
+ }
84
+
53
85
  /**
54
86
  * Parse an RGB or RGBA color string into its components
55
87
  * @param {string} rgb - RGB(A) color string (e.g., "rgb(255, 0, 0)" or "rgba(255, 0, 0, 0.5)")
@@ -60,6 +92,57 @@ function parseRGB(rgb) {
60
92
  return rgbvals;
61
93
  }
62
94
 
95
+ /**
96
+ * Parse an RGB/RGBA color string into a structured object.
97
+ * @param {string} colorStr - e.g., 'rgb(255, 128, 0)' or 'rgba(255, 128, 0, 0.5)'
98
+ * @returns {{ r: number, g: number, b: number, a?: number } | null}
99
+ */
100
+ function parseColor(colorStr) {
101
+ if (!colorStr) {
102
+ return null;
103
+ }
104
+
105
+ const match = parseRGB(colorStr);
106
+ if (!match) {
107
+ return null;
108
+ }
109
+
110
+ const result = {
111
+ r: parseInt(match[1], 10),
112
+ g: parseInt(match[2], 10),
113
+ b: parseInt(match[3], 10),
114
+ };
115
+
116
+ if (match[4] !== undefined) {
117
+ result.a = parseFloat(match[4]);
118
+ }
119
+
120
+ return result;
121
+ }
122
+
123
+ /**
124
+ * Calculate contrast ratio between two color strings.
125
+ * @param {string} color1 - e.g., 'rgb(0, 0, 0)'
126
+ * @param {string} color2 - e.g., 'rgb(255, 255, 255)'
127
+ * @returns {number|null} Contrast ratio (1 to 21) or null if colors can't be parsed
128
+ */
129
+ function getContrastRatio(color1, color2) {
130
+ const c1 = parseColor(color1);
131
+ const c2 = parseColor(color2);
132
+
133
+ if (!c1 || !c2) {
134
+ return null;
135
+ }
136
+
137
+ const lum1 = luminance(c1.r, c1.g, c1.b);
138
+ const lum2 = luminance(c2.r, c2.g, c2.b);
139
+
140
+ const lighter = Math.max(lum1, lum2);
141
+ const darker = Math.min(lum1, lum2);
142
+
143
+ return Math.round(((lighter + 0.05) / (darker + 0.05)) * 100) / 100;
144
+ }
145
+
63
146
  /**
64
147
  * Calculate color contrast ratio between foreground and background
65
148
  * @param {Element} el - DOM element to check
@@ -105,7 +188,7 @@ function getColorContrast(el) {
105
188
  lighter = fgLuminance;
106
189
  }
107
190
 
108
- return Math.round((lighter + 0.05) / (darker + 0.05) * 100) / 100;
191
+ return Math.round(((lighter + 0.05) / (darker + 0.05)) * 100) / 100;
109
192
  }
110
193
 
111
194
  /**
@@ -132,10 +215,11 @@ function testContrast(el, options = { level: 'AA' }) {
132
215
 
133
216
  // Check if element is offscreen
134
217
  const rect = el.getBoundingClientRect();
135
- const selfOffscreen = rect.left + rect.width <= 0 ||
136
- rect.top + rect.height <= 0 ||
137
- rect.left >= window.innerWidth ||
138
- rect.top >= window.innerHeight;
218
+ const selfOffscreen =
219
+ rect.left + rect.width <= 0 ||
220
+ rect.top + rect.height <= 0 ||
221
+ rect.left >= window.innerWidth ||
222
+ rect.top >= window.innerHeight;
139
223
 
140
224
  // Get direct text (excluding child elements)
141
225
  let selfDirectText = '';
@@ -157,14 +241,6 @@ function testContrast(el, options = { level: 'AA' }) {
157
241
  return true;
158
242
  }
159
243
 
160
- // JSDOM Test Compatibility: Handle test case for identical colors
161
- if (selfFG === selfBG || (selfFG === 'rgb(0, 0, 0)' && selfBG === 'rgb(0, 0, 0)')) {
162
- // Special case for test
163
- if (el.textContent === 'Test text') {
164
- return false;
165
- }
166
- }
167
-
168
244
  // Determine the contrast required based on the level passed to the function
169
245
  if (level === 'AA') {
170
246
  if (selfSize < 18) {
@@ -209,8 +285,10 @@ function testContrast(el, options = { level: 'AA' }) {
209
285
  // Only test contrast if there's a difference between the element and its parent
210
286
  // in terms of colors, font size, or font weight
211
287
  if (
212
- (selfFG !== parentFG || selfBG !== parentBG) ||
213
- (selfSize !== parentSize || selfWeight !== parentWeight)
288
+ selfFG !== parentFG ||
289
+ selfBG !== parentBG ||
290
+ selfSize !== parentSize ||
291
+ selfWeight !== parentWeight
214
292
  ) {
215
293
  const contrast = getColorContrast(el);
216
294
 
@@ -237,5 +315,8 @@ module.exports = {
237
315
  getComputedBackgroundColor,
238
316
  luminance,
239
317
  parseRGB,
240
- getColorContrast
318
+ parseColor,
319
+ getRelativeLuminance,
320
+ getContrastRatio,
321
+ getColorContrast,
241
322
  };