@pdanpdan/virtual-scroll 0.2.1 → 0.4.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 +278 -140
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +866 -112
- package/dist/index.js +1 -844
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1125 -0
- package/dist/index.mjs.map +1 -0
- package/dist/virtual-scroll.css +2 -0
- package/package.json +8 -5
- package/src/components/VirtualScroll.test.ts +527 -688
- package/src/components/VirtualScroll.vue +402 -209
- package/src/composables/useVirtualScroll.test.ts +241 -1447
- package/src/composables/useVirtualScroll.ts +544 -531
- package/src/index.ts +2 -0
- package/src/types.ts +535 -0
- package/src/utils/fenwick-tree.ts +38 -18
- package/src/utils/scroll.test.ts +148 -0
- package/src/utils/scroll.ts +40 -10
- package/src/utils/virtual-scroll-logic.test.ts +2517 -0
- package/src/utils/virtual-scroll-logic.ts +605 -0
- package/dist/index.css +0 -2
|
@@ -1,53 +1,39 @@
|
|
|
1
1
|
/* global ScrollToOptions */
|
|
2
|
-
import type { VirtualScrollProps } from '
|
|
2
|
+
import type { VirtualScrollProps } from '../types';
|
|
3
3
|
import type { Ref } from 'vue';
|
|
4
4
|
|
|
5
5
|
import { mount } from '@vue/test-utils';
|
|
6
6
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
7
|
import { defineComponent, nextTick, ref } from 'vue';
|
|
8
8
|
|
|
9
|
-
import { getPaddingX, getPaddingY } from '../utils/scroll';
|
|
10
9
|
import { useVirtualScroll } from './useVirtualScroll';
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
// --- Mocks ---
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
globalThis.ResizeObserver = class {
|
|
22
|
-
callback: ResizeObserverCallback;
|
|
23
|
-
static instances: ResizeObserverMock[] = [];
|
|
24
|
-
targets: Set<Element> = new Set();
|
|
25
|
-
|
|
26
|
-
constructor(callback: ResizeObserverCallback) {
|
|
27
|
-
this.callback = callback;
|
|
28
|
-
(this.constructor as unknown as { instances: ResizeObserverMock[]; }).instances.push(this as unknown as ResizeObserverMock);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
observe(target: Element) {
|
|
32
|
-
this.targets.add(target);
|
|
33
|
-
}
|
|
13
|
+
globalThis.ResizeObserver = class ResizeObserver {
|
|
14
|
+
observe = vi.fn();
|
|
15
|
+
unobserve = vi.fn();
|
|
16
|
+
disconnect = vi.fn();
|
|
17
|
+
};
|
|
34
18
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
19
|
+
Object.defineProperty(HTMLElement.prototype, 'clientHeight', { configurable: true, value: 500 });
|
|
20
|
+
Object.defineProperty(HTMLElement.prototype, 'clientWidth', { configurable: true, value: 500 });
|
|
21
|
+
Object.defineProperty(document.documentElement, 'clientHeight', { configurable: true, value: 500 });
|
|
22
|
+
Object.defineProperty(document.documentElement, 'clientWidth', { configurable: true, value: 500 });
|
|
23
|
+
Object.defineProperty(window, 'innerHeight', { configurable: true, value: 500 });
|
|
24
|
+
Object.defineProperty(window, 'innerWidth', { configurable: true, value: 500 });
|
|
38
25
|
|
|
39
|
-
|
|
40
|
-
|
|
26
|
+
globalThis.window.scrollTo = vi.fn().mockImplementation((options: ScrollToOptions) => {
|
|
27
|
+
if (options.left !== undefined) {
|
|
28
|
+
Object.defineProperty(window, 'scrollX', { configurable: true, value: options.left, writable: true });
|
|
41
29
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
this.callback(entries as ResizeObserverEntry[], this as unknown as ResizeObserver);
|
|
30
|
+
if (options.top !== undefined) {
|
|
31
|
+
Object.defineProperty(window, 'scrollY', { configurable: true, value: options.top, writable: true });
|
|
45
32
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
globalThis.window.scrollTo = vi.fn();
|
|
33
|
+
document.dispatchEvent(new Event('scroll'));
|
|
34
|
+
});
|
|
49
35
|
|
|
50
|
-
// Helper to test composable
|
|
36
|
+
// Helper to test composable
|
|
51
37
|
function setup<T>(propsValue: VirtualScrollProps<T>) {
|
|
52
38
|
const props = ref(propsValue) as Ref<VirtualScrollProps<T>>;
|
|
53
39
|
let result: ReturnType<typeof useVirtualScroll<T>>;
|
|
@@ -59,1497 +45,305 @@ function setup<T>(propsValue: VirtualScrollProps<T>) {
|
|
|
59
45
|
},
|
|
60
46
|
});
|
|
61
47
|
const wrapper = mount(TestComponent);
|
|
62
|
-
return { result: result!,
|
|
48
|
+
return { props, result: result!, wrapper };
|
|
63
49
|
}
|
|
64
50
|
|
|
65
|
-
const mockItems = Array.from({ length: 100 }, (_, i) => ({ id: i }));
|
|
66
|
-
const defaultProps: VirtualScrollProps<{ id: number; }> = {
|
|
67
|
-
items: mockItems,
|
|
68
|
-
itemSize: 50,
|
|
69
|
-
direction: 'vertical' as const,
|
|
70
|
-
bufferBefore: 2,
|
|
71
|
-
bufferAfter: 2,
|
|
72
|
-
container: window,
|
|
73
|
-
};
|
|
74
|
-
|
|
75
51
|
describe('useVirtualScroll', () => {
|
|
52
|
+
const mockItems = Array.from({ length: 100 }, (_, i) => ({ id: i }));
|
|
53
|
+
|
|
76
54
|
beforeEach(() => {
|
|
77
|
-
window.scrollX = 0;
|
|
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
|
-
});
|
|
90
55
|
vi.clearAllMocks();
|
|
91
|
-
|
|
56
|
+
Object.defineProperty(window, 'scrollX', { configurable: true, value: 0, writable: true });
|
|
57
|
+
Object.defineProperty(window, 'scrollY', { configurable: true, value: 0, writable: true });
|
|
92
58
|
});
|
|
93
59
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
it('should update total size when items length changes', async () => {
|
|
101
|
-
const { result, props } = setup({ ...defaultProps });
|
|
102
|
-
expect(result.totalHeight.value).toBe(5000);
|
|
103
|
-
|
|
104
|
-
props.value.items = Array.from({ length: 50 }, (_, i) => ({ id: i }));
|
|
105
|
-
await nextTick();
|
|
106
|
-
expect(result.totalHeight.value).toBe(2500);
|
|
60
|
+
it('calculates total dimensions correctly', async () => {
|
|
61
|
+
const { result } = setup({
|
|
62
|
+
container: window,
|
|
63
|
+
direction: 'vertical',
|
|
64
|
+
itemSize: 50,
|
|
65
|
+
items: mockItems,
|
|
107
66
|
});
|
|
108
67
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
expect(result.totalHeight.value).toBe(5000);
|
|
112
|
-
|
|
113
|
-
props.value.itemSize = 100;
|
|
114
|
-
await nextTick();
|
|
115
|
-
expect(result.totalHeight.value).toBe(10000);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('should recalculate when gaps change', async () => {
|
|
119
|
-
const { result, props } = setup({ ...defaultProps, gap: 10 });
|
|
120
|
-
expect(result.totalHeight.value).toBe(6000); // 100 * (50 + 10)
|
|
121
|
-
|
|
122
|
-
props.value.gap = 20;
|
|
123
|
-
await nextTick();
|
|
124
|
-
expect(result.totalHeight.value).toBe(7000); // 100 * (50 + 20)
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('should handle itemSize as a function', async () => {
|
|
128
|
-
const { result } = setup({
|
|
129
|
-
...defaultProps,
|
|
130
|
-
itemSize: (_item: { id: number; }, index: number) => 50 + index,
|
|
131
|
-
});
|
|
132
|
-
// 50*100 + (0+99)*100/2 = 5000 + 4950 = 9950
|
|
133
|
-
expect(result.totalHeight.value).toBe(9950);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('should handle direction both (grid mode)', async () => {
|
|
137
|
-
const { result } = setup({
|
|
138
|
-
...defaultProps,
|
|
139
|
-
direction: 'both',
|
|
140
|
-
columnCount: 10,
|
|
141
|
-
columnWidth: 100,
|
|
142
|
-
});
|
|
143
|
-
expect(result.totalWidth.value).toBe(1000);
|
|
144
|
-
expect(result.totalHeight.value).toBe(5000);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('should handle horizontal direction', async () => {
|
|
148
|
-
const { result } = setup({ ...defaultProps, direction: 'horizontal' });
|
|
149
|
-
expect(result.totalWidth.value).toBe(5000);
|
|
150
|
-
expect(result.totalHeight.value).toBe(0);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('should cover default values for buffer and gap', async () => {
|
|
154
|
-
const { result } = setup({
|
|
155
|
-
items: mockItems,
|
|
156
|
-
itemSize: 50,
|
|
157
|
-
} as unknown as VirtualScrollProps<{ id: number; }>);
|
|
158
|
-
expect(result.renderedItems.value.length).toBeGreaterThan(0);
|
|
159
|
-
});
|
|
68
|
+
expect(result.totalHeight.value).toBe(5000);
|
|
69
|
+
expect(result.totalWidth.value).toBe(500);
|
|
160
70
|
});
|
|
161
71
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
72
|
+
it('provides rendered items for the visible range', async () => {
|
|
73
|
+
const { result } = setup({
|
|
74
|
+
container: window,
|
|
75
|
+
direction: 'vertical',
|
|
76
|
+
itemSize: 50,
|
|
77
|
+
items: mockItems,
|
|
167
78
|
});
|
|
168
79
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
expect(result.renderedItems.value.length).toBeGreaterThan(0);
|
|
172
|
-
expect(result.scrollDetails.value.currentIndex).toBe(0);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('should handle horizontal non-fixed size range', async () => {
|
|
176
|
-
const container = document.createElement('div');
|
|
177
|
-
Object.defineProperty(container, 'clientWidth', { value: 500 });
|
|
178
|
-
Object.defineProperty(container, 'clientHeight', { value: 500 });
|
|
179
|
-
const { result } = setup({ ...defaultProps, direction: 'horizontal', itemSize: undefined, container });
|
|
180
|
-
for (let i = 0; i < 20; i++) {
|
|
181
|
-
result.updateItemSize(i, 50, 50);
|
|
182
|
-
}
|
|
183
|
-
await nextTick();
|
|
80
|
+
await nextTick();
|
|
81
|
+
await nextTick();
|
|
184
82
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
expect(result.scrollDetails.value.currentIndex).toBeGreaterThan(0);
|
|
189
|
-
});
|
|
83
|
+
// viewport 500, item 50 => 10 items + buffer 5 = 15 items
|
|
84
|
+
expect(result.renderedItems.value.length).toBe(15);
|
|
85
|
+
expect(result.renderedItems.value[ 0 ]!.index).toBe(0);
|
|
190
86
|
});
|
|
191
87
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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();
|
|
227
|
-
|
|
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);
|
|
235
|
-
});
|
|
236
|
-
|
|
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);
|
|
250
|
-
|
|
251
|
-
// Vertical
|
|
252
|
-
const { result: rV } = setup({ ...defaultProps, direction: 'vertical', itemSize: undefined });
|
|
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);
|
|
257
|
-
|
|
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);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it('should handle updateItemSize and trigger reactivity', async () => {
|
|
265
|
-
const { result } = setup({ ...defaultProps, itemSize: undefined });
|
|
266
|
-
expect(result.totalHeight.value).toBe(5000); // Default estimate
|
|
267
|
-
|
|
268
|
-
result.updateItemSize(0, 100, 100);
|
|
269
|
-
await nextTick();
|
|
270
|
-
expect(result.totalHeight.value).toBe(5050);
|
|
271
|
-
expect(result.renderedItems.value[ 0 ]!.size.height).toBe(100);
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it('should treat 0, null, undefined as dynamic itemSize', async () => {
|
|
275
|
-
for (const val of [ 0, null, undefined ]) {
|
|
276
|
-
const { result } = setup({ ...defaultProps, itemSize: val as unknown as undefined });
|
|
277
|
-
expect(result.totalHeight.value).toBe(5000);
|
|
278
|
-
result.updateItemSize(0, 100, 100);
|
|
279
|
-
await nextTick();
|
|
280
|
-
expect(result.totalHeight.value).toBe(5050);
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it('should treat 0, null, undefined as dynamic columnWidth', async () => {
|
|
285
|
-
for (const val of [ 0, null, undefined ]) {
|
|
286
|
-
const { result } = setup({
|
|
287
|
-
...defaultProps,
|
|
288
|
-
direction: 'both',
|
|
289
|
-
columnCount: 2,
|
|
290
|
-
columnWidth: val as unknown as undefined,
|
|
291
|
-
});
|
|
292
|
-
expect(result.getColumnWidth(0)).toBe(150);
|
|
293
|
-
const parent = document.createElement('div');
|
|
294
|
-
const col0 = document.createElement('div');
|
|
295
|
-
Object.defineProperty(col0, 'offsetWidth', { value: 200, configurable: true });
|
|
296
|
-
col0.dataset.colIndex = '0';
|
|
297
|
-
parent.appendChild(col0);
|
|
298
|
-
result.updateItemSize(0, 200, 50, parent);
|
|
299
|
-
await nextTick();
|
|
300
|
-
expect(result.totalWidth.value).toBe(350);
|
|
301
|
-
}
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
it('should handle dynamic column width with data-col-index', async () => {
|
|
305
|
-
const { result } = setup({
|
|
306
|
-
...defaultProps,
|
|
307
|
-
direction: 'both',
|
|
308
|
-
columnCount: 2,
|
|
309
|
-
columnWidth: undefined,
|
|
310
|
-
});
|
|
311
|
-
const parent = document.createElement('div');
|
|
312
|
-
const child1 = document.createElement('div');
|
|
313
|
-
Object.defineProperty(child1, 'offsetWidth', { value: 200 });
|
|
314
|
-
child1.dataset.colIndex = '0';
|
|
315
|
-
const child2 = document.createElement('div');
|
|
316
|
-
Object.defineProperty(child2, 'offsetWidth', { value: 300 });
|
|
317
|
-
child2.dataset.colIndex = '1';
|
|
318
|
-
parent.appendChild(child1);
|
|
319
|
-
parent.appendChild(child2);
|
|
320
|
-
|
|
321
|
-
result.updateItemSize(0, 500, 50, parent);
|
|
322
|
-
await nextTick();
|
|
323
|
-
expect(result.getColumnWidth(0)).toBe(200);
|
|
324
|
-
expect(result.getColumnWidth(1)).toBe(300);
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
it('should return early in updateItemSize if itemSize is fixed', async () => {
|
|
328
|
-
const { result } = setup({ ...defaultProps, itemSize: 50 });
|
|
329
|
-
result.updateItemSize(0, 100, 100);
|
|
330
|
-
expect(result.totalHeight.value).toBe(5000);
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
it('should use defaultItemSize and defaultColumnWidth when provided', () => {
|
|
334
|
-
const { result } = setup({
|
|
335
|
-
...defaultProps,
|
|
336
|
-
itemSize: undefined,
|
|
337
|
-
columnWidth: undefined,
|
|
338
|
-
defaultItemSize: 100,
|
|
339
|
-
defaultColumnWidth: 200,
|
|
340
|
-
direction: 'both',
|
|
341
|
-
columnCount: 10,
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
expect(result.totalHeight.value).toBe(100 * 100); // 100 items * 100 defaultItemSize
|
|
345
|
-
expect(result.totalWidth.value).toBe(10 * 200); // 10 columns * 200 defaultColumnWidth
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
it('should ignore small delta updates in updateItemSize', async () => {
|
|
349
|
-
const { result } = setup({ ...defaultProps, itemSize: undefined });
|
|
350
|
-
result.updateItemSize(0, 50.1, 50.1);
|
|
351
|
-
await nextTick();
|
|
352
|
-
expect(result.totalHeight.value).toBe(5000);
|
|
88
|
+
it('updates when scroll position changes', async () => {
|
|
89
|
+
const { result } = setup({
|
|
90
|
+
container: window,
|
|
91
|
+
direction: 'vertical',
|
|
92
|
+
itemSize: 50,
|
|
93
|
+
items: mockItems,
|
|
353
94
|
});
|
|
354
95
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
result.updateItemSize(0, 100, 100);
|
|
358
|
-
await nextTick();
|
|
359
|
-
expect(result.renderedItems.value[ 0 ]!.size.height).toBe(100);
|
|
96
|
+
await nextTick();
|
|
97
|
+
await nextTick();
|
|
360
98
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
expect(result.renderedItems.value[ 0 ]!.size.height).toBe(100);
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
it('should update item height in vertical mode', async () => {
|
|
367
|
-
const { result } = setup({ ...defaultProps, direction: 'vertical', itemSize: undefined });
|
|
368
|
-
result.updateItemSize(0, 100, 100);
|
|
369
|
-
await nextTick();
|
|
370
|
-
expect(result.renderedItems.value[ 0 ]!.size.height).toBe(100);
|
|
371
|
-
});
|
|
99
|
+
Object.defineProperty(window, 'scrollY', { configurable: true, value: 500, writable: true });
|
|
100
|
+
document.dispatchEvent(new Event('scroll'));
|
|
372
101
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
result.updateItemSize(0, 100, 50);
|
|
376
|
-
await nextTick();
|
|
377
|
-
expect(result.totalWidth.value).toBe(5050);
|
|
378
|
-
});
|
|
102
|
+
await nextTick();
|
|
103
|
+
await nextTick();
|
|
379
104
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
await nextTick();
|
|
384
|
-
expect(result.totalHeight.value).toBe(5050);
|
|
385
|
-
|
|
386
|
-
// Trigger initializeSizes by changing length
|
|
387
|
-
props.value.items = Array.from({ length: 101 }, (_, i) => ({ id: i }));
|
|
388
|
-
await nextTick();
|
|
389
|
-
// Should still be 100 for index 0, not reset to default 50
|
|
390
|
-
expect(result.totalHeight.value).toBe(5050 + 50);
|
|
391
|
-
});
|
|
105
|
+
// At 500px, start index is 500/50 = 10. With buffer 5, start is 5.
|
|
106
|
+
expect(result.scrollDetails.value.currentIndex).toBe(10);
|
|
107
|
+
expect(result.renderedItems.value[ 0 ]!.index).toBe(5);
|
|
392
108
|
});
|
|
393
109
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
// index 10. itemSize is 50 by default. totalWidth = 5000.
|
|
402
|
-
result.scrollToIndex(null, 10, { align: 'start', behavior: 'auto' });
|
|
403
|
-
await nextTick();
|
|
404
|
-
expect(result.scrollDetails.value.scrollOffset.x).toBe(500);
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
it('should handle scrollToIndex with window fallback when container is missing', async () => {
|
|
408
|
-
const { result } = setup({ ...defaultProps, container: undefined });
|
|
409
|
-
await nextTick();
|
|
410
|
-
result.scrollToIndex(10, 0);
|
|
411
|
-
await nextTick();
|
|
412
|
-
expect(window.scrollTo).toHaveBeenCalled();
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
it('should handle scrollToIndex out of bounds', async () => {
|
|
416
|
-
const { result } = setup({ ...defaultProps });
|
|
417
|
-
// Row past end
|
|
418
|
-
result.scrollToIndex(mockItems.length + 10, 0);
|
|
419
|
-
await nextTick();
|
|
420
|
-
expect(window.scrollTo).toHaveBeenCalled();
|
|
421
|
-
|
|
422
|
-
// Col past end (in grid mode)
|
|
423
|
-
const { result: r_grid } = setup({ ...defaultProps, direction: 'both', columnCount: 5, columnWidth: 100 });
|
|
424
|
-
r_grid.scrollToIndex(0, 10);
|
|
425
|
-
await nextTick();
|
|
426
|
-
expect(window.scrollTo).toHaveBeenCalled();
|
|
427
|
-
|
|
428
|
-
// Column past end in horizontal mode
|
|
429
|
-
const { result: r_horiz } = setup({ ...defaultProps, direction: 'horizontal' });
|
|
430
|
-
r_horiz.scrollToIndex(0, 200);
|
|
431
|
-
await nextTick();
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
it('should handle scrollToIndex auto alignment with padding', async () => {
|
|
435
|
-
const container = document.createElement('div');
|
|
436
|
-
Object.defineProperty(container, 'clientHeight', { value: 500, configurable: true });
|
|
437
|
-
Object.defineProperty(container, 'scrollTop', { value: 200, writable: true, configurable: true });
|
|
438
|
-
|
|
439
|
-
const { result } = setup({ ...defaultProps, container, itemSize: 50, scrollPaddingStart: 100 });
|
|
440
|
-
await nextTick();
|
|
441
|
-
|
|
442
|
-
// Current visible range: [scrollTop + paddingStart, scrollTop + viewport - paddingEnd] = [300, 700]
|
|
443
|
-
// Scroll to item at y=250. 250 < 300, so not visible.
|
|
444
|
-
// targetY < relativeScrollY + paddingStart (250 < 200 + 100)
|
|
445
|
-
result.scrollToIndex(5, null, 'auto');
|
|
446
|
-
await nextTick();
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
it('should hit scrollToIndex X calculation branches', async () => {
|
|
450
|
-
const { result: r_horiz } = setup({ ...defaultProps, direction: 'horizontal', itemSize: 100 });
|
|
451
|
-
await nextTick();
|
|
452
|
-
// colIndex null
|
|
453
|
-
r_horiz.scrollToIndex(0, null);
|
|
454
|
-
// rowIndex null
|
|
455
|
-
r_horiz.scrollToIndex(null, 5);
|
|
456
|
-
await nextTick();
|
|
110
|
+
it('supports programmatic scrolling', async () => {
|
|
111
|
+
const { result } = setup({
|
|
112
|
+
container: window,
|
|
113
|
+
direction: 'vertical',
|
|
114
|
+
itemSize: 50,
|
|
115
|
+
items: mockItems,
|
|
457
116
|
});
|
|
458
117
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
container.scrollTo = vi.fn();
|
|
462
|
-
const { result } = setup({ ...defaultProps, container });
|
|
463
|
-
result.scrollToOffset(100, 200);
|
|
464
|
-
expect(container.scrollTo).toHaveBeenCalled();
|
|
465
|
-
});
|
|
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
|
-
|
|
520
|
-
it('should handle scrollToIndex with null indices', async () => {
|
|
521
|
-
const { result } = setup({ ...defaultProps });
|
|
522
|
-
result.scrollToIndex(null, null);
|
|
523
|
-
await nextTick();
|
|
524
|
-
result.scrollToIndex(10, null);
|
|
525
|
-
await nextTick();
|
|
526
|
-
result.scrollToIndex(null, 10);
|
|
527
|
-
await nextTick();
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
it('should handle scrollToIndex auto alignment', async () => {
|
|
531
|
-
const container = document.createElement('div');
|
|
532
|
-
Object.defineProperty(container, 'clientHeight', { value: 500, configurable: true });
|
|
533
|
-
Object.defineProperty(container, 'clientWidth', { value: 500, configurable: true });
|
|
534
|
-
|
|
535
|
-
const { result } = setup({ ...defaultProps, container, itemSize: 50 });
|
|
536
|
-
await nextTick();
|
|
537
|
-
|
|
538
|
-
// Scroll down so some items are above
|
|
539
|
-
result.scrollToIndex(20, 0, 'start');
|
|
540
|
-
await nextTick();
|
|
541
|
-
|
|
542
|
-
// Auto align: already visible
|
|
543
|
-
result.scrollToIndex(20, null, 'auto');
|
|
544
|
-
await nextTick();
|
|
118
|
+
await nextTick();
|
|
119
|
+
await nextTick();
|
|
545
120
|
|
|
546
|
-
|
|
547
|
-
result.scrollToIndex(5, null, 'auto');
|
|
548
|
-
await nextTick();
|
|
549
|
-
|
|
550
|
-
// Auto align: below viewport (scroll down)
|
|
551
|
-
result.scrollToIndex(50, null, 'auto');
|
|
552
|
-
await nextTick();
|
|
553
|
-
|
|
554
|
-
// Horizontal auto align
|
|
555
|
-
const { result: r_horiz } = setup({ ...defaultProps, direction: 'horizontal', container, itemSize: 100 });
|
|
556
|
-
await nextTick();
|
|
557
|
-
|
|
558
|
-
r_horiz.scrollToIndex(0, 20, 'start');
|
|
559
|
-
await nextTick();
|
|
560
|
-
|
|
561
|
-
r_horiz.scrollToIndex(null, 5, 'auto');
|
|
562
|
-
await nextTick();
|
|
563
|
-
|
|
564
|
-
r_horiz.scrollToIndex(null, 50, 'auto');
|
|
565
|
-
await nextTick();
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
it('should handle scrollToIndex with various alignments', async () => {
|
|
569
|
-
const { result } = setup({ ...defaultProps });
|
|
570
|
-
result.scrollToIndex(50, 0, 'center');
|
|
571
|
-
await nextTick();
|
|
572
|
-
result.scrollToIndex(50, 0, 'end');
|
|
573
|
-
await nextTick();
|
|
574
|
-
result.scrollToIndex(50, 0, { x: 'center', y: 'end' });
|
|
575
|
-
await nextTick();
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
it('should handle scrollToOffset with window container', async () => {
|
|
579
|
-
const { result } = setup({ ...defaultProps, container: window });
|
|
580
|
-
result.scrollToOffset(null, 100);
|
|
581
|
-
expect(window.scrollTo).toHaveBeenCalledWith(expect.objectContaining({ top: 100 }));
|
|
582
|
-
|
|
583
|
-
result.scrollToOffset(null, 200, { behavior: 'smooth' });
|
|
584
|
-
expect(window.scrollTo).toHaveBeenCalledWith(expect.objectContaining({ top: 200, behavior: 'smooth' }));
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
it('should handle scrollToIndex with auto alignment and axis preservation', async () => {
|
|
588
|
-
const { result } = setup({ ...defaultProps });
|
|
589
|
-
// Axis preservation (null index)
|
|
590
|
-
result.scrollToIndex(10, null, 'auto');
|
|
591
|
-
await nextTick();
|
|
592
|
-
result.scrollToIndex(null, 5, 'auto');
|
|
593
|
-
await nextTick();
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
it('should handle scrollToOffset with nulls to keep current position', async () => {
|
|
597
|
-
const { result } = setup({ ...defaultProps, container: window });
|
|
598
|
-
window.scrollX = 50;
|
|
599
|
-
window.scrollY = 60;
|
|
600
|
-
|
|
601
|
-
// Pass null to keep current Y while updating X
|
|
602
|
-
result.scrollToOffset(100, null);
|
|
603
|
-
expect(window.scrollTo).toHaveBeenCalledWith(expect.objectContaining({ left: 100 }));
|
|
604
|
-
|
|
605
|
-
// Pass null to keep current X while updating Y
|
|
606
|
-
result.scrollToOffset(null, 200);
|
|
607
|
-
expect(window.scrollTo).toHaveBeenCalledWith(expect.objectContaining({ top: 200 }));
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
it('should handle scrollToOffset with both axes', async () => {
|
|
611
|
-
const { result } = setup({ ...defaultProps });
|
|
612
|
-
result.scrollToOffset(100, 200);
|
|
613
|
-
expect(window.scrollTo).toHaveBeenCalledWith(expect.objectContaining({ left: 100, top: 200 }));
|
|
614
|
-
});
|
|
615
|
-
|
|
616
|
-
it('should handle scrollToOffset fallback when scrollTo is missing', async () => {
|
|
617
|
-
const container = document.createElement('div');
|
|
618
|
-
(container as unknown as { scrollTo: unknown; }).scrollTo = undefined;
|
|
619
|
-
const { result } = setup({ ...defaultProps, container });
|
|
620
|
-
|
|
621
|
-
result.scrollToOffset(100, 200);
|
|
622
|
-
expect(container.scrollTop).toBe(200);
|
|
623
|
-
expect(container.scrollLeft).toBe(100);
|
|
624
|
-
|
|
625
|
-
// X only
|
|
626
|
-
result.scrollToOffset(300, null);
|
|
627
|
-
expect(container.scrollLeft).toBe(300);
|
|
628
|
-
|
|
629
|
-
// Y only
|
|
630
|
-
result.scrollToOffset(null, 400);
|
|
631
|
-
expect(container.scrollTop).toBe(400);
|
|
632
|
-
});
|
|
633
|
-
|
|
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);
|
|
638
|
-
|
|
639
|
-
result.stopProgrammaticScroll();
|
|
640
|
-
expect(result.scrollDetails.value.isProgrammaticScroll).toBe(false);
|
|
641
|
-
});
|
|
642
|
-
|
|
643
|
-
it('should handle scrollToIndex with element container having scrollTo', async () => {
|
|
644
|
-
const container = document.createElement('div');
|
|
645
|
-
container.scrollTo = vi.fn();
|
|
646
|
-
const { result } = setup({ ...defaultProps, container });
|
|
647
|
-
await nextTick();
|
|
648
|
-
|
|
649
|
-
result.scrollToIndex(10, 0, { behavior: 'auto' });
|
|
650
|
-
await nextTick();
|
|
651
|
-
expect(container.scrollTo).toHaveBeenCalled();
|
|
652
|
-
});
|
|
121
|
+
result.scrollToIndex(20, 0, { align: 'start', behavior: 'auto' });
|
|
653
122
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
(container as unknown as { scrollTo: unknown; }).scrollTo = undefined;
|
|
657
|
-
const { result } = setup({ ...defaultProps, container });
|
|
658
|
-
await nextTick();
|
|
659
|
-
|
|
660
|
-
// row only
|
|
661
|
-
result.scrollToIndex(10, null, { behavior: 'auto' });
|
|
662
|
-
await nextTick();
|
|
663
|
-
expect(container.scrollTop).toBeGreaterThan(0);
|
|
664
|
-
|
|
665
|
-
// col only
|
|
666
|
-
const { result: resH } = setup({ ...defaultProps, container, direction: 'horizontal' });
|
|
667
|
-
await nextTick();
|
|
668
|
-
resH.scrollToIndex(null, 10, { behavior: 'auto' });
|
|
669
|
-
await nextTick();
|
|
670
|
-
expect(container.scrollLeft).toBeGreaterThan(0);
|
|
671
|
-
});
|
|
123
|
+
await nextTick();
|
|
124
|
+
await nextTick();
|
|
672
125
|
|
|
673
|
-
|
|
674
|
-
|
|
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);
|
|
682
|
-
});
|
|
126
|
+
expect(window.scrollTo).toHaveBeenCalled();
|
|
127
|
+
expect(result.scrollDetails.value.currentIndex).toBe(20);
|
|
683
128
|
});
|
|
684
129
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
await nextTick();
|
|
692
|
-
});
|
|
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));
|
|
130
|
+
it('handles dynamic item sizes', async () => {
|
|
131
|
+
const { result } = setup({
|
|
132
|
+
container: window,
|
|
133
|
+
direction: 'vertical',
|
|
134
|
+
itemSize: 0, // dynamic
|
|
135
|
+
items: mockItems,
|
|
742
136
|
});
|
|
743
137
|
|
|
744
|
-
|
|
745
|
-
|
|
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
|
-
});
|
|
138
|
+
await nextTick();
|
|
139
|
+
await nextTick();
|
|
759
140
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
document.dispatchEvent(new Event('scroll'));
|
|
763
|
-
await nextTick();
|
|
764
|
-
});
|
|
141
|
+
// Initial estimate 100 * 40 = 4000
|
|
142
|
+
expect(result.totalHeight.value).toBe(4000);
|
|
765
143
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
setup({ ...defaultProps, container });
|
|
769
|
-
Object.defineProperty(container, 'scrollTop', { value: 100 });
|
|
770
|
-
container.dispatchEvent(new Event('scroll'));
|
|
771
|
-
await nextTick();
|
|
772
|
-
});
|
|
144
|
+
result.updateItemSize(0, 100, 100);
|
|
145
|
+
await nextTick();
|
|
773
146
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
Object.defineProperty(container, 'clientWidth', { value: 500, writable: true });
|
|
777
|
-
Object.defineProperty(container, 'clientHeight', { value: 500, writable: true });
|
|
778
|
-
const { result } = setup({ ...defaultProps, container });
|
|
779
|
-
await nextTick();
|
|
780
|
-
expect(result.scrollDetails.value.viewportSize.width).toBe(500);
|
|
781
|
-
|
|
782
|
-
Object.defineProperty(container, 'clientWidth', { value: 800 });
|
|
783
|
-
const observer = (globalThis.ResizeObserver as unknown as { instances: ResizeObserverMock[]; }).instances.find((i) => i.targets.has(container));
|
|
784
|
-
if (observer) {
|
|
785
|
-
observer.trigger([ { target: container } ]);
|
|
786
|
-
}
|
|
787
|
-
await nextTick();
|
|
788
|
-
expect(result.scrollDetails.value.viewportSize.width).toBe(800);
|
|
789
|
-
});
|
|
790
|
-
|
|
791
|
-
it('should handle isScrolling timeout', async () => {
|
|
792
|
-
vi.useFakeTimers();
|
|
793
|
-
const container = document.createElement('div');
|
|
794
|
-
const { result } = setup({ ...defaultProps, container });
|
|
795
|
-
container.dispatchEvent(new Event('scroll'));
|
|
796
|
-
await nextTick();
|
|
797
|
-
expect(result.scrollDetails.value.isScrolling).toBe(true);
|
|
798
|
-
vi.advanceTimersByTime(250);
|
|
799
|
-
await nextTick();
|
|
800
|
-
expect(result.scrollDetails.value.isScrolling).toBe(false);
|
|
801
|
-
vi.useRealTimers();
|
|
802
|
-
});
|
|
803
|
-
|
|
804
|
-
it('should handle container change in mount watcher', async () => {
|
|
805
|
-
const { props } = setup({ ...defaultProps });
|
|
806
|
-
await nextTick();
|
|
807
|
-
props.value.container = null;
|
|
808
|
-
await nextTick();
|
|
809
|
-
props.value.container = window;
|
|
810
|
-
await nextTick();
|
|
811
|
-
});
|
|
812
|
-
|
|
813
|
-
it('should handle window resize events', async () => {
|
|
814
|
-
setup({ ...defaultProps, container: window });
|
|
815
|
-
Object.defineProperty(window, 'innerWidth', { configurable: true, value: 1200 });
|
|
816
|
-
window.dispatchEvent(new Event('resize'));
|
|
817
|
-
await nextTick();
|
|
818
|
-
});
|
|
819
|
-
|
|
820
|
-
it('should cover handleScroll with document target', async () => {
|
|
821
|
-
setup({ ...defaultProps, container: window });
|
|
822
|
-
document.dispatchEvent(new Event('scroll'));
|
|
823
|
-
await nextTick();
|
|
824
|
-
});
|
|
825
|
-
|
|
826
|
-
it('should handle undefined window in handleScroll', async () => {
|
|
827
|
-
const originalWindow = globalThis.window;
|
|
828
|
-
const container = document.createElement('div');
|
|
829
|
-
setup({ ...defaultProps, container });
|
|
830
|
-
|
|
831
|
-
try {
|
|
832
|
-
(globalThis as unknown as { window: unknown; }).window = undefined;
|
|
833
|
-
container.dispatchEvent(new Event('scroll'));
|
|
834
|
-
await nextTick();
|
|
835
|
-
} finally {
|
|
836
|
-
globalThis.window = originalWindow;
|
|
837
|
-
}
|
|
838
|
-
});
|
|
147
|
+
// Now 1*100 + 99*40 = 100 + 3960 = 4060
|
|
148
|
+
expect(result.totalHeight.value).toBe(4060);
|
|
839
149
|
});
|
|
840
150
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
expect(result.getColumnWidth(0)).toBe(100);
|
|
850
|
-
expect(result.getColumnWidth(1)).toBe(200);
|
|
851
|
-
expect(result.totalWidth.value).toBe(600);
|
|
151
|
+
it('restores scroll position when items are prepended', async () => {
|
|
152
|
+
const items = Array.from({ length: 20 }, (_, i) => ({ id: i }));
|
|
153
|
+
const { props, result } = setup({
|
|
154
|
+
container: window,
|
|
155
|
+
direction: 'vertical',
|
|
156
|
+
itemSize: 50,
|
|
157
|
+
items,
|
|
158
|
+
restoreScrollOnPrepend: true,
|
|
852
159
|
});
|
|
853
160
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
...defaultProps,
|
|
857
|
-
direction: 'both',
|
|
858
|
-
columnCount: 2,
|
|
859
|
-
columnWidth: [ 0 ] as unknown as number[],
|
|
860
|
-
});
|
|
861
|
-
expect(result.getColumnWidth(0)).toBe(150); // DEFAULT_COLUMN_WIDTH
|
|
862
|
-
});
|
|
161
|
+
await nextTick();
|
|
162
|
+
await nextTick();
|
|
863
163
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
columnCount: 10,
|
|
869
|
-
columnWidth: (index: number) => (index % 2 === 0 ? 100 : 200),
|
|
870
|
-
});
|
|
871
|
-
expect(result.getColumnWidth(0)).toBe(100);
|
|
872
|
-
expect(result.totalWidth.value).toBe(1500);
|
|
873
|
-
});
|
|
164
|
+
// Scroll to index 5 (250px)
|
|
165
|
+
result.scrollToOffset(0, 250, { behavior: 'auto' });
|
|
166
|
+
await nextTick();
|
|
167
|
+
await nextTick();
|
|
874
168
|
|
|
875
|
-
|
|
876
|
-
const { result } = setup({
|
|
877
|
-
...defaultProps,
|
|
878
|
-
direction: 'both',
|
|
879
|
-
columnCount: 2,
|
|
880
|
-
columnWidth: undefined,
|
|
881
|
-
});
|
|
882
|
-
expect(result.getColumnWidth(0)).toBe(150);
|
|
883
|
-
});
|
|
169
|
+
expect(window.scrollY).toBe(250);
|
|
884
170
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
const { result } = setup({
|
|
888
|
-
...defaultProps,
|
|
889
|
-
direction: 'both',
|
|
890
|
-
columnCount: 50,
|
|
891
|
-
columnWidth: undefined,
|
|
892
|
-
container,
|
|
893
|
-
});
|
|
894
|
-
// Initialize some column widths
|
|
895
|
-
for (let i = 0; i < 20; i++) {
|
|
896
|
-
const parent = document.createElement('div');
|
|
897
|
-
const child = document.createElement('div');
|
|
898
|
-
Object.defineProperty(child, 'offsetWidth', { value: 100 });
|
|
899
|
-
child.dataset.colIndex = String(i);
|
|
900
|
-
parent.appendChild(child);
|
|
901
|
-
result.updateItemSize(0, 100, 50, parent);
|
|
902
|
-
}
|
|
903
|
-
await nextTick();
|
|
904
|
-
expect(result.columnRange.value.end).toBeGreaterThan(result.columnRange.value.start);
|
|
905
|
-
});
|
|
171
|
+
// Prepend 2 items (100px)
|
|
172
|
+
props.value.items = [ { id: -1 }, { id: -2 }, ...items ];
|
|
906
173
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
expect(result.columnRange.value.end).toBe(0);
|
|
911
|
-
});
|
|
174
|
+
await nextTick();
|
|
175
|
+
await nextTick();
|
|
176
|
+
await nextTick();
|
|
912
177
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
await nextTick();
|
|
916
|
-
expect(result.columnRange.value.start).toBe(0);
|
|
917
|
-
});
|
|
178
|
+
// Scroll should be adjusted to 350
|
|
179
|
+
expect(window.scrollY).toBe(350);
|
|
918
180
|
});
|
|
919
181
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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);
|
|
182
|
+
it('triggers correction when viewport dimensions change', async () => {
|
|
183
|
+
const { result } = setup({
|
|
184
|
+
container: window,
|
|
185
|
+
direction: 'vertical',
|
|
186
|
+
itemSize: 50,
|
|
187
|
+
items: mockItems,
|
|
930
188
|
});
|
|
931
189
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
await nextTick();
|
|
935
|
-
|
|
936
|
-
result.scrollToOffset(0, 100);
|
|
937
|
-
await nextTick();
|
|
938
|
-
|
|
939
|
-
const item0 = result.renderedItems.value.find((i) => i.index === 0);
|
|
940
|
-
expect(item0?.isStickyActive).toBe(true);
|
|
941
|
-
});
|
|
942
|
-
|
|
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 });
|
|
945
|
-
await nextTick();
|
|
946
|
-
|
|
947
|
-
// Scroll to index 20. Range starts at 20.
|
|
948
|
-
result.scrollToIndex(20, 0, { align: 'start', behavior: 'auto' });
|
|
949
|
-
await nextTick();
|
|
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);
|
|
955
|
-
});
|
|
956
|
-
|
|
957
|
-
it('should push sticky item when next sticky item approaches (vertical)', async () => {
|
|
958
|
-
const container = document.createElement('div');
|
|
959
|
-
Object.defineProperty(container, 'clientHeight', { value: 500 });
|
|
960
|
-
Object.defineProperty(container, 'scrollTop', { value: 480, writable: true });
|
|
961
|
-
const { result } = setup({ ...defaultProps, container, stickyIndices: [ 0, 10 ], itemSize: 50 });
|
|
962
|
-
// We need to trigger scroll to update scrollY
|
|
963
|
-
container.dispatchEvent(new Event('scroll'));
|
|
964
|
-
await nextTick();
|
|
190
|
+
await nextTick();
|
|
191
|
+
await nextTick();
|
|
965
192
|
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
193
|
+
// Scroll to item 50 auto
|
|
194
|
+
result.scrollToIndex(50, null, { align: 'auto', behavior: 'auto' });
|
|
195
|
+
await nextTick();
|
|
969
196
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
const { result } = setup({
|
|
976
|
-
...defaultProps,
|
|
977
|
-
direction: 'horizontal',
|
|
978
|
-
container,
|
|
979
|
-
stickyIndices: [ 0, 10 ],
|
|
980
|
-
itemSize: 50,
|
|
981
|
-
columnGap: 0,
|
|
982
|
-
});
|
|
983
|
-
container.dispatchEvent(new Event('scroll'));
|
|
984
|
-
await nextTick();
|
|
197
|
+
const initialScrollY = window.scrollY;
|
|
198
|
+
// item 50 at 2500. viewport 500. item 50 high.
|
|
199
|
+
// targetEnd = 2500 - (500 - 50) = 2050.
|
|
200
|
+
expect(initialScrollY).toBe(2050);
|
|
985
201
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
202
|
+
// Simulate viewport height decreasing
|
|
203
|
+
Object.defineProperty(document.documentElement, 'clientHeight', { configurable: true, value: 485 });
|
|
204
|
+
window.dispatchEvent(new Event('resize'));
|
|
989
205
|
|
|
990
|
-
|
|
991
|
-
|
|
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
|
-
});
|
|
206
|
+
await nextTick();
|
|
207
|
+
await nextTick();
|
|
1012
208
|
|
|
1013
|
-
|
|
1014
|
-
|
|
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
|
-
});
|
|
209
|
+
// It should have corrected to: 2500 - (485 - 50) = 2500 - 435 = 2065.
|
|
210
|
+
expect(window.scrollY).toBe(2065);
|
|
1033
211
|
});
|
|
1034
212
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
});
|
|
1044
|
-
|
|
1045
|
-
const items = Array.from({ length: 50 }, (_, i) => ({ id: i }));
|
|
1046
|
-
const { result, props } = setup({
|
|
1047
|
-
...defaultProps,
|
|
1048
|
-
items,
|
|
1049
|
-
container,
|
|
1050
|
-
itemSize: 50,
|
|
1051
|
-
restoreScrollOnPrepend: true,
|
|
1052
|
-
});
|
|
1053
|
-
container.dispatchEvent(new Event('scroll'));
|
|
1054
|
-
await nextTick();
|
|
1055
|
-
|
|
1056
|
-
expect(result.scrollDetails.value.scrollOffset.y).toBe(100);
|
|
1057
|
-
|
|
1058
|
-
// Prepend 2 items
|
|
1059
|
-
const newItems = [ { id: -1 }, { id: -2 }, ...items ];
|
|
1060
|
-
props.value.items = newItems;
|
|
1061
|
-
await nextTick();
|
|
1062
|
-
// Trigger initializeSizes
|
|
1063
|
-
await nextTick();
|
|
1064
|
-
|
|
1065
|
-
// Should have adjusted scroll by 2 * 50 = 100px. New scrollTop should be 200.
|
|
1066
|
-
expect(container.scrollTop).toBe(200);
|
|
1067
|
-
vi.useRealTimers();
|
|
1068
|
-
});
|
|
1069
|
-
|
|
1070
|
-
it('should restore scroll position when items are prepended (horizontal)', async () => {
|
|
1071
|
-
vi.useFakeTimers();
|
|
1072
|
-
const container = document.createElement('div');
|
|
1073
|
-
Object.defineProperty(container, 'clientWidth', { value: 500 });
|
|
1074
|
-
Object.defineProperty(container, 'scrollLeft', { value: 100, writable: true });
|
|
1075
|
-
container.scrollTo = vi.fn().mockImplementation((options) => {
|
|
213
|
+
it('renders sticky indices correctly using optimized search', async () => {
|
|
214
|
+
// Use an isolated container to avoid window pollution
|
|
215
|
+
const container = document.createElement('div');
|
|
216
|
+
Object.defineProperty(container, 'clientHeight', { configurable: true, value: 200 });
|
|
217
|
+
Object.defineProperty(container, 'clientWidth', { configurable: true, value: 500 });
|
|
218
|
+
// Mock scrollTo on container
|
|
219
|
+
container.scrollTo = vi.fn().mockImplementation((options: ScrollToOptions) => {
|
|
220
|
+
if (options.left !== undefined) {
|
|
1076
221
|
container.scrollLeft = options.left;
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
const items = Array.from({ length: 50 }, (_, i) => ({ id: i }));
|
|
1080
|
-
const { result, props } = setup({
|
|
1081
|
-
...defaultProps,
|
|
1082
|
-
direction: 'horizontal',
|
|
1083
|
-
items,
|
|
1084
|
-
container,
|
|
1085
|
-
itemSize: 50,
|
|
1086
|
-
restoreScrollOnPrepend: true,
|
|
1087
|
-
});
|
|
1088
|
-
container.dispatchEvent(new Event('scroll'));
|
|
1089
|
-
await nextTick();
|
|
1090
|
-
|
|
1091
|
-
expect(result.scrollDetails.value.scrollOffset.x).toBe(100);
|
|
1092
|
-
|
|
1093
|
-
// Prepend 2 items
|
|
1094
|
-
const newItems = [ { id: -1 }, { id: -2 }, ...items ];
|
|
1095
|
-
props.value.items = newItems;
|
|
1096
|
-
await nextTick();
|
|
1097
|
-
await nextTick();
|
|
1098
|
-
|
|
1099
|
-
expect(container.scrollLeft).toBe(200);
|
|
1100
|
-
vi.useRealTimers();
|
|
1101
|
-
});
|
|
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) => {
|
|
222
|
+
}
|
|
223
|
+
if (options.top !== undefined) {
|
|
1108
224
|
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
|
-
|
|
1132
|
-
it('should NOT restore scroll position when restoreScrollOnPrepend is false', async () => {
|
|
1133
|
-
const container = document.createElement('div');
|
|
1134
|
-
Object.defineProperty(container, 'scrollTop', { value: 100, writable: true });
|
|
1135
|
-
const items = Array.from({ length: 50 }, (_, i) => ({ id: i }));
|
|
1136
|
-
const { props } = setup({ ...defaultProps, items, container, restoreScrollOnPrepend: false });
|
|
1137
|
-
await nextTick();
|
|
1138
|
-
|
|
1139
|
-
const newItems = [ { id: -1 }, ...items ];
|
|
1140
|
-
props.value.items = newItems;
|
|
1141
|
-
await nextTick();
|
|
1142
|
-
await nextTick();
|
|
1143
|
-
expect(container.scrollTop).toBe(100);
|
|
1144
|
-
});
|
|
1145
|
-
|
|
1146
|
-
it('should NOT restore scroll position when first item does not match', async () => {
|
|
1147
|
-
const container = document.createElement('div');
|
|
1148
|
-
Object.defineProperty(container, 'scrollTop', { value: 100, writable: true });
|
|
1149
|
-
const items = Array.from({ length: 50 }, (_, i) => ({ id: i }));
|
|
1150
|
-
const { props } = setup({ ...defaultProps, items, container, restoreScrollOnPrepend: true });
|
|
1151
|
-
await nextTick();
|
|
1152
|
-
|
|
1153
|
-
const newItems = [ { id: -1 }, { id: 9999 } ]; // completely different
|
|
1154
|
-
props.value.items = newItems;
|
|
1155
|
-
await nextTick();
|
|
1156
|
-
await nextTick();
|
|
1157
|
-
expect(container.scrollTop).toBe(100);
|
|
225
|
+
}
|
|
226
|
+
container.dispatchEvent(new Event('scroll'));
|
|
1158
227
|
});
|
|
1159
228
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
229
|
+
const { result } = setup({
|
|
230
|
+
container,
|
|
231
|
+
direction: 'vertical',
|
|
232
|
+
itemSize: 50,
|
|
233
|
+
items: Array.from({ length: 20 }, (_, i) => ({ id: i })),
|
|
234
|
+
stickyIndices: [ 0, 10, 19 ],
|
|
235
|
+
bufferBefore: 0,
|
|
236
|
+
bufferAfter: 0,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
await nextTick();
|
|
240
|
+
await nextTick();
|
|
241
|
+
|
|
242
|
+
// 1. Initial scroll 0. Range [0, 4].
|
|
243
|
+
expect(result.renderedItems.value.map((i) => i.index)).toEqual([ 0, 1, 2, 3 ]);
|
|
244
|
+
|
|
245
|
+
// 2. Scroll to 100 (item 2). Range [2, 6].
|
|
246
|
+
container.scrollTop = 100;
|
|
247
|
+
container.dispatchEvent(new Event('scroll'));
|
|
248
|
+
await nextTick();
|
|
249
|
+
await nextTick();
|
|
250
|
+
|
|
251
|
+
const indices2 = result.renderedItems.value.map((i) => i.index).sort((a, b) => a - b);
|
|
252
|
+
expect(indices2).toEqual([ 0, 2, 3, 4, 5 ]);
|
|
253
|
+
expect(result.renderedItems.value.find((i) => i.index === 0)?.isStickyActive).toBe(true);
|
|
254
|
+
|
|
255
|
+
// 3. Scroll to 500 (item 10). Range [10, 14].
|
|
256
|
+
container.scrollTop = 500;
|
|
257
|
+
container.dispatchEvent(new Event('scroll'));
|
|
258
|
+
await nextTick();
|
|
259
|
+
await nextTick();
|
|
260
|
+
|
|
261
|
+
const indices3 = result.renderedItems.value.map((i) => i.index).sort((a, b) => a - b);
|
|
262
|
+
expect(indices3).toContain(0);
|
|
263
|
+
expect(indices3).toContain(10);
|
|
264
|
+
expect(indices3).toContain(11);
|
|
265
|
+
expect(indices3).toContain(12);
|
|
266
|
+
expect(indices3).toContain(13);
|
|
1171
267
|
});
|
|
1172
268
|
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
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();
|
|
269
|
+
it('updates item sizes and compensates scroll position', async () => {
|
|
270
|
+
const { result } = setup({
|
|
271
|
+
container: window,
|
|
272
|
+
direction: 'vertical',
|
|
273
|
+
itemSize: 0,
|
|
274
|
+
items: mockItems,
|
|
1185
275
|
});
|
|
1186
276
|
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
await nextTick();
|
|
1190
|
-
result.scrollToIndex(10, 0, 'start');
|
|
1191
|
-
// Trigger tree update
|
|
1192
|
-
result.updateItemSize(5, 100, 100);
|
|
1193
|
-
await nextTick();
|
|
1194
|
-
});
|
|
277
|
+
await nextTick();
|
|
278
|
+
await nextTick();
|
|
1195
279
|
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
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);
|
|
1254
|
-
await nextTick();
|
|
1255
|
-
});
|
|
1256
|
-
|
|
1257
|
-
it('should trigger scroll correction on tree update with pending scroll', async () => {
|
|
1258
|
-
const container = document.createElement('div');
|
|
1259
|
-
Object.defineProperty(container, 'clientHeight', { value: 500, configurable: true });
|
|
1260
|
-
Object.defineProperty(container, 'scrollHeight', { value: 5000, configurable: true });
|
|
1261
|
-
const { result } = setup({ ...defaultProps, container, itemSize: undefined });
|
|
1262
|
-
// Set a pending scroll
|
|
1263
|
-
result.scrollToIndex(10, null, { behavior: 'smooth' });
|
|
1264
|
-
|
|
1265
|
-
// Trigger tree update
|
|
1266
|
-
result.updateItemSize(0, 100, 100);
|
|
1267
|
-
await nextTick();
|
|
1268
|
-
});
|
|
280
|
+
// Scroll to item 10 (10 * 40 = 400px)
|
|
281
|
+
Object.defineProperty(window, 'scrollY', { configurable: true, value: 400, writable: true });
|
|
282
|
+
document.dispatchEvent(new Event('scroll'));
|
|
283
|
+
await nextTick();
|
|
1269
284
|
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
Object.defineProperty(container, 'clientHeight', { value: 500, configurable: true });
|
|
1274
|
-
Object.defineProperty(container, 'scrollHeight', { value: 5000, configurable: true });
|
|
1275
|
-
const { result } = setup({ ...defaultProps, container, itemSize: undefined });
|
|
1276
|
-
result.scrollToIndex(10, null, { behavior: 'smooth' });
|
|
285
|
+
// Update item 0 (above viewport) from 40 to 100
|
|
286
|
+
result.updateItemSize(0, 100, 100);
|
|
287
|
+
await nextTick();
|
|
1277
288
|
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
await nextTick();
|
|
1281
|
-
expect(result.scrollDetails.value.isScrolling).toBe(true);
|
|
1282
|
-
|
|
1283
|
-
// Wait for scroll timeout
|
|
1284
|
-
vi.advanceTimersByTime(250);
|
|
1285
|
-
await nextTick();
|
|
1286
|
-
expect(result.scrollDetails.value.isScrolling).toBe(false);
|
|
1287
|
-
vi.useRealTimers();
|
|
1288
|
-
});
|
|
289
|
+
// Scroll position should have been adjusted by 60px
|
|
290
|
+
expect(window.scrollY).toBe(460);
|
|
1289
291
|
});
|
|
1290
292
|
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
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
|
-
|
|
1379
|
-
it('should handle SSR range in range calculation', () => {
|
|
1380
|
-
const props = ref({
|
|
1381
|
-
items: mockItems,
|
|
1382
|
-
ssrRange: { start: 0, end: 10 },
|
|
1383
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1384
|
-
const result = useVirtualScroll(props);
|
|
1385
|
-
expect(result.renderedItems.value.length).toBe(10);
|
|
1386
|
-
});
|
|
1387
|
-
|
|
1388
|
-
it('should handle SSR range in columnRange calculation', () => {
|
|
1389
|
-
const props = ref({
|
|
1390
|
-
items: mockItems,
|
|
1391
|
-
columnCount: 10,
|
|
1392
|
-
ssrRange: { start: 0, end: 10, colStart: 0, colEnd: 5 },
|
|
1393
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1394
|
-
const result = useVirtualScroll(props);
|
|
1395
|
-
expect(result.columnRange.value.end).toBe(5);
|
|
1396
|
-
});
|
|
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
|
-
|
|
1409
|
-
it('should handle SSR range with both directions for total sizes', () => {
|
|
1410
|
-
const props = ref({
|
|
1411
|
-
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1412
|
-
direction: 'both',
|
|
1413
|
-
columnCount: 10,
|
|
1414
|
-
columnWidth: 100,
|
|
1415
|
-
itemSize: 50,
|
|
1416
|
-
ssrRange: { start: 10, end: 20, colStart: 2, colEnd: 5 },
|
|
1417
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1418
|
-
const result = useVirtualScroll(props);
|
|
1419
|
-
expect(result.totalWidth.value).toBe(300); // (5-2) * 100
|
|
1420
|
-
expect(result.totalHeight.value).toBe(500); // (20-10) * 50
|
|
1421
|
-
});
|
|
1422
|
-
|
|
1423
|
-
it('should handle SSR range with horizontal direction for total sizes', () => {
|
|
1424
|
-
const props = ref({
|
|
1425
|
-
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1426
|
-
direction: 'horizontal',
|
|
1427
|
-
itemSize: 50,
|
|
1428
|
-
ssrRange: { start: 10, end: 20 },
|
|
1429
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1430
|
-
const result = useVirtualScroll(props);
|
|
1431
|
-
expect(result.totalWidth.value).toBe(500); // (20-10) * 50
|
|
1432
|
-
});
|
|
1433
|
-
|
|
1434
|
-
it('should handle SSR range with vertical offset in renderedItems', () => {
|
|
1435
|
-
const props = ref({
|
|
1436
|
-
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1437
|
-
direction: 'vertical',
|
|
1438
|
-
itemSize: 50,
|
|
1439
|
-
ssrRange: { start: 10, end: 20 },
|
|
1440
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1441
|
-
const result = useVirtualScroll(props);
|
|
1442
|
-
expect(result.renderedItems.value[ 0 ]?.offset.y).toBe(0);
|
|
1443
|
-
});
|
|
1444
|
-
|
|
1445
|
-
it('should handle SSR range with dynamic horizontal offsets in renderedItems', () => {
|
|
1446
|
-
const props = ref({
|
|
1447
|
-
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1448
|
-
direction: 'horizontal',
|
|
1449
|
-
itemSize: undefined, // dynamic
|
|
1450
|
-
ssrRange: { start: 10, end: 20 },
|
|
1451
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1452
|
-
const result = useVirtualScroll(props);
|
|
1453
|
-
// ssrOffsetX = itemSizesX.query(10) = 10 * 50 = 500
|
|
1454
|
-
expect(result.renderedItems.value[ 0 ]?.offset.x).toBe(500);
|
|
1455
|
-
});
|
|
1456
|
-
|
|
1457
|
-
it('should handle SSR range with dynamic sizes for total sizes', () => {
|
|
1458
|
-
const props = ref({
|
|
1459
|
-
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1460
|
-
direction: 'vertical',
|
|
1461
|
-
itemSize: 0,
|
|
1462
|
-
ssrRange: { start: 10, end: 20 },
|
|
1463
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1464
|
-
const result = useVirtualScroll(props);
|
|
1465
|
-
expect(result.totalHeight.value).toBe(500);
|
|
293
|
+
it('supports refresh method', async () => {
|
|
294
|
+
const { result } = setup({
|
|
295
|
+
container: window,
|
|
296
|
+
direction: 'vertical',
|
|
297
|
+
itemSize: 50,
|
|
298
|
+
items: mockItems,
|
|
1466
299
|
});
|
|
1467
300
|
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
ssrRange: { start: 10, end: 20 },
|
|
1474
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1475
|
-
const result = useVirtualScroll(props);
|
|
1476
|
-
expect(result.totalWidth.value).toBe(500);
|
|
1477
|
-
});
|
|
301
|
+
await nextTick();
|
|
302
|
+
result.refresh();
|
|
303
|
+
await nextTick();
|
|
304
|
+
expect(result.totalHeight.value).toBe(5000);
|
|
305
|
+
});
|
|
1478
306
|
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
ssrRange: { start: 10, end: 20, colStart: 2, colEnd: 5 },
|
|
1486
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1487
|
-
const result = useVirtualScroll(props);
|
|
1488
|
-
expect(result.renderedItems.value[ 0 ]?.offset.y).toBe(0);
|
|
1489
|
-
expect(result.renderedItems.value[ 0 ]?.offset.x).toBe(-300);
|
|
307
|
+
it('supports getColumnWidth with various types', async () => {
|
|
308
|
+
const { result } = setup({
|
|
309
|
+
columnCount: 10,
|
|
310
|
+
columnWidth: [ 100, 200 ],
|
|
311
|
+
direction: 'both',
|
|
312
|
+
items: mockItems,
|
|
1490
313
|
});
|
|
1491
314
|
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
315
|
+
await nextTick();
|
|
316
|
+
expect(result.getColumnWidth(0)).toBe(100);
|
|
317
|
+
expect(result.getColumnWidth(1)).toBe(200);
|
|
318
|
+
expect(result.getColumnWidth(2)).toBe(100);
|
|
319
|
+
});
|
|
1497
320
|
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1505
|
-
const result = useVirtualScroll(props);
|
|
1506
|
-
expect(result.renderedItems.value[ 0 ]?.offset.x).toBe(-250);
|
|
321
|
+
it('updates column sizes from row element', async () => {
|
|
322
|
+
const { result } = setup({
|
|
323
|
+
columnCount: 5,
|
|
324
|
+
columnWidth: 0, // dynamic
|
|
325
|
+
direction: 'both',
|
|
326
|
+
items: mockItems,
|
|
1507
327
|
});
|
|
1508
328
|
|
|
1509
|
-
|
|
1510
|
-
const props = ref({
|
|
1511
|
-
items: Array.from({ length: 100 }, (_, i) => ({ id: i })),
|
|
1512
|
-
direction: 'both',
|
|
1513
|
-
columnCount: 20,
|
|
1514
|
-
columnWidth: 100,
|
|
1515
|
-
ssrRange: { start: 0, end: 10, colStart: 5, colEnd: 15 },
|
|
1516
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1517
|
-
const result = useVirtualScroll(props);
|
|
1518
|
-
// ssrOffsetX = columnSizes.query(5) = 5 * 100 = 500
|
|
1519
|
-
expect(result.renderedItems.value[ 0 ]?.offset.x).toBe(-500);
|
|
1520
|
-
});
|
|
329
|
+
await nextTick();
|
|
1521
330
|
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
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);
|
|
331
|
+
const rowEl = document.createElement('div');
|
|
332
|
+
const cell0 = document.createElement('div');
|
|
333
|
+
cell0.dataset.colIndex = '0';
|
|
334
|
+
Object.defineProperty(cell0, 'getBoundingClientRect', {
|
|
335
|
+
value: () => ({ width: 150 }),
|
|
1534
336
|
});
|
|
337
|
+
rowEl.appendChild(cell0);
|
|
1535
338
|
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
ssrRange: { start: 0, end: 10, colStart: 0, colEnd: 5 },
|
|
1543
|
-
}) as Ref<VirtualScrollProps<unknown>>;
|
|
1544
|
-
const result = useVirtualScroll(props);
|
|
1545
|
-
expect(result.totalWidth.value).toBe(500);
|
|
1546
|
-
});
|
|
1547
|
-
});
|
|
339
|
+
result.updateItemSizes([ {
|
|
340
|
+
blockSize: 100,
|
|
341
|
+
element: rowEl,
|
|
342
|
+
index: 0,
|
|
343
|
+
inlineSize: 0,
|
|
344
|
+
} ]);
|
|
1548
345
|
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
expect(getPaddingX({ x: 10 }, 'horizontal')).toBe(10);
|
|
1552
|
-
expect(getPaddingY({ y: 20 }, 'vertical')).toBe(20);
|
|
1553
|
-
});
|
|
346
|
+
await nextTick();
|
|
347
|
+
expect(result.getColumnWidth(0)).toBe(150);
|
|
1554
348
|
});
|
|
1555
349
|
});
|