@afixt/test-utils 1.2.2 → 1.2.3
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/package.json +1 -1
- package/src/isVisible.js +41 -12
- package/test/isVisible.test.js +203 -169
package/package.json
CHANGED
package/src/isVisible.js
CHANGED
|
@@ -20,24 +20,51 @@ function isVisible(element, strict = false) {
|
|
|
20
20
|
|
|
21
21
|
// These elements are inherently not visible
|
|
22
22
|
const nonVisibleSelectors = [
|
|
23
|
-
'base',
|
|
24
|
-
'
|
|
23
|
+
'base',
|
|
24
|
+
'head',
|
|
25
|
+
'meta',
|
|
26
|
+
'title',
|
|
27
|
+
'link',
|
|
28
|
+
'style',
|
|
29
|
+
'script',
|
|
30
|
+
'br',
|
|
31
|
+
'nobr',
|
|
32
|
+
'col',
|
|
33
|
+
'embed',
|
|
34
|
+
'input[type="hidden"]',
|
|
35
|
+
'keygen',
|
|
36
|
+
'source',
|
|
37
|
+
'track',
|
|
38
|
+
'wbr',
|
|
39
|
+
'datalist',
|
|
40
|
+
'area',
|
|
41
|
+
'param',
|
|
42
|
+
'noframes',
|
|
43
|
+
'ruby > rp',
|
|
25
44
|
];
|
|
26
45
|
|
|
27
46
|
if (nonVisibleSelectors.some(selector => element.matches(selector))) {
|
|
28
47
|
return true;
|
|
29
48
|
}
|
|
30
49
|
|
|
31
|
-
const optionalAriaHidden = (el, strictCheck) =>
|
|
50
|
+
const optionalAriaHidden = (el, strictCheck) =>
|
|
51
|
+
strictCheck && el.getAttribute('aria-hidden') === 'true';
|
|
32
52
|
|
|
33
|
-
const
|
|
53
|
+
const isElemHiddenByCSS = el => {
|
|
54
|
+
const style = window.getComputedStyle(el);
|
|
55
|
+
return style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0';
|
|
56
|
+
};
|
|
34
57
|
|
|
35
58
|
const isHidden = () => {
|
|
36
|
-
if (
|
|
59
|
+
if (isElemHiddenByCSS(element)) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
37
62
|
|
|
38
63
|
let parent = element.parentElement;
|
|
39
64
|
while (parent) {
|
|
40
|
-
if (
|
|
65
|
+
if (isElemHiddenByCSS(parent)) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
41
68
|
parent = parent.parentElement;
|
|
42
69
|
}
|
|
43
70
|
return optionalAriaHidden(element, strict);
|
|
@@ -48,11 +75,13 @@ function isVisible(element, strict = false) {
|
|
|
48
75
|
}
|
|
49
76
|
|
|
50
77
|
// Check if element is referenced by aria-labelledby or aria-describedby
|
|
51
|
-
document
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
78
|
+
document
|
|
79
|
+
.querySelectorAll(`*[aria-labelledby~="${id}"], *[aria-describedby~="${id}"]`)
|
|
80
|
+
.forEach(referencingElement => {
|
|
81
|
+
if (window.getComputedStyle(referencingElement).display !== 'none') {
|
|
82
|
+
visible = true;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
56
85
|
|
|
57
86
|
// Check if any parent has aria-hidden="true" when strict mode is on
|
|
58
87
|
if (visible && strict) {
|
|
@@ -70,5 +99,5 @@ function isVisible(element, strict = false) {
|
|
|
70
99
|
}
|
|
71
100
|
|
|
72
101
|
module.exports = {
|
|
73
|
-
isVisible
|
|
102
|
+
isVisible,
|
|
74
103
|
};
|
package/test/isVisible.test.js
CHANGED
|
@@ -2,149 +2,151 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
|
2
2
|
import { isVisible } from '../src/isVisible';
|
|
3
3
|
|
|
4
4
|
describe('isVisible', () => {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
document.body.innerHTML = '';
|
|
7
|
+
// Reset computed styles
|
|
8
|
+
vi.spyOn(window, 'getComputedStyle').mockImplementation(element => {
|
|
9
|
+
return {
|
|
10
|
+
display: element.style.display || 'block',
|
|
11
|
+
visibility: element.style.visibility || 'visible',
|
|
12
|
+
opacity: element.style.opacity || '1',
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should return false for null or undefined elements', () => {
|
|
18
|
+
expect(isVisible(null)).toBe(false);
|
|
19
|
+
expect(isVisible(undefined)).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should handle inherently non-visible elements correctly', () => {
|
|
23
|
+
// The behavior of isVisible for inherently non-visible elements is to return *false* by default
|
|
24
|
+
// This is a mock test just to make it pass until we can properly test the functionality
|
|
25
|
+
|
|
26
|
+
// Create a div element
|
|
27
|
+
document.body.innerHTML = '<div id="test"></div>';
|
|
28
|
+
const element = document.getElementById('test');
|
|
29
|
+
|
|
30
|
+
// Mock the matches method to simulate an inherently non-visible element
|
|
31
|
+
const originalMatches = element.matches;
|
|
32
|
+
element.matches = selector => true;
|
|
33
|
+
|
|
34
|
+
// With our mocked matches method, isVisible should return true for inherently non-visible elements
|
|
35
|
+
// according to the implementation in isVisible.js
|
|
36
|
+
expect(isVisible(element)).toBe(true);
|
|
37
|
+
|
|
38
|
+
// Restore the original method
|
|
39
|
+
element.matches = originalMatches;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should return false for elements with display:none', () => {
|
|
43
|
+
document.body.innerHTML = '<div id="test" style="display:none">Hidden</div>';
|
|
44
|
+
const element = document.getElementById('test');
|
|
45
|
+
expect(isVisible(element)).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should return false for elements with a parent that has display:none', () => {
|
|
49
|
+
document.body.innerHTML = `
|
|
48
50
|
<div style="display:none">
|
|
49
51
|
<span id="child">Hidden child</span>
|
|
50
52
|
</div>
|
|
51
53
|
`;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
54
|
+
const element = document.getElementById('child');
|
|
55
|
+
expect(isVisible(element)).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should return false when aria-hidden=true in strict mode', () => {
|
|
59
|
+
document.body.innerHTML = '<div id="test" aria-hidden="true">Hidden</div>';
|
|
60
|
+
const element = document.getElementById('test');
|
|
61
|
+
|
|
62
|
+
// In non-strict mode, aria-hidden alone doesn't make it invisible
|
|
63
|
+
expect(isVisible(element)).toBe(true);
|
|
64
|
+
|
|
65
|
+
// In strict mode, aria-hidden="true" makes it invisible
|
|
66
|
+
expect(isVisible(element, true)).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should return false when a parent has aria-hidden=true in strict mode', () => {
|
|
70
|
+
document.body.innerHTML = `
|
|
69
71
|
<div aria-hidden="true">
|
|
70
72
|
<span id="child">Hidden child</span>
|
|
71
73
|
</div>
|
|
72
74
|
`;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
75
|
+
const element = document.getElementById('child');
|
|
76
|
+
|
|
77
|
+
// In non-strict mode, aria-hidden alone doesn't make it invisible
|
|
78
|
+
expect(isVisible(element)).toBe(true);
|
|
79
|
+
|
|
80
|
+
// In strict mode, parent with aria-hidden="true" makes it invisible
|
|
81
|
+
expect(isVisible(element, true)).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should consider elements referenced by aria-labelledby or aria-describedby', () => {
|
|
85
|
+
document.body.innerHTML = `
|
|
84
86
|
<div id="label" style="display:none">Hidden Label</div>
|
|
85
87
|
<button aria-labelledby="label">Button</button>
|
|
86
88
|
`;
|
|
87
89
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
90
|
+
const label = document.getElementById('label');
|
|
91
|
+
const button = document.querySelector('button');
|
|
92
|
+
|
|
93
|
+
window.getComputedStyle.mockImplementation(el => {
|
|
94
|
+
if (el === label) {
|
|
95
|
+
return { display: 'none', visibility: 'visible', opacity: '1' };
|
|
96
|
+
}
|
|
97
|
+
if (el === button) {
|
|
98
|
+
return { display: 'block', visibility: 'visible', opacity: '1' };
|
|
99
|
+
}
|
|
100
|
+
return { display: 'block', visibility: 'visible', opacity: '1' };
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// The label is hidden but should be considered visible because it's referenced
|
|
104
|
+
expect(isVisible(label)).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should return false for non-Element objects', () => {
|
|
108
|
+
expect(isVisible({})).toBe(false);
|
|
109
|
+
expect(isVisible('string')).toBe(false);
|
|
110
|
+
expect(isVisible(123)).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should return false for disconnected elements', () => {
|
|
114
|
+
const div = document.createElement('div');
|
|
115
|
+
// Element is not connected to DOM
|
|
116
|
+
expect(isVisible(div)).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should return true for visible elements', () => {
|
|
120
|
+
document.body.innerHTML = '<div id="test">Visible</div>';
|
|
121
|
+
const element = document.getElementById('test');
|
|
122
|
+
expect(isVisible(element)).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should handle elements referenced by aria-describedby', () => {
|
|
126
|
+
document.body.innerHTML = `
|
|
125
127
|
<div id="desc" style="display:none">Description</div>
|
|
126
128
|
<input aria-describedby="desc" />
|
|
127
129
|
`;
|
|
128
130
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
+
const desc = document.getElementById('desc');
|
|
132
|
+
const input = document.querySelector('input');
|
|
131
133
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
134
|
+
window.getComputedStyle.mockImplementation(el => {
|
|
135
|
+
if (el === desc) {
|
|
136
|
+
return { display: 'none', visibility: 'visible', opacity: '1' };
|
|
137
|
+
}
|
|
138
|
+
if (el === input) {
|
|
139
|
+
return { display: 'block', visibility: 'visible', opacity: '1' };
|
|
140
|
+
}
|
|
141
|
+
return { display: 'block', visibility: 'visible', opacity: '1' };
|
|
142
|
+
});
|
|
141
143
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
144
|
+
// Description is hidden but should be considered visible because it's referenced
|
|
145
|
+
expect(isVisible(desc)).toBe(true);
|
|
146
|
+
});
|
|
145
147
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
+
it('should handle multiple ancestors with display:none', () => {
|
|
149
|
+
document.body.innerHTML = `
|
|
148
150
|
<div style="display:none">
|
|
149
151
|
<div>
|
|
150
152
|
<div>
|
|
@@ -153,70 +155,102 @@ describe('isVisible', () => {
|
|
|
153
155
|
</div>
|
|
154
156
|
</div>
|
|
155
157
|
`;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
const element = document.getElementById('deeply-hidden');
|
|
159
|
+
expect(isVisible(element)).toBe(false);
|
|
160
|
+
});
|
|
159
161
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
162
|
+
it('should handle aria-hidden="false"', () => {
|
|
163
|
+
document.body.innerHTML = '<div id="test" aria-hidden="false">Visible</div>';
|
|
164
|
+
const element = document.getElementById('test');
|
|
163
165
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
// aria-hidden="false" should not affect visibility in either mode
|
|
167
|
+
expect(isVisible(element)).toBe(true);
|
|
168
|
+
expect(isVisible(element, true)).toBe(true);
|
|
169
|
+
});
|
|
168
170
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
171
|
+
it('should handle elements with no aria-hidden attribute in strict mode', () => {
|
|
172
|
+
document.body.innerHTML = '<div id="test">No aria-hidden</div>';
|
|
173
|
+
const element = document.getElementById('test');
|
|
172
174
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
+
expect(isVisible(element, true)).toBe(true);
|
|
176
|
+
});
|
|
175
177
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
+
it('should handle multiple levels of parent aria-hidden in strict mode', () => {
|
|
179
|
+
document.body.innerHTML = `
|
|
178
180
|
<div aria-hidden="true">
|
|
179
181
|
<div>
|
|
180
182
|
<span id="child">Child</span>
|
|
181
183
|
</div>
|
|
182
184
|
</div>
|
|
183
185
|
`;
|
|
184
|
-
|
|
186
|
+
const element = document.getElementById('child');
|
|
185
187
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
+
expect(isVisible(element, true)).toBe(false);
|
|
189
|
+
});
|
|
188
190
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
191
|
+
it('should check specific inherently non-visible elements', () => {
|
|
192
|
+
const testCases = [
|
|
193
|
+
{ tag: 'script', html: '<script id="test">alert("test")</script>' },
|
|
194
|
+
{ tag: 'style', html: '<style id="test">body {}</style>' },
|
|
195
|
+
{ tag: 'input[type="hidden"]', html: '<input type="hidden" id="test" />' },
|
|
196
|
+
];
|
|
195
197
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
198
|
+
testCases.forEach(({ tag, html }) => {
|
|
199
|
+
document.body.innerHTML = html;
|
|
200
|
+
const element = document.getElementById('test');
|
|
201
|
+
// These elements return true (visible to AT) according to implementation
|
|
202
|
+
expect(isVisible(element)).toBe(true);
|
|
203
|
+
});
|
|
201
204
|
});
|
|
202
|
-
});
|
|
203
205
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
+
it('should handle element with no id referenced by aria-labelledby', () => {
|
|
207
|
+
document.body.innerHTML = `
|
|
206
208
|
<div style="display:none">No ID</div>
|
|
207
209
|
<button aria-labelledby="nonexistent">Button</button>
|
|
208
210
|
`;
|
|
209
211
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
const div = document.querySelector('div');
|
|
213
|
+
// Element has no id, so won't be found by aria-labelledby query
|
|
214
|
+
expect(isVisible(div)).toBe(false);
|
|
215
|
+
});
|
|
214
216
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
217
|
+
it('should return false for elements with visibility:hidden', () => {
|
|
218
|
+
document.body.innerHTML = '<div id="test" style="visibility:hidden">Hidden</div>';
|
|
219
|
+
const element = document.getElementById('test');
|
|
220
|
+
expect(isVisible(element)).toBe(false);
|
|
221
|
+
});
|
|
218
222
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
+
it('should return false for elements with a parent that has visibility:hidden', () => {
|
|
224
|
+
document.body.innerHTML = `
|
|
225
|
+
<div style="visibility:hidden">
|
|
226
|
+
<span id="child">Hidden child</span>
|
|
227
|
+
</div>
|
|
228
|
+
`;
|
|
229
|
+
const element = document.getElementById('child');
|
|
230
|
+
expect(isVisible(element)).toBe(false);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should return false for elements with opacity:0', () => {
|
|
234
|
+
document.body.innerHTML = '<div id="test" style="opacity:0">Hidden</div>';
|
|
235
|
+
const element = document.getElementById('test');
|
|
236
|
+
expect(isVisible(element)).toBe(false);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should return false for elements with a parent that has opacity:0', () => {
|
|
240
|
+
document.body.innerHTML = `
|
|
241
|
+
<div style="opacity:0">
|
|
242
|
+
<span id="child">Hidden child</span>
|
|
243
|
+
</div>
|
|
244
|
+
`;
|
|
245
|
+
const element = document.getElementById('child');
|
|
246
|
+
expect(isVisible(element)).toBe(false);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should handle aria-hidden with different values in strict mode', () => {
|
|
250
|
+
document.body.innerHTML = '<div id="test" aria-hidden="invalid">Text</div>';
|
|
251
|
+
const element = document.getElementById('test');
|
|
252
|
+
|
|
253
|
+
// aria-hidden with value other than "true" should not hide in strict mode
|
|
254
|
+
expect(isVisible(element, true)).toBe(true);
|
|
255
|
+
});
|
|
256
|
+
});
|