@afixt/test-utils 1.1.8 → 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.
@@ -19,7 +19,11 @@
19
19
  "Bash(npx vitest run:*)",
20
20
  "Bash(1)",
21
21
  "Bash(npm run test:playwright:css:*)",
22
- "Bash(gh run list:*)"
22
+ "Bash(gh run list:*)",
23
+ "Bash(gh pr create:*)",
24
+ "Bash(git checkout:*)",
25
+ "Bash(git pull:*)",
26
+ "Bash(git merge:*)"
23
27
  ]
24
28
  },
25
29
  "enableAllProjectMcpServers": false
@@ -0,0 +1,88 @@
1
+ name: PR Check
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [main, develop]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ validate-workflows:
12
+ name: Validate GitHub Actions Workflows
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - name: Checkout code
16
+ uses: actions/checkout@v4
17
+
18
+ - name: Validate workflow syntax
19
+ run: |
20
+ echo "Validating GitHub Actions workflow files..."
21
+ errors=0
22
+
23
+ for f in .github/workflows/*.yml .github/workflows/*.yaml; do
24
+ [ -f "$f" ] || continue
25
+ echo "Checking $f..."
26
+
27
+ # Basic YAML syntax check
28
+ if ! python3 -c "import yaml; yaml.safe_load(open('$f'))" 2>/dev/null; then
29
+ echo "ERROR: Invalid YAML in $f"
30
+ errors=$((errors + 1))
31
+ fi
32
+
33
+ # Check for required 'on' and 'jobs' keys
34
+ if ! grep -q '^on:' "$f" && ! grep -q "^'on':" "$f" && ! grep -q '^"on":' "$f"; then
35
+ echo "WARNING: Missing 'on' trigger in $f"
36
+ fi
37
+
38
+ if ! grep -q '^jobs:' "$f"; then
39
+ echo "WARNING: Missing 'jobs' section in $f"
40
+ fi
41
+ done
42
+
43
+ if [ $errors -gt 0 ]; then
44
+ echo "Found $errors workflow file(s) with errors"
45
+ exit 1
46
+ fi
47
+
48
+ echo "All workflow files are valid"
49
+
50
+ ci-check:
51
+ name: CI Readiness Check
52
+ runs-on: ubuntu-latest
53
+ steps:
54
+ - name: Checkout code
55
+ uses: actions/checkout@v4
56
+
57
+ - name: Detect project type
58
+ id: detect
59
+ run: |
60
+ if [ -f "package.json" ]; then
61
+ echo "type=node" >> $GITHUB_OUTPUT
62
+ elif [ -f "requirements.txt" ] || [ -f "pyproject.toml" ]; then
63
+ echo "type=python" >> $GITHUB_OUTPUT
64
+ else
65
+ echo "type=other" >> $GITHUB_OUTPUT
66
+ fi
67
+
68
+ - name: Setup Node.js
69
+ if: steps.detect.outputs.type == 'node'
70
+ uses: actions/setup-node@v4
71
+ with:
72
+ node-version: 'lts/*'
73
+ cache: 'npm'
74
+
75
+ - name: Install dependencies
76
+ if: steps.detect.outputs.type == 'node'
77
+ run: npm ci --ignore-scripts
78
+ continue-on-error: true
79
+
80
+ - name: Run lint check
81
+ if: steps.detect.outputs.type == 'node'
82
+ run: npm run lint --if-present
83
+ continue-on-error: true
84
+
85
+ - name: Run tests
86
+ if: steps.detect.outputs.type == 'node'
87
+ run: npm test --if-present
88
+ continue-on-error: true
@@ -5,9 +5,6 @@ on:
5
5
  branches: [main, develop]
6
6
  pull_request:
7
7
  branches: [main, develop]
8
- schedule:
9
- # Run comprehensive scan every Sunday at midnight
10
- - cron: '0 0 * * 0'
11
8
  workflow_dispatch:
12
9
 
13
10
  permissions:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afixt/test-utils",
3
- "version": "1.1.8",
3
+ "version": "1.2.0",
4
4
  "description": "Various utilities for accessibility testing",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -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
  };