@afixt/test-utils 1.2.3 → 2.0.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.
- package/.claude/settings.local.json +1 -6
- package/BROWSER_TESTING.md +42 -22
- package/CHANGELOG.md +40 -0
- package/CLAUDE.md +10 -9
- package/package.json +1 -1
- package/src/constants.js +438 -1
- package/src/domUtils.js +17 -38
- package/src/formUtils.js +7 -24
- package/src/getAccessibleName.js +20 -56
- package/src/getCSSGeneratedContent.js +2 -0
- package/src/getFocusableElements.js +12 -21
- package/src/getGeneratedContent.js +18 -11
- package/src/getImageText.js +22 -7
- package/src/hasValidAriaRole.js +11 -19
- package/src/index.js +4 -4
- package/src/interactiveRoles.js +2 -19
- package/src/isA11yVisible.js +95 -0
- package/src/isAriaAttributesValid.js +5 -64
- package/src/isFocusable.js +30 -10
- package/src/isHidden.js +44 -8
- package/src/listEventListeners.js +115 -10
- package/src/stringUtils.js +19 -98
- package/src/tableUtils.js +4 -36
- package/src/testContrast.js +54 -0
- package/test/domUtils.test.js +156 -0
- package/test/formUtils.test.js +0 -47
- package/test/getAccessibleName.test.js +39 -0
- package/test/getGeneratedContent.test.js +305 -241
- package/test/getImageText.test.js +158 -99
- package/test/index.test.js +54 -17
- package/test/{isVisible.test.js → isA11yVisible.test.js} +39 -33
- package/test/isFocusable.test.js +265 -272
- package/test/isHidden.test.js +257 -153
- package/test/listEventListeners.test.js +163 -44
- package/test/playwright/css-pseudo-elements.spec.js +3 -13
- package/test/stringUtils.test.js +55 -228
- package/test/testContrast.test.js +104 -2
- package/todo.md +2 -2
- package/src/isVisible.js +0 -103
package/src/isFocusable.js
CHANGED
|
@@ -1,21 +1,29 @@
|
|
|
1
|
+
const isHidden = require('./isHidden.js');
|
|
2
|
+
const hasHiddenParent = require('./hasHiddenParent.js');
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
|
-
* Determines if an element is focusable.
|
|
5
|
+
* Determines if an element is focusable via user interaction (tab order).
|
|
6
|
+
* Elements with tabindex="-1" are programmatically focusable via .focus()
|
|
7
|
+
* but are NOT considered focusable by this function, as they are not
|
|
8
|
+
* reachable through keyboard navigation.
|
|
3
9
|
* @param {Element} element - The HTML element to check.
|
|
4
10
|
* @returns {boolean} - Returns true if the element is focusable, otherwise false.
|
|
5
11
|
*/
|
|
6
12
|
function isFocusable(element) {
|
|
7
|
-
if (!element)
|
|
13
|
+
if (!element) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
8
16
|
|
|
9
17
|
const nodeName = element.nodeName.toLowerCase();
|
|
10
|
-
const tabIndex = element.getAttribute(
|
|
18
|
+
const tabIndex = element.getAttribute('tabindex');
|
|
11
19
|
|
|
12
20
|
// The element and all of its ancestors must be visible
|
|
13
|
-
if (element
|
|
21
|
+
if (isHidden(element) || hasHiddenParent(element)) {
|
|
14
22
|
return false;
|
|
15
23
|
}
|
|
16
24
|
|
|
17
|
-
// If tabindex is defined, its value must be
|
|
18
|
-
if (!isNaN(tabIndex) && tabIndex < 0) {
|
|
25
|
+
// If tabindex is defined, its value must be >= 0
|
|
26
|
+
if (tabIndex !== null && !isNaN(tabIndex) && parseInt(tabIndex, 10) < 0) {
|
|
19
27
|
return false;
|
|
20
28
|
}
|
|
21
29
|
|
|
@@ -25,14 +33,26 @@ function isFocusable(element) {
|
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
// If the element is a link, href must be defined
|
|
28
|
-
if (nodeName ===
|
|
29
|
-
return element.hasAttribute(
|
|
36
|
+
if (nodeName === 'a' || nodeName === 'area') {
|
|
37
|
+
return element.hasAttribute('href');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// contenteditable elements are focusable
|
|
41
|
+
if (
|
|
42
|
+
element.getAttribute('contenteditable') === 'true' ||
|
|
43
|
+
element.getAttribute('contenteditable') === ''
|
|
44
|
+
) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Any element with a non-negative tabindex is focusable
|
|
49
|
+
if (tabIndex !== null && !isNaN(tabIndex) && parseInt(tabIndex, 10) >= 0) {
|
|
50
|
+
return true;
|
|
30
51
|
}
|
|
31
52
|
|
|
32
|
-
// This is some other page element that is not normally focusable
|
|
33
53
|
return false;
|
|
34
54
|
}
|
|
35
55
|
|
|
36
56
|
module.exports = {
|
|
37
|
-
isFocusable
|
|
57
|
+
isFocusable,
|
|
38
58
|
};
|
package/src/isHidden.js
CHANGED
|
@@ -1,17 +1,53 @@
|
|
|
1
|
-
|
|
2
1
|
/**
|
|
3
2
|
* Checks if a given DOM element is hidden.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* By default checks: computed display:none, visibility:hidden, and the hidden attribute.
|
|
5
|
+
* Additional checks can be enabled via the options parameter.
|
|
7
6
|
*
|
|
8
7
|
* @param {HTMLElement} element - The DOM element to check.
|
|
9
|
-
* @
|
|
8
|
+
* @param {Object} [options={}] - Optional configuration.
|
|
9
|
+
* @param {boolean} [options.checkAriaHidden=false] - Also check aria-hidden="true".
|
|
10
|
+
* @param {boolean} [options.checkOpacity=false] - Also treat opacity:0 as hidden.
|
|
11
|
+
* @param {boolean} [options.checkDimensions=false] - Also treat zero width+height as hidden.
|
|
12
|
+
* @returns {boolean} True if the element is hidden.
|
|
10
13
|
*/
|
|
11
|
-
const isHidden = (element) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
const isHidden = (element, options = {}) => {
|
|
15
|
+
if (!element || !(element instanceof Element)) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { checkAriaHidden = false, checkOpacity = false, checkDimensions = false } = options;
|
|
20
|
+
|
|
21
|
+
// Check the hidden attribute
|
|
22
|
+
if (element.hasAttribute('hidden')) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Use computed style to catch CSS class/stylesheet rules
|
|
27
|
+
const style = window.getComputedStyle(element);
|
|
28
|
+
|
|
29
|
+
if (style.display === 'none') {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (style.visibility === 'hidden') {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Optional checks
|
|
38
|
+
if (checkAriaHidden && element.getAttribute('aria-hidden') === 'true') {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (checkOpacity && style.opacity === '0') {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (checkDimensions && element.offsetWidth === 0 && element.offsetHeight === 0) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return false;
|
|
15
51
|
};
|
|
16
52
|
|
|
17
53
|
module.exports = isHidden;
|
|
@@ -5,6 +5,33 @@ const eventListenersMap = new WeakMap();
|
|
|
5
5
|
const originalAddEventListener = EventTarget.prototype.addEventListener;
|
|
6
6
|
const originalRemoveEventListener = EventTarget.prototype.removeEventListener;
|
|
7
7
|
|
|
8
|
+
const KNOWN_EVENT_PROPERTIES = [
|
|
9
|
+
'onclick',
|
|
10
|
+
'ondblclick',
|
|
11
|
+
'oncontextmenu',
|
|
12
|
+
'onmousedown',
|
|
13
|
+
'onmouseup',
|
|
14
|
+
'onmouseover',
|
|
15
|
+
'onmouseout',
|
|
16
|
+
'onmouseenter',
|
|
17
|
+
'onmouseleave',
|
|
18
|
+
'onkeydown',
|
|
19
|
+
'onkeyup',
|
|
20
|
+
'onkeypress',
|
|
21
|
+
'onfocus',
|
|
22
|
+
'onblur',
|
|
23
|
+
'onchange',
|
|
24
|
+
'oninput',
|
|
25
|
+
'onsubmit',
|
|
26
|
+
'ontouchstart',
|
|
27
|
+
'ontouchend',
|
|
28
|
+
'ontouchmove',
|
|
29
|
+
'onscroll',
|
|
30
|
+
'onresize',
|
|
31
|
+
'onload',
|
|
32
|
+
'onerror',
|
|
33
|
+
];
|
|
34
|
+
|
|
8
35
|
// Override addEventListener to track event listeners
|
|
9
36
|
EventTarget.prototype.addEventListener = function (type, listener, options) {
|
|
10
37
|
if (!eventListenersMap.has(this)) {
|
|
@@ -25,14 +52,14 @@ EventTarget.prototype.removeEventListener = function (type, listener, options) {
|
|
|
25
52
|
if (eventListenersMap.has(this)) {
|
|
26
53
|
const listeners = eventListenersMap.get(this);
|
|
27
54
|
if (listeners[type]) {
|
|
28
|
-
listeners[type] = listeners[type].filter(
|
|
55
|
+
listeners[type] = listeners[type].filter(l => l.listener !== listener);
|
|
29
56
|
}
|
|
30
57
|
}
|
|
31
58
|
return originalRemoveEventListener.call(this, type, listener, options);
|
|
32
59
|
};
|
|
33
60
|
|
|
34
61
|
// Function to get XPath of an element
|
|
35
|
-
const getXPath =
|
|
62
|
+
const getXPath = element => {
|
|
36
63
|
if (element.id) {
|
|
37
64
|
return `//*[@id="${element.id}"]`;
|
|
38
65
|
}
|
|
@@ -52,9 +79,84 @@ const getXPath = (element) => {
|
|
|
52
79
|
return path;
|
|
53
80
|
};
|
|
54
81
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Collect inline attribute event handlers from an element
|
|
84
|
+
* @param {Element} element - The element to inspect
|
|
85
|
+
* @returns {Object} Map of event names to listener entry arrays
|
|
86
|
+
*/
|
|
87
|
+
const getAttributeHandlers = element => {
|
|
88
|
+
const handlers = {};
|
|
89
|
+
if (!element || !element.attributes) {
|
|
90
|
+
return handlers;
|
|
91
|
+
}
|
|
92
|
+
for (let i = 0; i < element.attributes.length; i++) {
|
|
93
|
+
const attr = element.attributes[i];
|
|
94
|
+
if (attr.name.startsWith('on')) {
|
|
95
|
+
const eventName = attr.name.slice(2);
|
|
96
|
+
handlers[eventName] = [{ listener: attr.value, bindingType: 'attribute' }];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return handlers;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Collect property-based event handlers from an element
|
|
104
|
+
* @param {Element} element - The element to inspect
|
|
105
|
+
* @param {Object} attributeHandlers - Already-detected attribute handlers to avoid duplicates
|
|
106
|
+
* @returns {Object} Map of event names to listener entry arrays
|
|
107
|
+
*/
|
|
108
|
+
const getPropertyHandlers = (element, attributeHandlers) => {
|
|
109
|
+
const handlers = {};
|
|
110
|
+
if (!element) {
|
|
111
|
+
return handlers;
|
|
112
|
+
}
|
|
113
|
+
for (const prop of KNOWN_EVENT_PROPERTIES) {
|
|
114
|
+
if (typeof element[prop] === 'function') {
|
|
115
|
+
const eventName = prop.slice(2);
|
|
116
|
+
if (!attributeHandlers[eventName]) {
|
|
117
|
+
handlers[eventName] = [{ listener: element[prop], bindingType: 'property' }];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return handlers;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get all event listeners on an element, including addEventListener, attribute, and property handlers
|
|
126
|
+
* @param {Element} element - The element to inspect
|
|
127
|
+
* @returns {Object} Map of event names to listener entry arrays
|
|
128
|
+
*/
|
|
129
|
+
const getEventListeners = element => {
|
|
130
|
+
const tracked = eventListenersMap.get(element) || {};
|
|
131
|
+
|
|
132
|
+
// Add bindingType to tracked entries
|
|
133
|
+
const result = {};
|
|
134
|
+
for (const eventName of Object.keys(tracked)) {
|
|
135
|
+
result[eventName] = tracked[eventName].map(entry => ({
|
|
136
|
+
...entry,
|
|
137
|
+
bindingType: 'addEventListener',
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Merge attribute handlers
|
|
142
|
+
const attrHandlers = getAttributeHandlers(element);
|
|
143
|
+
for (const eventName of Object.keys(attrHandlers)) {
|
|
144
|
+
if (!result[eventName]) {
|
|
145
|
+
result[eventName] = [];
|
|
146
|
+
}
|
|
147
|
+
result[eventName].push(...attrHandlers[eventName]);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Merge property handlers (excluding those already found as attributes)
|
|
151
|
+
const propHandlers = getPropertyHandlers(element, attrHandlers);
|
|
152
|
+
for (const eventName of Object.keys(propHandlers)) {
|
|
153
|
+
if (!result[eventName]) {
|
|
154
|
+
result[eventName] = [];
|
|
155
|
+
}
|
|
156
|
+
result[eventName].push(...propHandlers[eventName]);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return result;
|
|
58
160
|
};
|
|
59
161
|
|
|
60
162
|
/**
|
|
@@ -72,11 +174,14 @@ const listEventListeners = (rootElement = document) => {
|
|
|
72
174
|
function processElement(el) {
|
|
73
175
|
const listeners = getEventListeners(el);
|
|
74
176
|
if (Object.keys(listeners).length > 0) {
|
|
75
|
-
Object.keys(listeners).forEach(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
177
|
+
Object.keys(listeners).forEach(eventName => {
|
|
178
|
+
listeners[eventName].forEach(entry => {
|
|
179
|
+
eventListeners.push({
|
|
180
|
+
element: el.tagName.toLowerCase(),
|
|
181
|
+
xpath: getXPath(el),
|
|
182
|
+
event: eventName,
|
|
183
|
+
bindingType: entry.bindingType,
|
|
184
|
+
});
|
|
80
185
|
});
|
|
81
186
|
});
|
|
82
187
|
}
|
package/src/stringUtils.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { GENERIC_TITLES, GENERIC_LINK_TEXT } = require('./constants.js');
|
|
2
|
+
|
|
1
3
|
const stringUtils = (function () {
|
|
2
4
|
/**
|
|
3
5
|
* Check if a string is empty or only contains whitespace.
|
|
@@ -83,45 +85,6 @@ const stringUtils = (function () {
|
|
|
83
85
|
return str.pathname;
|
|
84
86
|
}
|
|
85
87
|
|
|
86
|
-
/**
|
|
87
|
-
* Extracts and concatenates all text content from a given DOM element, including text from text nodes,
|
|
88
|
-
* elements with aria-label attributes, and alt attributes of img elements.
|
|
89
|
-
*
|
|
90
|
-
* @param {Element} el - The DOM element from which to extract text.
|
|
91
|
-
* @returns {string} A string containing all concatenated text content from the element.
|
|
92
|
-
*/
|
|
93
|
-
function getAllText(el) {
|
|
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);
|
|
100
|
-
const textNodes = [];
|
|
101
|
-
let node;
|
|
102
|
-
let text;
|
|
103
|
-
|
|
104
|
-
while (walker.nextNode()) {
|
|
105
|
-
node = walker.currentNode;
|
|
106
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
|
107
|
-
text = node.nodeValue.trim();
|
|
108
|
-
if (text) {
|
|
109
|
-
textNodes.push(text);
|
|
110
|
-
} else {
|
|
111
|
-
textNodes.push(node.textContent.trim());
|
|
112
|
-
}
|
|
113
|
-
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
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'));
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return textNodes.join(' ');
|
|
123
|
-
}
|
|
124
|
-
|
|
125
88
|
/**
|
|
126
89
|
* Checks if the given element contains any text.
|
|
127
90
|
*
|
|
@@ -129,7 +92,16 @@ const stringUtils = (function () {
|
|
|
129
92
|
* @returns {boolean} True if the element contains text, false otherwise.
|
|
130
93
|
*/
|
|
131
94
|
function hasText(element) {
|
|
132
|
-
|
|
95
|
+
if (!element) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
// Check form element value (input, textarea, select)
|
|
99
|
+
if (element.value !== undefined && element.value !== '') {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
// Lazy require to avoid circular dependency (getAccessibleText requires stringUtils)
|
|
103
|
+
const { getAccessibleText } = require('./getAccessibleText.js');
|
|
104
|
+
return getAccessibleText(element).trim() !== '';
|
|
133
105
|
}
|
|
134
106
|
|
|
135
107
|
/**
|
|
@@ -156,14 +128,17 @@ const stringUtils = (function () {
|
|
|
156
128
|
if (!title) {
|
|
157
129
|
return false;
|
|
158
130
|
}
|
|
159
|
-
const genericTitles = ['iframe', 'frame', 'untitled', 'title', 'content', 'main', 'page'];
|
|
160
131
|
const normalized = title.toLowerCase().trim();
|
|
161
132
|
|
|
162
|
-
if (
|
|
133
|
+
if (GENERIC_TITLES.includes(normalized)) {
|
|
163
134
|
return true;
|
|
164
135
|
}
|
|
165
136
|
|
|
166
|
-
if (
|
|
137
|
+
if (
|
|
138
|
+
/^(frame|iframe|untitled|title|page|content|section|document|tab|slide|sheet|panel|window|screen|view|module|widget|region|form)\s?\d+$/i.test(
|
|
139
|
+
normalized
|
|
140
|
+
)
|
|
141
|
+
) {
|
|
167
142
|
return true;
|
|
168
143
|
}
|
|
169
144
|
|
|
@@ -180,32 +155,7 @@ const stringUtils = (function () {
|
|
|
180
155
|
if (!text) {
|
|
181
156
|
return false;
|
|
182
157
|
}
|
|
183
|
-
const
|
|
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;
|
|
158
|
+
const list = genericList || GENERIC_LINK_TEXT;
|
|
209
159
|
const lowerText = text.toLowerCase().trim();
|
|
210
160
|
return list.some(function (generic) {
|
|
211
161
|
return lowerText === generic;
|
|
@@ -248,33 +198,6 @@ const stringUtils = (function () {
|
|
|
248
198
|
});
|
|
249
199
|
}
|
|
250
200
|
|
|
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;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
201
|
return {
|
|
279
202
|
isEmpty,
|
|
280
203
|
isString,
|
|
@@ -283,9 +206,7 @@ const stringUtils = (function () {
|
|
|
283
206
|
isUpperCase,
|
|
284
207
|
isAlphaNumeric,
|
|
285
208
|
getPathFromUrl,
|
|
286
|
-
getAllText,
|
|
287
209
|
hasText,
|
|
288
|
-
textIncludingImgAlt,
|
|
289
210
|
isEmptyOrWhitespace,
|
|
290
211
|
isGenericTitle,
|
|
291
212
|
isGenericLinkText,
|
package/src/tableUtils.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* @module tableUtils
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const { GENERIC_SUMMARIES } = require('./constants.js');
|
|
7
|
+
|
|
6
8
|
const tableUtils = {
|
|
7
9
|
/**
|
|
8
10
|
* Check if table has multiple header rows (rows containing th elements).
|
|
@@ -137,43 +139,9 @@ const tableUtils = {
|
|
|
137
139
|
* @returns {boolean} True if the summary is generic
|
|
138
140
|
*/
|
|
139
141
|
isGenericSummary(summary, genericList) {
|
|
140
|
-
const
|
|
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
|
-
];
|
|
142
|
+
const list = genericList || GENERIC_SUMMARIES;
|
|
175
143
|
const normalized = summary.toLowerCase().trim();
|
|
176
|
-
return
|
|
144
|
+
return list.includes(normalized);
|
|
177
145
|
},
|
|
178
146
|
};
|
|
179
147
|
|
package/src/testContrast.js
CHANGED
|
@@ -1,3 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if an element or any of its ancestors has a background image set.
|
|
3
|
+
* Walks up the DOM tree until it finds a background image or an opaque background color.
|
|
4
|
+
* @param {Element} el - The element to check
|
|
5
|
+
* @returns {boolean} True if the element or a visible ancestor has a background image
|
|
6
|
+
*/
|
|
7
|
+
function hasBackgroundImage(el) {
|
|
8
|
+
if (!el || el.nodeType === 9 || !window.getComputedStyle) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let current = el;
|
|
13
|
+
while (current && current.nodeType !== 9) {
|
|
14
|
+
const styles = window.getComputedStyle(current);
|
|
15
|
+
if (!styles) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const bgImage = styles.getPropertyValue('background-image');
|
|
20
|
+
if (bgImage && bgImage !== 'none') {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// If this element has an opaque background color, stop traversal
|
|
25
|
+
// since no ancestor background image would be visible through it
|
|
26
|
+
const bgColor = styles.getPropertyValue('background-color');
|
|
27
|
+
const parsed = parseRGB(bgColor);
|
|
28
|
+
if (parsed) {
|
|
29
|
+
const isTransparent =
|
|
30
|
+
bgColor === 'rgba(0, 0, 0, 0)' ||
|
|
31
|
+
bgColor === 'transparent' ||
|
|
32
|
+
(parsed[4] !== undefined && parseFloat(parsed[4]) === 0);
|
|
33
|
+
const isSemiTransparent =
|
|
34
|
+
parsed[4] !== undefined && parseFloat(parsed[4]) > 0 && parseFloat(parsed[4]) < 1;
|
|
35
|
+
|
|
36
|
+
if (!isTransparent && !isSemiTransparent) {
|
|
37
|
+
// Opaque background color, no ancestor background image visible
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
current = current.parentElement;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
1
48
|
/**
|
|
2
49
|
* Get the computed background color for an element.
|
|
3
50
|
* @param {Element} el - the element to be tested
|
|
@@ -240,6 +287,12 @@ function testContrast(el, options = { level: 'AA' }) {
|
|
|
240
287
|
return false;
|
|
241
288
|
}
|
|
242
289
|
|
|
290
|
+
// Skip elements with a background image on the element itself or a visible ancestor.
|
|
291
|
+
// Contrast cannot be reliably tested against background images.
|
|
292
|
+
if (hasBackgroundImage(el)) {
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
|
|
243
296
|
const styles = window.getComputedStyle(el);
|
|
244
297
|
const selfFG = styles.getPropertyValue('color');
|
|
245
298
|
const selfBG = getComputedBackgroundColor(el);
|
|
@@ -346,6 +399,7 @@ function testContrast(el, options = { level: 'AA' }) {
|
|
|
346
399
|
|
|
347
400
|
module.exports = {
|
|
348
401
|
testContrast,
|
|
402
|
+
hasBackgroundImage,
|
|
349
403
|
getComputedBackgroundColor,
|
|
350
404
|
luminance,
|
|
351
405
|
parseRGB,
|