@afixt/test-utils 1.1.8 → 1.2.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.
@@ -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,150 @@ 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() !== '';
133
+ }
134
+
135
+ /**
136
+ * Checks if a string is empty or contains only whitespace/invisible characters,
137
+ * including zero-width spaces, no-break spaces, and other Unicode whitespace.
138
+ * @param {string} str - The string to check
139
+ * @returns {boolean} True if empty or whitespace/invisible only
140
+ */
141
+ function isEmptyOrWhitespace(str) {
142
+ if (!str) {
143
+ return true;
144
+ }
145
+ // eslint-disable-next-line no-misleading-character-class
146
+ const cleaned = str.replace(/[\s\u00A0\u200B\u200C\u200D\u2060\uFEFF]/g, '');
147
+ return cleaned.length === 0;
148
+ }
149
+
150
+ /**
151
+ * Checks if a title is generic, meaningless, or placeholder-like.
152
+ * @param {string} title - The title to check
153
+ * @returns {boolean} True if the title is generic/meaningless
154
+ */
155
+ function isGenericTitle(title) {
156
+ if (!title) {
157
+ return false;
158
+ }
159
+ const genericTitles = ['iframe', 'frame', 'untitled', 'title', 'content', 'main', 'page'];
160
+ const normalized = title.toLowerCase().trim();
161
+
162
+ if (genericTitles.includes(normalized)) {
163
+ return true;
164
+ }
165
+
166
+ if (/^(frame|iframe|untitled|title)\d*$/i.test(normalized)) {
167
+ return true;
168
+ }
169
+
170
+ return false;
171
+ }
172
+
173
+ /**
174
+ * Checks if the given text is generic/meaningless link text.
175
+ * @param {string} text - The accessible name to check
176
+ * @param {string[]} [genericList] - Optional custom list of generic link text
177
+ * @returns {boolean} True if the text is generic
178
+ */
179
+ function isGenericLinkText(text, genericList) {
180
+ if (!text) {
181
+ return false;
182
+ }
183
+ const defaults = [
184
+ 'click here',
185
+ 'here',
186
+ 'more',
187
+ 'read more',
188
+ 'learn more',
189
+ 'click',
190
+ 'link',
191
+ 'this',
192
+ 'page',
193
+ 'article',
194
+ 'continue',
195
+ 'go',
196
+ 'see more',
197
+ 'view',
198
+ 'download',
199
+ 'pdf',
200
+ 'document',
201
+ 'form',
202
+ 'submit',
203
+ 'button',
204
+ 'press',
205
+ 'select',
206
+ 'choose',
207
+ ];
208
+ const list = genericList || defaults;
209
+ const lowerText = text.toLowerCase().trim();
210
+ return list.some(function (generic) {
211
+ return lowerText === generic;
212
+ });
213
+ }
214
+
215
+ /**
216
+ * Get the actual visible text content of an element, ignoring aria-label.
217
+ * @param {Element} element - The DOM element
218
+ * @returns {string} The visible text content
219
+ */
220
+ function getActualVisibleText(element) {
221
+ if (!element) {
222
+ return '';
223
+ }
224
+ return (element.textContent || '').trim();
225
+ }
226
+
227
+ /**
228
+ * Checks if text contains a warning about new window/tab behavior.
229
+ * @param {string} text - The text to check
230
+ * @returns {boolean} True if the text contains a warning
231
+ */
232
+ function hasNewWindowWarning(text) {
233
+ if (!text) {
234
+ return false;
235
+ }
236
+ const warnings = [
237
+ 'new window',
238
+ 'new tab',
239
+ 'opens in new',
240
+ 'opens in a new',
241
+ 'opens new',
242
+ 'external link',
243
+ 'external site',
244
+ ];
245
+ const lowerText = text.toLowerCase();
246
+ return warnings.some(function (warning) {
247
+ return lowerText.includes(warning);
248
+ });
249
+ }
250
+
251
+ /**
252
+ * Extracts DOM text content from an element, including img alt text,
253
+ * but excluding ARIA attributes (aria-label, aria-labelledby).
254
+ * Useful for detecting actual visible/DOM text that may conflict with ARIA labels.
255
+ *
256
+ * @param {Element} root - The DOM element from which to extract text.
257
+ * @returns {string} Concatenated text from text nodes and img alt attributes.
258
+ */
259
+ function textIncludingImgAlt(root) {
260
+ let out = '';
261
+ const walker = document.createTreeWalker(
262
+ root,
263
+ NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT // eslint-disable-line no-bitwise
264
+ );
265
+ let n = walker.currentNode;
266
+ while (n) {
267
+ if (n.nodeType === Node.TEXT_NODE) {
268
+ out += n.nodeValue;
269
+ }
270
+ if (n.nodeType === Node.ELEMENT_NODE && n.tagName === 'IMG') {
271
+ out += n.getAttribute('alt') || '';
272
+ }
273
+ n = walker.nextNode();
274
+ }
275
+ return out;
135
276
  }
136
277
 
137
278
  return {
@@ -143,7 +284,13 @@ const stringUtils = (function () {
143
284
  isAlphaNumeric,
144
285
  getPathFromUrl,
145
286
  getAllText,
146
- hasText
287
+ hasText,
288
+ textIncludingImgAlt,
289
+ isEmptyOrWhitespace,
290
+ isGenericTitle,
291
+ isGenericLinkText,
292
+ getActualVisibleText,
293
+ hasNewWindowWarning,
147
294
  };
148
295
  })();
149
296
 
@@ -0,0 +1,180 @@
1
+ /**
2
+ * @file Table-related accessibility utilities
3
+ * @module tableUtils
4
+ */
5
+
6
+ const tableUtils = {
7
+ /**
8
+ * Check if table has multiple header rows (rows containing th elements).
9
+ * @param {HTMLElement} table - The table element
10
+ * @returns {boolean} True if table has multiple header rows
11
+ */
12
+ hasMultipleHeaderRows(table) {
13
+ const rows = table.querySelectorAll('tr');
14
+ let headerRowCount = 0;
15
+
16
+ for (let i = 0; i < rows.length; i++) {
17
+ const thCells = rows[i].querySelectorAll('th');
18
+ const tdCells = rows[i].querySelectorAll('td');
19
+
20
+ if (thCells.length > 0 && thCells.length >= tdCells.length) {
21
+ headerRowCount++;
22
+ }
23
+ }
24
+
25
+ return headerRowCount >= 3;
26
+ },
27
+
28
+ /**
29
+ * Check if table has header cells with large colspan/rowspan values.
30
+ * @param {HTMLElement} table - The table element
31
+ * @param {number} [maxColspan=4] - Maximum acceptable colspan
32
+ * @param {number} [maxRowspan=3] - Maximum acceptable rowspan
33
+ * @returns {boolean} True if table has complex header spans
34
+ */
35
+ hasComplexHeaderSpans(table, maxColspan, maxRowspan) {
36
+ maxColspan = maxColspan || 4;
37
+ maxRowspan = maxRowspan || 3;
38
+
39
+ const headerCells = table.querySelectorAll('th[colspan], th[rowspan]');
40
+
41
+ for (let i = 0; i < headerCells.length; i++) {
42
+ const colspan = parseInt(headerCells[i].getAttribute('colspan'), 10) || 1;
43
+ const rowspan = parseInt(headerCells[i].getAttribute('rowspan'), 10) || 1;
44
+
45
+ if (colspan > maxColspan || rowspan > maxRowspan) {
46
+ return true;
47
+ }
48
+ }
49
+
50
+ return false;
51
+ },
52
+
53
+ /**
54
+ * Count effective columns in a table (accounting for colspan).
55
+ * @param {HTMLElement} table - The table element
56
+ * @returns {number} Maximum column count
57
+ */
58
+ getMaxColumnCount(table) {
59
+ const rows = table.querySelectorAll('tr');
60
+ let maxCols = 0;
61
+
62
+ for (let i = 0; i < rows.length; i++) {
63
+ let colCount = 0;
64
+ const cells = rows[i].querySelectorAll('td, th');
65
+
66
+ for (let j = 0; j < cells.length; j++) {
67
+ const colspan = parseInt(cells[j].getAttribute('colspan'), 10) || 1;
68
+ colCount += colspan;
69
+ }
70
+
71
+ if (colCount > maxCols) {
72
+ maxCols = colCount;
73
+ }
74
+ }
75
+
76
+ return maxCols;
77
+ },
78
+
79
+ /**
80
+ * Check if table has multiple rows with colspan/rowspan on header cells,
81
+ * indicating multi-level grouped headers.
82
+ * @param {HTMLElement} table - The table element
83
+ * @returns {boolean} True if table has complex header groupings
84
+ */
85
+ hasMultiLevelHeaderGroupings(table) {
86
+ const rows = table.querySelectorAll('tr');
87
+ let rowsWithHeaderSpans = 0;
88
+
89
+ for (let i = 0; i < rows.length; i++) {
90
+ const spannedHeaders = rows[i].querySelectorAll('th[colspan], th[rowspan]');
91
+ if (spannedHeaders.length > 0) {
92
+ rowsWithHeaderSpans++;
93
+ }
94
+ }
95
+
96
+ return rowsWithHeaderSpans >= 2;
97
+ },
98
+
99
+ /**
100
+ * Checks if an HTML table has only header cells (th) and no data cells (td).
101
+ * Indicates headers potentially separated from their data.
102
+ * @param {HTMLTableElement} table - The table element to check
103
+ * @returns {boolean} True if table has only headers, no data
104
+ */
105
+ isHeaderOnlyTable(table) {
106
+ const hasHeaders = table.querySelectorAll('th').length > 0;
107
+ const hasDataCells = table.querySelectorAll('td').length > 0;
108
+ return hasHeaders && !hasDataCells;
109
+ },
110
+
111
+ /**
112
+ * Checks if an ARIA table has only header roles and no cell roles.
113
+ * @param {HTMLElement} ariaTable - The element with role="table"
114
+ * @returns {boolean} True if ARIA table has only headers, no data cells
115
+ */
116
+ isHeaderOnlyAriaTable(ariaTable) {
117
+ const hasHeaders =
118
+ ariaTable.querySelectorAll('[role="columnheader"], [role="rowheader"]').length > 0;
119
+ const hasDataCells =
120
+ ariaTable.querySelectorAll('[role="cell"], [role="gridcell"]').length > 0;
121
+ return hasHeaders && !hasDataCells;
122
+ },
123
+
124
+ /**
125
+ * Checks if a string contains only punctuation characters.
126
+ * @param {string} str - The string to check
127
+ * @returns {boolean} True if the string contains only punctuation
128
+ */
129
+ isPunctuationOnly(str) {
130
+ return /^[\p{P}\p{S}]+$/u.test(str);
131
+ },
132
+
133
+ /**
134
+ * Checks if a summary is generic/useless.
135
+ * @param {string} summary - The summary text to check
136
+ * @param {string[]} [genericList] - Optional custom list of generic summaries
137
+ * @returns {boolean} True if the summary is generic
138
+ */
139
+ isGenericSummary(summary, genericList) {
140
+ const GENERIC_SUMMARIES = genericList || [
141
+ 'table',
142
+ 'data',
143
+ 'data table',
144
+ 'information',
145
+ 'content',
146
+ 'main content',
147
+ 'layout',
148
+ 'layout table',
149
+ 'for layout',
150
+ 'table for layout purposes',
151
+ 'structural table',
152
+ 'this table is used for page layout',
153
+ 'header',
154
+ 'footer',
155
+ 'navigation',
156
+ 'nav',
157
+ 'body',
158
+ 'combobox',
159
+ 'links design table',
160
+ 'title and navigation',
161
+ 'main heading',
162
+ 'spacer',
163
+ 'spacer table',
164
+ 'menu',
165
+ 'n/a',
166
+ 'na',
167
+ 'none',
168
+ 'null',
169
+ 'empty',
170
+ 'blank',
171
+ 'undefined',
172
+ 'table summary',
173
+ 'summary',
174
+ ];
175
+ const normalized = summary.toLowerCase().trim();
176
+ return GENERIC_SUMMARIES.includes(normalized);
177
+ },
178
+ };
179
+
180
+ module.exports = tableUtils;
@@ -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,52 @@ 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
+ // Check for any fully transparent background (not just rgba(0,0,0,0))
29
+ const parsed = parseRGB(bgColor);
30
+ if (
31
+ bgColor === 'rgba(0, 0, 0, 0)' ||
32
+ bgColor === 'transparent' ||
33
+ (parsed && parsed[4] !== undefined && parseFloat(parsed[4]) === 0)
34
+ ) {
35
+ if (el.parentElement) {
36
+ return getComputedBackgroundColor(el.parentElement, options);
37
+ }
38
+ return opts.fallbackColor !== undefined ? fallbackColor : false;
39
+ }
40
+
41
+ // Handle semi-transparent backgrounds by blending with parent
42
+ if (parsed && parsed[4] !== undefined && parseFloat(parsed[4]) < 1) {
43
+ const alpha = parseFloat(parsed[4]);
44
+ const parentBg = el.parentElement
45
+ ? getComputedBackgroundColor(el.parentElement, options)
46
+ : fallbackColor;
47
+
48
+ if (parentBg && parentBg !== false) {
49
+ const parentParsed = parseRGB(parentBg);
50
+ if (parentParsed) {
51
+ // Alpha composite: result = fg * alpha + bg * (1 - alpha)
52
+ const r = Math.round(
53
+ parseInt(parsed[1], 10) * alpha +
54
+ parseInt(parentParsed[1], 10) * (1 - alpha)
55
+ );
56
+ const g = Math.round(
57
+ parseInt(parsed[2], 10) * alpha +
58
+ parseInt(parentParsed[2], 10) * (1 - alpha)
59
+ );
60
+ const b = Math.round(
61
+ parseInt(parsed[3], 10) * alpha +
62
+ parseInt(parentParsed[3], 10) * (1 - alpha)
63
+ );
64
+ return 'rgb(' + r + ', ' + g + ', ' + b + ')';
65
+ }
66
+ }
22
67
  }
23
68
 
24
69
  return bgColor;
25
70
  } else {
71
+ if (!skipBackgroundImages) {
72
+ return fallbackColor;
73
+ }
26
74
  return false;
27
75
  }
28
76
  }
@@ -40,9 +88,9 @@ function luminance(R8bit, G8bit, B8bit) {
40
88
  const GsRGB = G8bit / 255;
41
89
  const BsRGB = B8bit / 255;
42
90
 
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);
91
+ const R = RsRGB <= 0.03928 ? RsRGB / 12.92 : Math.pow((RsRGB + 0.055) / 1.055, 2.4);
92
+ const G = GsRGB <= 0.03928 ? GsRGB / 12.92 : Math.pow((GsRGB + 0.055) / 1.055, 2.4);
93
+ const B = BsRGB <= 0.03928 ? BsRGB / 12.92 : Math.pow((BsRGB + 0.055) / 1.055, 2.4);
46
94
 
47
95
  // For the sRGB colorspace, the relative luminance of a color is defined as:
48
96
  const L = 0.2126 * R + 0.7152 * G + 0.0722 * B;
@@ -50,6 +98,24 @@ function luminance(R8bit, G8bit, B8bit) {
50
98
  return L;
51
99
  }
52
100
 
101
+ /**
102
+ * Calculate relative luminance from an RGB object.
103
+ * Wrapper around luminance() that accepts an object instead of three separate arguments.
104
+ * @param {{ r: number, g: number, b: number }} rgb - Object with r, g, b properties (0-255)
105
+ * @returns {number|null} Relative luminance value (0-1) or null if input is invalid
106
+ */
107
+ function getRelativeLuminance(rgb) {
108
+ if (
109
+ !rgb ||
110
+ typeof rgb.r !== 'number' ||
111
+ typeof rgb.g !== 'number' ||
112
+ typeof rgb.b !== 'number'
113
+ ) {
114
+ return null;
115
+ }
116
+ return luminance(rgb.r, rgb.g, rgb.b);
117
+ }
118
+
53
119
  /**
54
120
  * Parse an RGB or RGBA color string into its components
55
121
  * @param {string} rgb - RGB(A) color string (e.g., "rgb(255, 0, 0)" or "rgba(255, 0, 0, 0.5)")
@@ -60,6 +126,57 @@ function parseRGB(rgb) {
60
126
  return rgbvals;
61
127
  }
62
128
 
129
+ /**
130
+ * Parse an RGB/RGBA color string into a structured object.
131
+ * @param {string} colorStr - e.g., 'rgb(255, 128, 0)' or 'rgba(255, 128, 0, 0.5)'
132
+ * @returns {{ r: number, g: number, b: number, a?: number } | null}
133
+ */
134
+ function parseColor(colorStr) {
135
+ if (!colorStr) {
136
+ return null;
137
+ }
138
+
139
+ const match = parseRGB(colorStr);
140
+ if (!match) {
141
+ return null;
142
+ }
143
+
144
+ const result = {
145
+ r: parseInt(match[1], 10),
146
+ g: parseInt(match[2], 10),
147
+ b: parseInt(match[3], 10),
148
+ };
149
+
150
+ if (match[4] !== undefined) {
151
+ result.a = parseFloat(match[4]);
152
+ }
153
+
154
+ return result;
155
+ }
156
+
157
+ /**
158
+ * Calculate contrast ratio between two color strings.
159
+ * @param {string} color1 - e.g., 'rgb(0, 0, 0)'
160
+ * @param {string} color2 - e.g., 'rgb(255, 255, 255)'
161
+ * @returns {number|null} Contrast ratio (1 to 21) or null if colors can't be parsed
162
+ */
163
+ function getContrastRatio(color1, color2) {
164
+ const c1 = parseColor(color1);
165
+ const c2 = parseColor(color2);
166
+
167
+ if (!c1 || !c2) {
168
+ return null;
169
+ }
170
+
171
+ const lum1 = luminance(c1.r, c1.g, c1.b);
172
+ const lum2 = luminance(c2.r, c2.g, c2.b);
173
+
174
+ const lighter = Math.max(lum1, lum2);
175
+ const darker = Math.min(lum1, lum2);
176
+
177
+ return Math.round(((lighter + 0.05) / (darker + 0.05)) * 100) / 100;
178
+ }
179
+
63
180
  /**
64
181
  * Calculate color contrast ratio between foreground and background
65
182
  * @param {Element} el - DOM element to check
@@ -105,7 +222,7 @@ function getColorContrast(el) {
105
222
  lighter = fgLuminance;
106
223
  }
107
224
 
108
- return Math.round((lighter + 0.05) / (darker + 0.05) * 100) / 100;
225
+ return Math.round(((lighter + 0.05) / (darker + 0.05)) * 100) / 100;
109
226
  }
110
227
 
111
228
  /**
@@ -132,10 +249,11 @@ function testContrast(el, options = { level: 'AA' }) {
132
249
 
133
250
  // Check if element is offscreen
134
251
  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;
252
+ const selfOffscreen =
253
+ rect.left + rect.width <= 0 ||
254
+ rect.top + rect.height <= 0 ||
255
+ rect.left >= window.innerWidth ||
256
+ rect.top >= window.innerHeight;
139
257
 
140
258
  // Get direct text (excluding child elements)
141
259
  let selfDirectText = '';
@@ -157,14 +275,6 @@ function testContrast(el, options = { level: 'AA' }) {
157
275
  return true;
158
276
  }
159
277
 
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
278
  // Determine the contrast required based on the level passed to the function
169
279
  if (level === 'AA') {
170
280
  if (selfSize < 18) {
@@ -209,8 +319,10 @@ function testContrast(el, options = { level: 'AA' }) {
209
319
  // Only test contrast if there's a difference between the element and its parent
210
320
  // in terms of colors, font size, or font weight
211
321
  if (
212
- (selfFG !== parentFG || selfBG !== parentBG) ||
213
- (selfSize !== parentSize || selfWeight !== parentWeight)
322
+ selfFG !== parentFG ||
323
+ selfBG !== parentBG ||
324
+ selfSize !== parentSize ||
325
+ selfWeight !== parentWeight
214
326
  ) {
215
327
  const contrast = getColorContrast(el);
216
328
 
@@ -237,5 +349,8 @@ module.exports = {
237
349
  getComputedBackgroundColor,
238
350
  luminance,
239
351
  parseRGB,
240
- getColorContrast
352
+ parseColor,
353
+ getRelativeLuminance,
354
+ getContrastRatio,
355
+ getColorContrast,
241
356
  };