mtrl-addons 0.1.2 → 0.2.1

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 (115) hide show
  1. package/build.js +139 -86
  2. package/package.json +13 -4
  3. package/scripts/debug/vlist-selection.ts +121 -0
  4. package/src/components/index.ts +5 -41
  5. package/src/components/{list → vlist}/config.ts +66 -95
  6. package/src/components/vlist/constants.ts +23 -0
  7. package/src/components/vlist/features/api.ts +322 -0
  8. package/src/components/vlist/features/index.ts +10 -0
  9. package/src/components/vlist/features/selection.ts +444 -0
  10. package/src/components/vlist/features/viewport.ts +65 -0
  11. package/src/components/vlist/index.ts +16 -0
  12. package/src/components/{list → vlist}/types.ts +104 -26
  13. package/src/components/vlist/vlist.ts +92 -0
  14. package/src/core/compose/features/gestures/index.ts +227 -0
  15. package/src/core/compose/features/gestures/longpress.ts +383 -0
  16. package/src/core/compose/features/gestures/pan.ts +424 -0
  17. package/src/core/compose/features/gestures/pinch.ts +475 -0
  18. package/src/core/compose/features/gestures/rotate.ts +485 -0
  19. package/src/core/compose/features/gestures/swipe.ts +492 -0
  20. package/src/core/compose/features/gestures/tap.ts +334 -0
  21. package/src/core/compose/features/index.ts +2 -38
  22. package/src/core/compose/index.ts +13 -29
  23. package/src/core/gestures/index.ts +31 -0
  24. package/src/core/gestures/longpress.ts +68 -0
  25. package/src/core/gestures/manager.ts +418 -0
  26. package/src/core/gestures/pan.ts +48 -0
  27. package/src/core/gestures/pinch.ts +58 -0
  28. package/src/core/gestures/rotate.ts +58 -0
  29. package/src/core/gestures/swipe.ts +66 -0
  30. package/src/core/gestures/tap.ts +45 -0
  31. package/src/core/gestures/types.ts +387 -0
  32. package/src/core/gestures/utils.ts +128 -0
  33. package/src/core/index.ts +27 -151
  34. package/src/core/layout/schema.ts +73 -35
  35. package/src/core/layout/types.ts +5 -2
  36. package/src/core/viewport/constants.ts +140 -0
  37. package/src/core/viewport/features/base.ts +73 -0
  38. package/src/core/viewport/features/collection.ts +882 -0
  39. package/src/core/viewport/features/events.ts +130 -0
  40. package/src/core/viewport/features/index.ts +20 -0
  41. package/src/core/{list-manager/features/viewport → viewport/features}/item-size.ts +27 -30
  42. package/src/core/{list-manager/features/viewport → viewport/features}/loading.ts +4 -4
  43. package/src/core/viewport/features/momentum.ts +260 -0
  44. package/src/core/viewport/features/placeholders.ts +335 -0
  45. package/src/core/viewport/features/rendering.ts +568 -0
  46. package/src/core/viewport/features/scrollbar.ts +434 -0
  47. package/src/core/viewport/features/scrolling.ts +618 -0
  48. package/src/core/viewport/features/utils.ts +88 -0
  49. package/src/core/viewport/features/virtual.ts +384 -0
  50. package/src/core/viewport/index.ts +31 -0
  51. package/src/core/viewport/types.ts +133 -0
  52. package/src/core/viewport/utils/speed-tracker.ts +79 -0
  53. package/src/core/viewport/viewport.ts +246 -0
  54. package/src/index.ts +0 -7
  55. package/src/styles/components/_vlist.scss +331 -0
  56. package/src/styles/index.scss +1 -1
  57. package/test/components/vlist-selection.test.ts +240 -0
  58. package/test/components/vlist.test.ts +63 -0
  59. package/test/core/collection/adapter.test.ts +161 -0
  60. package/bun.lock +0 -792
  61. package/src/components/list/api.ts +0 -314
  62. package/src/components/list/constants.ts +0 -56
  63. package/src/components/list/features/api.ts +0 -428
  64. package/src/components/list/features/index.ts +0 -31
  65. package/src/components/list/features/list-manager.ts +0 -502
  66. package/src/components/list/index.ts +0 -39
  67. package/src/components/list/list.ts +0 -234
  68. package/src/core/collection/base-collection.ts +0 -100
  69. package/src/core/collection/collection-composer.ts +0 -178
  70. package/src/core/collection/collection.ts +0 -745
  71. package/src/core/collection/constants.ts +0 -172
  72. package/src/core/collection/events.ts +0 -428
  73. package/src/core/collection/features/api/loading.ts +0 -279
  74. package/src/core/collection/features/operations/data-operations.ts +0 -147
  75. package/src/core/collection/index.ts +0 -104
  76. package/src/core/collection/state.ts +0 -497
  77. package/src/core/collection/types.ts +0 -404
  78. package/src/core/compose/features/collection.ts +0 -119
  79. package/src/core/compose/features/selection.ts +0 -213
  80. package/src/core/compose/features/styling.ts +0 -108
  81. package/src/core/list-manager/api.ts +0 -599
  82. package/src/core/list-manager/config.ts +0 -593
  83. package/src/core/list-manager/constants.ts +0 -268
  84. package/src/core/list-manager/features/api.ts +0 -58
  85. package/src/core/list-manager/features/collection/collection.ts +0 -705
  86. package/src/core/list-manager/features/collection/index.ts +0 -17
  87. package/src/core/list-manager/features/viewport/constants.ts +0 -42
  88. package/src/core/list-manager/features/viewport/index.ts +0 -16
  89. package/src/core/list-manager/features/viewport/placeholders.ts +0 -281
  90. package/src/core/list-manager/features/viewport/rendering.ts +0 -575
  91. package/src/core/list-manager/features/viewport/scrollbar.ts +0 -495
  92. package/src/core/list-manager/features/viewport/scrolling.ts +0 -795
  93. package/src/core/list-manager/features/viewport/template.ts +0 -220
  94. package/src/core/list-manager/features/viewport/viewport.ts +0 -654
  95. package/src/core/list-manager/features/viewport/virtual.ts +0 -309
  96. package/src/core/list-manager/index.ts +0 -279
  97. package/src/core/list-manager/list-manager.ts +0 -206
  98. package/src/core/list-manager/types.ts +0 -439
  99. package/src/core/list-manager/utils/calculations.ts +0 -290
  100. package/src/core/list-manager/utils/range-calculator.ts +0 -349
  101. package/src/core/list-manager/utils/speed-tracker.ts +0 -273
  102. package/src/styles/components/_list.scss +0 -244
  103. package/src/types/mtrl.d.ts +0 -6
  104. package/test/components/list.test.ts +0 -256
  105. package/test/core/collection/failed-ranges.test.ts +0 -270
  106. package/test/core/compose/features.test.ts +0 -183
  107. package/test/core/list-manager/features/collection.test.ts +0 -704
  108. package/test/core/list-manager/features/viewport.test.ts +0 -698
  109. package/test/core/list-manager/list-manager.test.ts +0 -593
  110. package/test/core/list-manager/utils/calculations.test.ts +0 -433
  111. package/test/core/list-manager/utils/range-calculator.test.ts +0 -569
  112. package/test/core/list-manager/utils/speed-tracker.test.ts +0 -530
  113. package/tsconfig.build.json +0 -23
  114. /package/src/components/{list → vlist}/features.ts +0 -0
  115. /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
- });