@afixt/test-utils 1.0.1 → 1.1.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 +8 -1
- package/README.md +21 -1
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/coverage-final.json +51 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +161 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +196 -0
- package/coverage/test-utils/docs/scripts/core.js.html +2263 -0
- package/coverage/test-utils/docs/scripts/core.min.js.html +151 -0
- package/coverage/test-utils/docs/scripts/index.html +176 -0
- package/coverage/test-utils/docs/scripts/resize.js.html +355 -0
- package/coverage/test-utils/docs/scripts/search.js.html +880 -0
- package/coverage/test-utils/docs/scripts/search.min.js.html +100 -0
- package/coverage/test-utils/docs/scripts/third-party/fuse.js.html +109 -0
- package/coverage/test-utils/docs/scripts/third-party/hljs-line-num-original.js.html +1192 -0
- package/coverage/test-utils/docs/scripts/third-party/hljs-line-num.js.html +85 -0
- package/coverage/test-utils/docs/scripts/third-party/hljs-original.js.html +15598 -0
- package/coverage/test-utils/docs/scripts/third-party/hljs.js.html +85 -0
- package/coverage/test-utils/docs/scripts/third-party/index.html +236 -0
- package/coverage/test-utils/docs/scripts/third-party/popper.js.html +100 -0
- package/coverage/test-utils/docs/scripts/third-party/tippy.js.html +88 -0
- package/coverage/test-utils/docs/scripts/third-party/tocbot.js.html +2098 -0
- package/coverage/test-utils/docs/scripts/third-party/tocbot.min.js.html +85 -0
- package/coverage/test-utils/index.html +131 -0
- package/coverage/test-utils/src/arrayUtils.js.html +283 -0
- package/coverage/test-utils/src/domUtils.js.html +622 -0
- package/coverage/test-utils/src/getAccessibleName.js.html +1444 -0
- package/coverage/test-utils/src/getAccessibleText.js.html +271 -0
- package/coverage/test-utils/src/getAriaAttributesByElement.js.html +142 -0
- package/coverage/test-utils/src/getCSSGeneratedContent.js.html +265 -0
- package/coverage/test-utils/src/getComputedRole.js.html +592 -0
- package/coverage/test-utils/src/getFocusableElements.js.html +163 -0
- package/coverage/test-utils/src/getGeneratedContent.js.html +130 -0
- package/coverage/test-utils/src/getImageText.js.html +160 -0
- package/coverage/test-utils/src/getStyleObject.js.html +220 -0
- package/coverage/test-utils/src/hasAccessibleName.js.html +166 -0
- package/coverage/test-utils/src/hasAttribute.js.html +130 -0
- package/coverage/test-utils/src/hasCSSGeneratedContent.js.html +145 -0
- package/coverage/test-utils/src/hasHiddenParent.js.html +172 -0
- package/coverage/test-utils/src/hasParent.js.html +247 -0
- package/coverage/test-utils/src/hasValidAriaAttributes.js.html +175 -0
- package/coverage/test-utils/src/hasValidAriaRole.js.html +172 -0
- package/coverage/test-utils/src/index.html +611 -0
- package/coverage/test-utils/src/index.js.html +274 -0
- package/coverage/test-utils/src/interactiveRoles.js.html +145 -0
- package/coverage/test-utils/src/isAriaAttributesValid.js.html +304 -0
- package/coverage/test-utils/src/isComplexTable.js.html +412 -0
- package/coverage/test-utils/src/isDataTable.js.html +799 -0
- package/coverage/test-utils/src/isFocusable.js.html +187 -0
- package/coverage/test-utils/src/isHidden.js.html +136 -0
- package/coverage/test-utils/src/isOffScreen.js.html +133 -0
- package/coverage/test-utils/src/isValidUrl.js.html +124 -0
- package/coverage/test-utils/src/isVisible.js.html +271 -0
- package/coverage/test-utils/src/listEventListeners.js.html +370 -0
- package/coverage/test-utils/src/queryCache.js.html +1156 -0
- package/coverage/test-utils/src/stringUtils.js.html +535 -0
- package/coverage/test-utils/src/testContrast.js.html +784 -0
- package/coverage/test-utils/src/testLang.js.html +1810 -0
- package/coverage/test-utils/src/testOrder.js.html +355 -0
- package/coverage/test-utils/vitest.config.browser.js.html +133 -0
- package/coverage/test-utils/vitest.config.js.html +157 -0
- package/package.json +12 -2
- package/src/arrayUtils.js +7 -12
- package/src/domUtils.js +1 -16
- package/src/getAccessibleName.js +3 -11
- package/src/getAccessibleText.js +3 -5
- package/src/getAriaAttributesByElement.js +1 -1
- package/src/getCSSGeneratedContent.js +7 -2
- package/src/getComputedRole.js +7 -2
- package/src/getFocusableElements.js +5 -1
- package/src/getGeneratedContent.js +5 -1
- package/src/getImageText.js +6 -2
- package/src/getStyleObject.js +5 -1
- package/src/hasAccessibleName.js +2 -10
- package/src/hasAttribute.js +5 -1
- package/src/hasCSSGeneratedContent.js +6 -2
- package/src/hasHiddenParent.js +2 -2
- package/src/hasParent.js +5 -1
- package/src/hasValidAriaAttributes.js +6 -2
- package/src/hasValidAriaRole.js +5 -1
- package/src/index.js +74 -31
- package/src/interactiveRoles.js +1 -1
- package/src/isAriaAttributesValid.js +6 -2
- package/src/isComplexTable.js +11 -4
- package/src/isDataTable.js +10 -5
- package/src/isFocusable.js +5 -1
- package/src/isHidden.js +1 -1
- package/src/isOffScreen.js +5 -1
- package/src/isValidUrl.js +5 -1
- package/src/isVisible.js +5 -1
- package/src/listEventListeners.js +95 -0
- package/src/queryCache.js +358 -0
- package/src/testContrast.js +5 -1
- package/src/testLang.js +19 -7
- package/src/testOrder.js +8 -3
- package/test/browser-setup.js +68 -0
- package/test/getCSSGeneratedContent.browser.test.js +125 -0
- package/test/hasCSSGeneratedContent.test.js +9 -7
- package/test/listEventListeners.test.js +310 -0
- package/test/queryCache.test.js +465 -0
- package/test/setup.js +3 -1
- package/test/testOrder.test.js +10 -115
- package/todo.md +1 -0
- package/vitest.config.browser.js +17 -0
- package/vitest.config.js +1 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module QueryCache
|
|
3
|
+
* @description Advanced DOM query caching utility with TTL, size limits, and performance optimizations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Sentinel value to distinguish between cache miss and cached null
|
|
7
|
+
const CACHE_MISS = Symbol('CACHE_MISS');
|
|
8
|
+
|
|
9
|
+
class QueryCache {
|
|
10
|
+
constructor(targetElement, options = {}) {
|
|
11
|
+
if (!targetElement || !(targetElement instanceof Element || targetElement === document)) {
|
|
12
|
+
throw new Error('QueryCache requires a valid DOM element or document');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
this.targetElement = targetElement;
|
|
16
|
+
this.cache = new Map();
|
|
17
|
+
this.computedStyleCache = new Map();
|
|
18
|
+
|
|
19
|
+
// Configuration options
|
|
20
|
+
this.options = {
|
|
21
|
+
maxCacheSize: options.maxCacheSize || 1000,
|
|
22
|
+
maxComputedStyleSize: options.maxComputedStyleSize || 500,
|
|
23
|
+
ttl: options.ttl || 60000, // Default 60 seconds
|
|
24
|
+
evictionStrategy: options.evictionStrategy || 'lru' // 'lru' or 'fifo'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Cache metadata for LRU/FIFO eviction
|
|
28
|
+
this.cacheMetadata = new Map();
|
|
29
|
+
this.computedStyleMetadata = new Map();
|
|
30
|
+
|
|
31
|
+
// Statistics
|
|
32
|
+
this.stats = {
|
|
33
|
+
hits: 0,
|
|
34
|
+
misses: 0,
|
|
35
|
+
evictions: 0
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get a single element by selector with caching
|
|
41
|
+
* @param {string} selector - CSS selector
|
|
42
|
+
* @returns {Element|null} - Cached or fresh query result
|
|
43
|
+
*/
|
|
44
|
+
querySelector(selector) {
|
|
45
|
+
if (typeof selector !== 'string' || !selector.trim()) {
|
|
46
|
+
throw new Error('querySelector requires a non-empty string selector');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const cacheKey = `single:${selector}`;
|
|
50
|
+
const cached = this._getCachedValue(cacheKey);
|
|
51
|
+
|
|
52
|
+
if (cached !== CACHE_MISS) {
|
|
53
|
+
this.stats.hits++;
|
|
54
|
+
return cached;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.stats.misses++;
|
|
58
|
+
const element = this.targetElement.querySelector(selector);
|
|
59
|
+
this._setCachedValue(cacheKey, element);
|
|
60
|
+
return element;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get elements by selector with caching
|
|
65
|
+
* @param {string} selector - CSS selector
|
|
66
|
+
* @returns {NodeList} - Cached or fresh query results
|
|
67
|
+
*/
|
|
68
|
+
querySelectorAll(selector) {
|
|
69
|
+
if (typeof selector !== 'string' || !selector.trim()) {
|
|
70
|
+
throw new Error('querySelectorAll requires a non-empty string selector');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const cacheKey = `all:${selector}`;
|
|
74
|
+
const cached = this._getCachedValue(cacheKey);
|
|
75
|
+
|
|
76
|
+
if (cached !== CACHE_MISS) {
|
|
77
|
+
this.stats.hits++;
|
|
78
|
+
return cached;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.stats.misses++;
|
|
82
|
+
const elements = this.targetElement.querySelectorAll(selector);
|
|
83
|
+
this._setCachedValue(cacheKey, elements);
|
|
84
|
+
return elements;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get computed style for an element with caching
|
|
89
|
+
* @param {Element} element - DOM element
|
|
90
|
+
* @returns {CSSStyleDeclaration} - Cached or fresh computed style
|
|
91
|
+
*/
|
|
92
|
+
getComputedStyle(element) {
|
|
93
|
+
if (!element || !(element instanceof Element)) {
|
|
94
|
+
throw new Error('getComputedStyle requires a valid DOM element');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const cached = this._getCachedComputedStyle(element);
|
|
98
|
+
|
|
99
|
+
if (cached !== CACHE_MISS) {
|
|
100
|
+
this.stats.hits++;
|
|
101
|
+
return cached;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.stats.misses++;
|
|
105
|
+
let computedStyle;
|
|
106
|
+
|
|
107
|
+
// Handle test environment gracefully
|
|
108
|
+
if (typeof global !== 'undefined' && (global.jest || global.vitest)) {
|
|
109
|
+
// In test environment, create a mock computed style with basic properties
|
|
110
|
+
computedStyle = this._createMockComputedStyle(element);
|
|
111
|
+
} else if (typeof window !== 'undefined' && window.getComputedStyle) {
|
|
112
|
+
try {
|
|
113
|
+
computedStyle = window.getComputedStyle(element);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
// Log the error and provide a fallback for environments where getComputedStyle fails
|
|
116
|
+
if (typeof console !== 'undefined' && console.error) {
|
|
117
|
+
console.error('getComputedStyle failed:', error);
|
|
118
|
+
}
|
|
119
|
+
computedStyle = this._createMockComputedStyle(element);
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
// Fallback for other environments
|
|
123
|
+
computedStyle = this._createMockComputedStyle(element);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this._setCachedComputedStyle(element, computedStyle);
|
|
127
|
+
return computedStyle;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Invalidate specific cache entries by selector pattern
|
|
132
|
+
* @param {string|RegExp} pattern - Selector pattern to match
|
|
133
|
+
* @returns {number} - Number of entries invalidated
|
|
134
|
+
*/
|
|
135
|
+
invalidate(pattern) {
|
|
136
|
+
let count = 0;
|
|
137
|
+
const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
|
|
138
|
+
|
|
139
|
+
for (const [key] of this.cache) {
|
|
140
|
+
if (regex.test(key)) {
|
|
141
|
+
this.cache.delete(key);
|
|
142
|
+
this.cacheMetadata.delete(key);
|
|
143
|
+
count++;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return count;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Invalidate computed style cache for specific elements
|
|
152
|
+
* @param {Element|Element[]} elements - Element(s) to invalidate
|
|
153
|
+
* @returns {number} - Number of entries invalidated
|
|
154
|
+
*/
|
|
155
|
+
invalidateComputedStyles(elements) {
|
|
156
|
+
const elemArray = Array.isArray(elements) ? elements : [elements];
|
|
157
|
+
let count = 0;
|
|
158
|
+
|
|
159
|
+
for (const element of elemArray) {
|
|
160
|
+
if (this.computedStyleCache.has(element)) {
|
|
161
|
+
this.computedStyleCache.delete(element);
|
|
162
|
+
this.computedStyleMetadata.delete(element);
|
|
163
|
+
count++;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return count;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get cache statistics
|
|
172
|
+
* @returns {Object} - Cache statistics
|
|
173
|
+
*/
|
|
174
|
+
getStats() {
|
|
175
|
+
return {
|
|
176
|
+
...this.stats,
|
|
177
|
+
cacheSize: this.cache.size,
|
|
178
|
+
computedStyleCacheSize: this.computedStyleCache.size,
|
|
179
|
+
hitRate: this.stats.hits / (this.stats.hits + this.stats.misses) || 0
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Clear all caches
|
|
185
|
+
*/
|
|
186
|
+
clear() {
|
|
187
|
+
this.cache.clear();
|
|
188
|
+
this.computedStyleCache.clear();
|
|
189
|
+
this.cacheMetadata.clear();
|
|
190
|
+
this.computedStyleMetadata.clear();
|
|
191
|
+
this.stats = {
|
|
192
|
+
hits: 0,
|
|
193
|
+
misses: 0,
|
|
194
|
+
evictions: 0
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Private method to get cached value with TTL check
|
|
200
|
+
* @private
|
|
201
|
+
*/
|
|
202
|
+
_getCachedValue(key) {
|
|
203
|
+
if (!this.cache.has(key)) {
|
|
204
|
+
return CACHE_MISS;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const metadata = this.cacheMetadata.get(key);
|
|
208
|
+
if (!metadata) {
|
|
209
|
+
return CACHE_MISS;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Check TTL
|
|
213
|
+
if (Date.now() - metadata.timestamp > this.options.ttl) {
|
|
214
|
+
this.cache.delete(key);
|
|
215
|
+
this.cacheMetadata.delete(key);
|
|
216
|
+
return CACHE_MISS;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Update access time for LRU
|
|
220
|
+
if (this.options.evictionStrategy === 'lru') {
|
|
221
|
+
metadata.lastAccessed = Date.now();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return this.cache.get(key);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Private method to set cached value with eviction
|
|
229
|
+
* @private
|
|
230
|
+
*/
|
|
231
|
+
_setCachedValue(key, value) {
|
|
232
|
+
// Check cache size limit
|
|
233
|
+
if (this.cache.size >= this.options.maxCacheSize) {
|
|
234
|
+
this._evictFromCache();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
this.cache.set(key, value);
|
|
238
|
+
this.cacheMetadata.set(key, {
|
|
239
|
+
timestamp: Date.now(),
|
|
240
|
+
lastAccessed: Date.now()
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Private method to get cached computed style with TTL check
|
|
246
|
+
* @private
|
|
247
|
+
*/
|
|
248
|
+
_getCachedComputedStyle(element) {
|
|
249
|
+
if (!this.computedStyleCache.has(element)) {
|
|
250
|
+
return CACHE_MISS;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const metadata = this.computedStyleMetadata.get(element);
|
|
254
|
+
if (!metadata) {
|
|
255
|
+
return CACHE_MISS;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Check TTL
|
|
259
|
+
if (Date.now() - metadata.timestamp > this.options.ttl) {
|
|
260
|
+
this.computedStyleCache.delete(element);
|
|
261
|
+
this.computedStyleMetadata.delete(element);
|
|
262
|
+
return CACHE_MISS;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Update access time for LRU
|
|
266
|
+
if (this.options.evictionStrategy === 'lru') {
|
|
267
|
+
metadata.lastAccessed = Date.now();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return this.computedStyleCache.get(element);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Private method to set cached computed style with eviction
|
|
275
|
+
* @private
|
|
276
|
+
*/
|
|
277
|
+
_setCachedComputedStyle(element, computedStyle) {
|
|
278
|
+
// Check cache size limit
|
|
279
|
+
if (this.computedStyleCache.size >= this.options.maxComputedStyleSize) {
|
|
280
|
+
this._evictFromComputedStyleCache();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
this.computedStyleCache.set(element, computedStyle);
|
|
284
|
+
this.computedStyleMetadata.set(element, {
|
|
285
|
+
timestamp: Date.now(),
|
|
286
|
+
lastAccessed: Date.now()
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Private method to evict entries from main cache
|
|
292
|
+
* @private
|
|
293
|
+
*/
|
|
294
|
+
_evictFromCache() {
|
|
295
|
+
let oldestKey = null;
|
|
296
|
+
let oldestTime = Infinity;
|
|
297
|
+
|
|
298
|
+
for (const [key, metadata] of this.cacheMetadata) {
|
|
299
|
+
const timeToCompare = this.options.evictionStrategy === 'lru'
|
|
300
|
+
? metadata.lastAccessed
|
|
301
|
+
: metadata.timestamp;
|
|
302
|
+
|
|
303
|
+
if (timeToCompare < oldestTime) {
|
|
304
|
+
oldestTime = timeToCompare;
|
|
305
|
+
oldestKey = key;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (oldestKey !== null) {
|
|
310
|
+
this.cache.delete(oldestKey);
|
|
311
|
+
this.cacheMetadata.delete(oldestKey);
|
|
312
|
+
this.stats.evictions++;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Private method to evict entries from computed style cache
|
|
318
|
+
* @private
|
|
319
|
+
*/
|
|
320
|
+
_evictFromComputedStyleCache() {
|
|
321
|
+
let oldestElement = null;
|
|
322
|
+
let oldestTime = Infinity;
|
|
323
|
+
|
|
324
|
+
for (const [element, metadata] of this.computedStyleMetadata) {
|
|
325
|
+
const timeToCompare = this.options.evictionStrategy === 'lru'
|
|
326
|
+
? metadata.lastAccessed
|
|
327
|
+
: metadata.timestamp;
|
|
328
|
+
|
|
329
|
+
if (timeToCompare < oldestTime) {
|
|
330
|
+
oldestTime = timeToCompare;
|
|
331
|
+
oldestElement = element;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (oldestElement !== null) {
|
|
336
|
+
this.computedStyleCache.delete(oldestElement);
|
|
337
|
+
this.computedStyleMetadata.delete(oldestElement);
|
|
338
|
+
this.stats.evictions++;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Private method to create mock computed style
|
|
344
|
+
* @private
|
|
345
|
+
*/
|
|
346
|
+
_createMockComputedStyle(element) {
|
|
347
|
+
return {
|
|
348
|
+
backgroundColor: element.style?.backgroundColor || 'rgba(0, 0, 0, 0)',
|
|
349
|
+
color: element.style?.color || 'rgb(0, 0, 0)',
|
|
350
|
+
display: element.style?.display || 'block',
|
|
351
|
+
visibility: element.style?.visibility || 'visible',
|
|
352
|
+
opacity: element.style?.opacity || '1',
|
|
353
|
+
getPropertyValue: (prop) => element.style?.[prop] || '',
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
module.exports = QueryCache;
|
package/src/testContrast.js
CHANGED
|
@@ -115,7 +115,7 @@ function getColorContrast(el) {
|
|
|
115
115
|
* @param {string} options.level - WCAG level to test against ('AA' or 'AAA')
|
|
116
116
|
* @returns {boolean} True if contrast requirements are met, false otherwise
|
|
117
117
|
*/
|
|
118
|
-
|
|
118
|
+
function testContrast(el, options = { level: 'AA' }) {
|
|
119
119
|
const level = options.level || 'AA';
|
|
120
120
|
let min;
|
|
121
121
|
|
|
@@ -231,3 +231,7 @@ export function testContrast(el, options = { level: 'AA' }) {
|
|
|
231
231
|
|
|
232
232
|
return true;
|
|
233
233
|
}
|
|
234
|
+
|
|
235
|
+
module.exports = {
|
|
236
|
+
testContrast
|
|
237
|
+
};
|
package/src/testLang.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { franc } = require('franc');
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* An array of language codes and their corresponding details.
|
|
3
5
|
* Each object in the array represents a language and contains the following properties:
|
|
@@ -7,7 +9,7 @@
|
|
|
7
9
|
* @property {string} English - The English name of the language.
|
|
8
10
|
* @property {string|null} [franc3] - The three-letter code used by the franc library, if available.
|
|
9
11
|
*/
|
|
10
|
-
|
|
12
|
+
const langCodes = [
|
|
11
13
|
{ alpha3: "aar", alpha2: "aa", English: "Afar", franc3: null },
|
|
12
14
|
{ alpha3: "abk", alpha2: "ab", English: "Abkhazian", franc3: null },
|
|
13
15
|
{ alpha3: "afr", alpha2: "af", English: "Afrikaans" },
|
|
@@ -285,7 +287,7 @@ export const langCodes = [
|
|
|
285
287
|
*
|
|
286
288
|
* @constant {string[]}
|
|
287
289
|
*/
|
|
288
|
-
|
|
290
|
+
const validLangCodes = [
|
|
289
291
|
"af",
|
|
290
292
|
"agq",
|
|
291
293
|
"ak",
|
|
@@ -489,7 +491,7 @@ export const validLangCodes = [
|
|
|
489
491
|
*
|
|
490
492
|
* @constant {string[]}
|
|
491
493
|
*/
|
|
492
|
-
|
|
494
|
+
const rtls = ["ar", "az", "dv", "he", "ku", "fa", "ur"];
|
|
493
495
|
|
|
494
496
|
|
|
495
497
|
/**
|
|
@@ -498,7 +500,7 @@ export const rtls = ["ar", "az", "dv", "he", "ku", "fa", "ur"];
|
|
|
498
500
|
* @param {string} threeLetterCode - The three-letter language code to convert.
|
|
499
501
|
* @returns {string|boolean} The corresponding two-letter language code, or false if not found.
|
|
500
502
|
*/
|
|
501
|
-
|
|
503
|
+
function getTwoLetterCode(threeLetterCode) {
|
|
502
504
|
if (threeLetterCode === "cmn") {
|
|
503
505
|
threeLetterCode = "chi";
|
|
504
506
|
}
|
|
@@ -522,7 +524,7 @@ export function getTwoLetterCode(threeLetterCode) {
|
|
|
522
524
|
* language attribute or if the detected language is
|
|
523
525
|
* undefined ("und"). Otherwise, returns `false`.
|
|
524
526
|
*/
|
|
525
|
-
|
|
527
|
+
function testLang(element) {
|
|
526
528
|
let selfLang =
|
|
527
529
|
element.getAttribute("lang") || element.getAttribute("xml:lang");
|
|
528
530
|
if (!selfLang) {
|
|
@@ -540,7 +542,7 @@ export function testLang(element) {
|
|
|
540
542
|
return getTwoLetterCode(threeLetterCode) === selfLang;
|
|
541
543
|
}
|
|
542
544
|
|
|
543
|
-
|
|
545
|
+
function getLang(element) {
|
|
544
546
|
let selfLang =
|
|
545
547
|
element.getAttribute("lang") || element.getAttribute("xml:lang");
|
|
546
548
|
if (!selfLang) {
|
|
@@ -565,7 +567,7 @@ export function getLang(element) {
|
|
|
565
567
|
}
|
|
566
568
|
}
|
|
567
569
|
|
|
568
|
-
|
|
570
|
+
function isDirValid(element) {
|
|
569
571
|
const lang = getLang(element);
|
|
570
572
|
if (element.hasAttribute("dir")) {
|
|
571
573
|
const dir = element.getAttribute("dir");
|
|
@@ -573,3 +575,13 @@ export function isDirValid(element) {
|
|
|
573
575
|
}
|
|
574
576
|
return !rtls.includes(lang);
|
|
575
577
|
}
|
|
578
|
+
|
|
579
|
+
module.exports = {
|
|
580
|
+
langCodes,
|
|
581
|
+
validLangCodes,
|
|
582
|
+
rtls,
|
|
583
|
+
getTwoLetterCode,
|
|
584
|
+
testLang,
|
|
585
|
+
getLang,
|
|
586
|
+
isDirValid
|
|
587
|
+
};
|
package/src/testOrder.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
const { getFocusableElements } = require("./getFocusableElements.js");
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
*
|
|
@@ -6,7 +6,7 @@ import { getFocusableElements } from "./getFocusableElements.js";
|
|
|
6
6
|
* @param {Element} b - Second element to compare
|
|
7
7
|
* @returns {number} - Sorting order based on visual position
|
|
8
8
|
*/
|
|
9
|
-
|
|
9
|
+
function sortByVisualOrder(a, b) {
|
|
10
10
|
const rectA = a.getBoundingClientRect();
|
|
11
11
|
const rectB = b.getBoundingClientRect();
|
|
12
12
|
|
|
@@ -21,7 +21,7 @@ export function sortByVisualOrder(a, b) {
|
|
|
21
21
|
* @param {Element} el - The container element to test focus order
|
|
22
22
|
* @returns {boolean} - True if the focus order matches visual order
|
|
23
23
|
*/
|
|
24
|
-
|
|
24
|
+
function testOrder(el) {
|
|
25
25
|
const els = getFocusableElements(el);
|
|
26
26
|
let focusOrder = [...els];
|
|
27
27
|
|
|
@@ -88,3 +88,8 @@ export function testOrder(el) {
|
|
|
88
88
|
}
|
|
89
89
|
return true;
|
|
90
90
|
}
|
|
91
|
+
|
|
92
|
+
module.exports = {
|
|
93
|
+
sortByVisualOrder,
|
|
94
|
+
testOrder
|
|
95
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Mock getComputedStyle to simulate real browser behavior for CSS tests
|
|
4
|
+
const originalGetComputedStyle = window.getComputedStyle;
|
|
5
|
+
|
|
6
|
+
window.getComputedStyle = vi.fn((element, pseudoElement) => {
|
|
7
|
+
// Handle pseudo-elements
|
|
8
|
+
if (pseudoElement) {
|
|
9
|
+
const classList = element.classList;
|
|
10
|
+
|
|
11
|
+
// Helper function to create mock computed style
|
|
12
|
+
const createMockStyle = (contentValue) => ({
|
|
13
|
+
content: contentValue,
|
|
14
|
+
getPropertyValue: (prop) => prop === 'content' ? contentValue : ''
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Simulate ::before pseudo-element content
|
|
18
|
+
if (pseudoElement === '::before') {
|
|
19
|
+
if (classList.contains('with-before')) {
|
|
20
|
+
return createMockStyle('"Before Content"');
|
|
21
|
+
}
|
|
22
|
+
if (classList.contains('with-both')) {
|
|
23
|
+
return createMockStyle('"Before Text"');
|
|
24
|
+
}
|
|
25
|
+
if (classList.contains('with-quotes')) {
|
|
26
|
+
return createMockStyle('"\'Quoted Text\'"');
|
|
27
|
+
}
|
|
28
|
+
if (classList.contains('empty-content')) {
|
|
29
|
+
return createMockStyle('""');
|
|
30
|
+
}
|
|
31
|
+
if (classList.contains('no-content')) {
|
|
32
|
+
return createMockStyle('none');
|
|
33
|
+
}
|
|
34
|
+
if (classList.contains('url-content')) {
|
|
35
|
+
return createMockStyle('url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")');
|
|
36
|
+
}
|
|
37
|
+
// Default: no content
|
|
38
|
+
return createMockStyle('none');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Simulate ::after pseudo-element content
|
|
42
|
+
if (pseudoElement === '::after') {
|
|
43
|
+
if (classList.contains('with-after')) {
|
|
44
|
+
return createMockStyle('"After Content"');
|
|
45
|
+
}
|
|
46
|
+
if (classList.contains('with-both')) {
|
|
47
|
+
return createMockStyle('"After Text"');
|
|
48
|
+
}
|
|
49
|
+
// Default: no content
|
|
50
|
+
return createMockStyle('none');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// For regular elements, try to use original if available or return default styles
|
|
55
|
+
try {
|
|
56
|
+
return originalGetComputedStyle.call(window, element, pseudoElement);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
// Fallback for JSDOM limitations
|
|
59
|
+
return {
|
|
60
|
+
display: 'block',
|
|
61
|
+
visibility: 'visible',
|
|
62
|
+
opacity: '1',
|
|
63
|
+
position: 'static',
|
|
64
|
+
width: 'auto',
|
|
65
|
+
height: 'auto'
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { getCSSGeneratedContent } from '../src/getCSSGeneratedContent';
|
|
3
|
+
|
|
4
|
+
describe('getCSSGeneratedContent (Browser)', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
document.body.innerHTML = '';
|
|
7
|
+
|
|
8
|
+
// Create a style element for pseudo-elements
|
|
9
|
+
const style = document.createElement('style');
|
|
10
|
+
style.innerHTML = `
|
|
11
|
+
.with-before::before {
|
|
12
|
+
content: "Before Content";
|
|
13
|
+
}
|
|
14
|
+
.with-after::after {
|
|
15
|
+
content: "After Content";
|
|
16
|
+
}
|
|
17
|
+
.with-both::before {
|
|
18
|
+
content: "Before Text";
|
|
19
|
+
}
|
|
20
|
+
.with-both::after {
|
|
21
|
+
content: "After Text";
|
|
22
|
+
}
|
|
23
|
+
.with-quotes::before {
|
|
24
|
+
content: "'Quoted Text'";
|
|
25
|
+
}
|
|
26
|
+
.empty-content::before {
|
|
27
|
+
content: "";
|
|
28
|
+
}
|
|
29
|
+
.no-content::before {
|
|
30
|
+
content: none;
|
|
31
|
+
}
|
|
32
|
+
.url-content::before {
|
|
33
|
+
content: url('data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7');
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
36
|
+
document.head.appendChild(style);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('::before pseudo-element', () => {
|
|
40
|
+
it('should detect text content in ::before pseudo-element', () => {
|
|
41
|
+
const div = document.createElement('div');
|
|
42
|
+
div.className = 'with-before';
|
|
43
|
+
document.body.appendChild(div);
|
|
44
|
+
|
|
45
|
+
const result = getCSSGeneratedContent(div);
|
|
46
|
+
expect(result).toBe('Before Content');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should detect quoted text content in ::before pseudo-element', () => {
|
|
50
|
+
const div = document.createElement('div');
|
|
51
|
+
div.className = 'with-quotes';
|
|
52
|
+
document.body.appendChild(div);
|
|
53
|
+
|
|
54
|
+
const result = getCSSGeneratedContent(div);
|
|
55
|
+
expect(result).toBe('Quoted Text');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should return false for empty content in ::before', () => {
|
|
59
|
+
const div = document.createElement('div');
|
|
60
|
+
div.className = 'empty-content';
|
|
61
|
+
document.body.appendChild(div);
|
|
62
|
+
|
|
63
|
+
const result = getCSSGeneratedContent(div);
|
|
64
|
+
expect(result).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should return false for content: none in ::before', () => {
|
|
68
|
+
const div = document.createElement('div');
|
|
69
|
+
div.className = 'no-content';
|
|
70
|
+
document.body.appendChild(div);
|
|
71
|
+
|
|
72
|
+
const result = getCSSGeneratedContent(div);
|
|
73
|
+
expect(result).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('::after pseudo-element', () => {
|
|
78
|
+
it('should detect text content in ::after pseudo-element', () => {
|
|
79
|
+
const div = document.createElement('div');
|
|
80
|
+
div.className = 'with-after';
|
|
81
|
+
document.body.appendChild(div);
|
|
82
|
+
|
|
83
|
+
const result = getCSSGeneratedContent(div);
|
|
84
|
+
expect(result).toBe('After Content');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('multiple pseudo-elements', () => {
|
|
89
|
+
it('should detect content when both ::before and ::after have content', () => {
|
|
90
|
+
const div = document.createElement('div');
|
|
91
|
+
div.className = 'with-both';
|
|
92
|
+
document.body.appendChild(div);
|
|
93
|
+
|
|
94
|
+
const result = getCSSGeneratedContent(div);
|
|
95
|
+
expect(result).toBe('Before Text After Text');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('no generated content', () => {
|
|
100
|
+
it('should return false for elements with no generated content', () => {
|
|
101
|
+
const div = document.createElement('div');
|
|
102
|
+
document.body.appendChild(div);
|
|
103
|
+
|
|
104
|
+
const result = getCSSGeneratedContent(div);
|
|
105
|
+
expect(result).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should return false for elements that do not exist', () => {
|
|
109
|
+
const result = getCSSGeneratedContent(null);
|
|
110
|
+
expect(result).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('complex content types', () => {
|
|
115
|
+
it('should handle URL content', () => {
|
|
116
|
+
const div = document.createElement('div');
|
|
117
|
+
div.className = 'url-content';
|
|
118
|
+
document.body.appendChild(div);
|
|
119
|
+
|
|
120
|
+
const result = getCSSGeneratedContent(div);
|
|
121
|
+
// URL content is preserved as-is (no quote stripping for URLs)
|
|
122
|
+
expect(result).toBe('url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")');
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|