@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.
- package/eslint.config.mjs +1 -1
- package/package.json +2 -1
- package/src/constants.js +231 -0
- package/src/cssUtils.js +77 -0
- package/src/domUtils.js +268 -12
- package/src/formUtils.js +175 -0
- package/src/getAccessibleText.js +60 -39
- package/src/getCSSGeneratedContent.js +39 -17
- package/src/index.js +18 -2
- package/src/stringUtils.js +149 -0
- package/src/tableUtils.js +180 -0
- package/src/testContrast.js +35 -1
- package/src/testLang.js +514 -444
- package/test/cssUtils.test.js +248 -0
- package/test/domUtils.test.js +815 -297
- package/test/formUtils.test.js +389 -0
- package/test/getAccessibleText.test.js +93 -0
- package/test/getCSSGeneratedContent.test.js +187 -232
- package/test/hasCSSGeneratedContent.test.js +37 -147
- package/test/playwright/css-pseudo-elements.spec.js +224 -91
- package/test/playwright/fixtures/css-pseudo-elements.html +6 -0
- package/test/stringUtils.test.js +222 -0
- package/test/tableUtils.test.js +340 -0
- package/vitest.config.js +28 -28
- package/test/getCSSGeneratedContent.browser.test.js +0 -125
|
@@ -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;
|
package/src/testContrast.js
CHANGED
|
@@ -25,13 +25,47 @@ function getComputedBackgroundColor(el, options) {
|
|
|
25
25
|
if (bgImage === 'none') {
|
|
26
26
|
const bgColor = styles.getPropertyValue('background-color');
|
|
27
27
|
|
|
28
|
-
|
|
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) {
|