@pdanpdan/virtual-scroll 0.2.0 → 0.3.0
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/README.md +182 -88
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +100 -35
- package/dist/index.js +1 -823
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +902 -0
- package/dist/index.mjs.map +1 -0
- package/dist/virtual-scroll.css +2 -0
- package/package.json +9 -6
- package/src/components/VirtualScroll.test.ts +397 -329
- package/src/components/VirtualScroll.vue +107 -25
- package/src/composables/useVirtualScroll.test.ts +1029 -255
- package/src/composables/useVirtualScroll.ts +176 -88
- package/src/utils/fenwick-tree.test.ts +80 -65
- package/dist/index.css +0 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* global ScrollToOptions */
|
|
1
2
|
import type { VirtualScrollProps } from './useVirtualScroll';
|
|
2
3
|
import type { Ref } from 'vue';
|
|
3
4
|
|
|
@@ -75,11 +76,22 @@ describe('useVirtualScroll', () => {
|
|
|
75
76
|
beforeEach(() => {
|
|
76
77
|
window.scrollX = 0;
|
|
77
78
|
window.scrollY = 0;
|
|
79
|
+
Object.defineProperty(window, 'innerHeight', { configurable: true, value: 500 });
|
|
80
|
+
Object.defineProperty(window, 'innerWidth', { configurable: true, value: 500 });
|
|
81
|
+
window.scrollTo = vi.fn().mockImplementation((options: ScrollToOptions) => {
|
|
82
|
+
if (options.left !== undefined) {
|
|
83
|
+
window.scrollX = options.left;
|
|
84
|
+
}
|
|
85
|
+
if (options.top !== undefined) {
|
|
86
|
+
window.scrollY = options.top;
|
|
87
|
+
}
|
|
88
|
+
window.dispatchEvent(new Event('scroll'));
|
|
89
|
+
});
|
|
78
90
|
vi.clearAllMocks();
|
|
79
91
|
vi.useRealTimers();
|
|
80
92
|
});
|
|
81
93
|
|
|
82
|
-
describe('initialization and
|
|
94
|
+
describe('initialization and dimensions', () => {
|
|
83
95
|
it('should initialize with correct total height', async () => {
|
|
84
96
|
const { result } = setup({ ...defaultProps });
|
|
85
97
|
expect(result.totalHeight.value).toBe(5000);
|
|
@@ -105,11 +117,11 @@ describe('useVirtualScroll', () => {
|
|
|
105
117
|
|
|
106
118
|
it('should recalculate when gaps change', async () => {
|
|
107
119
|
const { result, props } = setup({ ...defaultProps, gap: 10 });
|
|
108
|
-
expect(result.totalHeight.value).toBe(
|
|
120
|
+
expect(result.totalHeight.value).toBe(5990); // 100 * (50 + 10) - 10
|
|
109
121
|
|
|
110
122
|
props.value.gap = 20;
|
|
111
123
|
await nextTick();
|
|
112
|
-
expect(result.totalHeight.value).toBe(
|
|
124
|
+
expect(result.totalHeight.value).toBe(6980); // 100 * (50 + 20) - 20
|
|
113
125
|
});
|
|
114
126
|
|
|
115
127
|
it('should handle itemSize as a function', async () => {
|
|
@@ -118,6 +130,7 @@ describe('useVirtualScroll', () => {
|
|
|
118
130
|
itemSize: (_item: { id: number; }, index: number) => 50 + index,
|
|
119
131
|
});
|
|
120
132
|
// 50*100 + (0+99)*100/2 = 5000 + 4950 = 9950
|
|
133
|
+
// 9950 - gap(0) = 9950
|
|
121
134
|
expect(result.totalHeight.value).toBe(9950);
|
|
122
135
|
});
|
|
123
136
|
|
|
@@ -128,13 +141,13 @@ describe('useVirtualScroll', () => {
|
|
|
128
141
|
columnCount: 10,
|
|
129
142
|
columnWidth: 100,
|
|
130
143
|
});
|
|
131
|
-
expect(result.totalWidth.value).toBe(1000);
|
|
132
|
-
expect(result.totalHeight.value).toBe(5000);
|
|
144
|
+
expect(result.totalWidth.value).toBe(1000); // 10 * 100 - 0
|
|
145
|
+
expect(result.totalHeight.value).toBe(5000); // 100 * 50 - 0
|
|
133
146
|
});
|
|
134
147
|
|
|
135
148
|
it('should handle horizontal direction', async () => {
|
|
136
149
|
const { result } = setup({ ...defaultProps, direction: 'horizontal' });
|
|
137
|
-
expect(result.totalWidth.value).toBe(5000);
|
|
150
|
+
expect(result.totalWidth.value).toBe(5000); // 100 * 50 - 0
|
|
138
151
|
expect(result.totalHeight.value).toBe(0);
|
|
139
152
|
});
|
|
140
153
|
|
|
@@ -147,7 +160,7 @@ describe('useVirtualScroll', () => {
|
|
|
147
160
|
});
|
|
148
161
|
});
|
|
149
162
|
|
|
150
|
-
describe('range
|
|
163
|
+
describe('range calculation', () => {
|
|
151
164
|
it('should calculate rendered items based on scroll position', async () => {
|
|
152
165
|
const { result } = setup({ ...defaultProps });
|
|
153
166
|
expect(result.renderedItems.value.length).toBeGreaterThan(0);
|
|
@@ -175,45 +188,97 @@ describe('useVirtualScroll', () => {
|
|
|
175
188
|
await nextTick();
|
|
176
189
|
expect(result.scrollDetails.value.currentIndex).toBeGreaterThan(0);
|
|
177
190
|
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('dynamic sizing', () => {
|
|
194
|
+
it('should handle columnCount fallback in updateItemSizes', async () => {
|
|
195
|
+
const { result, props } = setup({
|
|
196
|
+
...defaultProps,
|
|
197
|
+
direction: 'both',
|
|
198
|
+
columnCount: 10,
|
|
199
|
+
columnWidth: undefined,
|
|
200
|
+
});
|
|
201
|
+
await nextTick();
|
|
202
|
+
|
|
203
|
+
const cell = document.createElement('div');
|
|
204
|
+
cell.dataset.colIndex = '0';
|
|
205
|
+
|
|
206
|
+
// Getter that returns 10 first time (for guard) and null second time (for fallback)
|
|
207
|
+
let count = 0;
|
|
208
|
+
Object.defineProperty(props.value, 'columnCount', {
|
|
209
|
+
get() {
|
|
210
|
+
count++;
|
|
211
|
+
return count === 1 ? 10 : null;
|
|
212
|
+
},
|
|
213
|
+
configurable: true,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
result.updateItemSizes([ { index: 0, inlineSize: 200, blockSize: 50, element: cell } ]);
|
|
217
|
+
await nextTick();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should handle updateItemSizes with direct cell element', async () => {
|
|
221
|
+
const { result } = setup({
|
|
222
|
+
...defaultProps,
|
|
223
|
+
direction: 'both',
|
|
224
|
+
columnCount: 2,
|
|
225
|
+
columnWidth: undefined,
|
|
226
|
+
});
|
|
227
|
+
await nextTick();
|
|
178
228
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
229
|
+
const cell = document.createElement('div');
|
|
230
|
+
Object.defineProperty(cell, 'offsetWidth', { value: 200 });
|
|
231
|
+
cell.dataset.colIndex = '0';
|
|
232
|
+
|
|
233
|
+
result.updateItemSizes([ { index: 0, inlineSize: 200, blockSize: 50, element: cell } ]);
|
|
234
|
+
await nextTick();
|
|
235
|
+
expect(result.getColumnWidth(0)).toBe(200);
|
|
182
236
|
});
|
|
183
237
|
|
|
184
|
-
it('should
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
238
|
+
it('should handle updateItemSizes initial measurement even if smaller than estimate', async () => {
|
|
239
|
+
// Horizontal
|
|
240
|
+
const { result: rH } = setup({ ...defaultProps, direction: 'horizontal', itemSize: undefined });
|
|
241
|
+
await nextTick();
|
|
242
|
+
// Estimate is 40 (new DEFAULT_ITEM_SIZE). Update with 30.
|
|
243
|
+
rH.updateItemSizes([ { index: 0, inlineSize: 30, blockSize: 30 } ]);
|
|
244
|
+
await nextTick();
|
|
245
|
+
expect(rH.renderedItems.value[ 0 ]?.size.width).toBe(30);
|
|
246
|
+
|
|
247
|
+
// Subsequent update with smaller size should also be applied now
|
|
248
|
+
rH.updateItemSizes([ { index: 0, inlineSize: 25, blockSize: 25 } ]);
|
|
249
|
+
await nextTick();
|
|
250
|
+
expect(rH.renderedItems.value[ 0 ]?.size.width).toBe(25);
|
|
188
251
|
|
|
189
|
-
//
|
|
190
|
-
result
|
|
252
|
+
// Vertical
|
|
253
|
+
const { result: rV } = setup({ ...defaultProps, direction: 'vertical', itemSize: undefined });
|
|
254
|
+
await nextTick();
|
|
255
|
+
rV.updateItemSizes([ { index: 0, inlineSize: 30, blockSize: 30 } ]);
|
|
191
256
|
await nextTick();
|
|
257
|
+
expect(rV.renderedItems.value[ 0 ]?.size.height).toBe(30);
|
|
192
258
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
259
|
+
// Subsequent update with smaller size should be applied
|
|
260
|
+
rV.updateItemSizes([ { index: 0, inlineSize: 20, blockSize: 20 } ]);
|
|
261
|
+
await nextTick();
|
|
262
|
+
expect(rV.renderedItems.value[ 0 ]?.size.height).toBe(20);
|
|
196
263
|
});
|
|
197
|
-
});
|
|
198
264
|
|
|
199
|
-
|
|
200
|
-
it('should update item size and trigger reactivity', async () => {
|
|
265
|
+
it('should handle updateItemSize and trigger reactivity', async () => {
|
|
201
266
|
const { result } = setup({ ...defaultProps, itemSize: undefined });
|
|
202
|
-
expect(result.totalHeight.value).toBe(
|
|
267
|
+
expect(result.totalHeight.value).toBe(4000); // 100 * 40
|
|
203
268
|
|
|
204
269
|
result.updateItemSize(0, 100, 100);
|
|
205
270
|
await nextTick();
|
|
206
|
-
expect(result.totalHeight.value).toBe(
|
|
271
|
+
expect(result.totalHeight.value).toBe(4060); // 4000 - 40 + 100
|
|
207
272
|
expect(result.renderedItems.value[ 0 ]!.size.height).toBe(100);
|
|
208
273
|
});
|
|
209
274
|
|
|
210
275
|
it('should treat 0, null, undefined as dynamic itemSize', async () => {
|
|
211
276
|
for (const val of [ 0, null, undefined ]) {
|
|
212
277
|
const { result } = setup({ ...defaultProps, itemSize: val as unknown as undefined });
|
|
213
|
-
expect(result.totalHeight.value).toBe(
|
|
278
|
+
expect(result.totalHeight.value).toBe(4000);
|
|
214
279
|
result.updateItemSize(0, 100, 100);
|
|
215
280
|
await nextTick();
|
|
216
|
-
expect(result.totalHeight.value).toBe(
|
|
281
|
+
expect(result.totalHeight.value).toBe(4060);
|
|
217
282
|
}
|
|
218
283
|
});
|
|
219
284
|
|
|
@@ -225,7 +290,7 @@ describe('useVirtualScroll', () => {
|
|
|
225
290
|
columnCount: 2,
|
|
226
291
|
columnWidth: val as unknown as undefined,
|
|
227
292
|
});
|
|
228
|
-
expect(result.getColumnWidth(0)).toBe(
|
|
293
|
+
expect(result.getColumnWidth(0)).toBe(100); // DEFAULT_COLUMN_WIDTH
|
|
229
294
|
const parent = document.createElement('div');
|
|
230
295
|
const col0 = document.createElement('div');
|
|
231
296
|
Object.defineProperty(col0, 'offsetWidth', { value: 200, configurable: true });
|
|
@@ -233,7 +298,7 @@ describe('useVirtualScroll', () => {
|
|
|
233
298
|
parent.appendChild(col0);
|
|
234
299
|
result.updateItemSize(0, 200, 50, parent);
|
|
235
300
|
await nextTick();
|
|
236
|
-
expect(result.totalWidth.value).toBe(
|
|
301
|
+
expect(result.totalWidth.value).toBe(300); // 200 + 100
|
|
237
302
|
}
|
|
238
303
|
});
|
|
239
304
|
|
|
@@ -281,14 +346,21 @@ describe('useVirtualScroll', () => {
|
|
|
281
346
|
expect(result.totalWidth.value).toBe(10 * 200); // 10 columns * 200 defaultColumnWidth
|
|
282
347
|
});
|
|
283
348
|
|
|
284
|
-
it('should ignore small delta updates in updateItemSize', async () => {
|
|
349
|
+
it('should ignore small delta updates in updateItemSize only after first measurement', async () => {
|
|
285
350
|
const { result } = setup({ ...defaultProps, itemSize: undefined });
|
|
286
|
-
|
|
351
|
+
// Default is 40. 40.1 is < 0.5 delta.
|
|
352
|
+
// First measurement should be accepted even if small delta from estimate.
|
|
353
|
+
result.updateItemSize(0, 40.1, 40.1);
|
|
287
354
|
await nextTick();
|
|
288
|
-
expect(result.
|
|
355
|
+
expect(result.renderedItems.value[ 0 ]!.size.height).toBe(40.1);
|
|
356
|
+
|
|
357
|
+
// Second measurement with small delta from first should be ignored.
|
|
358
|
+
result.updateItemSize(0, 40.2, 40.2);
|
|
359
|
+
await nextTick();
|
|
360
|
+
expect(result.renderedItems.value[ 0 ]!.size.height).toBe(40.1);
|
|
289
361
|
});
|
|
290
362
|
|
|
291
|
-
it('should
|
|
363
|
+
it('should update item height in both mode now (allow decreases)', async () => {
|
|
292
364
|
const { result } = setup({ ...defaultProps, direction: 'both', itemSize: undefined, columnCount: 2 });
|
|
293
365
|
result.updateItemSize(0, 100, 100);
|
|
294
366
|
await nextTick();
|
|
@@ -296,7 +368,7 @@ describe('useVirtualScroll', () => {
|
|
|
296
368
|
|
|
297
369
|
result.updateItemSize(0, 100, 80);
|
|
298
370
|
await nextTick();
|
|
299
|
-
expect(result.renderedItems.value[ 0 ]!.size.height).toBe(
|
|
371
|
+
expect(result.renderedItems.value[ 0 ]!.size.height).toBe(80);
|
|
300
372
|
});
|
|
301
373
|
|
|
302
374
|
it('should update item height in vertical mode', async () => {
|
|
@@ -304,116 +376,54 @@ describe('useVirtualScroll', () => {
|
|
|
304
376
|
result.updateItemSize(0, 100, 100);
|
|
305
377
|
await nextTick();
|
|
306
378
|
expect(result.renderedItems.value[ 0 ]!.size.height).toBe(100);
|
|
379
|
+
|
|
380
|
+
result.updateItemSize(0, 100, 70);
|
|
381
|
+
await nextTick();
|
|
382
|
+
expect(result.renderedItems.value[ 0 ]!.size.height).toBe(70);
|
|
307
383
|
});
|
|
308
384
|
|
|
309
385
|
it('should handle updateItemSize for horizontal direction', async () => {
|
|
310
386
|
const { result } = setup({ ...defaultProps, direction: 'horizontal', itemSize: undefined });
|
|
311
387
|
result.updateItemSize(0, 100, 50);
|
|
312
388
|
await nextTick();
|
|
313
|
-
expect(result.totalWidth.value).toBe(
|
|
389
|
+
expect(result.totalWidth.value).toBe(4060); // 4000 - 40 + 100
|
|
314
390
|
});
|
|
315
391
|
|
|
316
392
|
it('should preserve measurements in initializeSizes when dynamic', async () => {
|
|
317
393
|
const { result, props } = setup({ ...defaultProps, itemSize: undefined });
|
|
318
394
|
result.updateItemSize(0, 100, 100);
|
|
319
395
|
await nextTick();
|
|
320
|
-
expect(result.totalHeight.value).toBe(
|
|
396
|
+
expect(result.totalHeight.value).toBe(4060);
|
|
321
397
|
|
|
322
398
|
// Trigger initializeSizes by changing length
|
|
323
399
|
props.value.items = Array.from({ length: 101 }, (_, i) => ({ id: i }));
|
|
324
400
|
await nextTick();
|
|
325
|
-
// Should still be 100 for index 0, not reset to default
|
|
326
|
-
expect(result.totalHeight.value).toBe(
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
it('should track max dimensions in updateItemSize', async () => {
|
|
330
|
-
const { result } = setup({ ...defaultProps, direction: 'both', itemSize: undefined, columnCount: 2 });
|
|
331
|
-
// Initial maxWidth is 0 (since vertical direction didn't set it for X)
|
|
332
|
-
// Wait, in 'both' mode, initializeSizes sets it.
|
|
333
|
-
|
|
334
|
-
result.updateItemSize(0, 5000, 6000);
|
|
335
|
-
await nextTick();
|
|
336
|
-
// Should have hit maxWidth.value = width
|
|
401
|
+
// Should still be 100 for index 0, not reset to default 40
|
|
402
|
+
expect(result.totalHeight.value).toBe(4060 + 40);
|
|
337
403
|
});
|
|
404
|
+
});
|
|
338
405
|
|
|
339
|
-
|
|
406
|
+
describe('scrolling and API', () => {
|
|
407
|
+
it('should handle scrollToIndex with horizontal direction and dynamic item size', async () => {
|
|
340
408
|
const container = document.createElement('div');
|
|
341
|
-
Object.defineProperty(container, 'clientWidth', { value: 500 });
|
|
342
|
-
const { result } = setup({ ...defaultProps, direction: '
|
|
343
|
-
await nextTick();
|
|
344
|
-
const parent = document.createElement('div');
|
|
345
|
-
const spacer = document.createElement('div');
|
|
346
|
-
Object.defineProperty(spacer, 'offsetWidth', { value: 1000 });
|
|
347
|
-
parent.appendChild(spacer);
|
|
348
|
-
result.updateItemSize(0, 100, 50, parent);
|
|
409
|
+
Object.defineProperty(container, 'clientWidth', { configurable: true, value: 500 });
|
|
410
|
+
const { result } = setup({ ...defaultProps, container, direction: 'horizontal', itemSize: undefined });
|
|
349
411
|
await nextTick();
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
it('should allow columns to shrink on first measurement', async () => {
|
|
353
|
-
const { result } = setup({ ...defaultProps, direction: 'both', columnCount: 2, columnWidth: undefined });
|
|
354
|
-
// Default estimate is 150
|
|
355
|
-
expect(result.getColumnWidth(0)).toBe(150);
|
|
356
|
-
|
|
357
|
-
const parent = document.createElement('div');
|
|
358
|
-
const child = document.createElement('div');
|
|
359
|
-
Object.defineProperty(child, 'offsetWidth', { value: 100 });
|
|
360
|
-
child.dataset.colIndex = '0';
|
|
361
|
-
parent.appendChild(child);
|
|
362
412
|
|
|
363
|
-
//
|
|
364
|
-
result.
|
|
413
|
+
// index 10. itemSize is 40 by default. totalWidth = 4000.
|
|
414
|
+
result.scrollToIndex(null, 10, { align: 'start', behavior: 'auto' });
|
|
365
415
|
await nextTick();
|
|
366
|
-
expect(result.
|
|
416
|
+
expect(result.scrollDetails.value.scrollOffset.x).toBe(400);
|
|
367
417
|
});
|
|
368
418
|
|
|
369
|
-
it('should
|
|
370
|
-
const { result } = setup({ ...defaultProps,
|
|
371
|
-
// Default estimate is 50
|
|
372
|
-
expect(result.renderedItems.value[ 0 ]!.size.height).toBe(50);
|
|
373
|
-
|
|
374
|
-
// First measurement is 20 (smaller than 50)
|
|
375
|
-
result.updateItemSize(0, 50, 20);
|
|
376
|
-
await nextTick();
|
|
377
|
-
expect(result.renderedItems.value[ 0 ]!.size.height).toBe(20);
|
|
378
|
-
|
|
379
|
-
// Second measurement is 10 (smaller than 20) - should NOT shrink
|
|
380
|
-
result.updateItemSize(0, 50, 10);
|
|
381
|
-
await nextTick();
|
|
382
|
-
expect(result.renderedItems.value[ 0 ]!.size.height).toBe(20);
|
|
383
|
-
|
|
384
|
-
// Third measurement is 30 (larger than 20) - SHOULD grow
|
|
385
|
-
result.updateItemSize(0, 50, 30);
|
|
419
|
+
it('should handle scrollToIndex with window fallback when container is missing', async () => {
|
|
420
|
+
const { result } = setup({ ...defaultProps, container: undefined });
|
|
386
421
|
await nextTick();
|
|
387
|
-
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
it('should handle cells querySelector in updateItemSizes', async () => {
|
|
391
|
-
const { result } = setup({
|
|
392
|
-
...defaultProps,
|
|
393
|
-
direction: 'both',
|
|
394
|
-
columnCount: 2,
|
|
395
|
-
columnWidth: undefined,
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
const parent = document.createElement('div');
|
|
399
|
-
const child1 = document.createElement('div');
|
|
400
|
-
Object.defineProperty(child1, 'offsetWidth', { value: 200 });
|
|
401
|
-
child1.dataset.colIndex = '0';
|
|
402
|
-
const child2 = document.createElement('div');
|
|
403
|
-
Object.defineProperty(child2, 'offsetWidth', { value: 300 });
|
|
404
|
-
child2.dataset.colIndex = '1';
|
|
405
|
-
|
|
406
|
-
parent.appendChild(child1);
|
|
407
|
-
parent.appendChild(child2);
|
|
408
|
-
|
|
409
|
-
result.updateItemSizes([ { index: 0, inlineSize: 500, blockSize: 50, element: parent } ]);
|
|
422
|
+
result.scrollToIndex(10, 0);
|
|
410
423
|
await nextTick();
|
|
411
|
-
expect(
|
|
412
|
-
expect(result.getColumnWidth(1)).toBe(300);
|
|
424
|
+
expect(window.scrollTo).toHaveBeenCalled();
|
|
413
425
|
});
|
|
414
|
-
});
|
|
415
426
|
|
|
416
|
-
describe('scroll and offsets', () => {
|
|
417
427
|
it('should handle scrollToIndex out of bounds', async () => {
|
|
418
428
|
const { result } = setup({ ...defaultProps });
|
|
419
429
|
// Row past end
|
|
@@ -443,7 +453,7 @@ describe('useVirtualScroll', () => {
|
|
|
443
453
|
|
|
444
454
|
// Current visible range: [scrollTop + paddingStart, scrollTop + viewport - paddingEnd] = [300, 700]
|
|
445
455
|
// Scroll to item at y=250. 250 < 300, so not visible.
|
|
446
|
-
// targetY < relativeScrollY + paddingStart (250 < 200 + 100)
|
|
456
|
+
// targetY < relativeScrollY + paddingStart (250 < 200 + 100)
|
|
447
457
|
result.scrollToIndex(5, null, 'auto');
|
|
448
458
|
await nextTick();
|
|
449
459
|
});
|
|
@@ -466,6 +476,59 @@ describe('useVirtualScroll', () => {
|
|
|
466
476
|
expect(container.scrollTo).toHaveBeenCalled();
|
|
467
477
|
});
|
|
468
478
|
|
|
479
|
+
it('should handle scrollToOffset with currentX/currentY fallbacks', async () => {
|
|
480
|
+
const container = document.createElement('div');
|
|
481
|
+
Object.defineProperty(container, 'scrollLeft', { value: 50, writable: true });
|
|
482
|
+
Object.defineProperty(container, 'scrollTop', { value: 60, writable: true });
|
|
483
|
+
|
|
484
|
+
const { result } = setup({ ...defaultProps, container });
|
|
485
|
+
await nextTick();
|
|
486
|
+
|
|
487
|
+
// Pass null to x and y to trigger fallbacks to currentX and currentY
|
|
488
|
+
result.scrollToOffset(null, null);
|
|
489
|
+
await nextTick();
|
|
490
|
+
|
|
491
|
+
// scrollOffset.x = targetX - hostOffset.x + (isHorizontal ? paddingStartX : 0)
|
|
492
|
+
// targetX = currentX = 50. hostOffset.x = 0. isHorizontal = false.
|
|
493
|
+
// So scrollOffset.x = 50.
|
|
494
|
+
expect(result.scrollDetails.value.scrollOffset.x).toBe(50);
|
|
495
|
+
expect(result.scrollDetails.value.scrollOffset.y).toBe(60);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('should handle scrollToOffset with restricted direction for padding fallback', async () => {
|
|
499
|
+
const container = document.createElement('div');
|
|
500
|
+
container.scrollTo = vi.fn();
|
|
501
|
+
|
|
502
|
+
// Horizontal direction: isVertical will be false, so targetY padding fallback will be 0
|
|
503
|
+
const { result } = setup({ ...defaultProps, container, direction: 'horizontal', scrollPaddingStart: 10 });
|
|
504
|
+
await nextTick();
|
|
505
|
+
|
|
506
|
+
result.scrollToOffset(100, 100);
|
|
507
|
+
await nextTick();
|
|
508
|
+
// targetY = 100 + hostOffset.y - (isVertical ? paddingStartY : 0)
|
|
509
|
+
// Since isVertical is false, it uses 0. hostOffset.y is 0 here.
|
|
510
|
+
expect(container.scrollTo).toHaveBeenCalledWith(expect.objectContaining({
|
|
511
|
+
top: 100,
|
|
512
|
+
}));
|
|
513
|
+
|
|
514
|
+
// Vertical direction: isHorizontal will be false, so targetX padding fallback will be 0
|
|
515
|
+
const { result: r2 } = setup({ ...defaultProps, container, direction: 'vertical', scrollPaddingStart: 10 });
|
|
516
|
+
await nextTick();
|
|
517
|
+
r2.scrollToOffset(100, 100);
|
|
518
|
+
await nextTick();
|
|
519
|
+
expect(container.scrollTo).toHaveBeenCalledWith(expect.objectContaining({
|
|
520
|
+
left: 100,
|
|
521
|
+
}));
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('should handle scrollToOffset with window fallback when container is missing', async () => {
|
|
525
|
+
const { result } = setup({ ...defaultProps, container: undefined });
|
|
526
|
+
await nextTick();
|
|
527
|
+
result.scrollToOffset(100, 200);
|
|
528
|
+
await nextTick();
|
|
529
|
+
expect(window.scrollTo).toHaveBeenCalled();
|
|
530
|
+
});
|
|
531
|
+
|
|
469
532
|
it('should handle scrollToIndex with null indices', async () => {
|
|
470
533
|
const { result } = setup({ ...defaultProps });
|
|
471
534
|
result.scrollToIndex(null, null);
|
|
@@ -580,57 +643,54 @@ describe('useVirtualScroll', () => {
|
|
|
580
643
|
expect(container.scrollTop).toBe(400);
|
|
581
644
|
});
|
|
582
645
|
|
|
583
|
-
it('should
|
|
584
|
-
const { result } = setup(
|
|
585
|
-
result.scrollToIndex(10,
|
|
586
|
-
|
|
587
|
-
});
|
|
646
|
+
it('should stop programmatic scroll', async () => {
|
|
647
|
+
const { result } = setup(defaultProps);
|
|
648
|
+
result.scrollToIndex(10, null, { behavior: 'smooth' });
|
|
649
|
+
expect(result.scrollDetails.value.isProgrammaticScroll).toBe(true);
|
|
588
650
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
result.scrollToIndex(200, null);
|
|
592
|
-
await nextTick();
|
|
651
|
+
result.stopProgrammaticScroll();
|
|
652
|
+
expect(result.scrollDetails.value.isProgrammaticScroll).toBe(false);
|
|
593
653
|
});
|
|
594
654
|
|
|
595
|
-
it('should handle scrollToIndex
|
|
655
|
+
it('should handle scrollToIndex with element container having scrollTo', async () => {
|
|
596
656
|
const container = document.createElement('div');
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
container.scrollTo = vi.fn().mockImplementation((options) => {
|
|
600
|
-
container.scrollLeft = options.left;
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
const { result } = setup({ ...defaultProps, direction: 'horizontal', container, itemSize: 50, scrollPaddingStart: 100 });
|
|
657
|
+
container.scrollTo = vi.fn();
|
|
658
|
+
const { result } = setup({ ...defaultProps, container });
|
|
604
659
|
await nextTick();
|
|
605
660
|
|
|
606
|
-
|
|
607
|
-
// targetX < relativeScrollX + paddingStart (250 < 1100)
|
|
608
|
-
result.scrollToIndex(null, 5, 'auto');
|
|
661
|
+
result.scrollToIndex(10, 0, { behavior: 'auto' });
|
|
609
662
|
await nextTick();
|
|
610
|
-
expect(container.
|
|
663
|
+
expect(container.scrollTo).toHaveBeenCalled();
|
|
664
|
+
});
|
|
611
665
|
|
|
612
|
-
|
|
613
|
-
|
|
666
|
+
it('should handle scrollToIndex fallback when scrollTo is missing', async () => {
|
|
667
|
+
const container = document.createElement('div');
|
|
668
|
+
(container as unknown as { scrollTo: unknown; }).scrollTo = undefined;
|
|
669
|
+
const { result } = setup({ ...defaultProps, container });
|
|
614
670
|
await nextTick();
|
|
615
671
|
|
|
616
|
-
//
|
|
617
|
-
result.scrollToIndex(
|
|
672
|
+
// row only
|
|
673
|
+
result.scrollToIndex(10, null, { behavior: 'auto' });
|
|
618
674
|
await nextTick();
|
|
619
|
-
|
|
675
|
+
expect(container.scrollTop).toBeGreaterThan(0);
|
|
620
676
|
|
|
621
|
-
|
|
622
|
-
setup({ ...defaultProps, direction: '
|
|
677
|
+
// col only
|
|
678
|
+
const { result: resH } = setup({ ...defaultProps, container, direction: 'horizontal' });
|
|
679
|
+
await nextTick();
|
|
680
|
+
resH.scrollToIndex(null, 10, { behavior: 'auto' });
|
|
623
681
|
await nextTick();
|
|
624
|
-
|
|
682
|
+
expect(container.scrollLeft).toBeGreaterThan(0);
|
|
625
683
|
});
|
|
626
684
|
|
|
627
|
-
it('should
|
|
628
|
-
const {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
685
|
+
it('should skip undefined items in renderedItems', async () => {
|
|
686
|
+
const items = Array.from({ length: 10 }) as unknown[];
|
|
687
|
+
items[ 0 ] = { id: 0 };
|
|
688
|
+
// other indices are undefined
|
|
689
|
+
const { result } = setup({ ...defaultProps, items, itemSize: 50 });
|
|
690
|
+
await nextTick();
|
|
691
|
+
// only index 0 should be rendered
|
|
692
|
+
expect(result.renderedItems.value.length).toBe(1);
|
|
693
|
+
expect(result.renderedItems.value[ 0 ]?.index).toBe(0);
|
|
634
694
|
});
|
|
635
695
|
});
|
|
636
696
|
|
|
@@ -643,6 +703,72 @@ describe('useVirtualScroll', () => {
|
|
|
643
703
|
await nextTick();
|
|
644
704
|
});
|
|
645
705
|
|
|
706
|
+
it('should cover fallback branches for unknown targets and directions', async () => {
|
|
707
|
+
// 1. Unknown container type (hits 408, 445, 513, 718 else branches)
|
|
708
|
+
const unknownContainer = {
|
|
709
|
+
addEventListener: vi.fn(),
|
|
710
|
+
removeEventListener: vi.fn(),
|
|
711
|
+
} as unknown as HTMLElement;
|
|
712
|
+
|
|
713
|
+
const { result } = setup({
|
|
714
|
+
...defaultProps,
|
|
715
|
+
container: unknownContainer,
|
|
716
|
+
hostElement: document.createElement('div'),
|
|
717
|
+
});
|
|
718
|
+
await nextTick();
|
|
719
|
+
|
|
720
|
+
result.scrollToIndex(10, 0);
|
|
721
|
+
result.scrollToOffset(100, 100);
|
|
722
|
+
result.updateHostOffset();
|
|
723
|
+
|
|
724
|
+
// 2. Invalid direction (hits 958 else branch)
|
|
725
|
+
const { result: r2 } = setup({
|
|
726
|
+
...defaultProps,
|
|
727
|
+
direction: undefined as unknown as 'vertical',
|
|
728
|
+
stickyIndices: [ 0 ],
|
|
729
|
+
});
|
|
730
|
+
await nextTick();
|
|
731
|
+
window.dispatchEvent(new Event('scroll'));
|
|
732
|
+
await nextTick();
|
|
733
|
+
expect(r2.renderedItems.value.find((i) => i.index === 0)).toBeDefined();
|
|
734
|
+
|
|
735
|
+
// 3. Unknown target in handleScroll (hits 1100 else branch)
|
|
736
|
+
const container = document.createElement('div');
|
|
737
|
+
setup({ ...defaultProps, container });
|
|
738
|
+
const event = new Event('scroll');
|
|
739
|
+
Object.defineProperty(event, 'target', { value: { } });
|
|
740
|
+
container.dispatchEvent(event);
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
it('should cleanup events and observers when container changes', async () => {
|
|
744
|
+
const container = document.createElement('div');
|
|
745
|
+
const removeSpy = vi.spyOn(container, 'removeEventListener');
|
|
746
|
+
const { props } = setup({ ...defaultProps, container });
|
|
747
|
+
await nextTick();
|
|
748
|
+
|
|
749
|
+
// Change container to trigger cleanup of old one
|
|
750
|
+
props.value.container = document.createElement('div');
|
|
751
|
+
await nextTick();
|
|
752
|
+
|
|
753
|
+
expect(removeSpy).toHaveBeenCalledWith('scroll', expect.any(Function));
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
it('should cleanup when unmounted and container is window', async () => {
|
|
757
|
+
const { wrapper } = setup({ ...defaultProps, container: window });
|
|
758
|
+
await nextTick();
|
|
759
|
+
wrapper.unmount();
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it('should cleanup when unmounted', async () => {
|
|
763
|
+
const container = document.createElement('div');
|
|
764
|
+
const removeSpy = vi.spyOn(container, 'removeEventListener');
|
|
765
|
+
const { wrapper } = setup({ ...defaultProps, container });
|
|
766
|
+
await nextTick();
|
|
767
|
+
|
|
768
|
+
wrapper.unmount();
|
|
769
|
+
expect(removeSpy).toHaveBeenCalledWith('scroll', expect.any(Function));
|
|
770
|
+
});
|
|
771
|
+
|
|
646
772
|
it('should handle document scroll events', async () => {
|
|
647
773
|
setup({ ...defaultProps });
|
|
648
774
|
document.dispatchEvent(new Event('scroll'));
|
|
@@ -698,7 +824,7 @@ describe('useVirtualScroll', () => {
|
|
|
698
824
|
|
|
699
825
|
it('should handle window resize events', async () => {
|
|
700
826
|
setup({ ...defaultProps, container: window });
|
|
701
|
-
window
|
|
827
|
+
Object.defineProperty(window, 'innerWidth', { configurable: true, value: 1200 });
|
|
702
828
|
window.dispatchEvent(new Event('resize'));
|
|
703
829
|
await nextTick();
|
|
704
830
|
});
|
|
@@ -744,7 +870,7 @@ describe('useVirtualScroll', () => {
|
|
|
744
870
|
columnCount: 2,
|
|
745
871
|
columnWidth: [ 0 ] as unknown as number[],
|
|
746
872
|
});
|
|
747
|
-
expect(result.getColumnWidth(0)).toBe(
|
|
873
|
+
expect(result.getColumnWidth(0)).toBe(100); // DEFAULT_COLUMN_WIDTH
|
|
748
874
|
});
|
|
749
875
|
|
|
750
876
|
it('should handle columnWidth as a function', async () => {
|
|
@@ -755,7 +881,7 @@ describe('useVirtualScroll', () => {
|
|
|
755
881
|
columnWidth: (index: number) => (index % 2 === 0 ? 100 : 200),
|
|
756
882
|
});
|
|
757
883
|
expect(result.getColumnWidth(0)).toBe(100);
|
|
758
|
-
expect(result.totalWidth.value).toBe(1500);
|
|
884
|
+
expect(result.totalWidth.value).toBe(1500); // 5*100 + 5*200 - 0
|
|
759
885
|
});
|
|
760
886
|
|
|
761
887
|
it('should handle getColumnWidth fallback when dynamic', async () => {
|
|
@@ -765,7 +891,7 @@ describe('useVirtualScroll', () => {
|
|
|
765
891
|
columnCount: 2,
|
|
766
892
|
columnWidth: undefined,
|
|
767
893
|
});
|
|
768
|
-
expect(result.getColumnWidth(0)).toBe(
|
|
894
|
+
expect(result.getColumnWidth(0)).toBe(100);
|
|
769
895
|
});
|
|
770
896
|
|
|
771
897
|
it('should handle columnRange while loop coverage', async () => {
|
|
@@ -803,71 +929,43 @@ describe('useVirtualScroll', () => {
|
|
|
803
929
|
});
|
|
804
930
|
});
|
|
805
931
|
|
|
806
|
-
describe('
|
|
807
|
-
it('should
|
|
808
|
-
|
|
809
|
-
const { result } = setup({ ...defaultProps, container: window, itemSize: undefined });
|
|
810
|
-
await nextTick();
|
|
811
|
-
result.scrollToIndex(10, 0, 'start');
|
|
812
|
-
document.dispatchEvent(new Event('scroll'));
|
|
813
|
-
expect(result.scrollDetails.value.isScrolling).toBe(true);
|
|
814
|
-
vi.advanceTimersByTime(250);
|
|
932
|
+
describe('sticky and pushed items', () => {
|
|
933
|
+
it('should identify sticky items', async () => {
|
|
934
|
+
const { result } = setup({ ...defaultProps, stickyIndices: [ 0, 10 ] });
|
|
815
935
|
await nextTick();
|
|
816
|
-
expect(result.scrollDetails.value.isScrolling).toBe(false);
|
|
817
|
-
vi.useRealTimers();
|
|
818
|
-
});
|
|
819
936
|
|
|
820
|
-
|
|
821
|
-
const
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
result.updateItemSize(5, 100, 100);
|
|
826
|
-
await nextTick();
|
|
937
|
+
const items = result.renderedItems.value;
|
|
938
|
+
const item0 = items.find((i) => i.index === 0);
|
|
939
|
+
const item10 = items.find((i) => i.index === 10);
|
|
940
|
+
expect(item0?.isSticky).toBe(true);
|
|
941
|
+
expect(item10?.isSticky).toBe(true);
|
|
827
942
|
});
|
|
828
943
|
|
|
829
|
-
it('should
|
|
830
|
-
const { result
|
|
831
|
-
const host = document.createElement('div');
|
|
832
|
-
props.value.hostElement = host;
|
|
944
|
+
it('should make sticky items active when scrolled past', async () => {
|
|
945
|
+
const { result } = setup({ ...defaultProps, stickyIndices: [ 0 ] });
|
|
833
946
|
await nextTick();
|
|
834
|
-
result.updateHostOffset();
|
|
835
|
-
});
|
|
836
947
|
|
|
837
|
-
|
|
838
|
-
const host = document.createElement('div');
|
|
839
|
-
const { result } = setup({ ...defaultProps, container: host, hostElement: host });
|
|
948
|
+
result.scrollToOffset(0, 100);
|
|
840
949
|
await nextTick();
|
|
841
|
-
result.updateHostOffset();
|
|
842
|
-
});
|
|
843
|
-
|
|
844
|
-
it('should correctly calculate hostOffset when container is an HTMLElement', async () => {
|
|
845
|
-
const container = document.createElement('div');
|
|
846
|
-
const hostElement = document.createElement('div');
|
|
847
950
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
Object.defineProperty(container, 'scrollTop', { value: 50, writable: true, configurable: true });
|
|
851
|
-
|
|
852
|
-
const { result } = setup({ ...defaultProps, container, hostElement });
|
|
853
|
-
await nextTick();
|
|
854
|
-
result.updateHostOffset();
|
|
855
|
-
expect(result.scrollDetails.value.scrollOffset.y).toBeDefined();
|
|
951
|
+
const item0 = result.renderedItems.value.find((i) => i.index === 0);
|
|
952
|
+
expect(item0?.isStickyActive).toBe(true);
|
|
856
953
|
});
|
|
857
954
|
|
|
858
|
-
it('should
|
|
859
|
-
const { result } = setup({ ...defaultProps,
|
|
860
|
-
result.updateItemSize(0, 100, 100);
|
|
955
|
+
it('should include current sticky item in rendered items even if range is ahead', async () => {
|
|
956
|
+
const { result } = setup({ ...defaultProps, stickyIndices: [ 0 ], bufferBefore: 0 });
|
|
861
957
|
await nextTick();
|
|
862
|
-
expect(result.totalHeight.value).toBe(5050);
|
|
863
958
|
|
|
864
|
-
|
|
959
|
+
// Scroll to index 20. Range starts at 20.
|
|
960
|
+
result.scrollToIndex(20, 0, { align: 'start', behavior: 'auto' });
|
|
865
961
|
await nextTick();
|
|
866
|
-
|
|
962
|
+
|
|
963
|
+
expect(result.scrollDetails.value.range.start).toBe(20);
|
|
964
|
+
const item0 = result.renderedItems.value.find((i) => i.index === 0);
|
|
965
|
+
expect(item0).toBeDefined();
|
|
966
|
+
expect(item0?.isStickyActive).toBe(true);
|
|
867
967
|
});
|
|
868
|
-
});
|
|
869
968
|
|
|
870
|
-
describe('sticky header pushing', () => {
|
|
871
969
|
it('should push sticky item when next sticky item approaches (vertical)', async () => {
|
|
872
970
|
const container = document.createElement('div');
|
|
873
971
|
Object.defineProperty(container, 'clientHeight', { value: 500 });
|
|
@@ -875,7 +973,6 @@ describe('useVirtualScroll', () => {
|
|
|
875
973
|
const { result } = setup({ ...defaultProps, container, stickyIndices: [ 0, 10 ], itemSize: 50 });
|
|
876
974
|
// We need to trigger scroll to update scrollY
|
|
877
975
|
container.dispatchEvent(new Event('scroll'));
|
|
878
|
-
await nextTick();
|
|
879
976
|
|
|
880
977
|
const item0 = result.renderedItems.value.find((i) => i.index === 0);
|
|
881
978
|
expect(item0!.offset.y).toBeLessThanOrEqual(450);
|
|
@@ -895,38 +992,79 @@ describe('useVirtualScroll', () => {
|
|
|
895
992
|
columnGap: 0,
|
|
896
993
|
});
|
|
897
994
|
container.dispatchEvent(new Event('scroll'));
|
|
898
|
-
await nextTick();
|
|
899
995
|
|
|
900
996
|
const item0 = result.renderedItems.value.find((i) => i.index === 0);
|
|
901
997
|
expect(item0!.offset.x).toBeLessThanOrEqual(450);
|
|
902
998
|
});
|
|
903
|
-
});
|
|
904
999
|
|
|
905
|
-
|
|
906
|
-
it('should restore scroll position when items are prepended', async () => {
|
|
907
|
-
vi.useFakeTimers();
|
|
1000
|
+
it('should handle dynamic sticky item pushing in vertical mode', async () => {
|
|
908
1001
|
const container = document.createElement('div');
|
|
909
1002
|
Object.defineProperty(container, 'clientHeight', { value: 500 });
|
|
910
|
-
Object.defineProperty(container, 'scrollTop', { value:
|
|
911
|
-
container.scrollTo = vi.fn().mockImplementation((options) => {
|
|
912
|
-
container.scrollTop = options.top;
|
|
913
|
-
});
|
|
1003
|
+
Object.defineProperty(container, 'scrollTop', { value: 380, writable: true });
|
|
914
1004
|
|
|
915
|
-
const
|
|
916
|
-
const { result, props } = setup({
|
|
1005
|
+
const { result } = setup({
|
|
917
1006
|
...defaultProps,
|
|
918
|
-
items,
|
|
919
1007
|
container,
|
|
920
|
-
itemSize:
|
|
921
|
-
|
|
1008
|
+
itemSize: undefined, // dynamic
|
|
1009
|
+
stickyIndices: [ 0, 10 ],
|
|
922
1010
|
});
|
|
923
|
-
container.dispatchEvent(new Event('scroll'));
|
|
924
|
-
await nextTick();
|
|
925
1011
|
|
|
926
|
-
|
|
1012
|
+
// Item 0 is sticky. Item 10 is next sticky.
|
|
1013
|
+
// Default size = 40.
|
|
1014
|
+
// nextStickyY = itemSizesY.query(10) = 400.
|
|
1015
|
+
// distance = 400 - 380 = 20.
|
|
1016
|
+
// 20 < 40 (item 0 height), so it should be pushed.
|
|
1017
|
+
// stickyOffset.y = -(40 - 20) = -20.
|
|
1018
|
+
const stickyItem = result.renderedItems.value.find((i) => i.index === 0);
|
|
1019
|
+
expect(stickyItem?.stickyOffset.y).toBe(-20);
|
|
1020
|
+
});
|
|
927
1021
|
|
|
928
|
-
|
|
929
|
-
const
|
|
1022
|
+
it('should handle dynamic sticky item pushing in horizontal mode', async () => {
|
|
1023
|
+
const container = document.createElement('div');
|
|
1024
|
+
Object.defineProperty(container, 'clientWidth', { value: 500 });
|
|
1025
|
+
Object.defineProperty(container, 'scrollLeft', { value: 380, writable: true });
|
|
1026
|
+
|
|
1027
|
+
const { result } = setup({
|
|
1028
|
+
...defaultProps,
|
|
1029
|
+
container,
|
|
1030
|
+
direction: 'horizontal',
|
|
1031
|
+
itemSize: undefined, // dynamic
|
|
1032
|
+
stickyIndices: [ 0, 10 ],
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
// nextStickyX = itemSizesX.query(10) = 400.
|
|
1036
|
+
// distance = 400 - 380 = 20.
|
|
1037
|
+
// 20 < 40, so stickyOffset.x = -20.
|
|
1038
|
+
const stickyItem = result.renderedItems.value.find((i) => i.index === 0);
|
|
1039
|
+
expect(stickyItem?.stickyOffset.x).toBe(-20);
|
|
1040
|
+
});
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
describe('scroll restoration', () => {
|
|
1044
|
+
it('should restore scroll position when items are prepended', async () => {
|
|
1045
|
+
vi.useFakeTimers();
|
|
1046
|
+
const container = document.createElement('div');
|
|
1047
|
+
Object.defineProperty(container, 'clientHeight', { value: 500 });
|
|
1048
|
+
Object.defineProperty(container, 'scrollTop', { value: 100, writable: true });
|
|
1049
|
+
container.scrollTo = vi.fn().mockImplementation((options) => {
|
|
1050
|
+
container.scrollTop = options.top;
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
const items = Array.from({ length: 50 }, (_, i) => ({ id: i }));
|
|
1054
|
+
const { result, props } = setup({
|
|
1055
|
+
...defaultProps,
|
|
1056
|
+
items,
|
|
1057
|
+
container,
|
|
1058
|
+
itemSize: 50,
|
|
1059
|
+
restoreScrollOnPrepend: true,
|
|
1060
|
+
});
|
|
1061
|
+
container.dispatchEvent(new Event('scroll'));
|
|
1062
|
+
await nextTick();
|
|
1063
|
+
|
|
1064
|
+
expect(result.scrollDetails.value.scrollOffset.y).toBe(100);
|
|
1065
|
+
|
|
1066
|
+
// Prepend 2 items
|
|
1067
|
+
const newItems = [ { id: -1 }, { id: -2 }, ...items ];
|
|
930
1068
|
props.value.items = newItems;
|
|
931
1069
|
await nextTick();
|
|
932
1070
|
// Trigger initializeSizes
|
|
@@ -970,6 +1108,35 @@ describe('useVirtualScroll', () => {
|
|
|
970
1108
|
vi.useRealTimers();
|
|
971
1109
|
});
|
|
972
1110
|
|
|
1111
|
+
it('should restore scroll position with itemSize as function when prepending', async () => {
|
|
1112
|
+
vi.useFakeTimers();
|
|
1113
|
+
const container = document.createElement('div');
|
|
1114
|
+
Object.defineProperty(container, 'scrollTop', { value: 100, writable: true });
|
|
1115
|
+
container.scrollTo = vi.fn().mockImplementation((options) => {
|
|
1116
|
+
container.scrollTop = options.top;
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
const items = Array.from({ length: 50 }, (_, i) => ({ id: i }));
|
|
1120
|
+
const { props } = setup({
|
|
1121
|
+
...defaultProps,
|
|
1122
|
+
items,
|
|
1123
|
+
container,
|
|
1124
|
+
itemSize: (item: { id: number; }) => (item.id < 0 ? 100 : 50),
|
|
1125
|
+
restoreScrollOnPrepend: true,
|
|
1126
|
+
});
|
|
1127
|
+
await nextTick();
|
|
1128
|
+
|
|
1129
|
+
// Prepend 1 item with id -1 (size 100)
|
|
1130
|
+
const newItems = [ { id: -1 }, ...items ];
|
|
1131
|
+
props.value.items = newItems;
|
|
1132
|
+
await nextTick();
|
|
1133
|
+
await nextTick();
|
|
1134
|
+
|
|
1135
|
+
// Should have adjusted scroll by 100px. New scrollTop should be 200.
|
|
1136
|
+
expect(container.scrollTop).toBe(200);
|
|
1137
|
+
vi.useRealTimers();
|
|
1138
|
+
});
|
|
1139
|
+
|
|
973
1140
|
it('should NOT restore scroll position when restoreScrollOnPrepend is false', async () => {
|
|
974
1141
|
const container = document.createElement('div');
|
|
975
1142
|
Object.defineProperty(container, 'scrollTop', { value: 100, writable: true });
|
|
@@ -991,7 +1158,7 @@ describe('useVirtualScroll', () => {
|
|
|
991
1158
|
const { props } = setup({ ...defaultProps, items, container, restoreScrollOnPrepend: true });
|
|
992
1159
|
await nextTick();
|
|
993
1160
|
|
|
994
|
-
const newItems = [ { id: -1 }, { id: 9999 } ];
|
|
1161
|
+
const newItems = [ { id: -1 }, { id: 9999 } ];
|
|
995
1162
|
props.value.items = newItems;
|
|
996
1163
|
await nextTick();
|
|
997
1164
|
await nextTick();
|
|
@@ -1003,18 +1170,96 @@ describe('useVirtualScroll', () => {
|
|
|
1003
1170
|
Object.defineProperty(container, 'clientHeight', { value: 500, configurable: true });
|
|
1004
1171
|
Object.defineProperty(container, 'scrollHeight', { value: 5000, configurable: true });
|
|
1005
1172
|
const { result, props } = setup({ ...defaultProps, container, restoreScrollOnPrepend: true });
|
|
1006
|
-
|
|
1007
|
-
// pendingScroll should be set because it's not reached yet
|
|
1173
|
+
await nextTick();
|
|
1008
1174
|
|
|
1175
|
+
result.scrollToIndex(10, null, { behavior: 'smooth' });
|
|
1009
1176
|
props.value.items = [ { id: -1 }, ...props.value.items ];
|
|
1010
1177
|
await nextTick();
|
|
1011
1178
|
});
|
|
1179
|
+
});
|
|
1012
1180
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1181
|
+
describe('advanced logic and edge cases', () => {
|
|
1182
|
+
it('should trigger scroll correction when isScrolling becomes false', async () => {
|
|
1183
|
+
vi.useFakeTimers();
|
|
1184
|
+
const { result } = setup({ ...defaultProps, container: window, itemSize: undefined });
|
|
1185
|
+
await nextTick();
|
|
1186
|
+
result.scrollToIndex(10, 0, 'start');
|
|
1187
|
+
document.dispatchEvent(new Event('scroll'));
|
|
1188
|
+
expect(result.scrollDetails.value.isScrolling).toBe(true);
|
|
1189
|
+
vi.advanceTimersByTime(250);
|
|
1190
|
+
await nextTick();
|
|
1191
|
+
expect(result.scrollDetails.value.isScrolling).toBe(false);
|
|
1192
|
+
vi.useRealTimers();
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
it('should trigger scroll correction when treeUpdateFlag changes', async () => {
|
|
1196
|
+
const { result } = setup({ ...defaultProps, itemSize: undefined });
|
|
1197
|
+
await nextTick();
|
|
1198
|
+
result.scrollToIndex(10, 0, 'start');
|
|
1199
|
+
// Trigger tree update
|
|
1200
|
+
result.updateItemSize(5, 100, 100);
|
|
1201
|
+
await nextTick();
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
it('should cover updateHostOffset when container is window', async () => {
|
|
1205
|
+
const { result, props } = setup({ ...defaultProps, container: window });
|
|
1206
|
+
const host = document.createElement('div');
|
|
1207
|
+
props.value.hostElement = host;
|
|
1208
|
+
await nextTick();
|
|
1209
|
+
result.updateHostOffset();
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
it('should cover updateHostOffset when container is hostElement', async () => {
|
|
1213
|
+
const host = document.createElement('div');
|
|
1214
|
+
const { result } = setup({ ...defaultProps, container: host, hostElement: host });
|
|
1215
|
+
await nextTick();
|
|
1216
|
+
result.updateHostOffset();
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
it('should handle updateHostOffset with window fallback when container is missing', async () => {
|
|
1220
|
+
const { result, props } = setup({ ...defaultProps, container: undefined });
|
|
1221
|
+
const host = document.createElement('div');
|
|
1222
|
+
props.value.hostElement = host;
|
|
1223
|
+
await nextTick();
|
|
1224
|
+
result.updateHostOffset();
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
it('should correctly calculate hostOffset when container is an HTMLElement', async () => {
|
|
1228
|
+
const container = document.createElement('div');
|
|
1229
|
+
const hostElement = document.createElement('div');
|
|
1230
|
+
|
|
1231
|
+
container.getBoundingClientRect = vi.fn(() => ({ top: 100, left: 100, bottom: 200, right: 200, width: 100, height: 100, x: 100, y: 100, toJSON: () => '' }));
|
|
1232
|
+
hostElement.getBoundingClientRect = vi.fn(() => ({ top: 150, left: 150, bottom: 200, right: 200, width: 50, height: 50, x: 150, y: 150, toJSON: () => '' }));
|
|
1233
|
+
Object.defineProperty(container, 'scrollTop', { value: 50, writable: true, configurable: true });
|
|
1234
|
+
|
|
1235
|
+
const { result } = setup({ ...defaultProps, container, hostElement });
|
|
1236
|
+
await nextTick();
|
|
1237
|
+
result.updateHostOffset();
|
|
1238
|
+
expect(result.scrollDetails.value.scrollOffset.y).toBeDefined();
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
it('should cover refresh method', async () => {
|
|
1242
|
+
const { result } = setup({ ...defaultProps, itemSize: 0 });
|
|
1243
|
+
result.updateItemSize(0, 100, 100);
|
|
1244
|
+
await nextTick();
|
|
1245
|
+
expect(result.totalHeight.value).toBe(4060);
|
|
1246
|
+
|
|
1247
|
+
result.refresh();
|
|
1248
|
+
await nextTick();
|
|
1249
|
+
expect(result.totalHeight.value).toBe(4000);
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
it('should trigger scroll correction on tree update with string alignment', async () => {
|
|
1253
|
+
const container = document.createElement('div');
|
|
1254
|
+
Object.defineProperty(container, 'clientHeight', { value: 500, configurable: true });
|
|
1255
|
+
Object.defineProperty(container, 'scrollHeight', { value: 5000, configurable: true });
|
|
1256
|
+
const { result } = setup({ ...defaultProps, container, itemSize: undefined });
|
|
1257
|
+
// Set a pending scroll with string alignment
|
|
1258
|
+
result.scrollToIndex(10, null, 'start');
|
|
1259
|
+
|
|
1260
|
+
// Trigger tree update
|
|
1261
|
+
result.updateItemSize(0, 100, 100);
|
|
1016
1262
|
await nextTick();
|
|
1017
|
-
expect(result.totalWidth.value).toBe(5050);
|
|
1018
1263
|
});
|
|
1019
1264
|
|
|
1020
1265
|
it('should trigger scroll correction on tree update with pending scroll', async () => {
|
|
@@ -1049,10 +1294,168 @@ describe('useVirtualScroll', () => {
|
|
|
1049
1294
|
expect(result.scrollDetails.value.isScrolling).toBe(false);
|
|
1050
1295
|
vi.useRealTimers();
|
|
1051
1296
|
});
|
|
1297
|
+
|
|
1298
|
+
it('should update totals when function-based itemSize dependencies change and refresh is called', async () => {
|
|
1299
|
+
const defaultHeight = ref(50);
|
|
1300
|
+
const getRowHeight = () => defaultHeight.value;
|
|
1301
|
+
|
|
1302
|
+
const propsValue = ref({
|
|
1303
|
+
items: mockItems,
|
|
1304
|
+
direction: 'vertical' as const,
|
|
1305
|
+
itemSize: getRowHeight,
|
|
1306
|
+
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1307
|
+
|
|
1308
|
+
const result = useVirtualScroll(propsValue);
|
|
1309
|
+
expect(result.totalHeight.value).toBe(5000); // 100 * 50
|
|
1310
|
+
|
|
1311
|
+
defaultHeight.value = 60;
|
|
1312
|
+
// Total height should still be 5000 because getRowHeight reference didn't change
|
|
1313
|
+
// and initializeSizes hasn't been called automatically.
|
|
1314
|
+
expect(result.totalHeight.value).toBe(5000);
|
|
1315
|
+
|
|
1316
|
+
result.refresh();
|
|
1317
|
+
await nextTick();
|
|
1318
|
+
expect(result.totalHeight.value).toBe(6000);
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
it('should update totals via measurements even if itemSize is a function', async () => {
|
|
1322
|
+
const getRowHeight = () => 50;
|
|
1323
|
+
|
|
1324
|
+
const propsValue = ref({
|
|
1325
|
+
items: mockItems,
|
|
1326
|
+
direction: 'vertical' as const,
|
|
1327
|
+
itemSize: getRowHeight,
|
|
1328
|
+
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1329
|
+
|
|
1330
|
+
const result = useVirtualScroll(propsValue);
|
|
1331
|
+
expect(result.totalHeight.value).toBe(5000);
|
|
1332
|
+
|
|
1333
|
+
// Simulate ResizeObserver measurement
|
|
1334
|
+
result.updateItemSizes([ { index: 0, inlineSize: 100, blockSize: 70 } ]);
|
|
1335
|
+
await nextTick();
|
|
1336
|
+
|
|
1337
|
+
// Item 0 is now 70 instead of 50. Total: 50 * 99 + 70 = 4950 + 70 = 5020.
|
|
1338
|
+
expect(result.totalHeight.value).toBe(5020);
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
it('should update totals via measurements even if columnWidth is a function', async () => {
|
|
1342
|
+
const getColWidth = () => 100;
|
|
1343
|
+
|
|
1344
|
+
const propsValue = ref({
|
|
1345
|
+
items: mockItems,
|
|
1346
|
+
direction: 'both' as const,
|
|
1347
|
+
columnCount: 5,
|
|
1348
|
+
columnWidth: getColWidth,
|
|
1349
|
+
itemSize: 50,
|
|
1350
|
+
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1351
|
+
|
|
1352
|
+
const result = useVirtualScroll(propsValue);
|
|
1353
|
+
expect(result.totalWidth.value).toBe(500);
|
|
1354
|
+
|
|
1355
|
+
// Simulate ResizeObserver measurement on a cell
|
|
1356
|
+
// We need to provide an element with data-col-index
|
|
1357
|
+
const element = document.createElement('div');
|
|
1358
|
+
const cell = document.createElement('div');
|
|
1359
|
+
cell.dataset.colIndex = '0';
|
|
1360
|
+
Object.defineProperty(cell, 'offsetWidth', { value: 120 });
|
|
1361
|
+
element.appendChild(cell);
|
|
1362
|
+
|
|
1363
|
+
result.updateItemSizes([ { index: 0, inlineSize: 120, blockSize: 50, element } ]);
|
|
1364
|
+
await nextTick();
|
|
1365
|
+
|
|
1366
|
+
// Column 0 is now 120 instead of 100. Total: 100 * 4 + 120 = 520.
|
|
1367
|
+
expect(result.totalWidth.value).toBe(520);
|
|
1368
|
+
});
|
|
1052
1369
|
});
|
|
1053
1370
|
|
|
1054
1371
|
// eslint-disable-next-line test/prefer-lowercase-title
|
|
1055
1372
|
describe('SSR support', () => {
|
|
1373
|
+
it('should handle colBuffer when ssrRange is present and not scrolling', async () => {
|
|
1374
|
+
vi.useFakeTimers();
|
|
1375
|
+
const container = document.createElement('div');
|
|
1376
|
+
Object.defineProperty(container, 'clientWidth', { value: 500, configurable: true });
|
|
1377
|
+
Object.defineProperty(container, 'scrollLeft', { value: 0, writable: true, configurable: true });
|
|
1378
|
+
container.scrollTo = vi.fn().mockImplementation((options) => {
|
|
1379
|
+
if (options.left !== undefined) {
|
|
1380
|
+
Object.defineProperty(container, 'scrollLeft', { value: options.left, writable: true, configurable: true });
|
|
1381
|
+
}
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
const { result } = setup({
|
|
1385
|
+
...defaultProps,
|
|
1386
|
+
container,
|
|
1387
|
+
direction: 'both',
|
|
1388
|
+
columnCount: 20,
|
|
1389
|
+
columnWidth: 100,
|
|
1390
|
+
ssrRange: { start: 0, end: 10, colStart: 1, colEnd: 2 },
|
|
1391
|
+
initialScrollIndex: 0,
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
await nextTick(); // onMounted schedules hydration
|
|
1395
|
+
await nextTick(); // hydration tick 1
|
|
1396
|
+
await nextTick(); // hydration tick 2 (isHydrating = false)
|
|
1397
|
+
|
|
1398
|
+
expect(result.isHydrated.value).toBe(true);
|
|
1399
|
+
|
|
1400
|
+
// Scroll to col 5 (offset 500)
|
|
1401
|
+
result.scrollToIndex(null, 5, { align: 'start', behavior: 'auto' });
|
|
1402
|
+
await nextTick();
|
|
1403
|
+
|
|
1404
|
+
vi.runAllTimers(); // Clear isScrolling timeout
|
|
1405
|
+
await nextTick();
|
|
1406
|
+
|
|
1407
|
+
// start = findLowerBound(500) = 5.
|
|
1408
|
+
// colBuffer should be 0 because ssrRange is present and isScrolling is false.
|
|
1409
|
+
expect(result.columnRange.value.start).toBe(5);
|
|
1410
|
+
|
|
1411
|
+
// Now trigger a scroll to make isScrolling true
|
|
1412
|
+
container.dispatchEvent(new Event('scroll'));
|
|
1413
|
+
await nextTick();
|
|
1414
|
+
// isScrolling is now true. colBuffer should be 2.
|
|
1415
|
+
expect(result.columnRange.value.start).toBe(3);
|
|
1416
|
+
vi.useRealTimers();
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
it('should handle bufferBefore when ssrRange is present and not scrolling', async () => {
|
|
1420
|
+
vi.useFakeTimers();
|
|
1421
|
+
const container = document.createElement('div');
|
|
1422
|
+
Object.defineProperty(container, 'clientHeight', { value: 500 });
|
|
1423
|
+
Object.defineProperty(container, 'scrollTop', { value: 0, writable: true, configurable: true });
|
|
1424
|
+
container.scrollTo = vi.fn().mockImplementation((options) => {
|
|
1425
|
+
if (options.top !== undefined) {
|
|
1426
|
+
Object.defineProperty(container, 'scrollTop', { value: options.top, writable: true, configurable: true });
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
const { result } = setup({
|
|
1431
|
+
...defaultProps,
|
|
1432
|
+
container,
|
|
1433
|
+
itemSize: 50,
|
|
1434
|
+
bufferBefore: 5,
|
|
1435
|
+
ssrRange: { start: 0, end: 10 },
|
|
1436
|
+
initialScrollIndex: 10,
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
await nextTick(); // schedules hydration
|
|
1440
|
+
await nextTick(); // hydration tick scrolls to 10
|
|
1441
|
+
await nextTick();
|
|
1442
|
+
|
|
1443
|
+
vi.runAllTimers(); // Clear isScrolling timeout
|
|
1444
|
+
await nextTick();
|
|
1445
|
+
|
|
1446
|
+
expect(result.isHydrated.value).toBe(true);
|
|
1447
|
+
// start = floor(500 / 50) = 10.
|
|
1448
|
+
// Since ssrRange is present and isScrolling is false, bufferBefore should be 0.
|
|
1449
|
+
expect(result.renderedItems.value[ 0 ]?.index).toBe(10);
|
|
1450
|
+
|
|
1451
|
+
// Now trigger a scroll to make isScrolling true
|
|
1452
|
+
container.dispatchEvent(new Event('scroll'));
|
|
1453
|
+
await nextTick();
|
|
1454
|
+
// isScrolling is now true. bufferBefore should be 5.
|
|
1455
|
+
expect(result.renderedItems.value[ 0 ]?.index).toBe(5);
|
|
1456
|
+
vi.useRealTimers();
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1056
1459
|
it('should handle SSR range in range calculation', () => {
|
|
1057
1460
|
const props = ref({
|
|
1058
1461
|
items: mockItems,
|
|
@@ -1072,6 +1475,17 @@ describe('useVirtualScroll', () => {
|
|
|
1072
1475
|
expect(result.columnRange.value.end).toBe(5);
|
|
1073
1476
|
});
|
|
1074
1477
|
|
|
1478
|
+
it('should handle SSR range with colEnd fallback in columnRange calculation', () => {
|
|
1479
|
+
const props = ref({
|
|
1480
|
+
items: mockItems,
|
|
1481
|
+
columnCount: 10,
|
|
1482
|
+
ssrRange: { start: 0, end: 10, colStart: 0, colEnd: 0 },
|
|
1483
|
+
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1484
|
+
const result = useVirtualScroll(props);
|
|
1485
|
+
// colEnd is 0, so it should use columnCount (10)
|
|
1486
|
+
expect(result.columnRange.value.end).toBe(10);
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1075
1489
|
it('should handle SSR range with both directions for total sizes', () => {
|
|
1076
1490
|
const props = ref({
|
|
1077
1491
|
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
@@ -1082,8 +1496,8 @@ describe('useVirtualScroll', () => {
|
|
|
1082
1496
|
ssrRange: { start: 10, end: 20, colStart: 2, colEnd: 5 },
|
|
1083
1497
|
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1084
1498
|
const result = useVirtualScroll(props);
|
|
1085
|
-
expect(result.totalWidth.value).toBe(300); // (5-2) * 100
|
|
1086
|
-
expect(result.totalHeight.value).toBe(500); // (20-10) * 50
|
|
1499
|
+
expect(result.totalWidth.value).toBe(300); // (5-2) * 100 - gap(0)
|
|
1500
|
+
expect(result.totalHeight.value).toBe(500); // (20-10) * 50 - gap(0)
|
|
1087
1501
|
});
|
|
1088
1502
|
|
|
1089
1503
|
it('should handle SSR range with horizontal direction for total sizes', () => {
|
|
@@ -1094,29 +1508,30 @@ describe('useVirtualScroll', () => {
|
|
|
1094
1508
|
ssrRange: { start: 10, end: 20 },
|
|
1095
1509
|
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1096
1510
|
const result = useVirtualScroll(props);
|
|
1097
|
-
expect(result.totalWidth.value).toBe(500); // (20-10) * 50
|
|
1511
|
+
expect(result.totalWidth.value).toBe(500); // (20-10) * 50 - gap(0)
|
|
1098
1512
|
});
|
|
1099
1513
|
|
|
1100
|
-
it('should handle SSR range with
|
|
1514
|
+
it('should handle SSR range with vertical offset in renderedItems', () => {
|
|
1101
1515
|
const props = ref({
|
|
1102
1516
|
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1103
|
-
direction: '
|
|
1517
|
+
direction: 'vertical',
|
|
1104
1518
|
itemSize: 50,
|
|
1105
1519
|
ssrRange: { start: 10, end: 20 },
|
|
1106
1520
|
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1107
1521
|
const result = useVirtualScroll(props);
|
|
1108
|
-
expect(result.
|
|
1522
|
+
expect(result.renderedItems.value[ 0 ]?.offset.y).toBe(0);
|
|
1109
1523
|
});
|
|
1110
1524
|
|
|
1111
|
-
it('should handle SSR range with
|
|
1525
|
+
it('should handle SSR range with dynamic horizontal offsets in renderedItems', () => {
|
|
1112
1526
|
const props = ref({
|
|
1113
1527
|
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1114
|
-
direction: '
|
|
1115
|
-
itemSize:
|
|
1528
|
+
direction: 'horizontal',
|
|
1529
|
+
itemSize: undefined, // dynamic
|
|
1116
1530
|
ssrRange: { start: 10, end: 20 },
|
|
1117
1531
|
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1118
1532
|
const result = useVirtualScroll(props);
|
|
1119
|
-
|
|
1533
|
+
// ssrOffsetX = itemSizesX.query(10) = 10 * 40 = 400
|
|
1534
|
+
expect(result.renderedItems.value[ 0 ]?.offset.x).toBe(400);
|
|
1120
1535
|
});
|
|
1121
1536
|
|
|
1122
1537
|
it('should handle SSR range with dynamic sizes for total sizes', () => {
|
|
@@ -1127,7 +1542,7 @@ describe('useVirtualScroll', () => {
|
|
|
1127
1542
|
ssrRange: { start: 10, end: 20 },
|
|
1128
1543
|
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1129
1544
|
const result = useVirtualScroll(props);
|
|
1130
|
-
expect(result.totalHeight.value).toBe(
|
|
1545
|
+
expect(result.totalHeight.value).toBe(400); // (20-10) * 40 - gap(0)
|
|
1131
1546
|
});
|
|
1132
1547
|
|
|
1133
1548
|
it('should handle SSR range with dynamic horizontal sizes for total sizes', () => {
|
|
@@ -1138,7 +1553,7 @@ describe('useVirtualScroll', () => {
|
|
|
1138
1553
|
ssrRange: { start: 10, end: 20 },
|
|
1139
1554
|
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1140
1555
|
const result = useVirtualScroll(props);
|
|
1141
|
-
expect(result.totalWidth.value).toBe(
|
|
1556
|
+
expect(result.totalWidth.value).toBe(400); // (20-10) * 40 - 0 gap
|
|
1142
1557
|
});
|
|
1143
1558
|
|
|
1144
1559
|
it('should handle SSR range with both directions and dynamic offsets', () => {
|
|
@@ -1150,8 +1565,8 @@ describe('useVirtualScroll', () => {
|
|
|
1150
1565
|
ssrRange: { start: 10, end: 20, colStart: 2, colEnd: 5 },
|
|
1151
1566
|
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1152
1567
|
const result = useVirtualScroll(props);
|
|
1153
|
-
expect(result.
|
|
1154
|
-
expect(result.
|
|
1568
|
+
expect(result.totalWidth.value).toBe(300); // (5-2) * 100
|
|
1569
|
+
expect(result.totalHeight.value).toBe(400); // (20-10) * 40
|
|
1155
1570
|
});
|
|
1156
1571
|
|
|
1157
1572
|
it('should scroll to ssrRange on mount', async () => {
|
|
@@ -1184,6 +1599,20 @@ describe('useVirtualScroll', () => {
|
|
|
1184
1599
|
expect(result.renderedItems.value[ 0 ]?.offset.x).toBe(-500);
|
|
1185
1600
|
});
|
|
1186
1601
|
|
|
1602
|
+
it('should handle SSR range with direction "both" and colEnd falsy', () => {
|
|
1603
|
+
const propsValue = ref({
|
|
1604
|
+
columnCount: 10,
|
|
1605
|
+
columnWidth: 100,
|
|
1606
|
+
direction: 'both' as const,
|
|
1607
|
+
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1608
|
+
ssrRange: { colEnd: 0, colStart: 5, end: 10, start: 0 },
|
|
1609
|
+
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1610
|
+
const result = useVirtualScroll(propsValue);
|
|
1611
|
+
// colEnd is 0, so it should use colCount (10)
|
|
1612
|
+
// totalWidth = columnSizes.query(10) - columnSizes.query(5) = 1000 - 500 = 500
|
|
1613
|
+
expect(result.totalWidth.value).toBe(500);
|
|
1614
|
+
});
|
|
1615
|
+
|
|
1187
1616
|
it('should handle SSR range with colCount > 0 in totalWidth', () => {
|
|
1188
1617
|
const props = ref({
|
|
1189
1618
|
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
@@ -1195,20 +1624,365 @@ describe('useVirtualScroll', () => {
|
|
|
1195
1624
|
const result = useVirtualScroll(props);
|
|
1196
1625
|
expect(result.totalWidth.value).toBe(500);
|
|
1197
1626
|
});
|
|
1627
|
+
});
|
|
1198
1628
|
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
const { result } = setup({ ...defaultProps,
|
|
1202
|
-
|
|
1203
|
-
|
|
1629
|
+
describe('helpers', () => {
|
|
1630
|
+
it('should handle zero column count in totalWidth', async () => {
|
|
1631
|
+
const { result } = setup({ ...defaultProps, direction: 'both', columnCount: 0 });
|
|
1632
|
+
expect(result.totalWidth.value).toBe(0);
|
|
1633
|
+
});
|
|
1634
|
+
|
|
1635
|
+
it('should handle vertical direction in totalWidth', () => {
|
|
1636
|
+
const { result } = setup({ ...defaultProps, direction: 'vertical' });
|
|
1637
|
+
expect(result.totalWidth.value).toBe(0);
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
it('should handle horizontal direction in totalHeight', () => {
|
|
1641
|
+
const { result } = setup({ ...defaultProps, direction: 'horizontal' });
|
|
1642
|
+
expect(result.totalHeight.value).toBe(0);
|
|
1643
|
+
});
|
|
1644
|
+
|
|
1645
|
+
it('should handle zero items in totalWidth/totalHeight', async () => {
|
|
1646
|
+
const { result } = setup({ ...defaultProps, items: [] });
|
|
1647
|
+
expect(result.totalHeight.value).toBe(0);
|
|
1648
|
+
|
|
1649
|
+
const { result: rH } = setup({ ...defaultProps, direction: 'horizontal', items: [] });
|
|
1650
|
+
expect(rH.totalWidth.value).toBe(0);
|
|
1651
|
+
});
|
|
1652
|
+
|
|
1653
|
+
it('should cover SSR with zero items/columns', () => {
|
|
1654
|
+
const props = ref({
|
|
1655
|
+
items: [],
|
|
1656
|
+
direction: 'both',
|
|
1657
|
+
columnCount: 0,
|
|
1658
|
+
ssrRange: { start: 0, end: 0, colStart: 0, colEnd: 0 },
|
|
1659
|
+
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1660
|
+
const result = useVirtualScroll(props);
|
|
1661
|
+
expect(result.totalWidth.value).toBe(0);
|
|
1662
|
+
expect(result.totalHeight.value).toBe(0);
|
|
1663
|
+
});
|
|
1664
|
+
|
|
1665
|
+
it('should handle SSR range with both directions and no columns', () => {
|
|
1666
|
+
const props = ref({
|
|
1667
|
+
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1668
|
+
direction: 'both',
|
|
1669
|
+
columnCount: 0,
|
|
1670
|
+
ssrRange: { start: 10, end: 20, colStart: 0, colEnd: 0 },
|
|
1671
|
+
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1672
|
+
const result = useVirtualScroll(props);
|
|
1673
|
+
expect(result.totalWidth.value).toBe(0);
|
|
1674
|
+
});
|
|
1675
|
+
|
|
1676
|
+
it('should handle SSR range with direction both and colCount > 0 but colEnd <= colStart', () => {
|
|
1677
|
+
const props = ref({
|
|
1678
|
+
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1679
|
+
direction: 'both',
|
|
1680
|
+
columnCount: 10,
|
|
1681
|
+
ssrRange: { start: 0, end: 10, colStart: 5, colEnd: 5 },
|
|
1682
|
+
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1683
|
+
const result = useVirtualScroll(props);
|
|
1684
|
+
expect(result.totalWidth.value).toBe(0);
|
|
1685
|
+
});
|
|
1686
|
+
|
|
1687
|
+
it('should handle SSR range with vertical/both and end <= start', () => {
|
|
1688
|
+
const props = ref({
|
|
1689
|
+
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1690
|
+
direction: 'vertical',
|
|
1691
|
+
ssrRange: { start: 10, end: 10 },
|
|
1692
|
+
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1693
|
+
const result = useVirtualScroll(props);
|
|
1694
|
+
expect(result.totalHeight.value).toBe(0);
|
|
1695
|
+
});
|
|
1696
|
+
|
|
1697
|
+
it('should handle SSR range with dynamic horizontal sizes for total sizes', () => {
|
|
1698
|
+
const props = ref({
|
|
1699
|
+
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1700
|
+
direction: 'horizontal',
|
|
1701
|
+
itemSize: 0,
|
|
1702
|
+
ssrRange: { start: 10, end: 20 },
|
|
1703
|
+
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1704
|
+
const result = useVirtualScroll(props);
|
|
1705
|
+
expect(result.totalWidth.value).toBe(400); // (20-10) * 40
|
|
1706
|
+
});
|
|
1707
|
+
|
|
1708
|
+
it('should handle SSR range with both directions and dynamic offsets for total width', () => {
|
|
1709
|
+
const props = ref({
|
|
1710
|
+
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1711
|
+
direction: 'both',
|
|
1712
|
+
columnCount: 10,
|
|
1713
|
+
itemSize: 0,
|
|
1714
|
+
ssrRange: { start: 10, end: 20, colStart: 2, colEnd: 5 },
|
|
1715
|
+
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1716
|
+
const result = useVirtualScroll(props);
|
|
1717
|
+
expect(result.totalWidth.value).toBe(300);
|
|
1718
|
+
});
|
|
1719
|
+
|
|
1720
|
+
it('should handle updateItemSizes with index < 0', async () => {
|
|
1721
|
+
const { result } = setup({ ...defaultProps, itemSize: undefined });
|
|
1722
|
+
result.updateItemSizes([ { index: -1, inlineSize: 100, blockSize: 100 } ]);
|
|
1204
1723
|
await nextTick();
|
|
1205
|
-
//
|
|
1206
|
-
expect(result.
|
|
1724
|
+
// Should not change total height
|
|
1725
|
+
expect(result.totalHeight.value).toBe(4000);
|
|
1207
1726
|
});
|
|
1208
1727
|
|
|
1209
|
-
it('should
|
|
1728
|
+
it('should handle updateItemSizes with direction vertical and dynamic itemSize for X', async () => {
|
|
1729
|
+
const { result } = setup({ ...defaultProps, direction: 'vertical', itemSize: undefined });
|
|
1730
|
+
// Measured Items X should not be updated if direction is vertical
|
|
1731
|
+
result.updateItemSizes([ { index: 0, inlineSize: 100, blockSize: 100 } ]);
|
|
1732
|
+
await nextTick();
|
|
1733
|
+
expect(result.totalWidth.value).toBe(0);
|
|
1734
|
+
});
|
|
1735
|
+
|
|
1736
|
+
it('should handle SSR with horizontal direction and fixedItemSize', () => {
|
|
1737
|
+
const propsValue = ref({
|
|
1738
|
+
direction: 'horizontal' as const,
|
|
1739
|
+
itemSize: 50,
|
|
1740
|
+
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1741
|
+
ssrRange: { end: 20, start: 10 },
|
|
1742
|
+
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1743
|
+
const result = useVirtualScroll(propsValue);
|
|
1744
|
+
expect(result.totalWidth.value).toBe(500); // (20-10) * 50 - 0 gap
|
|
1745
|
+
});
|
|
1746
|
+
|
|
1747
|
+
it('should handle SSR with vertical direction and fixedItemSize', () => {
|
|
1748
|
+
const propsValue = ref({
|
|
1749
|
+
direction: 'vertical' as const,
|
|
1750
|
+
itemSize: 50,
|
|
1751
|
+
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1752
|
+
ssrRange: { end: 20, start: 10 },
|
|
1753
|
+
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1754
|
+
const result = useVirtualScroll(propsValue);
|
|
1755
|
+
expect(result.totalHeight.value).toBe(500); // (20-10) * 50 - 0 gap
|
|
1756
|
+
});
|
|
1757
|
+
|
|
1758
|
+
it('should handle SSR with direction both and fixedItemSize for totalHeight', () => {
|
|
1759
|
+
const propsValue = ref({
|
|
1760
|
+
direction: 'both' as const,
|
|
1761
|
+
columnCount: 10,
|
|
1762
|
+
itemSize: 50,
|
|
1763
|
+
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1764
|
+
ssrRange: { end: 20, start: 10 },
|
|
1765
|
+
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1766
|
+
const result = useVirtualScroll(propsValue);
|
|
1767
|
+
expect(result.totalHeight.value).toBe(500);
|
|
1768
|
+
});
|
|
1769
|
+
|
|
1770
|
+
it('should handle SSR range with direction both and colEnd falsy', () => {
|
|
1771
|
+
const propsValue = ref({
|
|
1772
|
+
columnCount: 10,
|
|
1773
|
+
columnWidth: 100,
|
|
1774
|
+
direction: 'both' as const,
|
|
1775
|
+
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1776
|
+
ssrRange: { colEnd: 0, colStart: 5, end: 10, start: 0 },
|
|
1777
|
+
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1778
|
+
const result = useVirtualScroll(propsValue);
|
|
1779
|
+
// colEnd is 0, so it should use colCount (10)
|
|
1780
|
+
// totalWidth = (10 - 5) * 100 = 500
|
|
1781
|
+
expect(result.totalWidth.value).toBe(500);
|
|
1782
|
+
});
|
|
1783
|
+
|
|
1784
|
+
it('should handle updateItemSizes with direction both and dynamic itemSize for Y', async () => {
|
|
1785
|
+
const { result } = setup({ ...defaultProps, direction: 'both', columnCount: 2, itemSize: undefined });
|
|
1786
|
+
// First measurement
|
|
1787
|
+
result.updateItemSizes([ { index: 0, inlineSize: 100, blockSize: 100 } ]);
|
|
1788
|
+
await nextTick();
|
|
1789
|
+
expect(result.renderedItems.value[ 0 ]!.size.height).toBe(100);
|
|
1790
|
+
|
|
1791
|
+
// Increase
|
|
1792
|
+
result.updateItemSizes([ { index: 0, inlineSize: 100, blockSize: 120 } ]);
|
|
1793
|
+
await nextTick();
|
|
1794
|
+
expect(result.renderedItems.value[ 0 ]!.size.height).toBe(120);
|
|
1795
|
+
|
|
1796
|
+
// Significant decrease
|
|
1797
|
+
result.updateItemSizes([ { index: 0, inlineSize: 100, blockSize: 100 } ]);
|
|
1798
|
+
await nextTick();
|
|
1799
|
+
expect(result.renderedItems.value[ 0 ]!.size.height).toBe(100);
|
|
1800
|
+
});
|
|
1801
|
+
|
|
1802
|
+
it('should handle object padding branches in helpers', () => {
|
|
1210
1803
|
expect(getPaddingX({ x: 10 }, 'horizontal')).toBe(10);
|
|
1211
1804
|
expect(getPaddingY({ y: 20 }, 'vertical')).toBe(20);
|
|
1212
1805
|
});
|
|
1806
|
+
|
|
1807
|
+
it('should cover totalWidth SSR len <= 0', () => {
|
|
1808
|
+
const propsValue = ref({
|
|
1809
|
+
items: mockItems,
|
|
1810
|
+
direction: 'horizontal' as const,
|
|
1811
|
+
itemSize: 50,
|
|
1812
|
+
ssrRange: { start: 10, end: 10 },
|
|
1813
|
+
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1814
|
+
const result = useVirtualScroll(propsValue);
|
|
1815
|
+
expect(result.totalWidth.value).toBe(0);
|
|
1816
|
+
});
|
|
1817
|
+
|
|
1818
|
+
it('should cover totalWidth SSR end <= start for dynamic sizes', () => {
|
|
1819
|
+
const propsValue = ref({
|
|
1820
|
+
items: mockItems,
|
|
1821
|
+
direction: 'horizontal' as const,
|
|
1822
|
+
itemSize: undefined,
|
|
1823
|
+
ssrRange: { start: 10, end: 10 },
|
|
1824
|
+
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1825
|
+
const result = useVirtualScroll(propsValue);
|
|
1826
|
+
expect(result.totalWidth.value).toBe(0);
|
|
1827
|
+
});
|
|
1828
|
+
|
|
1829
|
+
it('should cover totalWidth non-SSR items.length <= 0 for dynamic sizes', () => {
|
|
1830
|
+
const propsValue = ref({
|
|
1831
|
+
items: [],
|
|
1832
|
+
direction: 'horizontal' as const,
|
|
1833
|
+
itemSize: undefined,
|
|
1834
|
+
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1835
|
+
const result = useVirtualScroll(propsValue);
|
|
1836
|
+
expect(result.totalWidth.value).toBe(0);
|
|
1837
|
+
});
|
|
1838
|
+
|
|
1839
|
+
it('should cover totalHeight SSR len <= 0', () => {
|
|
1840
|
+
const propsValue = ref({
|
|
1841
|
+
items: mockItems,
|
|
1842
|
+
direction: 'vertical' as const,
|
|
1843
|
+
itemSize: 50,
|
|
1844
|
+
ssrRange: { start: 10, end: 10 },
|
|
1845
|
+
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1846
|
+
const result = useVirtualScroll(propsValue);
|
|
1847
|
+
expect(result.totalHeight.value).toBe(0);
|
|
1848
|
+
});
|
|
1849
|
+
|
|
1850
|
+
it('should cover totalHeight SSR end <= start for dynamic sizes', () => {
|
|
1851
|
+
const propsValue = ref({
|
|
1852
|
+
items: mockItems,
|
|
1853
|
+
direction: 'vertical' as const,
|
|
1854
|
+
itemSize: undefined,
|
|
1855
|
+
ssrRange: { start: 10, end: 10 },
|
|
1856
|
+
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1857
|
+
const result = useVirtualScroll(propsValue);
|
|
1858
|
+
expect(result.totalHeight.value).toBe(0);
|
|
1859
|
+
});
|
|
1860
|
+
|
|
1861
|
+
it('should cover totalHeight non-SSR items.length <= 0 for dynamic sizes', () => {
|
|
1862
|
+
const propsValue = ref({
|
|
1863
|
+
items: [],
|
|
1864
|
+
direction: 'vertical' as const,
|
|
1865
|
+
itemSize: undefined,
|
|
1866
|
+
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1867
|
+
const result = useVirtualScroll(propsValue);
|
|
1868
|
+
expect(result.totalHeight.value).toBe(0);
|
|
1869
|
+
});
|
|
1870
|
+
|
|
1871
|
+
describe('internal sizing stabilization and edge cases', () => {
|
|
1872
|
+
const mockItems = Array.from({ length: 10 }, (_, i) => ({ id: i }));
|
|
1873
|
+
|
|
1874
|
+
it('should skip re-initializing sizes for already measured dynamic items', async () => {
|
|
1875
|
+
const props = ref({
|
|
1876
|
+
items: mockItems,
|
|
1877
|
+
direction: 'both' as const,
|
|
1878
|
+
columnCount: 2,
|
|
1879
|
+
}) as Ref<VirtualScrollProps<{ id: number; }>>;
|
|
1880
|
+
|
|
1881
|
+
const result = useVirtualScroll(props);
|
|
1882
|
+
await nextTick();
|
|
1883
|
+
|
|
1884
|
+
// Measure item 0 and col 0
|
|
1885
|
+
const parent = document.createElement('div');
|
|
1886
|
+
const col0 = document.createElement('div');
|
|
1887
|
+
col0.dataset.colIndex = '0';
|
|
1888
|
+
Object.defineProperty(col0, 'offsetWidth', { value: 200 });
|
|
1889
|
+
parent.appendChild(col0);
|
|
1890
|
+
|
|
1891
|
+
result.updateItemSizes([ { index: 0, inlineSize: 200, blockSize: 150, element: parent } ]);
|
|
1892
|
+
await nextTick();
|
|
1893
|
+
|
|
1894
|
+
expect(result.getColumnWidth(0)).toBe(200);
|
|
1895
|
+
expect(result.renderedItems.value[ 0 ]?.size.height).toBe(150);
|
|
1896
|
+
|
|
1897
|
+
// Trigger initializeSizes by changing items length
|
|
1898
|
+
props.value.items = Array.from({ length: 11 }, (_, i) => ({ id: i }));
|
|
1899
|
+
await nextTick();
|
|
1900
|
+
|
|
1901
|
+
// Should NOT reset already measured item 0
|
|
1902
|
+
expect(result.getColumnWidth(0)).toBe(200);
|
|
1903
|
+
expect(result.renderedItems.value[ 0 ]?.size.height).toBe(150);
|
|
1904
|
+
});
|
|
1905
|
+
|
|
1906
|
+
it('should mark items as measured when fixed size matches current size within tolerance', async () => {
|
|
1907
|
+
const props = ref({
|
|
1908
|
+
items: mockItems,
|
|
1909
|
+
direction: 'horizontal' as const,
|
|
1910
|
+
itemSize: 50,
|
|
1911
|
+
}) as Ref<VirtualScrollProps<{ id: number; }>>;
|
|
1912
|
+
|
|
1913
|
+
useVirtualScroll(props);
|
|
1914
|
+
await nextTick();
|
|
1915
|
+
|
|
1916
|
+
// Trigger initializeSizes again with same prop
|
|
1917
|
+
props.value.columnGap = 0;
|
|
1918
|
+
await nextTick();
|
|
1919
|
+
// Hits the branch where Math.abs(current - target) <= 0.5
|
|
1920
|
+
});
|
|
1921
|
+
|
|
1922
|
+
it('should mark columns as measured when fixed width matches current width within tolerance', async () => {
|
|
1923
|
+
const props = ref({
|
|
1924
|
+
items: mockItems,
|
|
1925
|
+
direction: 'both' as const,
|
|
1926
|
+
columnCount: 2,
|
|
1927
|
+
columnWidth: 100,
|
|
1928
|
+
}) as Ref<VirtualScrollProps<{ id: number; }>>;
|
|
1929
|
+
|
|
1930
|
+
useVirtualScroll(props);
|
|
1931
|
+
await nextTick();
|
|
1932
|
+
|
|
1933
|
+
props.value.columnGap = 0;
|
|
1934
|
+
await nextTick();
|
|
1935
|
+
});
|
|
1936
|
+
|
|
1937
|
+
it('should reset item sizes when switching between horizontal and vertical directions', async () => {
|
|
1938
|
+
const props = ref({
|
|
1939
|
+
items: mockItems,
|
|
1940
|
+
direction: 'horizontal' as const,
|
|
1941
|
+
itemSize: 50,
|
|
1942
|
+
}) as Ref<VirtualScrollProps<{ id: number; }>>;
|
|
1943
|
+
|
|
1944
|
+
const result = useVirtualScroll(props);
|
|
1945
|
+
await nextTick();
|
|
1946
|
+
expect(result.totalWidth.value).toBe(500);
|
|
1947
|
+
|
|
1948
|
+
// Switch to vertical (resets X)
|
|
1949
|
+
props.value.direction = 'vertical';
|
|
1950
|
+
await nextTick();
|
|
1951
|
+
expect(result.totalWidth.value).toBe(0);
|
|
1952
|
+
|
|
1953
|
+
// Switch to both
|
|
1954
|
+
props.value.direction = 'both';
|
|
1955
|
+
props.value.columnCount = 10;
|
|
1956
|
+
props.value.columnWidth = 100;
|
|
1957
|
+
await nextTick();
|
|
1958
|
+
expect(result.totalHeight.value).toBe(500);
|
|
1959
|
+
expect(result.totalWidth.value).toBe(1000);
|
|
1960
|
+
|
|
1961
|
+
// Switch to horizontal (resets Y)
|
|
1962
|
+
props.value.direction = 'horizontal';
|
|
1963
|
+
await nextTick();
|
|
1964
|
+
await nextTick();
|
|
1965
|
+
expect(result.totalHeight.value).toBe(0);
|
|
1966
|
+
});
|
|
1967
|
+
|
|
1968
|
+
it('should skip re-initialization if dynamic size is already measured and non-zero', async () => {
|
|
1969
|
+
const props = ref({
|
|
1970
|
+
items: mockItems,
|
|
1971
|
+
direction: 'horizontal' as const,
|
|
1972
|
+
itemSize: undefined, // dynamic
|
|
1973
|
+
}) as Ref<VirtualScrollProps<{ id: number; }>>;
|
|
1974
|
+
|
|
1975
|
+
const result = useVirtualScroll(props);
|
|
1976
|
+
await nextTick();
|
|
1977
|
+
|
|
1978
|
+
result.updateItemSizes([ { index: 0, inlineSize: 100, blockSize: 50, element: document.createElement('div') } ]);
|
|
1979
|
+
await nextTick();
|
|
1980
|
+
|
|
1981
|
+
props.value.gap = 1;
|
|
1982
|
+
await nextTick();
|
|
1983
|
+
|
|
1984
|
+
expect(result.totalWidth.value).toBeGreaterThan(0);
|
|
1985
|
+
});
|
|
1986
|
+
});
|
|
1213
1987
|
});
|
|
1214
1988
|
});
|