@pdanpdan/virtual-scroll 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.css +1 -1
- package/dist/index.js +152 -131
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/VirtualScroll.test.ts +353 -332
- package/src/components/VirtualScroll.vue +24 -6
- package/src/composables/useVirtualScroll.test.ts +545 -204
- package/src/composables/useVirtualScroll.ts +28 -25
- package/src/utils/fenwick-tree.test.ts +80 -65
|
@@ -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);
|
|
@@ -147,7 +159,7 @@ describe('useVirtualScroll', () => {
|
|
|
147
159
|
});
|
|
148
160
|
});
|
|
149
161
|
|
|
150
|
-
describe('range
|
|
162
|
+
describe('range calculation', () => {
|
|
151
163
|
it('should calculate rendered items based on scroll position', async () => {
|
|
152
164
|
const { result } = setup({ ...defaultProps });
|
|
153
165
|
expect(result.renderedItems.value.length).toBeGreaterThan(0);
|
|
@@ -175,29 +187,81 @@ describe('useVirtualScroll', () => {
|
|
|
175
187
|
await nextTick();
|
|
176
188
|
expect(result.scrollDetails.value.currentIndex).toBeGreaterThan(0);
|
|
177
189
|
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('dynamic sizing', () => {
|
|
193
|
+
it('should handle columnCount fallback in updateItemSizes', async () => {
|
|
194
|
+
const { result, props } = setup({
|
|
195
|
+
...defaultProps,
|
|
196
|
+
direction: 'both',
|
|
197
|
+
columnCount: 10,
|
|
198
|
+
columnWidth: undefined,
|
|
199
|
+
});
|
|
200
|
+
await nextTick();
|
|
201
|
+
|
|
202
|
+
const cell = document.createElement('div');
|
|
203
|
+
cell.dataset.colIndex = '0';
|
|
204
|
+
|
|
205
|
+
// Getter that returns 10 first time (for guard) and null second time (for fallback)
|
|
206
|
+
let count = 0;
|
|
207
|
+
Object.defineProperty(props.value, 'columnCount', {
|
|
208
|
+
get() {
|
|
209
|
+
count++;
|
|
210
|
+
return count === 1 ? 10 : null;
|
|
211
|
+
},
|
|
212
|
+
configurable: true,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
result.updateItemSizes([ { index: 0, inlineSize: 200, blockSize: 50, element: cell } ]);
|
|
216
|
+
await nextTick();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should handle updateItemSizes with direct cell element', async () => {
|
|
220
|
+
const { result } = setup({
|
|
221
|
+
...defaultProps,
|
|
222
|
+
direction: 'both',
|
|
223
|
+
columnCount: 2,
|
|
224
|
+
columnWidth: undefined,
|
|
225
|
+
});
|
|
226
|
+
await nextTick();
|
|
178
227
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
228
|
+
const cell = document.createElement('div');
|
|
229
|
+
Object.defineProperty(cell, 'offsetWidth', { value: 200 });
|
|
230
|
+
cell.dataset.colIndex = '0';
|
|
231
|
+
|
|
232
|
+
result.updateItemSizes([ { index: 0, inlineSize: 200, blockSize: 50, element: cell } ]);
|
|
233
|
+
await nextTick();
|
|
234
|
+
expect(result.getColumnWidth(0)).toBe(200);
|
|
182
235
|
});
|
|
183
236
|
|
|
184
|
-
it('should
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
237
|
+
it('should handle updateItemSizes initial measurement even if smaller than estimate', async () => {
|
|
238
|
+
// Horizontal
|
|
239
|
+
const { result: rH } = setup({ ...defaultProps, direction: 'horizontal', itemSize: undefined });
|
|
240
|
+
await nextTick();
|
|
241
|
+
// Estimate is 50. Update with 40.
|
|
242
|
+
rH.updateItemSizes([ { index: 0, inlineSize: 40, blockSize: 40 } ]);
|
|
243
|
+
await nextTick();
|
|
244
|
+
expect(rH.renderedItems.value[ 0 ]?.size.width).toBe(40);
|
|
245
|
+
|
|
246
|
+
// Subsequent update with smaller size should be ignored
|
|
247
|
+
rH.updateItemSizes([ { index: 0, inlineSize: 30, blockSize: 30 } ]);
|
|
248
|
+
await nextTick();
|
|
249
|
+
expect(rH.renderedItems.value[ 0 ]?.size.width).toBe(40);
|
|
188
250
|
|
|
189
|
-
//
|
|
190
|
-
result
|
|
251
|
+
// Vertical
|
|
252
|
+
const { result: rV } = setup({ ...defaultProps, direction: 'vertical', itemSize: undefined });
|
|
191
253
|
await nextTick();
|
|
254
|
+
rV.updateItemSizes([ { index: 0, inlineSize: 40, blockSize: 40 } ]);
|
|
255
|
+
await nextTick();
|
|
256
|
+
expect(rV.renderedItems.value[ 0 ]?.size.height).toBe(40);
|
|
192
257
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
258
|
+
// Subsequent update with smaller size should be ignored
|
|
259
|
+
rV.updateItemSizes([ { index: 0, inlineSize: 30, blockSize: 30 } ]);
|
|
260
|
+
await nextTick();
|
|
261
|
+
expect(rV.renderedItems.value[ 0 ]?.size.height).toBe(40);
|
|
196
262
|
});
|
|
197
|
-
});
|
|
198
263
|
|
|
199
|
-
|
|
200
|
-
it('should update item size and trigger reactivity', async () => {
|
|
264
|
+
it('should handle updateItemSize and trigger reactivity', async () => {
|
|
201
265
|
const { result } = setup({ ...defaultProps, itemSize: undefined });
|
|
202
266
|
expect(result.totalHeight.value).toBe(5000); // Default estimate
|
|
203
267
|
|
|
@@ -325,95 +389,29 @@ describe('useVirtualScroll', () => {
|
|
|
325
389
|
// Should still be 100 for index 0, not reset to default 50
|
|
326
390
|
expect(result.totalHeight.value).toBe(5050 + 50);
|
|
327
391
|
});
|
|
392
|
+
});
|
|
328
393
|
|
|
329
|
-
|
|
330
|
-
|
|
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
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
it('should cover spacer skip heuristic in updateItemSize', async () => {
|
|
394
|
+
describe('scrolling and API', () => {
|
|
395
|
+
it('should handle scrollToIndex with horizontal direction and dynamic item size', async () => {
|
|
340
396
|
const container = document.createElement('div');
|
|
341
|
-
Object.defineProperty(container, 'clientWidth', { value: 500 });
|
|
342
|
-
const { result } = setup({ ...defaultProps, direction: '
|
|
397
|
+
Object.defineProperty(container, 'clientWidth', { configurable: true, value: 500 });
|
|
398
|
+
const { result } = setup({ ...defaultProps, container, direction: 'horizontal', itemSize: undefined });
|
|
343
399
|
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);
|
|
349
|
-
await nextTick();
|
|
350
|
-
});
|
|
351
400
|
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
363
|
-
// First measurement is 100
|
|
364
|
-
result.updateItemSize(0, 100, 50, parent);
|
|
401
|
+
// index 10. itemSize is 50 by default. totalWidth = 5000.
|
|
402
|
+
result.scrollToIndex(null, 10, { align: 'start', behavior: 'auto' });
|
|
365
403
|
await nextTick();
|
|
366
|
-
expect(result.
|
|
404
|
+
expect(result.scrollDetails.value.scrollOffset.x).toBe(500);
|
|
367
405
|
});
|
|
368
406
|
|
|
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);
|
|
407
|
+
it('should handle scrollToIndex with window fallback when container is missing', async () => {
|
|
408
|
+
const { result } = setup({ ...defaultProps, container: undefined });
|
|
376
409
|
await nextTick();
|
|
377
|
-
|
|
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);
|
|
410
|
+
result.scrollToIndex(10, 0);
|
|
386
411
|
await nextTick();
|
|
387
|
-
expect(
|
|
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 } ]);
|
|
410
|
-
await nextTick();
|
|
411
|
-
expect(result.getColumnWidth(0)).toBe(200);
|
|
412
|
-
expect(result.getColumnWidth(1)).toBe(300);
|
|
412
|
+
expect(window.scrollTo).toHaveBeenCalled();
|
|
413
413
|
});
|
|
414
|
-
});
|
|
415
414
|
|
|
416
|
-
describe('scroll and offsets', () => {
|
|
417
415
|
it('should handle scrollToIndex out of bounds', async () => {
|
|
418
416
|
const { result } = setup({ ...defaultProps });
|
|
419
417
|
// Row past end
|
|
@@ -443,7 +441,7 @@ describe('useVirtualScroll', () => {
|
|
|
443
441
|
|
|
444
442
|
// Current visible range: [scrollTop + paddingStart, scrollTop + viewport - paddingEnd] = [300, 700]
|
|
445
443
|
// Scroll to item at y=250. 250 < 300, so not visible.
|
|
446
|
-
// targetY < relativeScrollY + paddingStart (250 < 200 + 100)
|
|
444
|
+
// targetY < relativeScrollY + paddingStart (250 < 200 + 100)
|
|
447
445
|
result.scrollToIndex(5, null, 'auto');
|
|
448
446
|
await nextTick();
|
|
449
447
|
});
|
|
@@ -466,6 +464,59 @@ describe('useVirtualScroll', () => {
|
|
|
466
464
|
expect(container.scrollTo).toHaveBeenCalled();
|
|
467
465
|
});
|
|
468
466
|
|
|
467
|
+
it('should handle scrollToOffset with currentX/currentY fallbacks', async () => {
|
|
468
|
+
const container = document.createElement('div');
|
|
469
|
+
Object.defineProperty(container, 'scrollLeft', { value: 50, writable: true });
|
|
470
|
+
Object.defineProperty(container, 'scrollTop', { value: 60, writable: true });
|
|
471
|
+
|
|
472
|
+
const { result } = setup({ ...defaultProps, container });
|
|
473
|
+
await nextTick();
|
|
474
|
+
|
|
475
|
+
// Pass null to x and y to trigger fallbacks to currentX and currentY
|
|
476
|
+
result.scrollToOffset(null, null);
|
|
477
|
+
await nextTick();
|
|
478
|
+
|
|
479
|
+
// scrollOffset.x = targetX - hostOffset.x + (isHorizontal ? paddingStartX : 0)
|
|
480
|
+
// targetX = currentX = 50. hostOffset.x = 0. isHorizontal = false.
|
|
481
|
+
// So scrollOffset.x = 50.
|
|
482
|
+
expect(result.scrollDetails.value.scrollOffset.x).toBe(50);
|
|
483
|
+
expect(result.scrollDetails.value.scrollOffset.y).toBe(60);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('should handle scrollToOffset with restricted direction for padding fallback', async () => {
|
|
487
|
+
const container = document.createElement('div');
|
|
488
|
+
container.scrollTo = vi.fn();
|
|
489
|
+
|
|
490
|
+
// Horizontal direction: isVertical will be false, so targetY padding fallback will be 0
|
|
491
|
+
const { result } = setup({ ...defaultProps, container, direction: 'horizontal', scrollPaddingStart: 10 });
|
|
492
|
+
await nextTick();
|
|
493
|
+
|
|
494
|
+
result.scrollToOffset(100, 100);
|
|
495
|
+
await nextTick();
|
|
496
|
+
// targetY = 100 + hostOffset.y - (isVertical ? paddingStartY : 0)
|
|
497
|
+
// Since isVertical is false, it uses 0. hostOffset.y is 0 here.
|
|
498
|
+
expect(container.scrollTo).toHaveBeenCalledWith(expect.objectContaining({
|
|
499
|
+
top: 100,
|
|
500
|
+
}));
|
|
501
|
+
|
|
502
|
+
// Vertical direction: isHorizontal will be false, so targetX padding fallback will be 0
|
|
503
|
+
const { result: r2 } = setup({ ...defaultProps, container, direction: 'vertical', scrollPaddingStart: 10 });
|
|
504
|
+
await nextTick();
|
|
505
|
+
r2.scrollToOffset(100, 100);
|
|
506
|
+
await nextTick();
|
|
507
|
+
expect(container.scrollTo).toHaveBeenCalledWith(expect.objectContaining({
|
|
508
|
+
left: 100,
|
|
509
|
+
}));
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it('should handle scrollToOffset with window fallback when container is missing', async () => {
|
|
513
|
+
const { result } = setup({ ...defaultProps, container: undefined });
|
|
514
|
+
await nextTick();
|
|
515
|
+
result.scrollToOffset(100, 200);
|
|
516
|
+
await nextTick();
|
|
517
|
+
expect(window.scrollTo).toHaveBeenCalled();
|
|
518
|
+
});
|
|
519
|
+
|
|
469
520
|
it('should handle scrollToIndex with null indices', async () => {
|
|
470
521
|
const { result } = setup({ ...defaultProps });
|
|
471
522
|
result.scrollToIndex(null, null);
|
|
@@ -580,57 +631,54 @@ describe('useVirtualScroll', () => {
|
|
|
580
631
|
expect(container.scrollTop).toBe(400);
|
|
581
632
|
});
|
|
582
633
|
|
|
583
|
-
it('should
|
|
584
|
-
const { result } = setup(
|
|
585
|
-
result.scrollToIndex(10,
|
|
586
|
-
|
|
587
|
-
});
|
|
634
|
+
it('should stop programmatic scroll', async () => {
|
|
635
|
+
const { result } = setup(defaultProps);
|
|
636
|
+
result.scrollToIndex(10, null, { behavior: 'smooth' });
|
|
637
|
+
expect(result.scrollDetails.value.isProgrammaticScroll).toBe(true);
|
|
588
638
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
result.scrollToIndex(200, null);
|
|
592
|
-
await nextTick();
|
|
639
|
+
result.stopProgrammaticScroll();
|
|
640
|
+
expect(result.scrollDetails.value.isProgrammaticScroll).toBe(false);
|
|
593
641
|
});
|
|
594
642
|
|
|
595
|
-
it('should handle scrollToIndex
|
|
643
|
+
it('should handle scrollToIndex with element container having scrollTo', async () => {
|
|
596
644
|
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 });
|
|
645
|
+
container.scrollTo = vi.fn();
|
|
646
|
+
const { result } = setup({ ...defaultProps, container });
|
|
604
647
|
await nextTick();
|
|
605
648
|
|
|
606
|
-
|
|
607
|
-
// targetX < relativeScrollX + paddingStart (250 < 1100)
|
|
608
|
-
result.scrollToIndex(null, 5, 'auto');
|
|
649
|
+
result.scrollToIndex(10, 0, { behavior: 'auto' });
|
|
609
650
|
await nextTick();
|
|
610
|
-
expect(container.
|
|
651
|
+
expect(container.scrollTo).toHaveBeenCalled();
|
|
652
|
+
});
|
|
611
653
|
|
|
612
|
-
|
|
613
|
-
|
|
654
|
+
it('should handle scrollToIndex fallback when scrollTo is missing', async () => {
|
|
655
|
+
const container = document.createElement('div');
|
|
656
|
+
(container as unknown as { scrollTo: unknown; }).scrollTo = undefined;
|
|
657
|
+
const { result } = setup({ ...defaultProps, container });
|
|
614
658
|
await nextTick();
|
|
615
659
|
|
|
616
|
-
//
|
|
617
|
-
result.scrollToIndex(
|
|
660
|
+
// row only
|
|
661
|
+
result.scrollToIndex(10, null, { behavior: 'auto' });
|
|
618
662
|
await nextTick();
|
|
619
|
-
|
|
663
|
+
expect(container.scrollTop).toBeGreaterThan(0);
|
|
620
664
|
|
|
621
|
-
|
|
622
|
-
setup({ ...defaultProps, direction: '
|
|
665
|
+
// col only
|
|
666
|
+
const { result: resH } = setup({ ...defaultProps, container, direction: 'horizontal' });
|
|
667
|
+
await nextTick();
|
|
668
|
+
resH.scrollToIndex(null, 10, { behavior: 'auto' });
|
|
623
669
|
await nextTick();
|
|
624
|
-
|
|
670
|
+
expect(container.scrollLeft).toBeGreaterThan(0);
|
|
625
671
|
});
|
|
626
672
|
|
|
627
|
-
it('should
|
|
628
|
-
const {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
673
|
+
it('should skip undefined items in renderedItems', async () => {
|
|
674
|
+
const items = Array.from({ length: 10 }) as unknown[];
|
|
675
|
+
items[ 0 ] = { id: 0 };
|
|
676
|
+
// other indices are undefined
|
|
677
|
+
const { result } = setup({ ...defaultProps, items, itemSize: 50 });
|
|
678
|
+
await nextTick();
|
|
679
|
+
// only index 0 should be rendered
|
|
680
|
+
expect(result.renderedItems.value.length).toBe(1);
|
|
681
|
+
expect(result.renderedItems.value[ 0 ]?.index).toBe(0);
|
|
634
682
|
});
|
|
635
683
|
});
|
|
636
684
|
|
|
@@ -643,6 +691,72 @@ describe('useVirtualScroll', () => {
|
|
|
643
691
|
await nextTick();
|
|
644
692
|
});
|
|
645
693
|
|
|
694
|
+
it('should cover fallback branches for unknown targets and directions', async () => {
|
|
695
|
+
// 1. Unknown container type (hits 408, 445, 513, 718 else branches)
|
|
696
|
+
const unknownContainer = {
|
|
697
|
+
addEventListener: vi.fn(),
|
|
698
|
+
removeEventListener: vi.fn(),
|
|
699
|
+
} as unknown as HTMLElement;
|
|
700
|
+
|
|
701
|
+
const { result } = setup({
|
|
702
|
+
...defaultProps,
|
|
703
|
+
container: unknownContainer,
|
|
704
|
+
hostElement: document.createElement('div'),
|
|
705
|
+
});
|
|
706
|
+
await nextTick();
|
|
707
|
+
|
|
708
|
+
result.scrollToIndex(10, 0);
|
|
709
|
+
result.scrollToOffset(100, 100);
|
|
710
|
+
result.updateHostOffset();
|
|
711
|
+
|
|
712
|
+
// 2. Invalid direction (hits 958 else branch)
|
|
713
|
+
const { result: r2 } = setup({
|
|
714
|
+
...defaultProps,
|
|
715
|
+
direction: undefined as unknown as 'vertical',
|
|
716
|
+
stickyIndices: [ 0 ],
|
|
717
|
+
});
|
|
718
|
+
await nextTick();
|
|
719
|
+
window.dispatchEvent(new Event('scroll'));
|
|
720
|
+
await nextTick();
|
|
721
|
+
expect(r2.renderedItems.value.find((i) => i.index === 0)).toBeDefined();
|
|
722
|
+
|
|
723
|
+
// 3. Unknown target in handleScroll (hits 1100 else branch)
|
|
724
|
+
const container = document.createElement('div');
|
|
725
|
+
setup({ ...defaultProps, container });
|
|
726
|
+
const event = new Event('scroll');
|
|
727
|
+
Object.defineProperty(event, 'target', { value: { } });
|
|
728
|
+
container.dispatchEvent(event);
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it('should cleanup events and observers when container changes', async () => {
|
|
732
|
+
const container = document.createElement('div');
|
|
733
|
+
const removeSpy = vi.spyOn(container, 'removeEventListener');
|
|
734
|
+
const { props } = setup({ ...defaultProps, container });
|
|
735
|
+
await nextTick();
|
|
736
|
+
|
|
737
|
+
// Change container to trigger cleanup of old one
|
|
738
|
+
props.value.container = document.createElement('div');
|
|
739
|
+
await nextTick();
|
|
740
|
+
|
|
741
|
+
expect(removeSpy).toHaveBeenCalledWith('scroll', expect.any(Function));
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
it('should cleanup when unmounted and container is window', async () => {
|
|
745
|
+
const { wrapper } = setup({ ...defaultProps, container: window });
|
|
746
|
+
await nextTick();
|
|
747
|
+
wrapper.unmount();
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it('should cleanup when unmounted', async () => {
|
|
751
|
+
const container = document.createElement('div');
|
|
752
|
+
const removeSpy = vi.spyOn(container, 'removeEventListener');
|
|
753
|
+
const { wrapper } = setup({ ...defaultProps, container });
|
|
754
|
+
await nextTick();
|
|
755
|
+
|
|
756
|
+
wrapper.unmount();
|
|
757
|
+
expect(removeSpy).toHaveBeenCalledWith('scroll', expect.any(Function));
|
|
758
|
+
});
|
|
759
|
+
|
|
646
760
|
it('should handle document scroll events', async () => {
|
|
647
761
|
setup({ ...defaultProps });
|
|
648
762
|
document.dispatchEvent(new Event('scroll'));
|
|
@@ -698,7 +812,7 @@ describe('useVirtualScroll', () => {
|
|
|
698
812
|
|
|
699
813
|
it('should handle window resize events', async () => {
|
|
700
814
|
setup({ ...defaultProps, container: window });
|
|
701
|
-
window
|
|
815
|
+
Object.defineProperty(window, 'innerWidth', { configurable: true, value: 1200 });
|
|
702
816
|
window.dispatchEvent(new Event('resize'));
|
|
703
817
|
await nextTick();
|
|
704
818
|
});
|
|
@@ -803,71 +917,43 @@ describe('useVirtualScroll', () => {
|
|
|
803
917
|
});
|
|
804
918
|
});
|
|
805
919
|
|
|
806
|
-
describe('
|
|
807
|
-
it('should
|
|
808
|
-
|
|
809
|
-
const { result } = setup({ ...defaultProps, container: window, itemSize: undefined });
|
|
920
|
+
describe('sticky and pushed items', () => {
|
|
921
|
+
it('should identify sticky items', async () => {
|
|
922
|
+
const { result } = setup({ ...defaultProps, stickyIndices: [ 0, 10 ] });
|
|
810
923
|
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);
|
|
815
|
-
await nextTick();
|
|
816
|
-
expect(result.scrollDetails.value.isScrolling).toBe(false);
|
|
817
|
-
vi.useRealTimers();
|
|
818
|
-
});
|
|
819
924
|
|
|
820
|
-
|
|
821
|
-
const
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
result.updateItemSize(5, 100, 100);
|
|
826
|
-
await nextTick();
|
|
925
|
+
const items = result.renderedItems.value;
|
|
926
|
+
const item0 = items.find((i) => i.index === 0);
|
|
927
|
+
const item10 = items.find((i) => i.index === 10);
|
|
928
|
+
expect(item0?.isSticky).toBe(true);
|
|
929
|
+
expect(item10?.isSticky).toBe(true);
|
|
827
930
|
});
|
|
828
931
|
|
|
829
|
-
it('should
|
|
830
|
-
const { result
|
|
831
|
-
const host = document.createElement('div');
|
|
832
|
-
props.value.hostElement = host;
|
|
932
|
+
it('should make sticky items active when scrolled past', async () => {
|
|
933
|
+
const { result } = setup({ ...defaultProps, stickyIndices: [ 0 ] });
|
|
833
934
|
await nextTick();
|
|
834
|
-
result.updateHostOffset();
|
|
835
|
-
});
|
|
836
935
|
|
|
837
|
-
|
|
838
|
-
const host = document.createElement('div');
|
|
839
|
-
const { result } = setup({ ...defaultProps, container: host, hostElement: host });
|
|
936
|
+
result.scrollToOffset(0, 100);
|
|
840
937
|
await nextTick();
|
|
841
|
-
result.updateHostOffset();
|
|
842
|
-
});
|
|
843
938
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
const hostElement = document.createElement('div');
|
|
847
|
-
|
|
848
|
-
container.getBoundingClientRect = vi.fn(() => ({ top: 100, left: 100, bottom: 200, right: 200, width: 100, height: 100, x: 100, y: 100, toJSON: () => '' }));
|
|
849
|
-
hostElement.getBoundingClientRect = vi.fn(() => ({ top: 150, left: 150, bottom: 200, right: 200, width: 50, height: 50, x: 150, y: 150, toJSON: () => '' }));
|
|
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();
|
|
939
|
+
const item0 = result.renderedItems.value.find((i) => i.index === 0);
|
|
940
|
+
expect(item0?.isStickyActive).toBe(true);
|
|
856
941
|
});
|
|
857
942
|
|
|
858
|
-
it('should
|
|
859
|
-
const { result } = setup({ ...defaultProps,
|
|
860
|
-
result.updateItemSize(0, 100, 100);
|
|
943
|
+
it('should include current sticky item in rendered items even if range is ahead', async () => {
|
|
944
|
+
const { result } = setup({ ...defaultProps, stickyIndices: [ 0 ], bufferBefore: 0 });
|
|
861
945
|
await nextTick();
|
|
862
|
-
expect(result.totalHeight.value).toBe(5050);
|
|
863
946
|
|
|
864
|
-
|
|
947
|
+
// Scroll to index 20. Range starts at 20.
|
|
948
|
+
result.scrollToIndex(20, 0, { align: 'start', behavior: 'auto' });
|
|
865
949
|
await nextTick();
|
|
866
|
-
|
|
950
|
+
|
|
951
|
+
expect(result.scrollDetails.value.range.start).toBe(20);
|
|
952
|
+
const item0 = result.renderedItems.value.find((i) => i.index === 0);
|
|
953
|
+
expect(item0).toBeDefined();
|
|
954
|
+
expect(item0?.isStickyActive).toBe(true);
|
|
867
955
|
});
|
|
868
|
-
});
|
|
869
956
|
|
|
870
|
-
describe('sticky header pushing', () => {
|
|
871
957
|
it('should push sticky item when next sticky item approaches (vertical)', async () => {
|
|
872
958
|
const container = document.createElement('div');
|
|
873
959
|
Object.defineProperty(container, 'clientHeight', { value: 500 });
|
|
@@ -900,6 +986,50 @@ describe('useVirtualScroll', () => {
|
|
|
900
986
|
const item0 = result.renderedItems.value.find((i) => i.index === 0);
|
|
901
987
|
expect(item0!.offset.x).toBeLessThanOrEqual(450);
|
|
902
988
|
});
|
|
989
|
+
|
|
990
|
+
it('should handle dynamic sticky item pushing in vertical mode', async () => {
|
|
991
|
+
const container = document.createElement('div');
|
|
992
|
+
Object.defineProperty(container, 'clientHeight', { value: 500 });
|
|
993
|
+
Object.defineProperty(container, 'scrollTop', { value: 460, writable: true });
|
|
994
|
+
|
|
995
|
+
const { result } = setup({
|
|
996
|
+
...defaultProps,
|
|
997
|
+
container,
|
|
998
|
+
itemSize: undefined, // dynamic
|
|
999
|
+
stickyIndices: [ 0, 10 ],
|
|
1000
|
+
});
|
|
1001
|
+
await nextTick();
|
|
1002
|
+
|
|
1003
|
+
// Item 0 is sticky. Item 10 is next sticky.
|
|
1004
|
+
// Default size = 50.
|
|
1005
|
+
// nextStickyY = itemSizesY.query(10) = 500.
|
|
1006
|
+
// distance = 500 - 460 = 40.
|
|
1007
|
+
// 40 < 50 (item 0 height), so it should be pushed.
|
|
1008
|
+
// stickyOffset.y = -(50 - 40) = -10.
|
|
1009
|
+
const stickyItem = result.renderedItems.value.find((i) => i.index === 0);
|
|
1010
|
+
expect(stickyItem?.stickyOffset.y).toBe(-10);
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
it('should handle dynamic sticky item pushing in horizontal mode', async () => {
|
|
1014
|
+
const container = document.createElement('div');
|
|
1015
|
+
Object.defineProperty(container, 'clientWidth', { value: 500 });
|
|
1016
|
+
Object.defineProperty(container, 'scrollLeft', { value: 460, writable: true });
|
|
1017
|
+
|
|
1018
|
+
const { result } = setup({
|
|
1019
|
+
...defaultProps,
|
|
1020
|
+
container,
|
|
1021
|
+
direction: 'horizontal',
|
|
1022
|
+
itemSize: undefined, // dynamic
|
|
1023
|
+
stickyIndices: [ 0, 10 ],
|
|
1024
|
+
});
|
|
1025
|
+
await nextTick();
|
|
1026
|
+
|
|
1027
|
+
// nextStickyX = itemSizesX.query(10) = 500.
|
|
1028
|
+
// distance = 500 - 460 = 40.
|
|
1029
|
+
// 40 < 50, so stickyOffset.x = -10.
|
|
1030
|
+
const stickyItem = result.renderedItems.value.find((i) => i.index === 0);
|
|
1031
|
+
expect(stickyItem?.stickyOffset.x).toBe(-10);
|
|
1032
|
+
});
|
|
903
1033
|
});
|
|
904
1034
|
|
|
905
1035
|
describe('scroll restoration', () => {
|
|
@@ -970,6 +1100,35 @@ describe('useVirtualScroll', () => {
|
|
|
970
1100
|
vi.useRealTimers();
|
|
971
1101
|
});
|
|
972
1102
|
|
|
1103
|
+
it('should restore scroll position with itemSize as function when prepending', async () => {
|
|
1104
|
+
vi.useFakeTimers();
|
|
1105
|
+
const container = document.createElement('div');
|
|
1106
|
+
Object.defineProperty(container, 'scrollTop', { value: 100, writable: true });
|
|
1107
|
+
container.scrollTo = vi.fn().mockImplementation((options) => {
|
|
1108
|
+
container.scrollTop = options.top;
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
const items = Array.from({ length: 50 }, (_, i) => ({ id: i }));
|
|
1112
|
+
const { props } = setup({
|
|
1113
|
+
...defaultProps,
|
|
1114
|
+
items,
|
|
1115
|
+
container,
|
|
1116
|
+
itemSize: (item: { id: number; }) => (item.id < 0 ? 100 : 50),
|
|
1117
|
+
restoreScrollOnPrepend: true,
|
|
1118
|
+
});
|
|
1119
|
+
await nextTick();
|
|
1120
|
+
|
|
1121
|
+
// Prepend 1 item with id -1 (size 100)
|
|
1122
|
+
const newItems = [ { id: -1 }, ...items ];
|
|
1123
|
+
props.value.items = newItems;
|
|
1124
|
+
await nextTick();
|
|
1125
|
+
await nextTick();
|
|
1126
|
+
|
|
1127
|
+
// Should have adjusted scroll by 100px. New scrollTop should be 200.
|
|
1128
|
+
expect(container.scrollTop).toBe(200);
|
|
1129
|
+
vi.useRealTimers();
|
|
1130
|
+
});
|
|
1131
|
+
|
|
973
1132
|
it('should NOT restore scroll position when restoreScrollOnPrepend is false', async () => {
|
|
974
1133
|
const container = document.createElement('div');
|
|
975
1134
|
Object.defineProperty(container, 'scrollTop', { value: 100, writable: true });
|
|
@@ -1009,12 +1168,90 @@ describe('useVirtualScroll', () => {
|
|
|
1009
1168
|
props.value.items = [ { id: -1 }, ...props.value.items ];
|
|
1010
1169
|
await nextTick();
|
|
1011
1170
|
});
|
|
1171
|
+
});
|
|
1012
1172
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1173
|
+
describe('advanced logic and edge cases', () => {
|
|
1174
|
+
it('should trigger scroll correction when isScrolling becomes false', async () => {
|
|
1175
|
+
vi.useFakeTimers();
|
|
1176
|
+
const { result } = setup({ ...defaultProps, container: window, itemSize: undefined });
|
|
1177
|
+
await nextTick();
|
|
1178
|
+
result.scrollToIndex(10, 0, 'start');
|
|
1179
|
+
document.dispatchEvent(new Event('scroll'));
|
|
1180
|
+
expect(result.scrollDetails.value.isScrolling).toBe(true);
|
|
1181
|
+
vi.advanceTimersByTime(250);
|
|
1182
|
+
await nextTick();
|
|
1183
|
+
expect(result.scrollDetails.value.isScrolling).toBe(false);
|
|
1184
|
+
vi.useRealTimers();
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
it('should trigger scroll correction when treeUpdateFlag changes', async () => {
|
|
1188
|
+
const { result } = setup({ ...defaultProps, itemSize: undefined });
|
|
1189
|
+
await nextTick();
|
|
1190
|
+
result.scrollToIndex(10, 0, 'start');
|
|
1191
|
+
// Trigger tree update
|
|
1192
|
+
result.updateItemSize(5, 100, 100);
|
|
1193
|
+
await nextTick();
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
it('should cover updateHostOffset when container is window', async () => {
|
|
1197
|
+
const { result, props } = setup({ ...defaultProps, container: window });
|
|
1198
|
+
const host = document.createElement('div');
|
|
1199
|
+
props.value.hostElement = host;
|
|
1200
|
+
await nextTick();
|
|
1201
|
+
result.updateHostOffset();
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
it('should cover updateHostOffset when container is hostElement', async () => {
|
|
1205
|
+
const host = document.createElement('div');
|
|
1206
|
+
const { result } = setup({ ...defaultProps, container: host, hostElement: host });
|
|
1207
|
+
await nextTick();
|
|
1208
|
+
result.updateHostOffset();
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
it('should handle updateHostOffset with window fallback when container is missing', async () => {
|
|
1212
|
+
const { result, props } = setup({ ...defaultProps, container: undefined });
|
|
1213
|
+
const host = document.createElement('div');
|
|
1214
|
+
props.value.hostElement = host;
|
|
1215
|
+
await nextTick();
|
|
1216
|
+
result.updateHostOffset();
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
it('should correctly calculate hostOffset when container is an HTMLElement', async () => {
|
|
1220
|
+
const container = document.createElement('div');
|
|
1221
|
+
const hostElement = document.createElement('div');
|
|
1222
|
+
|
|
1223
|
+
container.getBoundingClientRect = vi.fn(() => ({ top: 100, left: 100, bottom: 200, right: 200, width: 100, height: 100, x: 100, y: 100, toJSON: () => '' }));
|
|
1224
|
+
hostElement.getBoundingClientRect = vi.fn(() => ({ top: 150, left: 150, bottom: 200, right: 200, width: 50, height: 50, x: 150, y: 150, toJSON: () => '' }));
|
|
1225
|
+
Object.defineProperty(container, 'scrollTop', { value: 50, writable: true, configurable: true });
|
|
1226
|
+
|
|
1227
|
+
const { result } = setup({ ...defaultProps, container, hostElement });
|
|
1228
|
+
await nextTick();
|
|
1229
|
+
result.updateHostOffset();
|
|
1230
|
+
expect(result.scrollDetails.value.scrollOffset.y).toBeDefined();
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
it('should cover refresh method', async () => {
|
|
1234
|
+
const { result } = setup({ ...defaultProps, itemSize: 0 });
|
|
1235
|
+
result.updateItemSize(0, 100, 100);
|
|
1236
|
+
await nextTick();
|
|
1237
|
+
expect(result.totalHeight.value).toBe(5050);
|
|
1238
|
+
|
|
1239
|
+
result.refresh();
|
|
1240
|
+
await nextTick();
|
|
1241
|
+
expect(result.totalHeight.value).toBe(5000);
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
it('should trigger scroll correction on tree update with string alignment', async () => {
|
|
1245
|
+
const container = document.createElement('div');
|
|
1246
|
+
Object.defineProperty(container, 'clientHeight', { value: 500, configurable: true });
|
|
1247
|
+
Object.defineProperty(container, 'scrollHeight', { value: 5000, configurable: true });
|
|
1248
|
+
const { result } = setup({ ...defaultProps, container, itemSize: undefined });
|
|
1249
|
+
// Set a pending scroll with string alignment
|
|
1250
|
+
result.scrollToIndex(10, null, 'start');
|
|
1251
|
+
|
|
1252
|
+
// Trigger tree update
|
|
1253
|
+
result.updateItemSize(0, 100, 100);
|
|
1016
1254
|
await nextTick();
|
|
1017
|
-
expect(result.totalWidth.value).toBe(5050);
|
|
1018
1255
|
});
|
|
1019
1256
|
|
|
1020
1257
|
it('should trigger scroll correction on tree update with pending scroll', async () => {
|
|
@@ -1053,6 +1290,92 @@ describe('useVirtualScroll', () => {
|
|
|
1053
1290
|
|
|
1054
1291
|
// eslint-disable-next-line test/prefer-lowercase-title
|
|
1055
1292
|
describe('SSR support', () => {
|
|
1293
|
+
it('should handle colBuffer when ssrRange is present and not scrolling', async () => {
|
|
1294
|
+
vi.useFakeTimers();
|
|
1295
|
+
const container = document.createElement('div');
|
|
1296
|
+
Object.defineProperty(container, 'clientWidth', { value: 500, configurable: true });
|
|
1297
|
+
Object.defineProperty(container, 'scrollLeft', { value: 0, writable: true, configurable: true });
|
|
1298
|
+
container.scrollTo = vi.fn().mockImplementation((options) => {
|
|
1299
|
+
if (options.left !== undefined) {
|
|
1300
|
+
Object.defineProperty(container, 'scrollLeft', { value: options.left, writable: true, configurable: true });
|
|
1301
|
+
}
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
const { result } = setup({
|
|
1305
|
+
...defaultProps,
|
|
1306
|
+
container,
|
|
1307
|
+
direction: 'both',
|
|
1308
|
+
columnCount: 20,
|
|
1309
|
+
columnWidth: 100,
|
|
1310
|
+
ssrRange: { start: 0, end: 10, colStart: 1, colEnd: 2 }, // SSR values
|
|
1311
|
+
initialScrollIndex: 0,
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
await nextTick(); // onMounted schedules hydration
|
|
1315
|
+
await nextTick(); // hydration tick 1
|
|
1316
|
+
await nextTick(); // hydration tick 2 (isHydrating = false)
|
|
1317
|
+
|
|
1318
|
+
expect(result.isHydrated.value).toBe(true);
|
|
1319
|
+
|
|
1320
|
+
// Scroll to col 5 (offset 500)
|
|
1321
|
+
result.scrollToIndex(null, 5, { align: 'start', behavior: 'auto' });
|
|
1322
|
+
await nextTick();
|
|
1323
|
+
|
|
1324
|
+
vi.runAllTimers(); // Clear isScrolling timeout
|
|
1325
|
+
await nextTick();
|
|
1326
|
+
|
|
1327
|
+
// start = findLowerBound(500) = 5.
|
|
1328
|
+
// colBuffer should be 0 because ssrRange is present and isScrolling is false.
|
|
1329
|
+
expect(result.columnRange.value.start).toBe(5);
|
|
1330
|
+
|
|
1331
|
+
// Now trigger a scroll to make isScrolling true
|
|
1332
|
+
container.dispatchEvent(new Event('scroll'));
|
|
1333
|
+
await nextTick();
|
|
1334
|
+
// isScrolling is now true. colBuffer should be 2.
|
|
1335
|
+
expect(result.columnRange.value.start).toBe(3);
|
|
1336
|
+
vi.useRealTimers();
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
it('should handle bufferBefore when ssrRange is present and not scrolling', async () => {
|
|
1340
|
+
vi.useFakeTimers();
|
|
1341
|
+
const container = document.createElement('div');
|
|
1342
|
+
Object.defineProperty(container, 'clientHeight', { value: 500 });
|
|
1343
|
+
Object.defineProperty(container, 'scrollTop', { value: 0, writable: true, configurable: true });
|
|
1344
|
+
container.scrollTo = vi.fn().mockImplementation((options) => {
|
|
1345
|
+
if (options.top !== undefined) {
|
|
1346
|
+
Object.defineProperty(container, 'scrollTop', { value: options.top, writable: true, configurable: true });
|
|
1347
|
+
}
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
const { result } = setup({
|
|
1351
|
+
...defaultProps,
|
|
1352
|
+
container,
|
|
1353
|
+
itemSize: 50,
|
|
1354
|
+
bufferBefore: 5,
|
|
1355
|
+
ssrRange: { start: 0, end: 10 },
|
|
1356
|
+
initialScrollIndex: 10,
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1359
|
+
await nextTick(); // schedules hydration
|
|
1360
|
+
await nextTick(); // hydration tick scrolls to 10
|
|
1361
|
+
await nextTick();
|
|
1362
|
+
|
|
1363
|
+
vi.runAllTimers(); // Clear isScrolling timeout
|
|
1364
|
+
await nextTick();
|
|
1365
|
+
|
|
1366
|
+
expect(result.isHydrated.value).toBe(true);
|
|
1367
|
+
// start = floor(500 / 50) = 10.
|
|
1368
|
+
// Since ssrRange is present and isScrolling is false, bufferBefore should be 0.
|
|
1369
|
+
expect(result.renderedItems.value[ 0 ]?.index).toBe(10);
|
|
1370
|
+
|
|
1371
|
+
// Now trigger a scroll to make isScrolling true
|
|
1372
|
+
container.dispatchEvent(new Event('scroll'));
|
|
1373
|
+
await nextTick();
|
|
1374
|
+
// isScrolling is now true. bufferBefore should be 5.
|
|
1375
|
+
expect(result.renderedItems.value[ 0 ]?.index).toBe(5);
|
|
1376
|
+
vi.useRealTimers();
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1056
1379
|
it('should handle SSR range in range calculation', () => {
|
|
1057
1380
|
const props = ref({
|
|
1058
1381
|
items: mockItems,
|
|
@@ -1072,6 +1395,17 @@ describe('useVirtualScroll', () => {
|
|
|
1072
1395
|
expect(result.columnRange.value.end).toBe(5);
|
|
1073
1396
|
});
|
|
1074
1397
|
|
|
1398
|
+
it('should handle SSR range with colEnd fallback in columnRange calculation', () => {
|
|
1399
|
+
const props = ref({
|
|
1400
|
+
items: mockItems,
|
|
1401
|
+
columnCount: 10,
|
|
1402
|
+
ssrRange: { start: 0, end: 10, colStart: 0, colEnd: 0 },
|
|
1403
|
+
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1404
|
+
const result = useVirtualScroll(props);
|
|
1405
|
+
// colEnd is 0, so it should use columnCount (10)
|
|
1406
|
+
expect(result.columnRange.value.end).toBe(10);
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1075
1409
|
it('should handle SSR range with both directions for total sizes', () => {
|
|
1076
1410
|
const props = ref({
|
|
1077
1411
|
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
@@ -1097,26 +1431,27 @@ describe('useVirtualScroll', () => {
|
|
|
1097
1431
|
expect(result.totalWidth.value).toBe(500); // (20-10) * 50
|
|
1098
1432
|
});
|
|
1099
1433
|
|
|
1100
|
-
it('should handle SSR range with
|
|
1434
|
+
it('should handle SSR range with vertical offset in renderedItems', () => {
|
|
1101
1435
|
const props = ref({
|
|
1102
1436
|
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1103
|
-
direction: '
|
|
1437
|
+
direction: 'vertical',
|
|
1104
1438
|
itemSize: 50,
|
|
1105
1439
|
ssrRange: { start: 10, end: 20 },
|
|
1106
1440
|
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1107
1441
|
const result = useVirtualScroll(props);
|
|
1108
|
-
expect(result.
|
|
1442
|
+
expect(result.renderedItems.value[ 0 ]?.offset.y).toBe(0);
|
|
1109
1443
|
});
|
|
1110
1444
|
|
|
1111
|
-
it('should handle SSR range with
|
|
1445
|
+
it('should handle SSR range with dynamic horizontal offsets in renderedItems', () => {
|
|
1112
1446
|
const props = ref({
|
|
1113
1447
|
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1114
|
-
direction: '
|
|
1115
|
-
itemSize:
|
|
1448
|
+
direction: 'horizontal',
|
|
1449
|
+
itemSize: undefined, // dynamic
|
|
1116
1450
|
ssrRange: { start: 10, end: 20 },
|
|
1117
1451
|
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1118
1452
|
const result = useVirtualScroll(props);
|
|
1119
|
-
|
|
1453
|
+
// ssrOffsetX = itemSizesX.query(10) = 10 * 50 = 500
|
|
1454
|
+
expect(result.renderedItems.value[ 0 ]?.offset.x).toBe(500);
|
|
1120
1455
|
});
|
|
1121
1456
|
|
|
1122
1457
|
it('should handle SSR range with dynamic sizes for total sizes', () => {
|
|
@@ -1184,6 +1519,20 @@ describe('useVirtualScroll', () => {
|
|
|
1184
1519
|
expect(result.renderedItems.value[ 0 ]?.offset.x).toBe(-500);
|
|
1185
1520
|
});
|
|
1186
1521
|
|
|
1522
|
+
it('should handle SSR range with direction "both" and colEnd falsy', () => {
|
|
1523
|
+
const propsValue = ref({
|
|
1524
|
+
columnCount: 10,
|
|
1525
|
+
columnWidth: 100,
|
|
1526
|
+
direction: 'both' as const,
|
|
1527
|
+
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1528
|
+
ssrRange: { colEnd: 0, colStart: 5, end: 10, start: 0 },
|
|
1529
|
+
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1530
|
+
const result = useVirtualScroll(propsValue);
|
|
1531
|
+
// colEnd is 0, so it should use colCount (10)
|
|
1532
|
+
// totalWidth = columnSizes.query(10) - columnSizes.query(5) = 1000 - 500 = 500
|
|
1533
|
+
expect(result.totalWidth.value).toBe(500);
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1187
1536
|
it('should handle SSR range with colCount > 0 in totalWidth', () => {
|
|
1188
1537
|
const props = ref({
|
|
1189
1538
|
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
@@ -1195,17 +1544,9 @@ describe('useVirtualScroll', () => {
|
|
|
1195
1544
|
const result = useVirtualScroll(props);
|
|
1196
1545
|
expect(result.totalWidth.value).toBe(500);
|
|
1197
1546
|
});
|
|
1547
|
+
});
|
|
1198
1548
|
|
|
1199
|
-
|
|
1200
|
-
// items array is mockItems (length 100)
|
|
1201
|
-
const { result } = setup({ ...defaultProps, stickyIndices: [ 1000 ] });
|
|
1202
|
-
// Scroll way past the end
|
|
1203
|
-
result.scrollToOffset(0, 100000);
|
|
1204
|
-
await nextTick();
|
|
1205
|
-
// prevStickyIdx will be 1000, which is out of bounds
|
|
1206
|
-
expect(result.renderedItems.value.length).toBe(0);
|
|
1207
|
-
});
|
|
1208
|
-
|
|
1549
|
+
describe('helpers', () => {
|
|
1209
1550
|
it('should cover object padding branches in helpers', () => {
|
|
1210
1551
|
expect(getPaddingX({ x: 10 }, 'horizontal')).toBe(10);
|
|
1211
1552
|
expect(getPaddingY({ y: 20 }, 'vertical')).toBe(20);
|