@afixt/test-utils 1.2.0 → 1.2.2

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.
@@ -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;
@@ -25,13 +25,47 @@ function getComputedBackgroundColor(el, options) {
25
25
  if (bgImage === 'none') {
26
26
  const bgColor = styles.getPropertyValue('background-color');
27
27
 
28
- if (bgColor === 'rgba(0, 0, 0, 0)') {
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
+ ) {
29
35
  if (el.parentElement) {
30
36
  return getComputedBackgroundColor(el.parentElement, options);
31
37
  }
32
38
  return opts.fallbackColor !== undefined ? fallbackColor : false;
33
39
  }
34
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
+ }
67
+ }
68
+
35
69
  return bgColor;
36
70
  } else {
37
71
  if (!skipBackgroundImages) {