@afixt/test-utils 2.1.1 → 2.3.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/BROWSER_TESTING.md +12 -12
- package/CHANGELOG.md +21 -0
- package/CLAUDE.md +1 -1
- package/package.json +1 -1
- package/src/constants.js +1 -0
- package/src/domUtils.js +45 -11
- package/src/formUtils.js +5 -2
- package/src/getAccessibleName.js +37 -51
- package/src/getAccessibleText.js +3 -3
- package/src/getComputedRole.js +187 -122
- package/src/getImageText.js +8 -1
- package/src/index.js +4 -0
- package/src/isA11yVisible.js +13 -7
- package/src/isHidden.js +11 -4
- package/src/shadowDomUtils.js +118 -0
- package/src/stringUtils.js +5 -2
- package/src/testContrast.js +42 -1
- package/test/domUtils.test.js +52 -0
- package/test/formUtils.test.js +39 -0
- package/test/getAccessibleName.test.js +83 -0
- package/test/getComputedRole.test.js +248 -176
- package/test/isA11yVisible.test.js +33 -0
- package/test/isHidden.test.js +18 -0
- package/test/playwright/colon-id-a11y-visible.spec.js +160 -0
- package/test/playwright/fixtures/colon-id-a11y-visible.html +48 -0
- package/test/shadowDomUtils.test.js +248 -0
- package/test/testContrast.test.js +42 -5
- package/todo.md +3 -2
package/BROWSER_TESTING.md
CHANGED
|
@@ -6,13 +6,13 @@ This project uses **JSDOM** for the main test suite with **Playwright** for CSS
|
|
|
6
6
|
|
|
7
7
|
## Current Status
|
|
8
8
|
|
|
9
|
-
- **
|
|
9
|
+
- **1101 tests passing** in JSDOM environment (8 conditionally skipped)
|
|
10
10
|
- **20 tests passing** in Playwright for CSS pseudo-element support
|
|
11
|
-
- **Total:
|
|
11
|
+
- **Total: 1121 tests passing** across both environments
|
|
12
12
|
|
|
13
13
|
### Implementation Complete
|
|
14
14
|
|
|
15
|
-
✅ JSDOM tests for all standard functionality (
|
|
15
|
+
✅ JSDOM tests for all standard functionality (1101 tests)
|
|
16
16
|
✅ Standalone Playwright tests for CSS pseudo-elements (20 tests)
|
|
17
17
|
✅ 8 CSS pseudo-element tests conditionally skipped in JSDOM, covered by Playwright
|
|
18
18
|
|
|
@@ -30,12 +30,12 @@ The 20 CSS pseudo-element tests run with standalone Playwright tests that inject
|
|
|
30
30
|
### JSDOM Tests (Main Test Suite)
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
npm test # Run all
|
|
33
|
+
npm test # Run all 1101 JSDOM tests
|
|
34
34
|
npm run test:coverage # Run with coverage
|
|
35
35
|
npm run test:watch # Watch mode
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
**Status:**
|
|
38
|
+
**Status:** 1101 tests passing (8 conditionally skipped)
|
|
39
39
|
|
|
40
40
|
**Pros:**
|
|
41
41
|
|
|
@@ -113,17 +113,17 @@ The Playwright tests inject browser-compatible versions of `getGeneratedContent`
|
|
|
113
113
|
|
|
114
114
|
```json
|
|
115
115
|
{
|
|
116
|
-
"test": "vitest run", // JSDOM tests (
|
|
116
|
+
"test": "vitest run", // JSDOM tests (1101 tests)
|
|
117
117
|
"test:coverage": "vitest run --coverage", // JSDOM with coverage
|
|
118
118
|
"test:playwright:css": "playwright test ...", // Playwright CSS tests (20 tests)
|
|
119
|
-
"test:all": "npm run test && npm run test:playwright:css" // All tests (
|
|
119
|
+
"test:all": "npm run test && npm run test:playwright:css" // All tests (1121 tests)
|
|
120
120
|
}
|
|
121
121
|
```
|
|
122
122
|
|
|
123
123
|
## Test Coverage
|
|
124
124
|
|
|
125
|
-
- **
|
|
126
|
-
- **
|
|
127
|
-
- **95.
|
|
128
|
-
- **
|
|
129
|
-
- **
|
|
125
|
+
- **88.98% statement coverage**
|
|
126
|
+
- **84.37% branch coverage**
|
|
127
|
+
- **95.21% function coverage**
|
|
128
|
+
- **89.04% line coverage**
|
|
129
|
+
- **1121 total tests passing** (1101 JSDOM + 20 Playwright)
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [2.2.0] - 2026-03-03
|
|
6
|
+
|
|
7
|
+
### Fixes
|
|
8
|
+
|
|
9
|
+
- **domUtils**: Add `inert` attribute and `content-visibility: hidden` detection to `isHiddenFromAT` (#62)
|
|
10
|
+
- **testContrast**: Return `null` (cannot-determine) instead of `true` for elements with background images (#63)
|
|
11
|
+
- **testContrast**: Return `null` for elements with CSS `filter` or `mix-blend-mode` that alter perceived color (#64)
|
|
12
|
+
- **getAccessibleName**: Replace internal `isNotVisible` with `isA11yVisible` for proper visibility checking, including aria-labelledby reference exceptions (#57)
|
|
13
|
+
- **getAccessibleName**: Handle zero-width spaces and non-breaking spaces in aria-label with `isEmptyOrWhitespace` (#70)
|
|
14
|
+
- **getComputedRole**: Add context-dependent implicit roles per HTML-AAM (td/gridcell in grid, header/footer as generic inside sectioning elements) (#68)
|
|
15
|
+
- **isHidden**: Distinguish `hidden="until-found"` from plain `hidden` attribute (#69)
|
|
16
|
+
- **formUtils**: Skip `aria-hidden="true"` labels when resolving `aria-labelledby` in `hasExplicitAccessibleName` (#67)
|
|
17
|
+
- **constants**: Add `template` to `NON_VISIBLE_SELECTORS` (#71)
|
|
18
|
+
- **domUtils, getAccessibleName**: Escape all ID interpolations in CSS selectors to handle special characters (#72)
|
|
19
|
+
|
|
20
|
+
## [2.1.1] - 2026-03-01
|
|
21
|
+
|
|
22
|
+
### Fixes
|
|
23
|
+
|
|
24
|
+
- **isA11yVisible**: Escape element IDs in CSS selectors to handle special characters like colons
|
|
25
|
+
|
|
5
26
|
## [2.1.0] - 2026-02-25
|
|
6
27
|
|
|
7
28
|
### Features
|
package/CLAUDE.md
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
|
|
45
45
|
### Test Status
|
|
46
46
|
|
|
47
|
-
- **
|
|
47
|
+
- **1129 tests passing** in JSDOM environment
|
|
48
48
|
- **8 tests skipped** - Conditionally skipped in JSDOM; require real browser CSS pseudo-element support
|
|
49
49
|
- **20 Playwright tests** cover CSS pseudo-element functionality in a real browser
|
|
50
50
|
- See `BROWSER_TESTING.md` for details on the hybrid testing approach
|
package/package.json
CHANGED
package/src/constants.js
CHANGED
package/src/domUtils.js
CHANGED
|
@@ -6,6 +6,18 @@ const {
|
|
|
6
6
|
SEMANTIC_CONTAINER_ROLES,
|
|
7
7
|
INTERACTIVE_HANDLER_ATTRIBUTES,
|
|
8
8
|
} = require('./constants.js');
|
|
9
|
+
const { deepQuerySelector, deepQuerySelectorAll } = require('./shadowDomUtils.js');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Escapes a string for use inside a CSS selector.
|
|
13
|
+
* Uses the native CSS.escape when available (all modern browsers),
|
|
14
|
+
* falls back to identity for environments that lack it (e.g. JSDOM).
|
|
15
|
+
*
|
|
16
|
+
* @param {string} value
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
const cssEscape =
|
|
20
|
+
typeof CSS !== 'undefined' && typeof CSS.escape === 'function' ? CSS.escape : value => value;
|
|
9
21
|
|
|
10
22
|
const domUtils = {
|
|
11
23
|
/**
|
|
@@ -98,7 +110,7 @@ const domUtils = {
|
|
|
98
110
|
* @returns {Element[]} An array of elements that have duplicate IDs.
|
|
99
111
|
*/
|
|
100
112
|
getElementsWithDuplicateIds() {
|
|
101
|
-
const nodes = document
|
|
113
|
+
const nodes = deepQuerySelectorAll(document, '[id]');
|
|
102
114
|
const ids = {};
|
|
103
115
|
const duplicates = [];
|
|
104
116
|
nodes.forEach(node => {
|
|
@@ -108,7 +120,7 @@ const domUtils = {
|
|
|
108
120
|
duplicates.push(id);
|
|
109
121
|
}
|
|
110
122
|
});
|
|
111
|
-
return duplicates.map(id => document
|
|
123
|
+
return duplicates.map(id => deepQuerySelector(document, `#${cssEscape(id)}`));
|
|
112
124
|
},
|
|
113
125
|
|
|
114
126
|
/**
|
|
@@ -162,10 +174,10 @@ const domUtils = {
|
|
|
162
174
|
}
|
|
163
175
|
|
|
164
176
|
// CSS-escape the ID for use in attribute selectors
|
|
165
|
-
const escaped =
|
|
177
|
+
const escaped = cssEscape(id);
|
|
166
178
|
|
|
167
179
|
// Direct attribute references
|
|
168
|
-
if (document
|
|
180
|
+
if (deepQuerySelector(document, '[for="' + escaped + '"]')) {
|
|
169
181
|
return true;
|
|
170
182
|
}
|
|
171
183
|
|
|
@@ -178,7 +190,7 @@ const domUtils = {
|
|
|
178
190
|
'aria-flowto',
|
|
179
191
|
];
|
|
180
192
|
for (const attr of ariaTokenAttrs) {
|
|
181
|
-
const elements = document
|
|
193
|
+
const elements = deepQuerySelectorAll(document, '[' + attr + ']');
|
|
182
194
|
for (const el of elements) {
|
|
183
195
|
const ids = el.getAttribute(attr).trim().split(/\s+/);
|
|
184
196
|
if (ids.includes(id)) {
|
|
@@ -190,18 +202,18 @@ const domUtils = {
|
|
|
190
202
|
// Single-ID ARIA references
|
|
191
203
|
const ariaSingleAttrs = ['aria-activedescendant', 'aria-errormessage'];
|
|
192
204
|
for (const attr of ariaSingleAttrs) {
|
|
193
|
-
if (document
|
|
205
|
+
if (deepQuerySelector(document, '[' + attr + '="' + escaped + '"]')) {
|
|
194
206
|
return true;
|
|
195
207
|
}
|
|
196
208
|
}
|
|
197
209
|
|
|
198
210
|
// Fragment references in href
|
|
199
|
-
if (document
|
|
211
|
+
if (deepQuerySelector(document, 'a[href="#' + escaped + '"]')) {
|
|
200
212
|
return true;
|
|
201
213
|
}
|
|
202
214
|
|
|
203
215
|
// Table headers attribute (space-separated list of IDs)
|
|
204
|
-
const headerElements = document
|
|
216
|
+
const headerElements = deepQuerySelectorAll(document, '[headers]');
|
|
205
217
|
for (const el of headerElements) {
|
|
206
218
|
const ids = el.getAttribute('headers').trim().split(/\s+/);
|
|
207
219
|
if (ids.includes(id)) {
|
|
@@ -210,7 +222,7 @@ const domUtils = {
|
|
|
210
222
|
}
|
|
211
223
|
|
|
212
224
|
// list attribute on input elements
|
|
213
|
-
if (document
|
|
225
|
+
if (deepQuerySelector(document, 'input[list="' + escaped + '"]')) {
|
|
214
226
|
return true;
|
|
215
227
|
}
|
|
216
228
|
|
|
@@ -226,10 +238,31 @@ const domUtils = {
|
|
|
226
238
|
if (!element) {
|
|
227
239
|
return false;
|
|
228
240
|
}
|
|
229
|
-
|
|
241
|
+
|
|
242
|
+
// Check aria-hidden (self or ancestor)
|
|
243
|
+
if (
|
|
230
244
|
element.getAttribute('aria-hidden') === 'true' ||
|
|
231
245
|
(element.closest && !!element.closest('[aria-hidden="true"]'))
|
|
232
|
-
)
|
|
246
|
+
) {
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Check inert (self or ancestor) — inert elements are removed from the a11y tree
|
|
251
|
+
let el = element;
|
|
252
|
+
while (el) {
|
|
253
|
+
if (el.hasAttribute && el.hasAttribute('inert')) {
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
el = el.parentElement;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Check content-visibility: hidden — genuinely hides content from AT
|
|
260
|
+
const style = window.getComputedStyle(element);
|
|
261
|
+
if (style.contentVisibility === 'hidden') {
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return false;
|
|
233
266
|
},
|
|
234
267
|
|
|
235
268
|
/**
|
|
@@ -397,3 +430,4 @@ const domUtils = {
|
|
|
397
430
|
};
|
|
398
431
|
|
|
399
432
|
module.exports = domUtils;
|
|
433
|
+
module.exports.cssEscape = cssEscape;
|
package/src/formUtils.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* @module formUtils
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const { isHiddenFromAT } = require('./domUtils.js');
|
|
7
|
+
const { deepGetElementById } = require('./shadowDomUtils.js');
|
|
8
|
+
|
|
6
9
|
const formUtils = {
|
|
7
10
|
/**
|
|
8
11
|
* Check if an element is a labellable form control per HTML spec.
|
|
@@ -49,8 +52,8 @@ const formUtils = {
|
|
|
49
52
|
if (element.hasAttribute('aria-labelledby')) {
|
|
50
53
|
const ids = element.getAttribute('aria-labelledby').trim().split(/\s+/);
|
|
51
54
|
for (let i = 0; i < ids.length; i++) {
|
|
52
|
-
const labelEl = document
|
|
53
|
-
if (labelEl && labelEl.textContent.trim()) {
|
|
55
|
+
const labelEl = deepGetElementById(document, ids[i]);
|
|
56
|
+
if (labelEl && !isHiddenFromAT(labelEl) && labelEl.textContent.trim()) {
|
|
54
57
|
return true;
|
|
55
58
|
}
|
|
56
59
|
}
|
package/src/getAccessibleName.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { isEmptyOrWhitespace } = require('./stringUtils.js');
|
|
2
2
|
const { getAccessibleText } = require('./getAccessibleText.js');
|
|
3
3
|
const {
|
|
4
4
|
TEXT_ROLES,
|
|
@@ -6,6 +6,9 @@ const {
|
|
|
6
6
|
LANDMARK_ROLES,
|
|
7
7
|
LANDMARK_ELEMENT_MAP,
|
|
8
8
|
} = require('./constants.js');
|
|
9
|
+
const { cssEscape } = require('./domUtils.js');
|
|
10
|
+
const { isA11yVisible } = require('./isA11yVisible.js');
|
|
11
|
+
const { deepGetElementById, deepQuerySelector } = require('./shadowDomUtils.js');
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
14
|
* Gets the accessible name of an element according to the accessible name calculation algorithm
|
|
@@ -26,9 +29,21 @@ function getAccessibleName(element) {
|
|
|
26
29
|
// the title won't be used in any meaningful way by Accessibility APIs
|
|
27
30
|
const unlabellable = 'head *, hr, param, caption, colgroup, col, tbody, tfoot, thead, tr';
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
if (matchesSelector(element, unlabellable)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Skip inherently non-visible semantic elements (script, style, template, etc.)
|
|
37
|
+
// but exclude 'area' since area elements have accessible names via alt attribute
|
|
38
|
+
const nonVisibleSelectorsWithoutArea = NON_VISIBLE_SELECTORS.filter(s => s !== 'area');
|
|
39
|
+
if (nonVisibleSelectorsWithoutArea.some(selector => matchesSelector(element, selector))) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// STEP 0 - Use isA11yVisible (strict mode) for visibility check.
|
|
44
|
+
// This respects aria-labelledby/describedby references, preventing false
|
|
45
|
+
// negatives for display:none elements that provide names to other elements.
|
|
46
|
+
if (!isA11yVisible(element, true)) {
|
|
32
47
|
return false;
|
|
33
48
|
}
|
|
34
49
|
|
|
@@ -44,7 +59,7 @@ function getAccessibleName(element) {
|
|
|
44
59
|
|
|
45
60
|
const text = [];
|
|
46
61
|
for (const id of ids) {
|
|
47
|
-
const labelElement = document
|
|
62
|
+
const labelElement = deepGetElementById(document, id);
|
|
48
63
|
if (!labelElement || !getAccessibleText(labelElement)) {
|
|
49
64
|
return false;
|
|
50
65
|
}
|
|
@@ -58,7 +73,7 @@ function getAccessibleName(element) {
|
|
|
58
73
|
// STEP 2.1 - if aria-label exists, return the text in it
|
|
59
74
|
if (element.hasAttribute('aria-label')) {
|
|
60
75
|
const ariaLabel = element.getAttribute('aria-label');
|
|
61
|
-
if (ariaLabel) {
|
|
76
|
+
if (!isEmptyOrWhitespace(ariaLabel)) {
|
|
62
77
|
return ariaLabel;
|
|
63
78
|
}
|
|
64
79
|
// there is no 'else' here because an empty aria-label is/ should be ignored and calculation continued
|
|
@@ -75,7 +90,7 @@ function getAccessibleName(element) {
|
|
|
75
90
|
// Use getAccessibleText which handles both text nodes and
|
|
76
91
|
// image alt text in the subtree (not just textContent)
|
|
77
92
|
const text = getAccessibleText(element);
|
|
78
|
-
if (!
|
|
93
|
+
if (!isEmptyOrWhitespace(text)) {
|
|
79
94
|
return text;
|
|
80
95
|
}
|
|
81
96
|
}
|
|
@@ -95,12 +110,15 @@ function getAccessibleName(element) {
|
|
|
95
110
|
)
|
|
96
111
|
) {
|
|
97
112
|
// first we choose the explicit relationship over all others.
|
|
98
|
-
if (
|
|
113
|
+
if (
|
|
114
|
+
element.id &&
|
|
115
|
+
deepQuerySelector(document, 'label[for="' + cssEscape(element.id) + '"]')
|
|
116
|
+
) {
|
|
99
117
|
id = element.id;
|
|
100
118
|
// Use only the *first* label that matches this ID.
|
|
101
119
|
// Sometimes JS libraries screw this up by hiding one of the
|
|
102
120
|
// labels or misnaming one
|
|
103
|
-
label = document
|
|
121
|
+
label = deepQuerySelector(document, 'label[for="' + cssEscape(id) + '"]');
|
|
104
122
|
if (label) {
|
|
105
123
|
return getAccessibleText(label);
|
|
106
124
|
}
|
|
@@ -210,11 +228,14 @@ function getAccessibleName(element) {
|
|
|
210
228
|
)
|
|
211
229
|
) {
|
|
212
230
|
// first we choose the explicit relationship over all others.
|
|
213
|
-
if (
|
|
231
|
+
if (
|
|
232
|
+
element.id &&
|
|
233
|
+
deepQuerySelector(document, 'label[for="' + cssEscape(element.id) + '"]')
|
|
234
|
+
) {
|
|
214
235
|
id = element.id;
|
|
215
236
|
|
|
216
237
|
//Use only the *first* label that matches this ID. Sometimes ppl screw this up
|
|
217
|
-
label = document
|
|
238
|
+
label = deepQuerySelector(document, 'label[for="' + cssEscape(id) + '"]');
|
|
218
239
|
if (label) {
|
|
219
240
|
return getAccessibleText(label);
|
|
220
241
|
}
|
|
@@ -359,7 +380,10 @@ function getAccessibleName(element) {
|
|
|
359
380
|
if (element.tagName.toLowerCase() === 'meter') {
|
|
360
381
|
// Check for label with for attribute
|
|
361
382
|
if (element.id) {
|
|
362
|
-
const label =
|
|
383
|
+
const label = deepQuerySelector(
|
|
384
|
+
document,
|
|
385
|
+
'label[for="' + cssEscape(element.id) + '"]'
|
|
386
|
+
);
|
|
363
387
|
if (label && strlen(getAccessibleText(label)) > 0) {
|
|
364
388
|
return getAccessibleText(label);
|
|
365
389
|
}
|
|
@@ -474,44 +498,6 @@ function getAccessibleName(element) {
|
|
|
474
498
|
}
|
|
475
499
|
}
|
|
476
500
|
|
|
477
|
-
/**
|
|
478
|
-
* Helper function to check if element is NOT visible
|
|
479
|
-
* @param {Element} element - The element to check
|
|
480
|
-
* @returns {boolean} True if element is not visible, false otherwise
|
|
481
|
-
*/
|
|
482
|
-
function isNotVisible(element) {
|
|
483
|
-
// Importing isVisible would be better, but for this standalone function we'll check it this way
|
|
484
|
-
if (!element) {
|
|
485
|
-
return true;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// Note: 'area' is filtered out because area elements DO have accessible names
|
|
489
|
-
// via the alt attribute and should participate in accessible name calculation
|
|
490
|
-
const nonVisibleSelectorsWithoutArea = NON_VISIBLE_SELECTORS.filter(s => s !== 'area');
|
|
491
|
-
|
|
492
|
-
if (nonVisibleSelectorsWithoutArea.some(selector => matchesSelector(element, selector))) {
|
|
493
|
-
return true; // Not visible in accessibility tree
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// Check if display is none
|
|
497
|
-
const isElemDisplayed = el => window.getComputedStyle(el).display === 'none';
|
|
498
|
-
|
|
499
|
-
if (isElemDisplayed(element)) {
|
|
500
|
-
return true;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// Check parent elements
|
|
504
|
-
let parent = element.parentElement;
|
|
505
|
-
while (parent) {
|
|
506
|
-
if (isElemDisplayed(parent)) {
|
|
507
|
-
return true;
|
|
508
|
-
}
|
|
509
|
-
parent = parent.parentElement;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
return element.getAttribute('aria-hidden') === 'true';
|
|
513
|
-
}
|
|
514
|
-
|
|
515
501
|
/**
|
|
516
502
|
* Helper function to check if an element matches a selector
|
|
517
503
|
* @param {Element} element - Element to check
|
|
@@ -541,7 +527,7 @@ function matchesSelector(element, selector) {
|
|
|
541
527
|
* @returns {number} The string length or 0
|
|
542
528
|
*/
|
|
543
529
|
function strlen(str) {
|
|
544
|
-
return typeof str === 'string' && !
|
|
530
|
+
return typeof str === 'string' && !isEmptyOrWhitespace(str) ? str.trim().length : 0;
|
|
545
531
|
}
|
|
546
532
|
|
|
547
533
|
// Export the function for CommonJS module usage
|
package/src/getAccessibleText.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { isEmptyOrWhitespace } = require('./stringUtils.js');
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Get all accessible text for an element, including aria-labels and content from children.
|
|
@@ -27,7 +27,7 @@ function getAccessibleText(el, options) {
|
|
|
27
27
|
// Check for aria-label first (highest priority)
|
|
28
28
|
if (el.hasAttribute('aria-label')) {
|
|
29
29
|
const ariaLabel = el.getAttribute('aria-label').trim();
|
|
30
|
-
if (ariaLabel) {
|
|
30
|
+
if (!isEmptyOrWhitespace(ariaLabel)) {
|
|
31
31
|
return ariaLabel;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -65,7 +65,7 @@ function collectSubtreeText(node, visibleOnly) {
|
|
|
65
65
|
for (let child = node.firstChild; child; child = child.nextSibling) {
|
|
66
66
|
if (child.nodeType === Node.TEXT_NODE) {
|
|
67
67
|
const text = child.nodeValue.trim();
|
|
68
|
-
if (!
|
|
68
|
+
if (!isEmptyOrWhitespace(text)) {
|
|
69
69
|
parts.push(text);
|
|
70
70
|
}
|
|
71
71
|
} else if (child.nodeType === Node.ELEMENT_NODE) {
|