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,704 +0,0 @@
1
- import {
2
- describe,
3
- it,
4
- expect,
5
- beforeEach,
6
- afterEach,
7
- mock,
8
- spyOn,
9
- } from "bun:test";
10
- import { createCollectionFeature } from "../../../../src/core/list-manager/features/collection";
11
- import { ListManagerEvents } from "../../../../src/core/list-manager/types";
12
- import type {
13
- FeatureContext,
14
- CollectionFeature,
15
- ItemRange,
16
- } from "../../../../src/core/list-manager/types";
17
-
18
- describe("Collection Feature", () => {
19
- let mockContext: FeatureContext;
20
- let collectionFeature: CollectionFeature;
21
- let emitSpy: ReturnType<typeof mock>;
22
- let mockAdapter: any;
23
-
24
- beforeEach(() => {
25
- // Reset all mocks
26
- mock.restore();
27
-
28
- // Setup mock context
29
- emitSpy = mock();
30
- mockContext = {
31
- config: {
32
- collection: {
33
- strategy: "page",
34
- pageSize: 20,
35
- loadTriggerDistance: 5,
36
- maxConcurrentRequests: 3,
37
- retryAttempts: 3,
38
- placeholderConfig: {
39
- enabled: true,
40
- template: (index: number) => ({
41
- id: `placeholder-${index}`,
42
- loading: true,
43
- }),
44
- fields: ["id", "name", "value"],
45
- },
46
- },
47
- virtual: {
48
- enabled: true,
49
- itemSize: "auto",
50
- estimatedItemSize: 50,
51
- overscan: 5,
52
- },
53
- debug: true,
54
- prefix: "test-list",
55
- componentName: "TestList",
56
- } as any,
57
- constants: {
58
- RANGE_LOADING: {
59
- DEFAULT_RANGE_SIZE: 20,
60
- MAX_CONCURRENT_REQUESTS: 3,
61
- RETRY_DELAY: 1000,
62
- TIMEOUT: 5000,
63
- },
64
- SPEED_THRESHOLDS: {
65
- FAST_SCROLL: 1000,
66
- SLOW_SCROLL: 100,
67
- DIRECTION_CHANGE: 0.3,
68
- },
69
- PLACEHOLDERS: {
70
- ENABLED: true,
71
- STRUCTURE_DETECTION: true,
72
- MAX_FIELDS: 10,
73
- },
74
- } as any,
75
- emit: emitSpy,
76
- };
77
-
78
- collectionFeature = createCollectionFeature(mockContext);
79
- });
80
-
81
- afterEach(() => {
82
- if (collectionFeature) {
83
- collectionFeature.destroy();
84
- }
85
- });
86
-
87
- describe("Initialization", () => {
88
- it("should create collection feature", () => {
89
- expect(collectionFeature).toBeDefined();
90
- expect(collectionFeature.paginationStrategy).toBe("page");
91
- });
92
-
93
- it("should initialize with default pagination strategy", () => {
94
- const noStrategyContext = {
95
- ...mockContext,
96
- config: { ...mockContext.config, collection: undefined },
97
- };
98
-
99
- const feature = createCollectionFeature(noStrategyContext as any);
100
- expect(feature.paginationStrategy).toBe("page"); // Default
101
-
102
- feature.destroy();
103
- });
104
-
105
- it("should emit initialization event", () => {
106
- collectionFeature.initialize();
107
-
108
- expect(emitSpy).toHaveBeenCalledWith(
109
- ListManagerEvents.COLLECTION_INITIALIZED,
110
- expect.objectContaining({
111
- strategy: "page",
112
- pageSize: 20,
113
- })
114
- );
115
- });
116
-
117
- it("should setup speed tracker", () => {
118
- collectionFeature.initialize();
119
-
120
- // Speed tracker should be available
121
- expect(collectionFeature.getSpeedTracker).toBeDefined();
122
- });
123
- });
124
-
125
- describe("Template Management", () => {
126
- beforeEach(() => {
127
- collectionFeature.initialize();
128
- });
129
-
130
- it("should set and use template", () => {
131
- const template = (item: any, index: number) => {
132
- const div = document.createElement("div");
133
- div.textContent = `${index}: ${item.name}`;
134
- return div;
135
- };
136
-
137
- collectionFeature.setTemplate(template);
138
-
139
- // Template should be stored and accessible
140
- expect(collectionFeature.hasTemplate()).toBe(true);
141
- });
142
-
143
- it("should handle template rendering", () => {
144
- const template = mock((item: any, index: number) => {
145
- const div = document.createElement("div");
146
- div.textContent = `${index}: ${item.name}`;
147
- return div;
148
- });
149
-
150
- collectionFeature.setTemplate(template);
151
-
152
- const testItem = { id: 1, name: "Test Item" };
153
- const element = collectionFeature.renderItem(testItem, 5);
154
-
155
- expect(template).toHaveBeenCalledWith(testItem, 5);
156
- expect(element).toBeDefined();
157
- });
158
- });
159
-
160
- describe("Data Management", () => {
161
- beforeEach(() => {
162
- collectionFeature.initialize();
163
- });
164
-
165
- it("should set and store items", () => {
166
- const items = [
167
- { id: 1, name: "Item 1" },
168
- { id: 2, name: "Item 2" },
169
- { id: 3, name: "Item 3" },
170
- ];
171
-
172
- collectionFeature.setItems(items);
173
-
174
- expect(collectionFeature.getTotalItems()).toBe(3);
175
- expect(collectionFeature.hasItem(1)).toBe(true);
176
- expect(collectionFeature.getItem(1)).toEqual(items[1]);
177
- });
178
-
179
- it("should handle item updates", () => {
180
- const items = [
181
- { id: 1, name: "Item 1" },
182
- { id: 2, name: "Item 2" },
183
- ];
184
-
185
- collectionFeature.setItems(items);
186
-
187
- const updatedItem = { id: 1, name: "Updated Item 1" };
188
- collectionFeature.updateItem(0, updatedItem);
189
-
190
- expect(collectionFeature.getItem(0)).toEqual(updatedItem);
191
- });
192
-
193
- it("should get items in range", () => {
194
- const items = Array.from({ length: 50 }, (_, i) => ({
195
- id: i,
196
- name: `Item ${i}`,
197
- }));
198
- collectionFeature.setItems(items);
199
-
200
- const range: ItemRange = { start: 10, end: 19 };
201
- const rangeItems = collectionFeature.getItemsInRange(range);
202
-
203
- expect(rangeItems).toHaveLength(10);
204
- expect(rangeItems[0].item.id).toBe(10);
205
- expect(rangeItems[9].item.id).toBe(19);
206
- });
207
-
208
- it("should handle sparse data with placeholders", () => {
209
- collectionFeature.setTotalItems(100);
210
-
211
- // Set some items
212
- const items = [
213
- { id: 20, name: "Item 20" },
214
- { id: 21, name: "Item 21" },
215
- ];
216
- collectionFeature.setRangeItems({ start: 20, end: 21 }, items);
217
-
218
- const range: ItemRange = { start: 15, end: 25 };
219
- const rangeItems = collectionFeature.getItemsInRange(range);
220
-
221
- expect(rangeItems).toHaveLength(11);
222
-
223
- // Should have actual items and placeholders
224
- const actualItems = rangeItems.filter((item) => !item.item.loading);
225
- const placeholders = rangeItems.filter((item) => item.item.loading);
226
-
227
- expect(actualItems).toHaveLength(2);
228
- expect(placeholders).toHaveLength(9);
229
- });
230
- });
231
-
232
- describe("Pagination Strategies", () => {
233
- beforeEach(() => {
234
- collectionFeature.initialize();
235
- });
236
-
237
- it("should handle page-based pagination", () => {
238
- collectionFeature.adaptPaginationStrategy("page");
239
-
240
- const range: ItemRange = { start: 40, end: 59 };
241
- const params = collectionFeature.getRangeParams(range);
242
-
243
- expect(params.strategy).toBe("page");
244
- expect(params.page).toBe(3); // Page 3 for items 40-59 with pageSize 20
245
- expect(params.limit).toBe(20);
246
- });
247
-
248
- it("should handle offset-based pagination", () => {
249
- collectionFeature.adaptPaginationStrategy("offset");
250
-
251
- const range: ItemRange = { start: 30, end: 49 };
252
- const params = collectionFeature.getRangeParams(range);
253
-
254
- expect(params.strategy).toBe("offset");
255
- expect(params.offset).toBe(30);
256
- expect(params.limit).toBe(20);
257
- });
258
-
259
- it("should handle cursor-based pagination", () => {
260
- collectionFeature.adaptPaginationStrategy("cursor");
261
-
262
- const range: ItemRange = { start: 25, end: 44 };
263
- const params = collectionFeature.getRangeParams(range);
264
-
265
- expect(params.strategy).toBe("cursor");
266
- expect(params.after).toBe("item-24"); // Cursor after item 24
267
- expect(params.limit).toBe(20);
268
- });
269
-
270
- it("should emit strategy change events", () => {
271
- collectionFeature.adaptPaginationStrategy("cursor");
272
-
273
- expect(emitSpy).toHaveBeenCalledWith(
274
- ListManagerEvents.PAGINATION_STRATEGY_CHANGED,
275
- expect.objectContaining({
276
- strategy: "cursor",
277
- previousStrategy: "page",
278
- })
279
- );
280
- });
281
- });
282
-
283
- describe("Range Loading", () => {
284
- beforeEach(() => {
285
- collectionFeature.initialize();
286
- collectionFeature.setTotalItems(200);
287
- });
288
-
289
- it("should trigger loading for missing ranges", () => {
290
- const visibleRange: ItemRange = { start: 50, end: 69 };
291
-
292
- collectionFeature.handleVisibleRangeChange(visibleRange);
293
-
294
- expect(emitSpy).toHaveBeenCalledWith(
295
- ListManagerEvents.LOADING_TRIGGERED,
296
- expect.objectContaining({
297
- range: expect.objectContaining({
298
- start: expect.any(Number),
299
- end: expect.any(Number),
300
- }),
301
- strategy: expect.any(String),
302
- })
303
- );
304
- });
305
-
306
- it("should not trigger loading for already loaded ranges", () => {
307
- // Pre-load some data
308
- const items = Array.from({ length: 20 }, (_, i) => ({
309
- id: i + 50,
310
- name: `Item ${i + 50}`,
311
- }));
312
- collectionFeature.setRangeItems({ start: 50, end: 69 }, items);
313
-
314
- const visibleRange: ItemRange = { start: 55, end: 65 };
315
- collectionFeature.handleVisibleRangeChange(visibleRange);
316
-
317
- // Should not trigger loading for already loaded range
318
- const loadingCalls = emitSpy.mock.calls.filter(
319
- (call) => call[0] === ListManagerEvents.LOADING_TRIGGERED
320
- );
321
- expect(loadingCalls).toHaveLength(0);
322
- });
323
-
324
- it("should handle concurrent range loading", () => {
325
- const range1: ItemRange = { start: 0, end: 19 };
326
- const range2: ItemRange = { start: 20, end: 39 };
327
- const range3: ItemRange = { start: 40, end: 59 };
328
-
329
- // Trigger multiple range loads quickly
330
- collectionFeature.handleVisibleRangeChange(range1);
331
- collectionFeature.handleVisibleRangeChange(range2);
332
- collectionFeature.handleVisibleRangeChange(range3);
333
-
334
- // Should respect max concurrent requests limit
335
- const loadingCalls = emitSpy.mock.calls.filter(
336
- (call) => call[0] === ListManagerEvents.LOADING_TRIGGERED
337
- );
338
- expect(loadingCalls.length).toBeLessThanOrEqual(3); // Max concurrent
339
- });
340
-
341
- it("should emit loading completed events", () => {
342
- const range: ItemRange = { start: 80, end: 99 };
343
- const items = Array.from({ length: 20 }, (_, i) => ({
344
- id: i + 80,
345
- name: `Item ${i + 80}`,
346
- }));
347
-
348
- collectionFeature.setRangeItems(range, items);
349
-
350
- expect(emitSpy).toHaveBeenCalledWith(
351
- ListManagerEvents.LOADING_COMPLETED,
352
- expect.objectContaining({
353
- range,
354
- itemCount: 20,
355
- })
356
- );
357
- });
358
- });
359
-
360
- describe("Speed-Based Loading", () => {
361
- beforeEach(() => {
362
- collectionFeature.initialize();
363
- collectionFeature.setTotalItems(500);
364
- });
365
-
366
- it("should adapt loading based on scroll speed", () => {
367
- // Simulate fast scrolling
368
- collectionFeature.handleScrollPositionChange(1000, "forward");
369
- collectionFeature.handleScrollPositionChange(2000, "forward");
370
- collectionFeature.handleScrollPositionChange(3000, "forward");
371
-
372
- const speedTracker = collectionFeature.getSpeedTracker();
373
- expect(speedTracker.velocity).toBeGreaterThan(0);
374
-
375
- // Should trigger aggressive loading for fast scroll
376
- const visibleRange: ItemRange = { start: 100, end: 119 };
377
- collectionFeature.handleVisibleRangeChange(visibleRange);
378
-
379
- expect(emitSpy).toHaveBeenCalledWith(
380
- ListManagerEvents.LOADING_TRIGGERED,
381
- expect.objectContaining({
382
- strategy: expect.stringMatching(/aggressive|normal/),
383
- })
384
- );
385
- });
386
-
387
- it("should use conservative loading for slow scrolling", () => {
388
- // Simulate slow scrolling
389
- collectionFeature.handleScrollPositionChange(100, "forward");
390
- collectionFeature.handleScrollPositionChange(110, "forward");
391
-
392
- const visibleRange: ItemRange = { start: 40, end: 59 };
393
- collectionFeature.handleVisibleRangeChange(visibleRange);
394
-
395
- // Should use conservative strategy for slow scroll
396
- expect(emitSpy).toHaveBeenCalledWith(
397
- ListManagerEvents.LOADING_TRIGGERED,
398
- expect.objectContaining({
399
- strategy: expect.stringMatching(/conservative|normal/),
400
- })
401
- );
402
- });
403
-
404
- it("should handle direction changes", () => {
405
- // Scroll forward then backward
406
- collectionFeature.handleScrollPositionChange(500, "forward");
407
- collectionFeature.handleScrollPositionChange(300, "backward");
408
-
409
- const speedTracker = collectionFeature.getSpeedTracker();
410
- expect(speedTracker.direction).toBe("backward");
411
- });
412
- });
413
-
414
- describe("Placeholder System", () => {
415
- beforeEach(() => {
416
- collectionFeature.initialize();
417
- collectionFeature.setTotalItems(100);
418
- });
419
-
420
- it("should generate placeholders for missing items", () => {
421
- const range: ItemRange = { start: 20, end: 29 };
422
- const placeholders = collectionFeature.generatePlaceholders(range);
423
-
424
- expect(placeholders).toHaveLength(10);
425
- expect(placeholders[0].loading).toBe(true);
426
- expect(placeholders[0].id).toBe("placeholder-20");
427
- });
428
-
429
- it("should detect item structure from actual data", () => {
430
- const sampleItems = [
431
- { id: 1, name: "Item 1", value: 100, category: "A" },
432
- { id: 2, name: "Item 2", value: 200, category: "B" },
433
- ];
434
-
435
- collectionFeature.setItems(sampleItems);
436
-
437
- const range: ItemRange = { start: 10, end: 12 };
438
- const placeholders = collectionFeature.generatePlaceholders(range);
439
-
440
- // Placeholders should have similar structure
441
- expect(placeholders[0]).toHaveProperty("id");
442
- expect(placeholders[0]).toHaveProperty("name");
443
- expect(placeholders[0]).toHaveProperty("value");
444
- expect(placeholders[0]).toHaveProperty("category");
445
- });
446
-
447
- it("should show placeholders while loading", () => {
448
- const range: ItemRange = { start: 50, end: 59 };
449
-
450
- // Request data (should show placeholders immediately)
451
- collectionFeature.handleVisibleRangeChange(range);
452
-
453
- const items = collectionFeature.getItemsInRange(range);
454
- const placeholderCount = items.filter((item) => item.item.loading).length;
455
-
456
- expect(placeholderCount).toBeGreaterThan(0);
457
-
458
- expect(emitSpy).toHaveBeenCalledWith(
459
- ListManagerEvents.PLACEHOLDERS_SHOWN,
460
- expect.objectContaining({
461
- range,
462
- count: placeholderCount,
463
- })
464
- );
465
- });
466
-
467
- it("should replace placeholders with actual data", () => {
468
- const range: ItemRange = { start: 30, end: 39 };
469
-
470
- // First, get placeholders
471
- collectionFeature.handleVisibleRangeChange(range);
472
- let items = collectionFeature.getItemsInRange(range);
473
- let placeholderCount = items.filter((item) => item.item.loading).length;
474
- expect(placeholderCount).toBeGreaterThan(0);
475
-
476
- // Then load actual data
477
- const actualItems = Array.from({ length: 10 }, (_, i) => ({
478
- id: i + 30,
479
- name: `Item ${i + 30}`,
480
- }));
481
- collectionFeature.setRangeItems(range, actualItems);
482
-
483
- // Placeholders should be replaced
484
- items = collectionFeature.getItemsInRange(range);
485
- placeholderCount = items.filter((item) => item.item.loading).length;
486
- expect(placeholderCount).toBe(0);
487
- });
488
- });
489
-
490
- describe("Error Handling", () => {
491
- beforeEach(() => {
492
- collectionFeature.initialize();
493
- collectionFeature.setTotalItems(100);
494
- });
495
-
496
- it("should handle loading errors", () => {
497
- const range: ItemRange = { start: 40, end: 59 };
498
- const error = new Error("Network error");
499
-
500
- collectionFeature.handleRangeError(range, error);
501
-
502
- expect(emitSpy).toHaveBeenCalledWith(
503
- ListManagerEvents.LOADING_ERROR,
504
- expect.objectContaining({
505
- range,
506
- error: error.message,
507
- retryAttempt: expect.any(Number),
508
- })
509
- );
510
- });
511
-
512
- it("should retry failed requests", () => {
513
- const range: ItemRange = { start: 60, end: 79 };
514
- const error = new Error("Timeout");
515
-
516
- // First failure
517
- collectionFeature.handleRangeError(range, error);
518
-
519
- // Should emit retry event
520
- expect(emitSpy).toHaveBeenCalledWith(
521
- ListManagerEvents.LOADING_RETRY,
522
- expect.objectContaining({
523
- range,
524
- attempt: 1,
525
- })
526
- );
527
- });
528
-
529
- it("should stop retrying after max attempts", () => {
530
- const range: ItemRange = { start: 80, end: 99 };
531
- const error = new Error("Persistent error");
532
-
533
- // Exhaust retry attempts
534
- for (let i = 0; i < 5; i++) {
535
- collectionFeature.handleRangeError(range, error);
536
- }
537
-
538
- expect(emitSpy).toHaveBeenCalledWith(
539
- ListManagerEvents.LOADING_FAILED,
540
- expect.objectContaining({
541
- range,
542
- finalError: error.message,
543
- })
544
- );
545
- });
546
- });
547
-
548
- describe("Memory Management", () => {
549
- beforeEach(() => {
550
- collectionFeature.initialize();
551
- });
552
-
553
- it("should clean up distant ranges", () => {
554
- collectionFeature.setTotalItems(1000);
555
-
556
- // Load multiple ranges
557
- const ranges = [
558
- { start: 0, end: 19 },
559
- { start: 200, end: 219 },
560
- { start: 400, end: 419 },
561
- { start: 600, end: 619 },
562
- { start: 800, end: 819 },
563
- ];
564
-
565
- ranges.forEach((range) => {
566
- const items = Array.from({ length: 20 }, (_, i) => ({
567
- id: i + range.start,
568
- name: `Item ${i + range.start}`,
569
- }));
570
- collectionFeature.setRangeItems(range, items);
571
- });
572
-
573
- // Move to a visible range in the middle
574
- const visibleRange: ItemRange = { start: 400, end: 419 };
575
- collectionFeature.handleVisibleRangeChange(visibleRange);
576
-
577
- // Should trigger cleanup of distant ranges
578
- expect(emitSpy).toHaveBeenCalledWith(
579
- ListManagerEvents.MEMORY_CLEANUP,
580
- expect.objectContaining({
581
- cleanedRanges: expect.any(Array),
582
- totalItemsRemoved: expect.any(Number),
583
- })
584
- );
585
- });
586
-
587
- it("should preserve recently accessed ranges", () => {
588
- collectionFeature.setTotalItems(500);
589
-
590
- // Load and access some ranges
591
- const recentRange: ItemRange = { start: 100, end: 119 };
592
- const items = Array.from({ length: 20 }, (_, i) => ({
593
- id: i + 100,
594
- name: `Item ${i + 100}`,
595
- }));
596
- collectionFeature.setRangeItems(recentRange, items);
597
-
598
- // Access the range (marks as recently used)
599
- collectionFeature.getItemsInRange(recentRange);
600
-
601
- // Move away and trigger cleanup
602
- const farRange: ItemRange = { start: 400, end: 419 };
603
- collectionFeature.handleVisibleRangeChange(farRange);
604
-
605
- // Recent range should still be available
606
- expect(collectionFeature.hasRangeData(recentRange)).toBe(true);
607
- });
608
- });
609
-
610
- describe("Performance Monitoring", () => {
611
- beforeEach(() => {
612
- collectionFeature.initialize();
613
- collectionFeature.setTotalItems(200);
614
- });
615
-
616
- it("should track loading performance", () => {
617
- const range: ItemRange = { start: 60, end: 79 };
618
- const startTime = Date.now();
619
-
620
- // Simulate loading start
621
- collectionFeature.handleVisibleRangeChange(range);
622
-
623
- // Simulate loading completion after delay
624
- setTimeout(() => {
625
- const items = Array.from({ length: 20 }, (_, i) => ({
626
- id: i + 60,
627
- name: `Item ${i + 60}`,
628
- }));
629
- collectionFeature.setRangeItems(range, items);
630
-
631
- expect(emitSpy).toHaveBeenCalledWith(
632
- ListManagerEvents.PERFORMANCE_METRIC,
633
- expect.objectContaining({
634
- metric: "loadingTime",
635
- value: expect.any(Number),
636
- range,
637
- })
638
- );
639
- }, 100);
640
- });
641
-
642
- it("should monitor memory usage", () => {
643
- // Load substantial amount of data
644
- for (let i = 0; i < 10; i++) {
645
- const range: ItemRange = { start: i * 20, end: (i + 1) * 20 - 1 };
646
- const items = Array.from({ length: 20 }, (_, j) => ({
647
- id: j + i * 20,
648
- name: `Item ${j + i * 20}`,
649
- }));
650
- collectionFeature.setRangeItems(range, items);
651
- }
652
-
653
- expect(emitSpy).toHaveBeenCalledWith(
654
- ListManagerEvents.PERFORMANCE_METRIC,
655
- expect.objectContaining({
656
- metric: "memoryUsage",
657
- value: expect.any(Number),
658
- })
659
- );
660
- });
661
- });
662
-
663
- describe("Cleanup and Destruction", () => {
664
- it("should clean up resources on destroy", () => {
665
- collectionFeature.initialize();
666
-
667
- // Load some data
668
- const items = Array.from({ length: 50 }, (_, i) => ({
669
- id: i,
670
- name: `Item ${i}`,
671
- }));
672
- collectionFeature.setItems(items);
673
-
674
- collectionFeature.destroy();
675
-
676
- expect(emitSpy).toHaveBeenCalledWith(
677
- ListManagerEvents.COLLECTION_DESTROYED,
678
- expect.any(Object)
679
- );
680
-
681
- // Data should be cleared
682
- expect(collectionFeature.getTotalItems()).toBe(0);
683
- });
684
-
685
- it("should cancel pending requests on destroy", () => {
686
- collectionFeature.initialize();
687
- collectionFeature.setTotalItems(100);
688
-
689
- // Trigger loading
690
- const range: ItemRange = { start: 20, end: 39 };
691
- collectionFeature.handleVisibleRangeChange(range);
692
-
693
- // Destroy before loading completes
694
- collectionFeature.destroy();
695
-
696
- expect(emitSpy).toHaveBeenCalledWith(
697
- ListManagerEvents.LOADING_CANCELLED,
698
- expect.objectContaining({
699
- cancelledRanges: expect.any(Array),
700
- })
701
- );
702
- });
703
- });
704
- });