@sc4rfurryx/proteusjs 1.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/API.md +438 -0
- package/FEATURES.md +286 -0
- package/LICENSE +21 -0
- package/README.md +645 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/proteus.cjs.js +16014 -0
- package/dist/proteus.cjs.js.map +1 -0
- package/dist/proteus.d.ts +3018 -0
- package/dist/proteus.esm.js +16005 -0
- package/dist/proteus.esm.js.map +1 -0
- package/dist/proteus.esm.min.js +8 -0
- package/dist/proteus.esm.min.js.map +1 -0
- package/dist/proteus.js +16020 -0
- package/dist/proteus.js.map +1 -0
- package/dist/proteus.min.js +8 -0
- package/dist/proteus.min.js.map +1 -0
- package/package.json +98 -0
- package/src/__tests__/mvp-integration.test.ts +518 -0
- package/src/accessibility/AccessibilityEngine.ts +2106 -0
- package/src/accessibility/ScreenReaderSupport.ts +444 -0
- package/src/accessibility/__tests__/ScreenReaderSupport.test.ts +435 -0
- package/src/animations/FLIPAnimationSystem.ts +491 -0
- package/src/compatibility/BrowserCompatibility.ts +1076 -0
- package/src/containers/BreakpointSystem.ts +347 -0
- package/src/containers/ContainerBreakpoints.ts +726 -0
- package/src/containers/ContainerManager.ts +370 -0
- package/src/containers/ContainerUnits.ts +336 -0
- package/src/containers/ContextIsolation.ts +394 -0
- package/src/containers/ElementQueries.ts +411 -0
- package/src/containers/SmartContainer.ts +536 -0
- package/src/containers/SmartContainers.ts +376 -0
- package/src/containers/__tests__/ContainerBreakpoints.test.ts +411 -0
- package/src/containers/__tests__/SmartContainers.test.ts +281 -0
- package/src/content/ResponsiveImages.ts +570 -0
- package/src/core/EventSystem.ts +147 -0
- package/src/core/MemoryManager.ts +321 -0
- package/src/core/PerformanceMonitor.ts +238 -0
- package/src/core/PluginSystem.ts +275 -0
- package/src/core/ProteusJS.test.ts +164 -0
- package/src/core/ProteusJS.ts +962 -0
- package/src/developer/PerformanceProfiler.ts +567 -0
- package/src/developer/VisualDebuggingTools.ts +656 -0
- package/src/developer/ZeroConfigSystem.ts +593 -0
- package/src/index.ts +35 -0
- package/src/integration.test.ts +227 -0
- package/src/layout/AdaptiveGrid.ts +429 -0
- package/src/layout/ContentReordering.ts +532 -0
- package/src/layout/FlexboxEnhancer.ts +406 -0
- package/src/layout/FlowLayout.ts +545 -0
- package/src/layout/SpacingSystem.ts +512 -0
- package/src/observers/IntersectionObserverPolyfill.ts +289 -0
- package/src/observers/ObserverManager.ts +299 -0
- package/src/observers/ResizeObserverPolyfill.ts +179 -0
- package/src/performance/BatchDOMOperations.ts +519 -0
- package/src/performance/CSSOptimizationEngine.ts +646 -0
- package/src/performance/CacheOptimizationSystem.ts +601 -0
- package/src/performance/EfficientEventHandler.ts +740 -0
- package/src/performance/LazyEvaluationSystem.ts +532 -0
- package/src/performance/MemoryManagementSystem.ts +497 -0
- package/src/performance/PerformanceMonitor.ts +931 -0
- package/src/performance/__tests__/BatchDOMOperations.test.ts +309 -0
- package/src/performance/__tests__/EfficientEventHandler.test.ts +268 -0
- package/src/performance/__tests__/PerformanceMonitor.test.ts +422 -0
- package/src/polyfills/BrowserPolyfills.ts +586 -0
- package/src/polyfills/__tests__/BrowserPolyfills.test.ts +328 -0
- package/src/test/setup.ts +115 -0
- package/src/theming/SmartThemeSystem.ts +591 -0
- package/src/types/index.ts +134 -0
- package/src/typography/ClampScaling.ts +356 -0
- package/src/typography/FluidTypography.ts +759 -0
- package/src/typography/LineHeightOptimization.ts +430 -0
- package/src/typography/LineHeightOptimizer.ts +326 -0
- package/src/typography/TextFitting.ts +355 -0
- package/src/typography/TypographicScale.ts +428 -0
- package/src/typography/VerticalRhythm.ts +369 -0
- package/src/typography/__tests__/FluidTypography.test.ts +432 -0
- package/src/typography/__tests__/LineHeightOptimization.test.ts +436 -0
- package/src/utils/Logger.ts +173 -0
- package/src/utils/debounce.ts +259 -0
- package/src/utils/performance.ts +371 -0
- package/src/utils/support.ts +106 -0
- package/src/utils/version.ts +24 -0
@@ -0,0 +1,309 @@
|
|
1
|
+
/**
|
2
|
+
* Tests for Batch DOM Operations
|
3
|
+
*/
|
4
|
+
|
5
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
6
|
+
import { BatchDOMOperations } from '../BatchDOMOperations';
|
7
|
+
|
8
|
+
// Mock requestAnimationFrame
|
9
|
+
global.requestAnimationFrame = vi.fn((callback) => {
|
10
|
+
setTimeout(callback, 16);
|
11
|
+
return 1;
|
12
|
+
});
|
13
|
+
|
14
|
+
global.cancelAnimationFrame = vi.fn();
|
15
|
+
|
16
|
+
describe('BatchDOMOperations', () => {
|
17
|
+
let batchDOM: BatchDOMOperations;
|
18
|
+
let testElement: HTMLElement;
|
19
|
+
|
20
|
+
beforeEach(() => {
|
21
|
+
batchDOM = new BatchDOMOperations({
|
22
|
+
maxBatchSize: 10,
|
23
|
+
frameTimeLimit: 16,
|
24
|
+
separateReadWrite: true,
|
25
|
+
autoFlush: false // Disable for testing
|
26
|
+
});
|
27
|
+
|
28
|
+
testElement = document.createElement('div');
|
29
|
+
testElement.style.width = '100px';
|
30
|
+
testElement.style.height = '100px';
|
31
|
+
document.body.appendChild(testElement);
|
32
|
+
});
|
33
|
+
|
34
|
+
afterEach(() => {
|
35
|
+
batchDOM.destroy();
|
36
|
+
document.body.removeChild(testElement);
|
37
|
+
vi.clearAllMocks();
|
38
|
+
});
|
39
|
+
|
40
|
+
describe('Read Operations', () => {
|
41
|
+
it('should queue and execute read operations', async () => {
|
42
|
+
const result = await batchDOM.queueRead(
|
43
|
+
testElement,
|
44
|
+
() => testElement.offsetWidth
|
45
|
+
);
|
46
|
+
|
47
|
+
expect(result).toBe(100);
|
48
|
+
});
|
49
|
+
|
50
|
+
it('should batch multiple reads efficiently', async () => {
|
51
|
+
const reads = [
|
52
|
+
batchDOM.queueRead(testElement, () => testElement.offsetWidth),
|
53
|
+
batchDOM.queueRead(testElement, () => testElement.offsetHeight),
|
54
|
+
batchDOM.queueRead(testElement, () => testElement.offsetTop)
|
55
|
+
];
|
56
|
+
|
57
|
+
const results = await Promise.all(reads);
|
58
|
+
|
59
|
+
expect(results[0]).toBe(100); // width
|
60
|
+
expect(results[1]).toBe(100); // height
|
61
|
+
expect(typeof results[2]).toBe('number'); // top
|
62
|
+
});
|
63
|
+
|
64
|
+
it('should handle read operation errors', async () => {
|
65
|
+
await expect(
|
66
|
+
batchDOM.queueRead(testElement, () => {
|
67
|
+
throw new Error('Test error');
|
68
|
+
})
|
69
|
+
).rejects.toThrow('Test error');
|
70
|
+
});
|
71
|
+
});
|
72
|
+
|
73
|
+
describe('Write Operations', () => {
|
74
|
+
it('should queue and execute write operations', async () => {
|
75
|
+
await batchDOM.queueWrite(
|
76
|
+
testElement,
|
77
|
+
() => {
|
78
|
+
testElement.style.backgroundColor = 'red';
|
79
|
+
}
|
80
|
+
);
|
81
|
+
|
82
|
+
expect(testElement.style.backgroundColor).toBe('red');
|
83
|
+
});
|
84
|
+
|
85
|
+
it('should batch multiple writes efficiently', async () => {
|
86
|
+
const writes = [
|
87
|
+
batchDOM.queueWrite(testElement, () => {
|
88
|
+
testElement.style.color = 'blue';
|
89
|
+
}),
|
90
|
+
batchDOM.queueWrite(testElement, () => {
|
91
|
+
testElement.style.fontSize = '20px';
|
92
|
+
})
|
93
|
+
];
|
94
|
+
|
95
|
+
await Promise.all(writes);
|
96
|
+
|
97
|
+
expect(testElement.style.color).toBe('blue');
|
98
|
+
expect(testElement.style.fontSize).toBe('20px');
|
99
|
+
});
|
100
|
+
});
|
101
|
+
|
102
|
+
describe('Batch Style Operations', () => {
|
103
|
+
it('should batch multiple style changes', async () => {
|
104
|
+
await batchDOM.batchStyles(testElement, {
|
105
|
+
'background-color': 'green',
|
106
|
+
'border': '1px solid black',
|
107
|
+
'padding': '10px'
|
108
|
+
});
|
109
|
+
|
110
|
+
expect(testElement.style.backgroundColor).toBe('green');
|
111
|
+
expect(testElement.style.border).toBe('1px solid black');
|
112
|
+
expect(testElement.style.padding).toBe('10px');
|
113
|
+
});
|
114
|
+
});
|
115
|
+
|
116
|
+
describe('Batch Class Operations', () => {
|
117
|
+
it('should batch class changes', async () => {
|
118
|
+
testElement.className = 'initial-class';
|
119
|
+
|
120
|
+
await batchDOM.batchClasses(testElement, {
|
121
|
+
add: ['new-class', 'another-class'],
|
122
|
+
remove: ['initial-class'],
|
123
|
+
toggle: ['toggle-class']
|
124
|
+
});
|
125
|
+
|
126
|
+
expect(testElement.classList.contains('new-class')).toBe(true);
|
127
|
+
expect(testElement.classList.contains('another-class')).toBe(true);
|
128
|
+
expect(testElement.classList.contains('initial-class')).toBe(false);
|
129
|
+
expect(testElement.classList.contains('toggle-class')).toBe(true);
|
130
|
+
});
|
131
|
+
});
|
132
|
+
|
133
|
+
describe('Batch Attribute Operations', () => {
|
134
|
+
it('should batch attribute changes', async () => {
|
135
|
+
await batchDOM.batchAttributes(testElement, {
|
136
|
+
'data-test': 'value',
|
137
|
+
'aria-label': 'Test element',
|
138
|
+
'title': null // Should remove
|
139
|
+
});
|
140
|
+
|
141
|
+
expect(testElement.getAttribute('data-test')).toBe('value');
|
142
|
+
expect(testElement.getAttribute('aria-label')).toBe('Test element');
|
143
|
+
expect(testElement.hasAttribute('title')).toBe(false);
|
144
|
+
});
|
145
|
+
});
|
146
|
+
|
147
|
+
describe('Batch Read Operations', () => {
|
148
|
+
it('should batch multiple property reads', async () => {
|
149
|
+
const results = await batchDOM.batchReads(testElement, {
|
150
|
+
width: () => testElement.offsetWidth,
|
151
|
+
height: () => testElement.offsetHeight,
|
152
|
+
tagName: () => testElement.tagName
|
153
|
+
});
|
154
|
+
|
155
|
+
expect(results.width).toBe(100);
|
156
|
+
expect(results.height).toBe(100);
|
157
|
+
expect(results.tagName).toBe('DIV');
|
158
|
+
});
|
159
|
+
});
|
160
|
+
|
161
|
+
describe('Element Measurement', () => {
|
162
|
+
it('should measure element dimensions', async () => {
|
163
|
+
const measurements = await batchDOM.measureElement(
|
164
|
+
testElement,
|
165
|
+
['width', 'height', 'top', 'left']
|
166
|
+
);
|
167
|
+
|
168
|
+
expect(measurements.width).toBe(100);
|
169
|
+
expect(measurements.height).toBe(100);
|
170
|
+
expect(typeof measurements.top).toBe('number');
|
171
|
+
expect(typeof measurements.left).toBe('number');
|
172
|
+
});
|
173
|
+
|
174
|
+
it('should measure only requested dimensions', async () => {
|
175
|
+
const measurements = await batchDOM.measureElement(
|
176
|
+
testElement,
|
177
|
+
['width']
|
178
|
+
);
|
179
|
+
|
180
|
+
expect(measurements.width).toBe(100);
|
181
|
+
expect(measurements.height).toBeUndefined();
|
182
|
+
});
|
183
|
+
});
|
184
|
+
|
185
|
+
describe('Priority Handling', () => {
|
186
|
+
it('should respect operation priorities', async () => {
|
187
|
+
const executionOrder: string[] = [];
|
188
|
+
|
189
|
+
// Add operations with different priorities
|
190
|
+
const lowPriority = batchDOM.queueWrite(testElement, () => {
|
191
|
+
executionOrder.push('low');
|
192
|
+
}, 'low');
|
193
|
+
|
194
|
+
const highPriority = batchDOM.queueWrite(testElement, () => {
|
195
|
+
executionOrder.push('high');
|
196
|
+
}, 'high');
|
197
|
+
|
198
|
+
const normalPriority = batchDOM.queueWrite(testElement, () => {
|
199
|
+
executionOrder.push('normal');
|
200
|
+
}, 'normal');
|
201
|
+
|
202
|
+
await Promise.all([lowPriority, highPriority, normalPriority]);
|
203
|
+
|
204
|
+
// High priority should execute first
|
205
|
+
expect(executionOrder[0]).toBe('high');
|
206
|
+
});
|
207
|
+
});
|
208
|
+
|
209
|
+
describe('Read/Write Separation', () => {
|
210
|
+
it('should separate reads and writes to prevent layout thrashing', async () => {
|
211
|
+
const executionOrder: string[] = [];
|
212
|
+
|
213
|
+
// Mix reads and writes
|
214
|
+
const read1 = batchDOM.queueRead(testElement, () => {
|
215
|
+
executionOrder.push('read1');
|
216
|
+
return testElement.offsetWidth;
|
217
|
+
});
|
218
|
+
|
219
|
+
const write1 = batchDOM.queueWrite(testElement, () => {
|
220
|
+
executionOrder.push('write1');
|
221
|
+
testElement.style.width = '200px';
|
222
|
+
});
|
223
|
+
|
224
|
+
const read2 = batchDOM.queueRead(testElement, () => {
|
225
|
+
executionOrder.push('read2');
|
226
|
+
return testElement.offsetHeight;
|
227
|
+
});
|
228
|
+
|
229
|
+
const write2 = batchDOM.queueWrite(testElement, () => {
|
230
|
+
executionOrder.push('write2');
|
231
|
+
testElement.style.height = '200px';
|
232
|
+
});
|
233
|
+
|
234
|
+
await Promise.all([read1, write1, read2, write2]);
|
235
|
+
|
236
|
+
// All reads should come before writes
|
237
|
+
const readIndices = executionOrder
|
238
|
+
.map((op, index) => op.startsWith('read') ? index : -1)
|
239
|
+
.filter(index => index !== -1);
|
240
|
+
|
241
|
+
const writeIndices = executionOrder
|
242
|
+
.map((op, index) => op.startsWith('write') ? index : -1)
|
243
|
+
.filter(index => index !== -1);
|
244
|
+
|
245
|
+
const lastReadIndex = Math.max(...readIndices);
|
246
|
+
const firstWriteIndex = Math.min(...writeIndices);
|
247
|
+
|
248
|
+
expect(lastReadIndex).toBeLessThan(firstWriteIndex);
|
249
|
+
});
|
250
|
+
});
|
251
|
+
|
252
|
+
describe('Performance Metrics', () => {
|
253
|
+
it('should track performance metrics', async () => {
|
254
|
+
const initialMetrics = batchDOM.getMetrics();
|
255
|
+
|
256
|
+
await batchDOM.queueRead(testElement, () => testElement.offsetWidth);
|
257
|
+
await batchDOM.queueWrite(testElement, () => {
|
258
|
+
testElement.style.color = 'red';
|
259
|
+
});
|
260
|
+
|
261
|
+
const updatedMetrics = batchDOM.getMetrics();
|
262
|
+
|
263
|
+
expect(updatedMetrics.totalOperations).toBeGreaterThan(initialMetrics.totalOperations);
|
264
|
+
expect(updatedMetrics.readOperations).toBeGreaterThan(initialMetrics.readOperations);
|
265
|
+
expect(updatedMetrics.writeOperations).toBeGreaterThan(initialMetrics.writeOperations);
|
266
|
+
});
|
267
|
+
});
|
268
|
+
|
269
|
+
describe('Flush Operations', () => {
|
270
|
+
it('should flush all queued operations', async () => {
|
271
|
+
let executed = false;
|
272
|
+
|
273
|
+
// Queue operation without auto-flush
|
274
|
+
batchDOM.queueWrite(testElement, () => {
|
275
|
+
executed = true;
|
276
|
+
});
|
277
|
+
|
278
|
+
// Should not be executed yet
|
279
|
+
expect(executed).toBe(false);
|
280
|
+
|
281
|
+
// Flush manually
|
282
|
+
await batchDOM.flush();
|
283
|
+
|
284
|
+
// Should be executed now
|
285
|
+
expect(executed).toBe(true);
|
286
|
+
});
|
287
|
+
});
|
288
|
+
|
289
|
+
describe('Cleanup', () => {
|
290
|
+
it('should clear all queues', () => {
|
291
|
+
batchDOM.queueRead(testElement, () => testElement.offsetWidth);
|
292
|
+
batchDOM.queueWrite(testElement, () => {
|
293
|
+
testElement.style.color = 'red';
|
294
|
+
});
|
295
|
+
|
296
|
+
const metricsBeforeClear = batchDOM.getMetrics();
|
297
|
+
expect(metricsBeforeClear.totalOperations).toBeGreaterThan(0);
|
298
|
+
|
299
|
+
batchDOM.clear();
|
300
|
+
|
301
|
+
// Queues should be empty after clear
|
302
|
+
// Note: We can't directly test queue size, but we can test that no new operations are processed
|
303
|
+
});
|
304
|
+
|
305
|
+
it('should destroy properly', () => {
|
306
|
+
expect(() => batchDOM.destroy()).not.toThrow();
|
307
|
+
});
|
308
|
+
});
|
309
|
+
});
|
@@ -0,0 +1,268 @@
|
|
1
|
+
/**
|
2
|
+
* Tests for Efficient Event Handler
|
3
|
+
*/
|
4
|
+
|
5
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
6
|
+
import { EfficientEventHandler } from '../EfficientEventHandler';
|
7
|
+
|
8
|
+
// Mock ResizeObserver and IntersectionObserver
|
9
|
+
global.ResizeObserver = vi.fn().mockImplementation((callback) => ({
|
10
|
+
observe: vi.fn(),
|
11
|
+
unobserve: vi.fn(),
|
12
|
+
disconnect: vi.fn(),
|
13
|
+
}));
|
14
|
+
|
15
|
+
global.IntersectionObserver = vi.fn().mockImplementation((callback) => ({
|
16
|
+
observe: vi.fn(),
|
17
|
+
unobserve: vi.fn(),
|
18
|
+
disconnect: vi.fn(),
|
19
|
+
}));
|
20
|
+
|
21
|
+
// Mock requestAnimationFrame
|
22
|
+
global.requestAnimationFrame = vi.fn((callback) => {
|
23
|
+
setTimeout(callback, 16);
|
24
|
+
return 1;
|
25
|
+
});
|
26
|
+
|
27
|
+
global.cancelAnimationFrame = vi.fn();
|
28
|
+
|
29
|
+
describe('EfficientEventHandler', () => {
|
30
|
+
let eventHandler: EfficientEventHandler;
|
31
|
+
let testElement: HTMLElement;
|
32
|
+
|
33
|
+
beforeEach(() => {
|
34
|
+
eventHandler = new EfficientEventHandler({
|
35
|
+
debounceDelay: 10,
|
36
|
+
throttleDelay: 10,
|
37
|
+
useRAF: false, // Use timer for testing
|
38
|
+
batchOperations: true,
|
39
|
+
performanceTarget: 16
|
40
|
+
});
|
41
|
+
|
42
|
+
testElement = document.createElement('div');
|
43
|
+
document.body.appendChild(testElement);
|
44
|
+
});
|
45
|
+
|
46
|
+
afterEach(() => {
|
47
|
+
eventHandler.destroy();
|
48
|
+
document.body.removeChild(testElement);
|
49
|
+
vi.clearAllMocks();
|
50
|
+
});
|
51
|
+
|
52
|
+
describe('Initialization', () => {
|
53
|
+
it('should initialize without errors', () => {
|
54
|
+
expect(() => eventHandler.initialize()).not.toThrow();
|
55
|
+
});
|
56
|
+
|
57
|
+
it('should setup observers when initialized', () => {
|
58
|
+
eventHandler.initialize();
|
59
|
+
expect(global.ResizeObserver).toHaveBeenCalled();
|
60
|
+
expect(global.IntersectionObserver).toHaveBeenCalled();
|
61
|
+
});
|
62
|
+
});
|
63
|
+
|
64
|
+
describe('Resize Observation', () => {
|
65
|
+
it('should register resize observation', () => {
|
66
|
+
eventHandler.initialize();
|
67
|
+
|
68
|
+
const callback = vi.fn();
|
69
|
+
const operationId = eventHandler.observeResize(testElement, callback);
|
70
|
+
|
71
|
+
expect(operationId).toBeDefined();
|
72
|
+
expect(typeof operationId).toBe('string');
|
73
|
+
});
|
74
|
+
|
75
|
+
it('should call resize callback when triggered', async () => {
|
76
|
+
eventHandler.initialize();
|
77
|
+
|
78
|
+
const callback = vi.fn();
|
79
|
+
eventHandler.observeResize(testElement, callback);
|
80
|
+
|
81
|
+
// Simulate resize
|
82
|
+
const resizeObserverInstance = (global.ResizeObserver as any).mock.instances[0];
|
83
|
+
const resizeCallback = (global.ResizeObserver as any).mock.calls[0][0];
|
84
|
+
|
85
|
+
const mockEntry = {
|
86
|
+
target: testElement,
|
87
|
+
contentRect: { width: 100, height: 100 },
|
88
|
+
borderBoxSize: [{ inlineSize: 100, blockSize: 100 }],
|
89
|
+
contentBoxSize: [{ inlineSize: 100, blockSize: 100 }],
|
90
|
+
devicePixelContentBoxSize: [{ inlineSize: 100, blockSize: 100 }]
|
91
|
+
};
|
92
|
+
|
93
|
+
resizeCallback([mockEntry]);
|
94
|
+
|
95
|
+
// Wait for processing
|
96
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
97
|
+
|
98
|
+
expect(callback).toHaveBeenCalled();
|
99
|
+
});
|
100
|
+
});
|
101
|
+
|
102
|
+
describe('Intersection Observation', () => {
|
103
|
+
it('should register intersection observation', () => {
|
104
|
+
eventHandler.initialize();
|
105
|
+
|
106
|
+
const callback = vi.fn();
|
107
|
+
const operationId = eventHandler.observeIntersection(testElement, callback);
|
108
|
+
|
109
|
+
expect(operationId).toBeDefined();
|
110
|
+
expect(typeof operationId).toBe('string');
|
111
|
+
});
|
112
|
+
});
|
113
|
+
|
114
|
+
describe('Debounced Events', () => {
|
115
|
+
it('should add debounced event listener', () => {
|
116
|
+
const callback = vi.fn();
|
117
|
+
const operationId = eventHandler.addDebouncedListener(testElement, 'click', callback);
|
118
|
+
|
119
|
+
expect(operationId).toBeDefined();
|
120
|
+
});
|
121
|
+
|
122
|
+
it('should debounce rapid events', async () => {
|
123
|
+
const callback = vi.fn();
|
124
|
+
eventHandler.addDebouncedListener(testElement, 'click', callback, 20);
|
125
|
+
|
126
|
+
// Trigger multiple events rapidly
|
127
|
+
testElement.click();
|
128
|
+
testElement.click();
|
129
|
+
testElement.click();
|
130
|
+
|
131
|
+
// Should not be called immediately
|
132
|
+
expect(callback).not.toHaveBeenCalled();
|
133
|
+
|
134
|
+
// Wait for debounce delay
|
135
|
+
await new Promise(resolve => setTimeout(resolve, 30));
|
136
|
+
|
137
|
+
// Should be called only once after debounce
|
138
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
139
|
+
});
|
140
|
+
});
|
141
|
+
|
142
|
+
describe('Throttled Events', () => {
|
143
|
+
it('should add throttled event listener', () => {
|
144
|
+
const callback = vi.fn();
|
145
|
+
const operationId = eventHandler.addThrottledListener(testElement, 'scroll', callback);
|
146
|
+
|
147
|
+
expect(operationId).toBeDefined();
|
148
|
+
});
|
149
|
+
});
|
150
|
+
|
151
|
+
describe('Performance Metrics', () => {
|
152
|
+
it('should provide performance metrics', () => {
|
153
|
+
const metrics = eventHandler.getMetrics();
|
154
|
+
|
155
|
+
expect(metrics).toHaveProperty('frameTime');
|
156
|
+
expect(metrics).toHaveProperty('fps');
|
157
|
+
expect(metrics).toHaveProperty('operationsPerFrame');
|
158
|
+
expect(metrics).toHaveProperty('queueSize');
|
159
|
+
expect(metrics).toHaveProperty('droppedFrames');
|
160
|
+
expect(metrics).toHaveProperty('averageLatency');
|
161
|
+
});
|
162
|
+
|
163
|
+
it('should update metrics during operation', async () => {
|
164
|
+
eventHandler.initialize();
|
165
|
+
|
166
|
+
const callback = vi.fn();
|
167
|
+
eventHandler.observeResize(testElement, callback);
|
168
|
+
|
169
|
+
const initialMetrics = eventHandler.getMetrics();
|
170
|
+
|
171
|
+
// Trigger some operations
|
172
|
+
const resizeCallback = (global.ResizeObserver as any).mock.calls[0][0];
|
173
|
+
resizeCallback([{
|
174
|
+
target: testElement,
|
175
|
+
contentRect: { width: 100, height: 100 }
|
176
|
+
}]);
|
177
|
+
|
178
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
179
|
+
|
180
|
+
const updatedMetrics = eventHandler.getMetrics();
|
181
|
+
expect(updatedMetrics.queueSize).toBeGreaterThanOrEqual(0);
|
182
|
+
});
|
183
|
+
});
|
184
|
+
|
185
|
+
describe('Operation Management', () => {
|
186
|
+
it('should remove operations from queue', () => {
|
187
|
+
eventHandler.initialize();
|
188
|
+
|
189
|
+
const callback = vi.fn();
|
190
|
+
const operationId = eventHandler.observeResize(testElement, callback);
|
191
|
+
|
192
|
+
eventHandler.removeOperation(operationId);
|
193
|
+
|
194
|
+
// Operation should be removed from queue
|
195
|
+
const metrics = eventHandler.getMetrics();
|
196
|
+
expect(metrics.queueSize).toBe(0);
|
197
|
+
});
|
198
|
+
|
199
|
+
it('should flush all operations', async () => {
|
200
|
+
eventHandler.initialize();
|
201
|
+
|
202
|
+
const callback = vi.fn();
|
203
|
+
eventHandler.observeResize(testElement, callback);
|
204
|
+
|
205
|
+
// Add operation to queue
|
206
|
+
const resizeCallback = (global.ResizeObserver as any).mock.calls[0][0];
|
207
|
+
resizeCallback([{
|
208
|
+
target: testElement,
|
209
|
+
contentRect: { width: 100, height: 100 }
|
210
|
+
}]);
|
211
|
+
|
212
|
+
// Flush operations
|
213
|
+
eventHandler.flush();
|
214
|
+
|
215
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
216
|
+
|
217
|
+
expect(callback).toHaveBeenCalled();
|
218
|
+
});
|
219
|
+
});
|
220
|
+
|
221
|
+
describe('Cleanup', () => {
|
222
|
+
it('should clean up resources on destroy', () => {
|
223
|
+
eventHandler.initialize();
|
224
|
+
|
225
|
+
const resizeObserverInstance = (global.ResizeObserver as any).mock.instances[0];
|
226
|
+
const intersectionObserverInstance = (global.IntersectionObserver as any).mock.instances[0];
|
227
|
+
|
228
|
+
eventHandler.destroy();
|
229
|
+
|
230
|
+
expect(resizeObserverInstance.disconnect).toHaveBeenCalled();
|
231
|
+
expect(intersectionObserverInstance.disconnect).toHaveBeenCalled();
|
232
|
+
});
|
233
|
+
});
|
234
|
+
|
235
|
+
describe('Performance Budgeting', () => {
|
236
|
+
it('should respect performance budget', async () => {
|
237
|
+
const handler = new EfficientEventHandler({
|
238
|
+
performanceTarget: 1, // Very low budget
|
239
|
+
maxBatchSize: 1,
|
240
|
+
useRAF: false
|
241
|
+
});
|
242
|
+
|
243
|
+
handler.initialize();
|
244
|
+
|
245
|
+
const callbacks = Array.from({ length: 10 }, () => vi.fn());
|
246
|
+
|
247
|
+
// Add many operations
|
248
|
+
callbacks.forEach((callback, index) => {
|
249
|
+
handler.observeResize(testElement, callback);
|
250
|
+
});
|
251
|
+
|
252
|
+
// Trigger operations
|
253
|
+
const resizeCallback = (global.ResizeObserver as any).mock.calls[0][0];
|
254
|
+
resizeCallback(callbacks.map(() => ({
|
255
|
+
target: testElement,
|
256
|
+
contentRect: { width: 100, height: 100 }
|
257
|
+
})));
|
258
|
+
|
259
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
260
|
+
|
261
|
+
// Not all callbacks should be executed due to budget constraints
|
262
|
+
const executedCount = callbacks.filter(cb => cb.mock.calls.length > 0).length;
|
263
|
+
expect(executedCount).toBeLessThan(callbacks.length);
|
264
|
+
|
265
|
+
handler.destroy();
|
266
|
+
});
|
267
|
+
});
|
268
|
+
});
|