@afixt/test-utils 1.3.0 → 2.0.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.
- 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 +13 -71
- 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 +54 -98
- package/src/tableUtils.js +4 -36
- package/test/domUtils.test.js +156 -0
- package/test/formUtils.test.js +0 -47
- 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 +115 -228
- 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;
|
|
@@ -224,6 +174,40 @@ const stringUtils = (function () {
|
|
|
224
174
|
return (element.textContent || '').trim();
|
|
225
175
|
}
|
|
226
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Checks if an accessible name contains the visible text as a whole-word phrase.
|
|
179
|
+
* Prevents false positives where aria-label is a superset of visible text
|
|
180
|
+
* (e.g. "Report a Concern Opens in new window" contains "Report a Concern").
|
|
181
|
+
* Uses word-boundary matching to avoid partial matches
|
|
182
|
+
* (e.g. "Homepage" does NOT contain "Home" as a whole word).
|
|
183
|
+
* @param {string} accessibleName - The accessible name (e.g. aria-label value)
|
|
184
|
+
* @param {string} visibleText - The visible text content of the element
|
|
185
|
+
* @returns {boolean} True if the accessible name contains the visible text at word boundaries
|
|
186
|
+
*/
|
|
187
|
+
function containsVisibleText(accessibleName, visibleText) {
|
|
188
|
+
if (!accessibleName || !visibleText) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
const haystack = accessibleName.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
192
|
+
const needle = visibleText.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
193
|
+
if (!needle) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
const index = haystack.indexOf(needle);
|
|
197
|
+
if (index === -1) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
// Check word boundaries
|
|
201
|
+
if (index > 0 && /\w/.test(haystack.charAt(index - 1))) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
const endIndex = index + needle.length;
|
|
205
|
+
if (endIndex < haystack.length && /\w/.test(haystack.charAt(endIndex))) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
227
211
|
/**
|
|
228
212
|
* Checks if text contains a warning about new window/tab behavior.
|
|
229
213
|
* @param {string} text - The text to check
|
|
@@ -248,33 +232,6 @@ const stringUtils = (function () {
|
|
|
248
232
|
});
|
|
249
233
|
}
|
|
250
234
|
|
|
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
235
|
return {
|
|
279
236
|
isEmpty,
|
|
280
237
|
isString,
|
|
@@ -283,13 +240,12 @@ const stringUtils = (function () {
|
|
|
283
240
|
isUpperCase,
|
|
284
241
|
isAlphaNumeric,
|
|
285
242
|
getPathFromUrl,
|
|
286
|
-
getAllText,
|
|
287
243
|
hasText,
|
|
288
|
-
textIncludingImgAlt,
|
|
289
244
|
isEmptyOrWhitespace,
|
|
290
245
|
isGenericTitle,
|
|
291
246
|
isGenericLinkText,
|
|
292
247
|
getActualVisibleText,
|
|
248
|
+
containsVisibleText,
|
|
293
249
|
hasNewWindowWarning,
|
|
294
250
|
};
|
|
295
251
|
})();
|
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
|
|