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.
- package/AI.md +28 -230
- package/CLAUDE.md +882 -0
- package/build.js +253 -24
- package/package.json +14 -4
- package/scripts/debug/vlist-selection.ts +121 -0
- package/src/components/index.ts +5 -41
- package/src/components/{list → vlist}/config.ts +66 -95
- package/src/components/vlist/constants.ts +23 -0
- package/src/components/vlist/features/api.ts +626 -0
- package/src/components/vlist/features/index.ts +10 -0
- package/src/components/vlist/features/selection.ts +436 -0
- package/src/components/vlist/features/viewport.ts +59 -0
- package/src/components/vlist/index.ts +17 -0
- package/src/components/{list → vlist}/types.ts +242 -32
- package/src/components/vlist/vlist.ts +92 -0
- package/src/core/compose/features/gestures/index.ts +227 -0
- package/src/core/compose/features/gestures/longpress.ts +383 -0
- package/src/core/compose/features/gestures/pan.ts +424 -0
- package/src/core/compose/features/gestures/pinch.ts +475 -0
- package/src/core/compose/features/gestures/rotate.ts +485 -0
- package/src/core/compose/features/gestures/swipe.ts +492 -0
- package/src/core/compose/features/gestures/tap.ts +334 -0
- package/src/core/compose/features/index.ts +2 -38
- package/src/core/compose/index.ts +13 -29
- package/src/core/gestures/index.ts +31 -0
- package/src/core/gestures/longpress.ts +68 -0
- package/src/core/gestures/manager.ts +418 -0
- package/src/core/gestures/pan.ts +48 -0
- package/src/core/gestures/pinch.ts +58 -0
- package/src/core/gestures/rotate.ts +58 -0
- package/src/core/gestures/swipe.ts +66 -0
- package/src/core/gestures/tap.ts +45 -0
- package/src/core/gestures/types.ts +387 -0
- package/src/core/gestures/utils.ts +128 -0
- package/src/core/index.ts +27 -151
- package/src/core/layout/schema.ts +153 -72
- package/src/core/layout/types.ts +5 -2
- package/src/core/viewport/constants.ts +145 -0
- package/src/core/viewport/features/base.ts +73 -0
- package/src/core/viewport/features/collection.ts +1182 -0
- package/src/core/viewport/features/events.ts +130 -0
- package/src/core/viewport/features/index.ts +20 -0
- package/src/core/{list-manager/features/viewport → viewport/features}/item-size.ts +31 -34
- package/src/core/{list-manager/features/viewport → viewport/features}/loading.ts +4 -4
- package/src/core/viewport/features/momentum.ts +269 -0
- package/src/core/viewport/features/placeholders.ts +335 -0
- package/src/core/viewport/features/rendering.ts +962 -0
- package/src/core/viewport/features/scrollbar.ts +434 -0
- package/src/core/viewport/features/scrolling.ts +634 -0
- package/src/core/viewport/features/utils.ts +94 -0
- package/src/core/viewport/features/virtual.ts +525 -0
- package/src/core/viewport/index.ts +31 -0
- package/src/core/viewport/types.ts +133 -0
- package/src/core/viewport/utils/speed-tracker.ts +79 -0
- package/src/core/viewport/viewport.ts +265 -0
- package/src/index.ts +0 -7
- package/src/styles/components/_vlist.scss +352 -0
- package/src/styles/index.scss +1 -1
- package/test/components/vlist-selection.test.ts +240 -0
- package/test/components/vlist.test.ts +63 -0
- package/test/core/collection/adapter.test.ts +161 -0
- package/bun.lock +0 -792
- package/src/components/list/api.ts +0 -314
- package/src/components/list/constants.ts +0 -56
- package/src/components/list/features/api.ts +0 -428
- package/src/components/list/features/index.ts +0 -31
- package/src/components/list/features/list-manager.ts +0 -502
- package/src/components/list/index.ts +0 -39
- package/src/components/list/list.ts +0 -234
- package/src/core/collection/base-collection.ts +0 -100
- package/src/core/collection/collection-composer.ts +0 -178
- package/src/core/collection/collection.ts +0 -745
- package/src/core/collection/constants.ts +0 -172
- package/src/core/collection/events.ts +0 -428
- package/src/core/collection/features/api/loading.ts +0 -279
- package/src/core/collection/features/operations/data-operations.ts +0 -147
- package/src/core/collection/index.ts +0 -104
- package/src/core/collection/state.ts +0 -497
- package/src/core/collection/types.ts +0 -404
- package/src/core/compose/features/collection.ts +0 -119
- package/src/core/compose/features/selection.ts +0 -213
- package/src/core/compose/features/styling.ts +0 -108
- package/src/core/list-manager/api.ts +0 -599
- package/src/core/list-manager/config.ts +0 -593
- package/src/core/list-manager/constants.ts +0 -268
- package/src/core/list-manager/features/api.ts +0 -58
- package/src/core/list-manager/features/collection/collection.ts +0 -705
- package/src/core/list-manager/features/collection/index.ts +0 -17
- package/src/core/list-manager/features/viewport/constants.ts +0 -42
- package/src/core/list-manager/features/viewport/index.ts +0 -16
- package/src/core/list-manager/features/viewport/placeholders.ts +0 -281
- package/src/core/list-manager/features/viewport/rendering.ts +0 -575
- package/src/core/list-manager/features/viewport/scrollbar.ts +0 -495
- package/src/core/list-manager/features/viewport/scrolling.ts +0 -795
- package/src/core/list-manager/features/viewport/template.ts +0 -220
- package/src/core/list-manager/features/viewport/viewport.ts +0 -654
- package/src/core/list-manager/features/viewport/virtual.ts +0 -309
- package/src/core/list-manager/index.ts +0 -279
- package/src/core/list-manager/list-manager.ts +0 -206
- package/src/core/list-manager/types.ts +0 -439
- package/src/core/list-manager/utils/calculations.ts +0 -290
- package/src/core/list-manager/utils/range-calculator.ts +0 -349
- package/src/core/list-manager/utils/speed-tracker.ts +0 -273
- package/src/styles/components/_list.scss +0 -244
- package/src/types/mtrl.d.ts +0 -6
- package/test/components/list.test.ts +0 -256
- package/test/core/collection/failed-ranges.test.ts +0 -270
- package/test/core/compose/features.test.ts +0 -183
- package/test/core/list-manager/features/collection.test.ts +0 -704
- package/test/core/list-manager/features/viewport.test.ts +0 -698
- package/test/core/list-manager/list-manager.test.ts +0 -593
- package/test/core/list-manager/utils/calculations.test.ts +0 -433
- package/test/core/list-manager/utils/range-calculator.test.ts +0 -569
- package/test/core/list-manager/utils/speed-tracker.test.ts +0 -530
- package/tsconfig.build.json +0 -23
- /package/src/components/{list → vlist}/features.ts +0 -0
- /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
|
-
});
|