mtrl-addons 0.1.2 → 0.2.2

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.
Files changed (117) hide show
  1. package/AI.md +28 -230
  2. package/CLAUDE.md +882 -0
  3. package/build.js +253 -24
  4. package/package.json +14 -4
  5. package/scripts/debug/vlist-selection.ts +121 -0
  6. package/src/components/index.ts +5 -41
  7. package/src/components/{list → vlist}/config.ts +66 -95
  8. package/src/components/vlist/constants.ts +23 -0
  9. package/src/components/vlist/features/api.ts +626 -0
  10. package/src/components/vlist/features/index.ts +10 -0
  11. package/src/components/vlist/features/selection.ts +436 -0
  12. package/src/components/vlist/features/viewport.ts +59 -0
  13. package/src/components/vlist/index.ts +17 -0
  14. package/src/components/{list → vlist}/types.ts +242 -32
  15. package/src/components/vlist/vlist.ts +92 -0
  16. package/src/core/compose/features/gestures/index.ts +227 -0
  17. package/src/core/compose/features/gestures/longpress.ts +383 -0
  18. package/src/core/compose/features/gestures/pan.ts +424 -0
  19. package/src/core/compose/features/gestures/pinch.ts +475 -0
  20. package/src/core/compose/features/gestures/rotate.ts +485 -0
  21. package/src/core/compose/features/gestures/swipe.ts +492 -0
  22. package/src/core/compose/features/gestures/tap.ts +334 -0
  23. package/src/core/compose/features/index.ts +2 -38
  24. package/src/core/compose/index.ts +13 -29
  25. package/src/core/gestures/index.ts +31 -0
  26. package/src/core/gestures/longpress.ts +68 -0
  27. package/src/core/gestures/manager.ts +418 -0
  28. package/src/core/gestures/pan.ts +48 -0
  29. package/src/core/gestures/pinch.ts +58 -0
  30. package/src/core/gestures/rotate.ts +58 -0
  31. package/src/core/gestures/swipe.ts +66 -0
  32. package/src/core/gestures/tap.ts +45 -0
  33. package/src/core/gestures/types.ts +387 -0
  34. package/src/core/gestures/utils.ts +128 -0
  35. package/src/core/index.ts +27 -151
  36. package/src/core/layout/schema.ts +153 -72
  37. package/src/core/layout/types.ts +5 -2
  38. package/src/core/viewport/constants.ts +145 -0
  39. package/src/core/viewport/features/base.ts +73 -0
  40. package/src/core/viewport/features/collection.ts +1182 -0
  41. package/src/core/viewport/features/events.ts +130 -0
  42. package/src/core/viewport/features/index.ts +20 -0
  43. package/src/core/{list-manager/features/viewport → viewport/features}/item-size.ts +31 -34
  44. package/src/core/{list-manager/features/viewport → viewport/features}/loading.ts +4 -4
  45. package/src/core/viewport/features/momentum.ts +269 -0
  46. package/src/core/viewport/features/placeholders.ts +335 -0
  47. package/src/core/viewport/features/rendering.ts +962 -0
  48. package/src/core/viewport/features/scrollbar.ts +434 -0
  49. package/src/core/viewport/features/scrolling.ts +634 -0
  50. package/src/core/viewport/features/utils.ts +94 -0
  51. package/src/core/viewport/features/virtual.ts +525 -0
  52. package/src/core/viewport/index.ts +31 -0
  53. package/src/core/viewport/types.ts +133 -0
  54. package/src/core/viewport/utils/speed-tracker.ts +79 -0
  55. package/src/core/viewport/viewport.ts +265 -0
  56. package/src/index.ts +0 -7
  57. package/src/styles/components/_vlist.scss +352 -0
  58. package/src/styles/index.scss +1 -1
  59. package/test/components/vlist-selection.test.ts +240 -0
  60. package/test/components/vlist.test.ts +63 -0
  61. package/test/core/collection/adapter.test.ts +161 -0
  62. package/bun.lock +0 -792
  63. package/src/components/list/api.ts +0 -314
  64. package/src/components/list/constants.ts +0 -56
  65. package/src/components/list/features/api.ts +0 -428
  66. package/src/components/list/features/index.ts +0 -31
  67. package/src/components/list/features/list-manager.ts +0 -502
  68. package/src/components/list/index.ts +0 -39
  69. package/src/components/list/list.ts +0 -234
  70. package/src/core/collection/base-collection.ts +0 -100
  71. package/src/core/collection/collection-composer.ts +0 -178
  72. package/src/core/collection/collection.ts +0 -745
  73. package/src/core/collection/constants.ts +0 -172
  74. package/src/core/collection/events.ts +0 -428
  75. package/src/core/collection/features/api/loading.ts +0 -279
  76. package/src/core/collection/features/operations/data-operations.ts +0 -147
  77. package/src/core/collection/index.ts +0 -104
  78. package/src/core/collection/state.ts +0 -497
  79. package/src/core/collection/types.ts +0 -404
  80. package/src/core/compose/features/collection.ts +0 -119
  81. package/src/core/compose/features/selection.ts +0 -213
  82. package/src/core/compose/features/styling.ts +0 -108
  83. package/src/core/list-manager/api.ts +0 -599
  84. package/src/core/list-manager/config.ts +0 -593
  85. package/src/core/list-manager/constants.ts +0 -268
  86. package/src/core/list-manager/features/api.ts +0 -58
  87. package/src/core/list-manager/features/collection/collection.ts +0 -705
  88. package/src/core/list-manager/features/collection/index.ts +0 -17
  89. package/src/core/list-manager/features/viewport/constants.ts +0 -42
  90. package/src/core/list-manager/features/viewport/index.ts +0 -16
  91. package/src/core/list-manager/features/viewport/placeholders.ts +0 -281
  92. package/src/core/list-manager/features/viewport/rendering.ts +0 -575
  93. package/src/core/list-manager/features/viewport/scrollbar.ts +0 -495
  94. package/src/core/list-manager/features/viewport/scrolling.ts +0 -795
  95. package/src/core/list-manager/features/viewport/template.ts +0 -220
  96. package/src/core/list-manager/features/viewport/viewport.ts +0 -654
  97. package/src/core/list-manager/features/viewport/virtual.ts +0 -309
  98. package/src/core/list-manager/index.ts +0 -279
  99. package/src/core/list-manager/list-manager.ts +0 -206
  100. package/src/core/list-manager/types.ts +0 -439
  101. package/src/core/list-manager/utils/calculations.ts +0 -290
  102. package/src/core/list-manager/utils/range-calculator.ts +0 -349
  103. package/src/core/list-manager/utils/speed-tracker.ts +0 -273
  104. package/src/styles/components/_list.scss +0 -244
  105. package/src/types/mtrl.d.ts +0 -6
  106. package/test/components/list.test.ts +0 -256
  107. package/test/core/collection/failed-ranges.test.ts +0 -270
  108. package/test/core/compose/features.test.ts +0 -183
  109. package/test/core/list-manager/features/collection.test.ts +0 -704
  110. package/test/core/list-manager/features/viewport.test.ts +0 -698
  111. package/test/core/list-manager/list-manager.test.ts +0 -593
  112. package/test/core/list-manager/utils/calculations.test.ts +0 -433
  113. package/test/core/list-manager/utils/range-calculator.test.ts +0 -569
  114. package/test/core/list-manager/utils/speed-tracker.test.ts +0 -530
  115. package/tsconfig.build.json +0 -23
  116. /package/src/components/{list → vlist}/features.ts +0 -0
  117. /package/src/core/{compose → viewport}/features/performance.ts +0 -0
@@ -1,698 +0,0 @@
1
- import {
2
- describe,
3
- it,
4
- expect,
5
- beforeEach,
6
- afterEach,
7
- mock,
8
- spyOn,
9
- } from "bun:test";
10
- import { createViewportFeature } from "../../../../src/core/list-manager/features/viewport";
11
- import { ListManagerEvents } from "../../../../src/core/list-manager/types";
12
- import type {
13
- FeatureContext,
14
- ViewportFeature,
15
- } from "../../../../src/core/list-manager/types";
16
-
17
- // Mock DOM environment
18
- const mockContainer = {
19
- getBoundingClientRect: mock(() => ({
20
- width: 400,
21
- height: 600,
22
- top: 0,
23
- left: 0,
24
- })),
25
- scrollTop: 0,
26
- scrollLeft: 0,
27
- clientWidth: 400,
28
- clientHeight: 600,
29
- scrollWidth: 400,
30
- scrollHeight: 600,
31
- addEventListener: mock(),
32
- removeEventListener: mock(),
33
- appendChild: mock(),
34
- removeChild: mock(),
35
- querySelector: mock(),
36
- querySelectorAll: mock(() => []),
37
- style: {},
38
- dataset: {},
39
- classList: {
40
- add: mock(),
41
- remove: mock(),
42
- contains: mock(() => false),
43
- toggle: mock(),
44
- },
45
- } as unknown as HTMLElement;
46
-
47
- // Mock scroll element
48
- const mockScrollElement = {
49
- scrollTop: 0,
50
- scrollLeft: 0,
51
- addEventListener: mock(),
52
- removeEventListener: mock(),
53
- style: {},
54
- getBoundingClientRect: mock(() => ({
55
- width: 400,
56
- height: 600,
57
- top: 0,
58
- left: 0,
59
- })),
60
- } as unknown as HTMLElement;
61
-
62
- // Mock content element
63
- const mockContentElement = {
64
- style: {},
65
- getBoundingClientRect: mock(() => ({
66
- width: 400,
67
- height: 5000,
68
- top: 0,
69
- left: 0,
70
- })),
71
- } as unknown as HTMLElement;
72
-
73
- // Mock scrollbar elements
74
- const mockScrollbar = {
75
- style: {},
76
- addEventListener: mock(),
77
- removeEventListener: mock(),
78
- } as unknown as HTMLElement;
79
-
80
- const mockScrollbarThumb = {
81
- style: {},
82
- addEventListener: mock(),
83
- removeEventListener: mock(),
84
- } as unknown as HTMLElement;
85
-
86
- describe("Viewport Feature", () => {
87
- let mockContext: FeatureContext;
88
- let viewportFeature: ViewportFeature;
89
- let emitSpy: ReturnType<typeof mock>;
90
-
91
- beforeEach(() => {
92
- // Reset all mocks
93
- mock.restore();
94
-
95
- // Setup mock context
96
- emitSpy = mock();
97
- mockContext = {
98
- config: {
99
- container: mockContainer,
100
- virtual: {
101
- enabled: true,
102
- itemSize: "auto",
103
- estimatedItemSize: 50,
104
- overscan: 5,
105
- },
106
- orientation: {
107
- orientation: "vertical",
108
- reverse: false,
109
- crossAxisAlignment: "stretch",
110
- },
111
- debug: true,
112
- prefix: "test-list",
113
- componentName: "TestList",
114
- } as any,
115
- constants: {
116
- VIRTUAL_SCROLL: {
117
- DEFAULT_ITEM_SIZE: 50,
118
- OVERSCAN_BUFFER: 5,
119
- MEASUREMENT_CACHE_SIZE: 500,
120
- SCROLL_DEBOUNCE: 16,
121
- },
122
- SCROLLBAR: {
123
- TRACK_SIZE: 12,
124
- THUMB_MIN_SIZE: 20,
125
- HOVER_TIMEOUT: 300,
126
- },
127
- } as any,
128
- emit: emitSpy,
129
- };
130
-
131
- // Mock DOM queries
132
- mockContainer.querySelector = mock((selector: string) => {
133
- if (selector.includes("scroll")) return mockScrollElement;
134
- if (selector.includes("content")) return mockContentElement;
135
- if (selector.includes("scrollbar-track")) return mockScrollbar;
136
- if (selector.includes("scrollbar-thumb")) return mockScrollbarThumb;
137
- return null;
138
- });
139
-
140
- viewportFeature = createViewportFeature(mockContainer, mockContext);
141
- });
142
-
143
- afterEach(() => {
144
- if (viewportFeature) {
145
- viewportFeature.destroy();
146
- }
147
- });
148
-
149
- describe("Initialization", () => {
150
- it("should create viewport feature", () => {
151
- expect(viewportFeature).toBeDefined();
152
- expect(viewportFeature.orientation).toBe("vertical");
153
- expect(viewportFeature.estimatedItemSize).toBe(50);
154
- });
155
-
156
- it("should initialize DOM structure", () => {
157
- viewportFeature.initialize();
158
-
159
- expect(mockContainer.appendChild).toHaveBeenCalled();
160
- expect(mockContainer.addEventListener).toHaveBeenCalledWith(
161
- "scroll",
162
- expect.any(Function)
163
- );
164
- });
165
-
166
- it("should setup orientation-specific properties", () => {
167
- expect(viewportFeature.orientation).toBe("vertical");
168
-
169
- // Test horizontal orientation
170
- const horizontalContext = {
171
- ...mockContext,
172
- config: {
173
- ...mockContext.config,
174
- orientation: {
175
- orientation: "horizontal",
176
- reverse: false,
177
- crossAxisAlignment: "stretch",
178
- },
179
- },
180
- };
181
-
182
- const horizontalFeature = createViewportFeature(
183
- mockContainer,
184
- horizontalContext as any
185
- );
186
- expect(horizontalFeature.orientation).toBe("horizontal");
187
-
188
- horizontalFeature.destroy();
189
- });
190
-
191
- it("should emit initialization event", () => {
192
- viewportFeature.initialize();
193
-
194
- expect(emitSpy).toHaveBeenCalledWith(
195
- ListManagerEvents.VIEWPORT_INITIALIZED,
196
- expect.objectContaining({
197
- orientation: "vertical",
198
- containerSize: expect.any(Number),
199
- })
200
- );
201
- });
202
- });
203
-
204
- describe("Virtual Scrolling", () => {
205
- beforeEach(() => {
206
- viewportFeature.initialize();
207
- viewportFeature.setTotalItems(100);
208
- });
209
-
210
- it("should calculate visible range", () => {
211
- // Set scroll position
212
- viewportFeature.virtualScrollPosition = 500;
213
-
214
- const range = viewportFeature.calculateVisibleRange();
215
-
216
- expect(range.start).toBeGreaterThanOrEqual(0);
217
- expect(range.end).toBeLessThanOrEqual(100);
218
- expect(range.end).toBeGreaterThan(range.start);
219
- });
220
-
221
- it("should handle overscan buffer", () => {
222
- viewportFeature.virtualScrollPosition = 500;
223
-
224
- const rangeNoOverscan = viewportFeature.calculateVisibleRange();
225
-
226
- // Temporarily increase overscan
227
- const originalOverscan = mockContext.config.virtual.overscan;
228
- mockContext.config.virtual.overscan = 10;
229
-
230
- const rangeWithOverscan = viewportFeature.calculateVisibleRange();
231
-
232
- expect(rangeWithOverscan.end - rangeWithOverscan.start).toBeGreaterThan(
233
- rangeNoOverscan.end - rangeNoOverscan.start
234
- );
235
-
236
- // Restore overscan
237
- mockContext.config.virtual.overscan = originalOverscan;
238
- });
239
-
240
- it("should scroll to specific index", () => {
241
- const initialPosition = viewportFeature.virtualScrollPosition;
242
-
243
- viewportFeature.scrollToIndex(20, "start");
244
-
245
- expect(viewportFeature.virtualScrollPosition).not.toBe(initialPosition);
246
- expect(emitSpy).toHaveBeenCalledWith(
247
- ListManagerEvents.SCROLL_POSITION_CHANGED,
248
- expect.objectContaining({
249
- position: expect.any(Number),
250
- direction: expect.any(String),
251
- })
252
- );
253
- });
254
-
255
- it("should handle different scroll alignments", () => {
256
- const startPosition = viewportFeature.virtualScrollPosition;
257
-
258
- viewportFeature.scrollToIndex(10, "start");
259
- const startScroll = viewportFeature.virtualScrollPosition;
260
-
261
- viewportFeature.scrollToIndex(10, "center");
262
- const centerScroll = viewportFeature.virtualScrollPosition;
263
-
264
- viewportFeature.scrollToIndex(10, "end");
265
- const endScroll = viewportFeature.virtualScrollPosition;
266
-
267
- // All positions should be different
268
- expect(new Set([startScroll, centerScroll, endScroll]).size).toBe(3);
269
- });
270
-
271
- it("should respect boundaries when scrolling", () => {
272
- // Try to scroll to negative index
273
- viewportFeature.scrollToIndex(-5, "start");
274
- expect(viewportFeature.virtualScrollPosition).toBeGreaterThanOrEqual(0);
275
-
276
- // Try to scroll beyond total items
277
- viewportFeature.scrollToIndex(200, "start");
278
- const totalSize = viewportFeature.calculateTotalVirtualSize();
279
- const containerSize = viewportFeature.getContainerSize();
280
- expect(viewportFeature.virtualScrollPosition).toBeLessThanOrEqual(
281
- totalSize - containerSize
282
- );
283
- });
284
- });
285
-
286
- describe("Container Positioning", () => {
287
- beforeEach(() => {
288
- viewportFeature.initialize();
289
- viewportFeature.setTotalItems(100);
290
- });
291
-
292
- it("should update container position", () => {
293
- const contentElement = mockContentElement;
294
- const initialTransform = contentElement.style.transform;
295
-
296
- viewportFeature.virtualScrollPosition = 200;
297
- viewportFeature.updateContainerPosition();
298
-
299
- // Should apply transform for virtual scrolling
300
- expect(contentElement.style.transform).toBeDefined();
301
- });
302
-
303
- it("should handle orientation-specific transforms", () => {
304
- viewportFeature.virtualScrollPosition = 100;
305
- viewportFeature.updateContainerPosition();
306
-
307
- const verticalTransform = mockContentElement.style.transform;
308
-
309
- // Test horizontal orientation
310
- const horizontalContext = {
311
- ...mockContext,
312
- config: {
313
- ...mockContext.config,
314
- orientation: {
315
- orientation: "horizontal",
316
- reverse: false,
317
- crossAxisAlignment: "stretch",
318
- },
319
- },
320
- };
321
-
322
- const horizontalFeature = createViewportFeature(
323
- mockContainer,
324
- horizontalContext as any
325
- );
326
- horizontalFeature.initialize();
327
- horizontalFeature.virtualScrollPosition = 100;
328
- horizontalFeature.updateContainerPosition();
329
-
330
- // Transforms should be different for different orientations
331
- expect(mockContentElement.style.transform).toBeDefined();
332
-
333
- horizontalFeature.destroy();
334
- });
335
-
336
- it("should handle reverse orientation", () => {
337
- const reverseContext = {
338
- ...mockContext,
339
- config: {
340
- ...mockContext.config,
341
- orientation: {
342
- orientation: "vertical",
343
- reverse: true,
344
- crossAxisAlignment: "stretch",
345
- },
346
- },
347
- };
348
-
349
- const reverseFeature = createViewportFeature(
350
- mockContainer,
351
- reverseContext as any
352
- );
353
- reverseFeature.initialize();
354
- reverseFeature.virtualScrollPosition = 100;
355
- reverseFeature.updateContainerPosition();
356
-
357
- expect(mockContentElement.style.transform).toBeDefined();
358
-
359
- reverseFeature.destroy();
360
- });
361
- });
362
-
363
- describe("Scrollbar Management", () => {
364
- beforeEach(() => {
365
- viewportFeature.initialize();
366
- viewportFeature.setTotalItems(100);
367
- });
368
-
369
- it("should update scrollbar appearance", () => {
370
- viewportFeature.virtualScrollPosition = 500;
371
- viewportFeature.updateScrollbar();
372
-
373
- // Scrollbar should be visible when content overflows
374
- expect(mockScrollbar.style.display).not.toBe("none");
375
- });
376
-
377
- it("should hide scrollbar when content fits", () => {
378
- // Set small total size that fits in container
379
- viewportFeature.setTotalItems(5);
380
- viewportFeature.updateScrollbar();
381
-
382
- // Scrollbar should be hidden
383
- expect(mockScrollbar.style.display).toBe("none");
384
- });
385
-
386
- it("should update scrollbar thumb position", () => {
387
- viewportFeature.virtualScrollPosition = 1000;
388
- viewportFeature.updateScrollbar();
389
-
390
- // Thumb should have position based on scroll ratio
391
- const thumbStyle = mockScrollbarThumb.style;
392
- expect(thumbStyle.top || thumbStyle.left).toBeDefined();
393
- });
394
-
395
- it("should handle scrollbar interaction", () => {
396
- viewportFeature.updateScrollbar();
397
-
398
- // Verify event listeners are attached
399
- expect(mockScrollbar.addEventListener).toHaveBeenCalledWith(
400
- "mousedown",
401
- expect.any(Function)
402
- );
403
- expect(mockScrollbarThumb.addEventListener).toHaveBeenCalledWith(
404
- "mousedown",
405
- expect.any(Function)
406
- );
407
- });
408
- });
409
-
410
- describe("Item Size Management", () => {
411
- beforeEach(() => {
412
- viewportFeature.initialize();
413
- viewportFeature.setTotalItems(100);
414
- });
415
-
416
- it("should measure item size", () => {
417
- const mockElement = {
418
- getBoundingClientRect: mock(() => ({ width: 100, height: 75 })),
419
- } as unknown as HTMLElement;
420
-
421
- const size = viewportFeature.measureItemSize(mockElement, 5);
422
-
423
- expect(size).toBe(75); // Height for vertical orientation
424
- expect(viewportFeature.hasMeasuredSize(5)).toBe(true);
425
- });
426
-
427
- it("should cache measured sizes", () => {
428
- const mockElement = {
429
- getBoundingClientRect: mock(() => ({ width: 100, height: 60 })),
430
- } as unknown as HTMLElement;
431
-
432
- viewportFeature.measureItemSize(mockElement, 10);
433
- const cachedSize = viewportFeature.getMeasuredSize(10);
434
-
435
- expect(cachedSize).toBe(60);
436
- });
437
-
438
- it("should use estimated size for unmeasured items", () => {
439
- const size = viewportFeature.getItemSize(25);
440
-
441
- expect(size).toBe(50); // Default estimated size
442
- });
443
-
444
- it("should calculate total virtual size with mixed measured/estimated", () => {
445
- // Measure some items
446
- const mockElement1 = {
447
- getBoundingClientRect: mock(() => ({ height: 80 })),
448
- } as unknown as HTMLElement;
449
- const mockElement2 = {
450
- getBoundingClientRect: mock(() => ({ height: 40 })),
451
- } as unknown as HTMLElement;
452
-
453
- viewportFeature.measureItemSize(mockElement1, 0);
454
- viewportFeature.measureItemSize(mockElement2, 1);
455
-
456
- const totalSize = viewportFeature.calculateTotalVirtualSize();
457
-
458
- // Should be: 80 + 40 + (98 * 50) = 5020
459
- expect(totalSize).toBe(5020);
460
- });
461
- });
462
-
463
- describe("Viewport Information", () => {
464
- beforeEach(() => {
465
- viewportFeature.initialize();
466
- viewportFeature.setTotalItems(100);
467
- });
468
-
469
- it("should provide comprehensive viewport info", () => {
470
- viewportFeature.virtualScrollPosition = 300;
471
-
472
- const info = viewportFeature.getViewportInfo();
473
-
474
- expect(info).toHaveProperty("containerSize");
475
- expect(info).toHaveProperty("totalVirtualSize");
476
- expect(info).toHaveProperty("visibleRange");
477
- expect(info).toHaveProperty("virtualScrollPosition");
478
-
479
- expect(info.containerSize).toBe(600); // Height for vertical
480
- expect(info.virtualScrollPosition).toBe(300);
481
- expect(info.totalVirtualSize).toBeGreaterThan(0);
482
- });
483
-
484
- it("should get container size based on orientation", () => {
485
- expect(viewportFeature.getContainerSize()).toBe(600); // Height for vertical
486
-
487
- // Test horizontal
488
- const horizontalContext = {
489
- ...mockContext,
490
- config: {
491
- ...mockContext.config,
492
- orientation: {
493
- orientation: "horizontal",
494
- reverse: false,
495
- crossAxisAlignment: "stretch",
496
- },
497
- },
498
- };
499
-
500
- const horizontalFeature = createViewportFeature(
501
- mockContainer,
502
- horizontalContext as any
503
- );
504
- expect(horizontalFeature.getContainerSize()).toBe(400); // Width for horizontal
505
-
506
- horizontalFeature.destroy();
507
- });
508
- });
509
-
510
- describe("Event Handling", () => {
511
- beforeEach(() => {
512
- viewportFeature.initialize();
513
- viewportFeature.setTotalItems(100);
514
- });
515
-
516
- it("should handle scroll events", () => {
517
- const scrollHandler = mockContainer.addEventListener.mock.calls.find(
518
- (call) => call[0] === "scroll"
519
- )?.[1];
520
-
521
- expect(scrollHandler).toBeDefined();
522
-
523
- // Simulate scroll event
524
- mockScrollElement.scrollTop = 200;
525
- scrollHandler?.({ target: mockScrollElement });
526
-
527
- expect(emitSpy).toHaveBeenCalledWith(
528
- ListManagerEvents.SCROLL_POSITION_CHANGED,
529
- expect.objectContaining({
530
- position: expect.any(Number),
531
- })
532
- );
533
- });
534
-
535
- it("should handle resize events", () => {
536
- const resizeHandler = mockContainer.addEventListener.mock.calls.find(
537
- (call) => call[0] === "resize"
538
- )?.[1];
539
-
540
- if (resizeHandler) {
541
- // Mock container size change
542
- mockContainer.getBoundingClientRect = mock(() => ({
543
- width: 500,
544
- height: 800,
545
- }));
546
-
547
- resizeHandler?.({});
548
-
549
- expect(emitSpy).toHaveBeenCalledWith(
550
- ListManagerEvents.VIEWPORT_CHANGED,
551
- expect.any(Object)
552
- );
553
- }
554
- });
555
-
556
- it("should throttle scroll events", () => {
557
- const scrollHandler = mockContainer.addEventListener.mock.calls.find(
558
- (call) => call[0] === "scroll"
559
- )?.[1];
560
-
561
- // Simulate rapid scroll events
562
- for (let i = 0; i < 10; i++) {
563
- mockScrollElement.scrollTop = i * 10;
564
- scrollHandler?.({ target: mockScrollElement });
565
- }
566
-
567
- // Should not emit for every single scroll
568
- expect(emitSpy.mock.calls.length).toBeLessThan(10);
569
- });
570
- });
571
-
572
- describe("Performance Optimizations", () => {
573
- beforeEach(() => {
574
- viewportFeature.initialize();
575
- viewportFeature.setTotalItems(1000); // Large list
576
- });
577
-
578
- it("should limit measurement cache size", () => {
579
- // Measure many items
580
- for (let i = 0; i < 1000; i++) {
581
- const mockElement = {
582
- getBoundingClientRect: mock(() => ({ height: 50 + (i % 10) })),
583
- } as unknown as HTMLElement;
584
-
585
- viewportFeature.measureItemSize(mockElement, i);
586
- }
587
-
588
- // Cache should be limited
589
- const cacheSize = Array.from({ length: 1000 }, (_, i) => i).filter((i) =>
590
- viewportFeature.hasMeasuredSize(i)
591
- ).length;
592
-
593
- expect(cacheSize).toBeLessThanOrEqual(500); // Default cache limit
594
- });
595
-
596
- it("should debounce rapid updates", () => {
597
- const updateSpy = spyOn(viewportFeature, "updateContainerPosition");
598
-
599
- // Simulate rapid position changes
600
- for (let i = 0; i < 10; i++) {
601
- viewportFeature.virtualScrollPosition = i * 100;
602
- viewportFeature.updateContainerPosition();
603
- }
604
-
605
- // Should be called, but potentially debounced
606
- expect(updateSpy).toHaveBeenCalled();
607
- });
608
- });
609
-
610
- describe("Cleanup and Destruction", () => {
611
- it("should clean up event listeners on destroy", () => {
612
- viewportFeature.initialize();
613
-
614
- const addedListeners = mockContainer.addEventListener.mock.calls.length;
615
-
616
- viewportFeature.destroy();
617
-
618
- expect(mockContainer.removeEventListener).toHaveBeenCalledTimes(
619
- addedListeners
620
- );
621
- });
622
-
623
- it("should clean up DOM elements on destroy", () => {
624
- viewportFeature.initialize();
625
-
626
- viewportFeature.destroy();
627
-
628
- expect(mockContainer.removeChild).toHaveBeenCalled();
629
- });
630
-
631
- it("should emit destruction event", () => {
632
- viewportFeature.initialize();
633
- viewportFeature.destroy();
634
-
635
- expect(emitSpy).toHaveBeenCalledWith(
636
- ListManagerEvents.VIEWPORT_DESTROYED,
637
- expect.any(Object)
638
- );
639
- });
640
-
641
- it("should handle multiple destroy calls gracefully", () => {
642
- viewportFeature.initialize();
643
-
644
- expect(() => {
645
- viewportFeature.destroy();
646
- viewportFeature.destroy();
647
- }).not.toThrow();
648
- });
649
- });
650
-
651
- describe("Edge Cases", () => {
652
- it("should handle zero items", () => {
653
- viewportFeature.initialize();
654
- viewportFeature.setTotalItems(0);
655
-
656
- const range = viewportFeature.calculateVisibleRange();
657
- expect(range).toEqual({ start: 0, end: 0 });
658
-
659
- const totalSize = viewportFeature.calculateTotalVirtualSize();
660
- expect(totalSize).toBe(0);
661
- });
662
-
663
- it("should handle single item", () => {
664
- viewportFeature.initialize();
665
- viewportFeature.setTotalItems(1);
666
-
667
- const range = viewportFeature.calculateVisibleRange();
668
- expect(range.start).toBe(0);
669
- expect(range.end).toBe(0);
670
- });
671
-
672
- it("should handle very large lists", () => {
673
- viewportFeature.initialize();
674
- viewportFeature.setTotalItems(1000000);
675
-
676
- expect(() => {
677
- viewportFeature.calculateVisibleRange();
678
- viewportFeature.calculateTotalVirtualSize();
679
- viewportFeature.scrollToIndex(500000, "center");
680
- }).not.toThrow();
681
- });
682
-
683
- it("should handle container resize to zero", () => {
684
- viewportFeature.initialize();
685
-
686
- // Mock zero-size container
687
- mockContainer.getBoundingClientRect = mock(() => ({
688
- width: 0,
689
- height: 0,
690
- }));
691
-
692
- expect(() => {
693
- viewportFeature.updateContainerPosition();
694
- viewportFeature.calculateVisibleRange();
695
- }).not.toThrow();
696
- });
697
- });
698
- });