@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,7 +1,8 @@
|
|
|
1
|
-
import type { ScrollDetails } from '../composables/useVirtualScroll';
|
|
1
|
+
import type { ScrollAlignment, ScrollAlignmentOptions, ScrollDetails, ScrollToIndexOptions } from '../composables/useVirtualScroll';
|
|
2
|
+
import type { DOMWrapper, VueWrapper } from '@vue/test-utils';
|
|
2
3
|
|
|
3
4
|
import { mount } from '@vue/test-utils';
|
|
4
|
-
import { describe, expect, it
|
|
5
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
5
6
|
import { defineComponent, nextTick, ref } from 'vue';
|
|
6
7
|
|
|
7
8
|
import VirtualScroll from './VirtualScroll.vue';
|
|
@@ -45,11 +46,19 @@ globalThis.ResizeObserver = class {
|
|
|
45
46
|
// eslint-disable-next-line test/prefer-lowercase-title
|
|
46
47
|
describe('VirtualScroll component', () => {
|
|
47
48
|
const mockItems = Array.from({ length: 100 }, (_, i) => ({ id: i, label: `Item ${ i }` }));
|
|
49
|
+
|
|
48
50
|
interface VSInstance {
|
|
49
|
-
scrollToIndex: (rowIndex: number | null, colIndex: number | null, options?: unknown) => void;
|
|
50
|
-
scrollToOffset: (x: number | null, y: number | null, options?: unknown) => void;
|
|
51
|
-
setItemRef: (el: unknown, index: number) => void;
|
|
52
51
|
scrollDetails: ScrollDetails<unknown>;
|
|
52
|
+
scrollToIndex: (rowIndex: number | null, colIndex: number | null, options?: ScrollAlignment | ScrollAlignmentOptions | ScrollToIndexOptions) => void;
|
|
53
|
+
scrollToOffset: (x: number | null, y: number | null, options?: { behavior?: 'auto' | 'smooth'; }) => void;
|
|
54
|
+
setItemRef: (el: unknown, index: number) => void;
|
|
55
|
+
stopProgrammaticScroll: () => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface TestCompInstance {
|
|
59
|
+
mockItems: typeof mockItems;
|
|
60
|
+
show: boolean;
|
|
61
|
+
showFooter: boolean;
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
describe('rendering and structure', () => {
|
|
@@ -78,10 +87,49 @@ describe('VirtualScroll component', () => {
|
|
|
78
87
|
footer: '<div class="footer">Footer</div>',
|
|
79
88
|
},
|
|
80
89
|
});
|
|
90
|
+
expect(wrapper.find('.virtual-scroll-header').exists()).toBe(true);
|
|
81
91
|
expect(wrapper.find('.header').exists()).toBe(true);
|
|
92
|
+
expect(wrapper.find('.virtual-scroll-footer').exists()).toBe(true);
|
|
82
93
|
expect(wrapper.find('.footer').exists()).toBe(true);
|
|
83
94
|
});
|
|
84
95
|
|
|
96
|
+
it('should not render header and footer slots when absent', () => {
|
|
97
|
+
const wrapper = mount(VirtualScroll, {
|
|
98
|
+
props: {
|
|
99
|
+
items: mockItems,
|
|
100
|
+
itemSize: 50,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
expect(wrapper.find('.virtual-scroll-header').exists()).toBe(false);
|
|
104
|
+
expect(wrapper.find('.virtual-scroll-footer').exists()).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should render debug information when debug prop is true', async () => {
|
|
108
|
+
const wrapper = mount(VirtualScroll, {
|
|
109
|
+
props: {
|
|
110
|
+
items: mockItems.slice(0, 5),
|
|
111
|
+
itemSize: 50,
|
|
112
|
+
debug: true,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
await nextTick();
|
|
116
|
+
expect(wrapper.find('.virtual-scroll-debug-info').exists()).toBe(true);
|
|
117
|
+
expect(wrapper.find('.virtual-scroll-item').classes()).toContain('virtual-scroll--debug');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should not render debug information when debug prop is false', async () => {
|
|
121
|
+
const wrapper = mount(VirtualScroll, {
|
|
122
|
+
props: {
|
|
123
|
+
items: mockItems.slice(0, 5),
|
|
124
|
+
itemSize: 50,
|
|
125
|
+
debug: false,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
await nextTick();
|
|
129
|
+
expect(wrapper.find('.virtual-scroll-debug-info').exists()).toBe(false);
|
|
130
|
+
expect(wrapper.find('.virtual-scroll-item').classes()).not.toContain('virtual-scroll--debug');
|
|
131
|
+
});
|
|
132
|
+
|
|
85
133
|
it('should handle missing slots gracefully', () => {
|
|
86
134
|
const wrapper = mount(VirtualScroll, {
|
|
87
135
|
props: {
|
|
@@ -186,9 +234,43 @@ describe('VirtualScroll component', () => {
|
|
|
186
234
|
expect(wrapper.find('tr.virtual-scroll-spacer').exists()).toBe(true);
|
|
187
235
|
expect(wrapper.find('tr.virtual-scroll-item').exists()).toBe(true);
|
|
188
236
|
});
|
|
237
|
+
|
|
238
|
+
it('should handle table rendering without header and footer', async () => {
|
|
239
|
+
const wrapper = mount(VirtualScroll, {
|
|
240
|
+
props: {
|
|
241
|
+
items: mockItems.slice(0, 5),
|
|
242
|
+
containerTag: 'table',
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
await nextTick();
|
|
246
|
+
expect(wrapper.find('thead').exists()).toBe(false);
|
|
247
|
+
expect(wrapper.find('tfoot').exists()).toBe(false);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should cover all template branches for slots and tags', async () => {
|
|
251
|
+
for (const tag of [ 'div', 'table' ] as const) {
|
|
252
|
+
for (const loading of [ true, false ]) {
|
|
253
|
+
for (const withSlots of [ true, false ]) {
|
|
254
|
+
const slots = withSlots
|
|
255
|
+
? {
|
|
256
|
+
header: tag === 'table' ? '<tr><td>H</td></tr>' : '<div>H</div>',
|
|
257
|
+
footer: tag === 'table' ? '<tr><td>F</td></tr>' : '<div>F</div>',
|
|
258
|
+
loading: tag === 'table' ? '<tr><td>L</td></tr>' : '<div>L</div>',
|
|
259
|
+
}
|
|
260
|
+
: {};
|
|
261
|
+
const wrapper = mount(VirtualScroll, {
|
|
262
|
+
props: { items: mockItems.slice(0, 1), containerTag: tag, loading },
|
|
263
|
+
slots,
|
|
264
|
+
});
|
|
265
|
+
await nextTick();
|
|
266
|
+
wrapper.unmount();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
});
|
|
189
271
|
});
|
|
190
272
|
|
|
191
|
-
describe('styling and
|
|
273
|
+
describe('styling and dimensions', () => {
|
|
192
274
|
it('should render items horizontally when direction is horizontal', async () => {
|
|
193
275
|
const wrapper = mount(VirtualScroll, {
|
|
194
276
|
props: {
|
|
@@ -280,140 +362,202 @@ describe('VirtualScroll component', () => {
|
|
|
280
362
|
await nextTick();
|
|
281
363
|
// This covers the branch where container is NOT host element and NOT window
|
|
282
364
|
});
|
|
283
|
-
});
|
|
284
365
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const wrapper = mount(VirtualScroll, { props: { items: mockItems, itemSize: 50 } });
|
|
288
|
-
await nextTick();
|
|
289
|
-
expect(wrapper.emitted('scroll')).toBeDefined();
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it('should not emit scroll before hydration', async () => {
|
|
293
|
-
const wrapper = mount(VirtualScroll, {
|
|
366
|
+
it('should cover object padding branches in virtualScrollProps', () => {
|
|
367
|
+
mount(VirtualScroll, {
|
|
294
368
|
props: {
|
|
295
|
-
items: mockItems.slice(0,
|
|
296
|
-
|
|
369
|
+
items: mockItems.slice(0, 1),
|
|
370
|
+
scrollPaddingStart: { x: 10, y: 20 },
|
|
371
|
+
scrollPaddingEnd: { x: 30, y: 40 },
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
mount(VirtualScroll, {
|
|
375
|
+
props: {
|
|
376
|
+
items: mockItems.slice(0, 1),
|
|
377
|
+
direction: 'horizontal',
|
|
378
|
+
scrollPaddingStart: 10,
|
|
379
|
+
scrollPaddingEnd: 20,
|
|
297
380
|
},
|
|
298
381
|
});
|
|
299
|
-
// Hydration is delayed via nextTick in useVirtualScroll when initialScrollIndex is set
|
|
300
|
-
// Trigger scrollDetails update
|
|
301
|
-
await wrapper.setProps({ items: mockItems.slice(0, 10) });
|
|
302
|
-
expect(wrapper.emitted('scroll')).toBeUndefined();
|
|
303
|
-
await nextTick();
|
|
304
|
-
// Still might not be hydrated because it's nextTick within nextTick?
|
|
305
|
-
// Actually, useVirtualScroll uses nextTick inside onMounted.
|
|
306
|
-
// mount() calls onMounted.
|
|
307
|
-
// So we need one nextTick to reach isHydrated = true.
|
|
308
|
-
await nextTick();
|
|
309
|
-
expect(wrapper.emitted('scroll')).toBeDefined();
|
|
310
382
|
});
|
|
383
|
+
});
|
|
311
384
|
|
|
312
|
-
|
|
313
|
-
|
|
385
|
+
describe('keyboard navigation', () => {
|
|
386
|
+
let wrapper: VueWrapper<VSInstance>;
|
|
387
|
+
let container: DOMWrapper<Element>;
|
|
388
|
+
let el: HTMLElement;
|
|
389
|
+
|
|
390
|
+
beforeEach(async () => {
|
|
391
|
+
wrapper = mount(VirtualScroll, {
|
|
314
392
|
props: {
|
|
315
393
|
items: mockItems,
|
|
316
394
|
itemSize: 50,
|
|
395
|
+
direction: 'vertical',
|
|
317
396
|
},
|
|
318
|
-
})
|
|
319
|
-
|
|
397
|
+
}) as unknown as VueWrapper<VSInstance>;
|
|
320
398
|
await nextTick();
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const emits = wrapper.emitted('visibleRangeChange');
|
|
324
|
-
expect(emits).toBeTruthy();
|
|
325
|
-
const firstEmit = (emits as unknown[][])[ 0 ]![ 0 ] as { start: number; };
|
|
326
|
-
expect(firstEmit).toMatchObject({ start: 0 });
|
|
399
|
+
container = wrapper.find('.virtual-scroll-container');
|
|
400
|
+
el = container.element as HTMLElement;
|
|
327
401
|
|
|
328
|
-
//
|
|
329
|
-
|
|
330
|
-
Object.defineProperty(
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
await nextTick();
|
|
402
|
+
// Mock dimensions
|
|
403
|
+
Object.defineProperty(el, 'clientHeight', { value: 500, configurable: true });
|
|
404
|
+
Object.defineProperty(el, 'clientWidth', { value: 500, configurable: true });
|
|
405
|
+
Object.defineProperty(el, 'offsetHeight', { value: 500, configurable: true });
|
|
406
|
+
Object.defineProperty(el, 'offsetWidth', { value: 500, configurable: true });
|
|
334
407
|
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
expect(lastEmit.start).toBeGreaterThan(0);
|
|
408
|
+
const observers = (globalThis.ResizeObserver as unknown as { instances: ResizeObserverMock[]; }).instances.filter((i) => i.targets.has(el));
|
|
409
|
+
observers.forEach((i) => i.trigger([ { target: el, contentRect: { width: 500, height: 500 } as unknown as DOMRectReadOnly } ]));
|
|
410
|
+
await nextTick();
|
|
339
411
|
});
|
|
340
412
|
|
|
341
|
-
it('should handle
|
|
342
|
-
|
|
413
|
+
it('should handle Home key', async () => {
|
|
414
|
+
el.scrollTop = 1000;
|
|
415
|
+
el.scrollLeft = 500;
|
|
416
|
+
await container.trigger('keydown', { key: 'Home' });
|
|
343
417
|
await nextTick();
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
Object.defineProperty(el, 'clientHeight', { value: 500, configurable: true });
|
|
418
|
+
expect(el.scrollTop).toBe(0);
|
|
419
|
+
expect(el.scrollLeft).toBe(0);
|
|
420
|
+
});
|
|
348
421
|
|
|
422
|
+
it('should handle End key (vertical)', async () => {
|
|
423
|
+
el.scrollLeft = 0;
|
|
349
424
|
await container.trigger('keydown', { key: 'End' });
|
|
350
425
|
await nextTick();
|
|
351
|
-
|
|
426
|
+
// totalHeight = 100 items * 50px = 5000px
|
|
427
|
+
// viewportHeight = 500px
|
|
428
|
+
// scrollToIndex(99, 0, 'end') -> targetY = 99 * 50 = 4950
|
|
429
|
+
// alignment 'end' -> targetY = 4950 - (500 - 50) = 4500
|
|
430
|
+
expect(el.scrollTop).toBe(4500);
|
|
431
|
+
expect(el.scrollLeft).toBe(0);
|
|
432
|
+
});
|
|
352
433
|
|
|
353
|
-
|
|
434
|
+
it('should handle End key (horizontal)', async () => {
|
|
435
|
+
await wrapper.setProps({ direction: 'horizontal' });
|
|
354
436
|
await nextTick();
|
|
437
|
+
// Trigger resize again for horizontal
|
|
438
|
+
const observers = (globalThis.ResizeObserver as unknown as { instances: ResizeObserverMock[]; }).instances.filter((i) => i.targets.has(el));
|
|
439
|
+
observers.forEach((i) => i.trigger([ { target: el, contentRect: { width: 500, height: 500 } as unknown as DOMRectReadOnly } ]));
|
|
440
|
+
await nextTick();
|
|
441
|
+
|
|
442
|
+
el.scrollTop = 0;
|
|
443
|
+
await container.trigger('keydown', { key: 'End' });
|
|
444
|
+
await nextTick();
|
|
445
|
+
expect(el.scrollLeft).toBe(4500);
|
|
355
446
|
expect(el.scrollTop).toBe(0);
|
|
356
447
|
});
|
|
357
448
|
|
|
358
|
-
it('should handle
|
|
359
|
-
|
|
449
|
+
it('should handle End key in both mode', async () => {
|
|
450
|
+
await wrapper.setProps({ columnCount: 5, columnWidth: 100, direction: 'both' });
|
|
360
451
|
await nextTick();
|
|
361
|
-
const container = wrapper.find('.virtual-scroll-container');
|
|
362
|
-
const el = container.element as HTMLElement;
|
|
363
|
-
Object.defineProperty(el, 'scrollWidth', { value: 5000, configurable: true });
|
|
364
|
-
Object.defineProperty(el, 'clientWidth', { value: 500, configurable: true });
|
|
365
452
|
|
|
366
|
-
|
|
453
|
+
// Trigger a resize
|
|
454
|
+
const observers = (globalThis.ResizeObserver as unknown as { instances: ResizeObserverMock[]; }).instances.filter((i) => i.targets.has(el));
|
|
455
|
+
observers.forEach((i) => i.trigger([ { target: el, contentRect: { width: 500, height: 500 } as unknown as DOMRectReadOnly } ]));
|
|
367
456
|
await nextTick();
|
|
368
|
-
expect(el.scrollLeft).toBeGreaterThan(0);
|
|
369
457
|
|
|
370
|
-
await container.trigger('keydown', { key: '
|
|
458
|
+
await container.trigger('keydown', { key: 'End' });
|
|
371
459
|
await nextTick();
|
|
460
|
+
|
|
461
|
+
// items: 100 (rows), height: 50 -> totalHeight: 5000
|
|
462
|
+
// columns: 5, width: 100 -> totalWidth: 500
|
|
463
|
+
// viewport: 500x500
|
|
464
|
+
// scrollToIndex(99, 4, 'end')
|
|
465
|
+
// targetY = 99 * 50 = 4950. end alignment: 4950 - (500 - 50) = 4500
|
|
466
|
+
// targetX = 4 * 100 = 400. end alignment: 400 - (500 - 100) = 0
|
|
467
|
+
expect(el.scrollTop).toBe(4500);
|
|
372
468
|
expect(el.scrollLeft).toBe(0);
|
|
373
469
|
});
|
|
374
470
|
|
|
375
|
-
it('should handle
|
|
376
|
-
|
|
377
|
-
props: {
|
|
378
|
-
items: mockItems,
|
|
379
|
-
itemSize: 50,
|
|
380
|
-
},
|
|
381
|
-
});
|
|
382
|
-
|
|
471
|
+
it('should handle End key with empty items', async () => {
|
|
472
|
+
await wrapper.setProps({ items: [] });
|
|
383
473
|
await nextTick();
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
474
|
+
await container.trigger('keydown', { key: 'End' });
|
|
475
|
+
await nextTick();
|
|
476
|
+
});
|
|
387
477
|
|
|
478
|
+
it('should handle ArrowDown / ArrowUp', async () => {
|
|
479
|
+
el.scrollLeft = 0;
|
|
388
480
|
await container.trigger('keydown', { key: 'ArrowDown' });
|
|
389
|
-
|
|
481
|
+
await nextTick();
|
|
482
|
+
expect(el.scrollTop).toBe(40);
|
|
483
|
+
expect(el.scrollLeft).toBe(0);
|
|
390
484
|
|
|
391
485
|
await container.trigger('keydown', { key: 'ArrowUp' });
|
|
392
|
-
|
|
486
|
+
await nextTick();
|
|
487
|
+
expect(el.scrollTop).toBe(0);
|
|
488
|
+
expect(el.scrollLeft).toBe(0);
|
|
489
|
+
});
|
|
393
490
|
|
|
491
|
+
it('should handle ArrowRight / ArrowLeft', async () => {
|
|
492
|
+
await wrapper.setProps({ direction: 'horizontal' });
|
|
493
|
+
await nextTick();
|
|
494
|
+
el.scrollTop = 0;
|
|
394
495
|
await container.trigger('keydown', { key: 'ArrowRight' });
|
|
395
|
-
|
|
496
|
+
await nextTick();
|
|
497
|
+
expect(el.scrollLeft).toBe(40);
|
|
498
|
+
expect(el.scrollTop).toBe(0);
|
|
396
499
|
|
|
397
500
|
await container.trigger('keydown', { key: 'ArrowLeft' });
|
|
398
|
-
|
|
501
|
+
await nextTick();
|
|
502
|
+
expect(el.scrollLeft).toBe(0);
|
|
503
|
+
expect(el.scrollTop).toBe(0);
|
|
504
|
+
});
|
|
399
505
|
|
|
506
|
+
it('should handle PageDown / PageUp', async () => {
|
|
507
|
+
el.scrollLeft = 0;
|
|
400
508
|
await container.trigger('keydown', { key: 'PageDown' });
|
|
401
|
-
|
|
509
|
+
await nextTick();
|
|
510
|
+
expect(el.scrollTop).toBe(500);
|
|
511
|
+
expect(el.scrollLeft).toBe(0);
|
|
402
512
|
|
|
403
513
|
await container.trigger('keydown', { key: 'PageUp' });
|
|
404
|
-
|
|
514
|
+
await nextTick();
|
|
515
|
+
expect(el.scrollTop).toBe(0);
|
|
516
|
+
expect(el.scrollLeft).toBe(0);
|
|
405
517
|
});
|
|
406
518
|
|
|
407
|
-
it('should handle
|
|
408
|
-
|
|
519
|
+
it('should handle PageDown / PageUp in horizontal mode', async () => {
|
|
520
|
+
await wrapper.setProps({ direction: 'horizontal' });
|
|
521
|
+
await nextTick();
|
|
522
|
+
el.scrollTop = 0;
|
|
523
|
+
await container.trigger('keydown', { key: 'PageDown' });
|
|
524
|
+
await nextTick();
|
|
525
|
+
expect(el.scrollLeft).toBe(500);
|
|
526
|
+
expect(el.scrollTop).toBe(0);
|
|
527
|
+
|
|
528
|
+
await container.trigger('keydown', { key: 'PageUp' });
|
|
529
|
+
await nextTick();
|
|
530
|
+
expect(el.scrollLeft).toBe(0);
|
|
531
|
+
expect(el.scrollTop).toBe(0);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('should not scroll past the end using PageDown', async () => {
|
|
535
|
+
// items: 100, itemSize: 50 -> totalHeight = 5000
|
|
536
|
+
// viewportHeight: 500 -> maxScroll = 4500
|
|
537
|
+
(wrapper.vm as unknown as VSInstance).scrollToOffset(null, 4400);
|
|
538
|
+
await nextTick();
|
|
539
|
+
await container.trigger('keydown', { key: 'PageDown' });
|
|
540
|
+
await nextTick();
|
|
541
|
+
expect(el.scrollTop).toBe(4500);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('should not scroll past the end using ArrowDown', async () => {
|
|
545
|
+
// maxScroll = 4500
|
|
546
|
+
(wrapper.vm as unknown as VSInstance).scrollToOffset(null, 4480);
|
|
409
547
|
await nextTick();
|
|
410
|
-
const container = wrapper.find('.virtual-scroll-container');
|
|
411
548
|
await container.trigger('keydown', { key: 'ArrowDown' });
|
|
412
|
-
|
|
549
|
+
await nextTick();
|
|
550
|
+
expect(el.scrollTop).toBe(4500);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it('should handle unhandled keys', async () => {
|
|
554
|
+
const event = new KeyboardEvent('keydown', { key: 'Enter', cancelable: true });
|
|
555
|
+
el.dispatchEvent(event);
|
|
556
|
+
expect(event.defaultPrevented).toBe(false);
|
|
413
557
|
});
|
|
414
558
|
});
|
|
415
559
|
|
|
416
|
-
describe('
|
|
560
|
+
describe('resize and observers', () => {
|
|
417
561
|
it('should update item size on resize', async () => {
|
|
418
562
|
const wrapper = mount(VirtualScroll, { props: { items: mockItems.slice(0, 5) } });
|
|
419
563
|
await nextTick();
|
|
@@ -449,7 +593,6 @@ describe('VirtualScroll component', () => {
|
|
|
449
593
|
observer.trigger([ { target: host } ]);
|
|
450
594
|
}
|
|
451
595
|
await nextTick();
|
|
452
|
-
// Should have called updateHostOffset (internal)
|
|
453
596
|
});
|
|
454
597
|
|
|
455
598
|
it('should observe cell resize with data-col-index', async () => {
|
|
@@ -501,13 +644,6 @@ describe('VirtualScroll component', () => {
|
|
|
501
644
|
await nextTick();
|
|
502
645
|
});
|
|
503
646
|
|
|
504
|
-
it('should handle missing footerRef gracefully in onMounted', () => {
|
|
505
|
-
mount(VirtualScroll, {
|
|
506
|
-
props: { items: mockItems, itemSize: 50 },
|
|
507
|
-
slots: { header: '<div>H</div>' },
|
|
508
|
-
});
|
|
509
|
-
});
|
|
510
|
-
|
|
511
647
|
it('should observe footer on mount if slot exists', async () => {
|
|
512
648
|
const wrapper = mount(VirtualScroll, {
|
|
513
649
|
props: { items: mockItems, itemSize: 50 },
|
|
@@ -525,7 +661,7 @@ describe('VirtualScroll component', () => {
|
|
|
525
661
|
setup() {
|
|
526
662
|
const show = ref(true);
|
|
527
663
|
const showFooter = ref(true);
|
|
528
|
-
return { show, showFooter
|
|
664
|
+
return { mockItems, show, showFooter };
|
|
529
665
|
},
|
|
530
666
|
template: `
|
|
531
667
|
<VirtualScroll :items="mockItems">
|
|
@@ -537,175 +673,61 @@ describe('VirtualScroll component', () => {
|
|
|
537
673
|
const wrapper = mount(TestComp);
|
|
538
674
|
await nextTick();
|
|
539
675
|
|
|
540
|
-
|
|
541
|
-
(wrapper.vm as unknown as
|
|
542
|
-
(wrapper.vm as unknown as { show: boolean; showFooter: boolean; }).showFooter = false;
|
|
543
|
-
await nextTick();
|
|
544
|
-
|
|
545
|
-
// Toggle on to trigger 'observe' (newEl)
|
|
546
|
-
(wrapper.vm as unknown as { show: boolean; showFooter: boolean; }).show = true;
|
|
547
|
-
(wrapper.vm as unknown as { show: boolean; showFooter: boolean; }).showFooter = true;
|
|
548
|
-
await nextTick();
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
it('should cover ResizeObserver cell measurement', async () => {
|
|
552
|
-
const wrapper = mount(VirtualScroll, {
|
|
553
|
-
props: { items: mockItems.slice(0, 1), direction: 'both', columnCount: 5 },
|
|
554
|
-
slots: { item: '<template #item><div data-col-index="0">cell</div></template>' },
|
|
555
|
-
});
|
|
556
|
-
await nextTick();
|
|
557
|
-
const cell = wrapper.find('[data-col-index="0"]').element;
|
|
558
|
-
const observer = (globalThis.ResizeObserver as unknown as { instances: ResizeObserverMock[]; }).instances.find((i) => i.targets.has(cell));
|
|
559
|
-
if (observer) {
|
|
560
|
-
observer.trigger([ { target: cell } ]);
|
|
561
|
-
}
|
|
562
|
-
await nextTick();
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
it('should handle keyboard navigation End key horizontal', async () => {
|
|
566
|
-
const wrapper = mount(VirtualScroll, {
|
|
567
|
-
props: { items: mockItems, direction: 'horizontal', itemSize: 50 },
|
|
568
|
-
});
|
|
676
|
+
(wrapper.vm as unknown as TestCompInstance).show = false;
|
|
677
|
+
(wrapper.vm as unknown as TestCompInstance).showFooter = false;
|
|
569
678
|
await nextTick();
|
|
570
|
-
const container = wrapper.find('.virtual-scroll-container');
|
|
571
|
-
const el = container.element as HTMLElement;
|
|
572
|
-
Object.defineProperty(el, 'scrollWidth', { value: 5000, configurable: true });
|
|
573
|
-
Object.defineProperty(el, 'clientWidth', { value: 500, configurable: true });
|
|
574
679
|
|
|
575
|
-
|
|
680
|
+
(wrapper.vm as unknown as TestCompInstance).show = true;
|
|
681
|
+
(wrapper.vm as unknown as TestCompInstance).showFooter = true;
|
|
576
682
|
await nextTick();
|
|
577
|
-
expect(el.scrollLeft).toBeGreaterThan(0);
|
|
578
683
|
});
|
|
579
684
|
|
|
580
|
-
it('should
|
|
581
|
-
const wrapper = mount(VirtualScroll, {
|
|
582
|
-
props: { items: mockItems, direction: 'vertical', itemSize: 50 },
|
|
583
|
-
});
|
|
584
|
-
await nextTick();
|
|
585
|
-
const container = wrapper.find('.virtual-scroll-container');
|
|
586
|
-
const el = container.element as HTMLElement;
|
|
587
|
-
Object.defineProperty(el, 'scrollHeight', { value: 5000, configurable: true });
|
|
588
|
-
Object.defineProperty(el, 'clientHeight', { value: 500, configurable: true });
|
|
589
|
-
|
|
590
|
-
await container.trigger('keydown', { key: 'End' });
|
|
591
|
-
await nextTick();
|
|
592
|
-
expect(el.scrollTop).toBeGreaterThan(0);
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
it('should handle keyboard navigation End key with empty items', async () => {
|
|
596
|
-
const wrapper = mount(VirtualScroll, {
|
|
597
|
-
props: { items: [], direction: 'vertical', itemSize: 50 },
|
|
598
|
-
});
|
|
599
|
-
await nextTick();
|
|
600
|
-
const container = wrapper.find('.virtual-scroll-container');
|
|
601
|
-
await container.trigger('keydown', { key: 'End' });
|
|
602
|
-
await nextTick();
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
it('should handle keyboard navigation End key with columnCount 0 in both mode', async () => {
|
|
606
|
-
const wrapper = mount(VirtualScroll, {
|
|
607
|
-
props: { items: mockItems, direction: 'both', columnCount: 0, itemSize: 50 },
|
|
608
|
-
});
|
|
609
|
-
await nextTick();
|
|
610
|
-
const container = wrapper.find('.virtual-scroll-container');
|
|
611
|
-
await container.trigger('keydown', { key: 'End' });
|
|
612
|
-
await nextTick();
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
it('should handle keyboard navigation End key in both mode', async () => {
|
|
616
|
-
const wrapper = mount(VirtualScroll, {
|
|
617
|
-
props: { items: mockItems, direction: 'both', columnCount: 5, itemSize: 50, columnWidth: 100 },
|
|
618
|
-
});
|
|
619
|
-
await nextTick();
|
|
620
|
-
const container = wrapper.find('.virtual-scroll-container');
|
|
621
|
-
await container.trigger('keydown', { key: 'End' });
|
|
622
|
-
await nextTick();
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
it('should handle load event for horizontal direction', async () => {
|
|
685
|
+
it('should cleanup observers on unmount', async () => {
|
|
626
686
|
const wrapper = mount(VirtualScroll, {
|
|
627
|
-
props: { items: mockItems
|
|
687
|
+
props: { items: mockItems, stickyFooter: true, stickyHeader: true },
|
|
688
|
+
slots: { footer: '<div>F</div>', header: '<div>H</div>' },
|
|
628
689
|
});
|
|
629
690
|
await nextTick();
|
|
630
|
-
|
|
631
|
-
await nextTick();
|
|
632
|
-
await nextTick();
|
|
633
|
-
expect(wrapper.emitted('load')).toBeDefined();
|
|
691
|
+
wrapper.unmount();
|
|
634
692
|
});
|
|
635
693
|
|
|
636
|
-
it('should
|
|
637
|
-
|
|
694
|
+
it('should ignore elements with missing or invalid data attributes in itemResizeObserver', async () => {
|
|
695
|
+
mount(VirtualScroll, { props: { items: mockItems.slice(0, 1) } });
|
|
638
696
|
await nextTick();
|
|
639
|
-
const
|
|
640
|
-
const observer = (globalThis.ResizeObserver as unknown as { instances: ResizeObserverMock[]; }).instances.find((i) => i.targets.has(item));
|
|
697
|
+
const observer = (globalThis.ResizeObserver as unknown as { instances: ResizeObserverMock[]; }).instances[ 0 ]!;
|
|
641
698
|
|
|
642
|
-
//
|
|
643
|
-
|
|
699
|
+
// 1. Invalid index string
|
|
700
|
+
const div1 = document.createElement('div');
|
|
701
|
+
div1.dataset.index = 'invalid';
|
|
702
|
+
observer.trigger([ { target: div1, contentRect: { width: 100, height: 100 } as unknown as DOMRectReadOnly } ]);
|
|
644
703
|
|
|
645
|
-
//
|
|
646
|
-
const
|
|
647
|
-
observer
|
|
648
|
-
});
|
|
704
|
+
// 2. Missing index and colIndex
|
|
705
|
+
const div2 = document.createElement('div');
|
|
706
|
+
observer.trigger([ { target: div2, contentRect: { width: 100, height: 100 } as unknown as DOMRectReadOnly } ]);
|
|
649
707
|
|
|
650
|
-
it('should cleanup observers on unmount', async () => {
|
|
651
|
-
const wrapper = mount(VirtualScroll, {
|
|
652
|
-
props: { items: mockItems, stickyHeader: true, stickyFooter: true },
|
|
653
|
-
slots: { header: '<div>H</div>', footer: '<div>F</div>' },
|
|
654
|
-
});
|
|
655
708
|
await nextTick();
|
|
656
|
-
wrapper.unmount();
|
|
657
709
|
});
|
|
658
710
|
});
|
|
659
711
|
|
|
660
712
|
describe('grid mode logic', () => {
|
|
661
|
-
it('should cover
|
|
662
|
-
mount(VirtualScroll, { props: { items: mockItems.slice(0, 1) } });
|
|
663
|
-
await nextTick();
|
|
664
|
-
const observer = (globalThis.ResizeObserver as unknown as { instances: ResizeObserverMock[]; }).instances.find((i) => i.callback.toString().includes('colIndex'));
|
|
665
|
-
const div = document.createElement('div');
|
|
666
|
-
div.dataset.colIndex = '0';
|
|
667
|
-
observer!.trigger([ { target: div } ]);
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
it('should cover firstRenderedIndex watcher for grid old/new and other branches', async () => {
|
|
671
|
-
// Test direction !== 'both' branch
|
|
672
|
-
const wrapperV = mount(VirtualScroll, {
|
|
673
|
-
props: { items: mockItems, direction: 'vertical', itemSize: 50 },
|
|
674
|
-
});
|
|
675
|
-
await nextTick();
|
|
676
|
-
(wrapperV.vm as unknown as VSInstance).scrollToIndex(10, 0);
|
|
677
|
-
await nextTick();
|
|
678
|
-
await nextTick();
|
|
679
|
-
|
|
713
|
+
it('should cover firstRenderedIndex watcher for grid', async () => {
|
|
680
714
|
const wrapper = mount(VirtualScroll, {
|
|
681
715
|
props: {
|
|
682
|
-
|
|
683
|
-
direction: 'both',
|
|
716
|
+
bufferBefore: 2,
|
|
684
717
|
columnCount: 5,
|
|
718
|
+
direction: 'both',
|
|
685
719
|
itemSize: 50,
|
|
686
|
-
|
|
687
|
-
bufferAfter: 10,
|
|
720
|
+
items: mockItems,
|
|
688
721
|
},
|
|
689
722
|
slots: {
|
|
690
723
|
item: '<template #item="{ index }"><div class="cell" :data-col-index="0">Item {{ index }}</div></template>',
|
|
691
724
|
},
|
|
692
725
|
});
|
|
693
|
-
const container = wrapper.find('.virtual-scroll-container').element as HTMLElement;
|
|
694
|
-
Object.defineProperty(container, 'clientHeight', { value: 200, configurable: true });
|
|
695
|
-
Object.defineProperty(container, 'clientWidth', { value: 200, configurable: true });
|
|
696
|
-
|
|
697
|
-
// Trigger host resize observer
|
|
698
|
-
(globalThis.ResizeObserver as unknown as { instances: ResizeObserverMock[]; }).instances.forEach((i) => {
|
|
699
|
-
if (i.targets.has(container)) {
|
|
700
|
-
i.trigger([ { target: container } ]);
|
|
701
|
-
}
|
|
702
|
-
});
|
|
703
726
|
await nextTick();
|
|
704
|
-
|
|
705
727
|
const vm = wrapper.vm as unknown as VSInstance;
|
|
706
728
|
|
|
707
|
-
//
|
|
708
|
-
vm.scrollToIndex(10, 0, {
|
|
729
|
+
// Scroll to 10
|
|
730
|
+
vm.scrollToIndex(10, 0, { align: 'start', behavior: 'auto' });
|
|
709
731
|
await nextTick();
|
|
710
732
|
await nextTick();
|
|
711
733
|
|
|
@@ -713,64 +735,24 @@ describe('VirtualScroll component', () => {
|
|
|
713
735
|
const itemResizeObserver = (globalThis.ResizeObserver as unknown as { instances: ResizeObserverMock[]; }).instances.find((i) => i.targets.has(item8));
|
|
714
736
|
expect(itemResizeObserver).toBeDefined();
|
|
715
737
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
// Scroll to 9. range starts at 9-2 = 7.
|
|
720
|
-
// oldIdx was 8. newIdx is 7. Item 8 is still in DOM.
|
|
721
|
-
vm.scrollToIndex(9, 0, { behavior: 'auto', align: 'start' });
|
|
722
|
-
await nextTick();
|
|
723
|
-
await nextTick();
|
|
724
|
-
|
|
725
|
-
// Item 8 should have its cells unobserved
|
|
726
|
-
expect(itemResizeObserver!.targets.has(cell8!)).toBe(false);
|
|
727
|
-
|
|
728
|
-
// Item 7 should have its cells observed
|
|
729
|
-
const item7 = wrapper.find('.virtual-scroll-item[data-index="7"]').element;
|
|
730
|
-
const cell7 = item7.querySelector('[data-col-index="0"]');
|
|
731
|
-
expect(itemResizeObserver!.targets.has(cell7!)).toBe(true);
|
|
732
|
-
|
|
733
|
-
// Scroll to 50. range starts at 50-2 = 48.
|
|
734
|
-
// oldIdx was 7. Item 7 is definitely NOT in DOM anymore.
|
|
735
|
-
// This covers the if (oldEl) branch being false.
|
|
736
|
-
vm.scrollToIndex(50, 0, { behavior: 'auto', align: 'start' });
|
|
738
|
+
// Scroll to 9
|
|
739
|
+
vm.scrollToIndex(9, 0, { align: 'start', behavior: 'auto' });
|
|
737
740
|
await nextTick();
|
|
738
741
|
await nextTick();
|
|
739
|
-
});
|
|
740
|
-
|
|
741
|
-
it('should cover firstRenderedIndex watcher logic for grid cells', async () => {
|
|
742
|
-
const wrapper = mount(VirtualScroll, {
|
|
743
|
-
props: {
|
|
744
|
-
items: mockItems,
|
|
745
|
-
direction: 'both',
|
|
746
|
-
columnCount: 5,
|
|
747
|
-
itemSize: 50,
|
|
748
|
-
},
|
|
749
|
-
slots: {
|
|
750
|
-
item: '<template #item="{ index }"><div :data-col-index="0">Item {{ index }}</div></template>',
|
|
751
|
-
},
|
|
752
|
-
});
|
|
753
|
-
await nextTick();
|
|
754
742
|
|
|
755
|
-
//
|
|
756
|
-
|
|
757
|
-
const vm = wrapper.vm as unknown as VSInstance;
|
|
758
|
-
vm.scrollToIndex(10, 0);
|
|
743
|
+
// Scroll to 50
|
|
744
|
+
vm.scrollToIndex(50, 0, { align: 'start', behavior: 'auto' });
|
|
759
745
|
await nextTick();
|
|
760
|
-
// This should trigger the watcher (oldIdx 0 -> newIdx 10)
|
|
761
|
-
|
|
762
|
-
// Scroll back
|
|
763
|
-
vm.scrollToIndex(0, 0);
|
|
764
746
|
await nextTick();
|
|
765
747
|
});
|
|
766
748
|
|
|
767
749
|
it('should cover firstRenderedIndex watcher when items becomes empty', async () => {
|
|
768
750
|
const wrapper = mount(VirtualScroll, {
|
|
769
751
|
props: {
|
|
770
|
-
items: mockItems,
|
|
771
|
-
direction: 'both',
|
|
772
752
|
columnCount: 5,
|
|
753
|
+
direction: 'both',
|
|
773
754
|
itemSize: 50,
|
|
755
|
+
items: mockItems,
|
|
774
756
|
},
|
|
775
757
|
});
|
|
776
758
|
await nextTick();
|
|
@@ -779,52 +761,17 @@ describe('VirtualScroll component', () => {
|
|
|
779
761
|
});
|
|
780
762
|
});
|
|
781
763
|
|
|
782
|
-
describe('internal methods and exports', () => {
|
|
783
|
-
it('should handle setItemRef', async () => {
|
|
784
|
-
const wrapper = mount(VirtualScroll, { props: { items: mockItems.slice(0, 1) } });
|
|
785
|
-
await nextTick();
|
|
786
|
-
const vm = wrapper.vm as unknown as VSInstance;
|
|
787
|
-
const item = wrapper.find('.virtual-scroll-item').element as HTMLElement;
|
|
788
|
-
vm.setItemRef(item, 0);
|
|
789
|
-
vm.setItemRef(null, 0);
|
|
790
|
-
vm.setItemRef(null, 999);
|
|
791
|
-
});
|
|
792
|
-
|
|
793
|
-
it('should handle setItemRef with NaN index', async () => {
|
|
794
|
-
mount(VirtualScroll, { props: { items: mockItems.slice(0, 1) } });
|
|
795
|
-
await nextTick();
|
|
796
|
-
const observer = (globalThis.ResizeObserver as unknown as { instances: ResizeObserverMock[]; }).instances[ 0 ];
|
|
797
|
-
const div = document.createElement('div');
|
|
798
|
-
// No data-index
|
|
799
|
-
observer?.trigger([ { target: div, contentRect: { width: 100, height: 100 } as DOMRectReadOnly } ]);
|
|
800
|
-
});
|
|
801
|
-
|
|
802
|
-
it('should handle firstRenderedIndex being undefined', async () => {
|
|
803
|
-
// items empty
|
|
804
|
-
mount(VirtualScroll, { props: { items: [] } });
|
|
805
|
-
await nextTick();
|
|
806
|
-
});
|
|
807
|
-
|
|
808
|
-
it('should expose methods', () => {
|
|
809
|
-
const wrapper = mount(VirtualScroll, { props: { items: mockItems, itemSize: 50 } });
|
|
810
|
-
expect(typeof (wrapper.vm as unknown as VSInstance).scrollToIndex).toBe('function');
|
|
811
|
-
expect(typeof (wrapper.vm as unknown as VSInstance).scrollToOffset).toBe('function');
|
|
812
|
-
});
|
|
813
|
-
});
|
|
814
|
-
|
|
815
764
|
describe('infinite scroll and loading', () => {
|
|
816
765
|
it('should emit load event when reaching scroll end (vertical)', async () => {
|
|
817
766
|
const wrapper = mount(VirtualScroll, {
|
|
818
767
|
props: {
|
|
819
|
-
items: mockItems.slice(0, 10),
|
|
820
768
|
itemSize: 50,
|
|
769
|
+
items: mockItems.slice(0, 10),
|
|
821
770
|
loadDistance: 400,
|
|
822
|
-
useRAF: false,
|
|
823
771
|
},
|
|
824
772
|
});
|
|
825
773
|
await nextTick();
|
|
826
774
|
|
|
827
|
-
// Scroll to near end
|
|
828
775
|
(wrapper.vm as unknown as VSInstance).scrollToOffset(0, 250);
|
|
829
776
|
await nextTick();
|
|
830
777
|
await nextTick();
|
|
@@ -836,16 +783,14 @@ describe('VirtualScroll component', () => {
|
|
|
836
783
|
it('should emit load event when reaching scroll end (horizontal)', async () => {
|
|
837
784
|
const wrapper = mount(VirtualScroll, {
|
|
838
785
|
props: {
|
|
839
|
-
items: mockItems.slice(0, 10),
|
|
840
|
-
itemSize: 50,
|
|
841
786
|
direction: 'horizontal',
|
|
787
|
+
itemSize: 50,
|
|
788
|
+
items: mockItems.slice(0, 10),
|
|
842
789
|
loadDistance: 400,
|
|
843
|
-
useRAF: false,
|
|
844
790
|
},
|
|
845
791
|
});
|
|
846
792
|
await nextTick();
|
|
847
793
|
|
|
848
|
-
// Scroll to near end
|
|
849
794
|
(wrapper.vm as unknown as VSInstance).scrollToOffset(250, 0);
|
|
850
795
|
await nextTick();
|
|
851
796
|
await nextTick();
|
|
@@ -857,8 +802,8 @@ describe('VirtualScroll component', () => {
|
|
|
857
802
|
it('should not emit load event when loading is true', async () => {
|
|
858
803
|
const wrapper = mount(VirtualScroll, {
|
|
859
804
|
props: {
|
|
860
|
-
items: mockItems.slice(0, 10),
|
|
861
805
|
itemSize: 50,
|
|
806
|
+
items: mockItems.slice(0, 10),
|
|
862
807
|
loadDistance: 100,
|
|
863
808
|
loading: true,
|
|
864
809
|
},
|
|
@@ -877,11 +822,11 @@ describe('VirtualScroll component', () => {
|
|
|
877
822
|
expect(wrapper.emitted('load')).toBeUndefined();
|
|
878
823
|
});
|
|
879
824
|
|
|
880
|
-
it('should render loading slot
|
|
825
|
+
it('should render loading slot correctly', async () => {
|
|
881
826
|
const wrapper = mount(VirtualScroll, {
|
|
882
827
|
props: {
|
|
883
|
-
items: mockItems.slice(0, 10),
|
|
884
828
|
itemSize: 50,
|
|
829
|
+
items: mockItems.slice(0, 10),
|
|
885
830
|
loading: true,
|
|
886
831
|
},
|
|
887
832
|
slots: {
|
|
@@ -890,23 +835,99 @@ describe('VirtualScroll component', () => {
|
|
|
890
835
|
});
|
|
891
836
|
await nextTick();
|
|
892
837
|
expect(wrapper.find('.loading-indicator').exists()).toBe(true);
|
|
893
|
-
|
|
838
|
+
|
|
839
|
+
await wrapper.setProps({ direction: 'horizontal' });
|
|
840
|
+
await nextTick();
|
|
841
|
+
expect((wrapper.find('.virtual-scroll-loading').element as HTMLElement).style.display).toBe('inline-block');
|
|
894
842
|
});
|
|
895
843
|
|
|
896
|
-
it('should
|
|
844
|
+
it('should toggle loading slot visibility based on loading prop', async () => {
|
|
897
845
|
const wrapper = mount(VirtualScroll, {
|
|
898
846
|
props: {
|
|
899
|
-
items: mockItems.slice(0,
|
|
900
|
-
|
|
901
|
-
direction: 'horizontal',
|
|
902
|
-
loading: true,
|
|
847
|
+
items: mockItems.slice(0, 5),
|
|
848
|
+
loading: false,
|
|
903
849
|
},
|
|
904
850
|
slots: {
|
|
905
|
-
loading: '<div class="
|
|
851
|
+
loading: '<div class="loader">Loading...</div>',
|
|
906
852
|
},
|
|
907
853
|
});
|
|
908
854
|
await nextTick();
|
|
909
|
-
|
|
855
|
+
|
|
856
|
+
expect(wrapper.find('.loader').exists()).toBe(false);
|
|
857
|
+
expect(wrapper.find('.virtual-scroll-loading').exists()).toBe(false);
|
|
858
|
+
|
|
859
|
+
await wrapper.setProps({ loading: true });
|
|
860
|
+
await nextTick();
|
|
861
|
+
expect(wrapper.find('.loader').exists()).toBe(true);
|
|
862
|
+
expect(wrapper.find('.virtual-scroll-loading').exists()).toBe(true);
|
|
863
|
+
|
|
864
|
+
await wrapper.setProps({ loading: false });
|
|
865
|
+
await nextTick();
|
|
866
|
+
expect(wrapper.find('.loader').exists()).toBe(false);
|
|
867
|
+
expect(wrapper.find('.virtual-scroll-loading').exists()).toBe(false);
|
|
868
|
+
});
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
describe('internal methods and exports', () => {
|
|
872
|
+
it('should handle setItemRef', async () => {
|
|
873
|
+
const wrapper = mount(VirtualScroll, { props: { items: mockItems.slice(0, 1) } });
|
|
874
|
+
await nextTick();
|
|
875
|
+
const vm = wrapper.vm as unknown as VSInstance;
|
|
876
|
+
const item = wrapper.find('.virtual-scroll-item').element as HTMLElement;
|
|
877
|
+
vm.setItemRef(item, 0);
|
|
878
|
+
vm.setItemRef(null, 0);
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
it('should handle setItemRef with NaN index', async () => {
|
|
882
|
+
mount(VirtualScroll, { props: { items: mockItems.slice(0, 1) } });
|
|
883
|
+
await nextTick();
|
|
884
|
+
const observer = (globalThis.ResizeObserver as unknown as { instances: ResizeObserverMock[]; }).instances[ 0 ];
|
|
885
|
+
const div = document.createElement('div');
|
|
886
|
+
observer?.trigger([ { target: div, contentRect: { width: 100, height: 100 } as unknown as DOMRectReadOnly } ]);
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
it('should expose methods', () => {
|
|
890
|
+
const wrapper = mount(VirtualScroll, { props: { items: mockItems, itemSize: 50 } });
|
|
891
|
+
expect(typeof (wrapper.vm as unknown as VSInstance).scrollToIndex).toBe('function');
|
|
892
|
+
expect(typeof (wrapper.vm as unknown as VSInstance).scrollToOffset).toBe('function');
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
it('should emit visibleRangeChange on scroll and hydration', async () => {
|
|
896
|
+
const wrapper = mount(VirtualScroll, {
|
|
897
|
+
props: { itemSize: 50, items: mockItems },
|
|
898
|
+
});
|
|
899
|
+
await nextTick();
|
|
900
|
+
await nextTick();
|
|
901
|
+
expect(wrapper.emitted('visibleRangeChange')).toBeDefined();
|
|
902
|
+
|
|
903
|
+
const container = wrapper.find('.virtual-scroll-container').element as HTMLElement;
|
|
904
|
+
Object.defineProperty(container, 'scrollTop', { value: 500, writable: true });
|
|
905
|
+
await container.dispatchEvent(new Event('scroll'));
|
|
906
|
+
await nextTick();
|
|
907
|
+
await nextTick();
|
|
908
|
+
expect(wrapper.emitted('visibleRangeChange')!.length).toBeGreaterThan(1);
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
it('should not emit scroll event before hydration in watch', async () => {
|
|
912
|
+
// initialScrollIndex triggers delayed hydration via nextTick in useVirtualScroll
|
|
913
|
+
const wrapper = mount(VirtualScroll, {
|
|
914
|
+
props: {
|
|
915
|
+
initialScrollIndex: 5,
|
|
916
|
+
itemSize: 50,
|
|
917
|
+
items: mockItems.slice(0, 10),
|
|
918
|
+
},
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
// Before first nextTick, isHydrated is false.
|
|
922
|
+
// Changing items will trigger scrollDetails update.
|
|
923
|
+
await wrapper.setProps({ items: mockItems.slice(0, 20) });
|
|
924
|
+
|
|
925
|
+
// Line 196 in VirtualScroll.vue should be hit here (return if !isHydrated)
|
|
926
|
+
expect(wrapper.emitted('scroll')).toBeUndefined();
|
|
927
|
+
|
|
928
|
+
await nextTick(); // hydration tick
|
|
929
|
+
await nextTick(); // one more for good measure
|
|
930
|
+
expect(wrapper.emitted('scroll')).toBeDefined();
|
|
910
931
|
});
|
|
911
932
|
});
|
|
912
933
|
});
|