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,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
|
-
});
|