Skip to content

Commit 16c786a

Browse files
authored
test(virtual-core): split index.test.ts into topic files (#1328)
1 parent 77cb05e commit 16c786a

10 files changed

Lines changed: 6675 additions & 6619 deletions

‎packages/virtual-core/tests/adjustment.test.ts‎

Lines changed: 515 additions & 0 deletions
Large diffs are not rendered by default.

‎packages/virtual-core/tests/anchor-end.test.ts‎

Lines changed: 664 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
import { expect, test, vi } from 'vitest'
2+
import {
3+
Virtualizer,
4+
elementScroll,
5+
observeElementOffset,
6+
observeElementRect,
7+
observeWindowOffset,
8+
windowScroll,
9+
} from '../src/index'
10+
11+
// ─── elementScroll / windowScroll public exports ─────────────────────────────
12+
13+
function makeBaseInstance(scrollEl: any, opts: any = {}) {
14+
return {
15+
scrollElement: scrollEl,
16+
options: {
17+
horizontal: false,
18+
...opts,
19+
},
20+
} as any
21+
}
22+
23+
test('elementScroll: calls scrollTo with top + behavior on the scroll element', () => {
24+
const scrollTo = vi.fn()
25+
const scrollEl = { scrollTo }
26+
elementScroll(100, { behavior: 'smooth' }, makeBaseInstance(scrollEl) as any)
27+
expect(scrollTo).toHaveBeenCalledWith({ top: 100, behavior: 'smooth' })
28+
})
29+
30+
test('elementScroll: applies adjustments offset', () => {
31+
const scrollTo = vi.fn()
32+
const scrollEl = { scrollTo }
33+
elementScroll(
34+
100,
35+
{ adjustments: 50, behavior: 'auto' },
36+
makeBaseInstance(scrollEl) as any,
37+
)
38+
expect(scrollTo).toHaveBeenCalledWith({ top: 150, behavior: 'auto' })
39+
})
40+
41+
test('elementScroll: uses left when horizontal is true', () => {
42+
const scrollTo = vi.fn()
43+
const scrollEl = { scrollTo }
44+
elementScroll(
45+
100,
46+
{ behavior: 'auto' },
47+
makeBaseInstance(scrollEl, { horizontal: true }) as any,
48+
)
49+
expect(scrollTo).toHaveBeenCalledWith({ left: 100, behavior: 'auto' })
50+
})
51+
52+
test('windowScroll: calls scrollTo with top + behavior on the window', () => {
53+
const scrollTo = vi.fn()
54+
const win = { scrollTo }
55+
windowScroll(250, { behavior: 'smooth' }, makeBaseInstance(win) as any)
56+
expect(scrollTo).toHaveBeenCalledWith({ top: 250, behavior: 'smooth' })
57+
})
58+
59+
test('windowScroll: applies adjustments + horizontal', () => {
60+
const scrollTo = vi.fn()
61+
const win = { scrollTo }
62+
windowScroll(
63+
250,
64+
{ adjustments: -10, behavior: 'auto' },
65+
makeBaseInstance(win, { horizontal: true }) as any,
66+
)
67+
expect(scrollTo).toHaveBeenCalledWith({ left: 240, behavior: 'auto' })
68+
})
69+
70+
test('elementScroll / windowScroll: no-op when scrollElement is null', () => {
71+
expect(() =>
72+
elementScroll(100, {}, makeBaseInstance(null) as any),
73+
).not.toThrow()
74+
expect(() =>
75+
windowScroll(100, {}, makeBaseInstance(null) as any),
76+
).not.toThrow()
77+
})
78+
79+
// ─── observeElementOffset / observeWindowOffset ──────────────────────────────
80+
81+
function makeObserveInstance(
82+
element: any,
83+
opts: {
84+
horizontal?: boolean
85+
isRtl?: boolean
86+
useScrollendEvent?: boolean
87+
isScrollingResetDelay?: number
88+
} = {},
89+
targetWindow: any = {
90+
setTimeout: globalThis.setTimeout.bind(globalThis),
91+
clearTimeout: globalThis.clearTimeout.bind(globalThis),
92+
},
93+
) {
94+
return {
95+
scrollElement: element,
96+
targetWindow,
97+
options: {
98+
horizontal: false,
99+
isRtl: false,
100+
useScrollendEvent: false,
101+
isScrollingResetDelay: 150,
102+
...opts,
103+
},
104+
} as any
105+
}
106+
107+
test('observeElementOffset: returns undefined when scrollElement is null', () => {
108+
const cb = vi.fn()
109+
expect(
110+
observeElementOffset(makeObserveInstance(null) as any, cb),
111+
).toBeUndefined()
112+
expect(cb).not.toHaveBeenCalled()
113+
})
114+
115+
test('observeElementOffset: attaches scroll listener and fires callback with scrollTop', () => {
116+
const cb = vi.fn()
117+
const listeners = new Map<string, EventListener>()
118+
const el: any = {
119+
scrollTop: 50,
120+
scrollLeft: 0,
121+
addEventListener: (name: string, fn: any) => listeners.set(name, fn),
122+
removeEventListener: (name: string) => listeners.delete(name),
123+
}
124+
const cleanup = observeElementOffset(makeObserveInstance(el) as any, cb)
125+
expect(listeners.has('scroll')).toBe(true)
126+
// No scrollend listener by default
127+
expect(listeners.has('scrollend')).toBe(false)
128+
// Trigger scroll
129+
listeners.get('scroll')!({} as Event)
130+
expect(cb).toHaveBeenCalledWith(50, true)
131+
cleanup?.()
132+
expect(listeners.has('scroll')).toBe(false)
133+
})
134+
135+
test('observeElementOffset: fallback reads offset after programmatic changes', () => {
136+
vi.useFakeTimers()
137+
try {
138+
const cb = vi.fn()
139+
const listeners = new Map<string, EventListener>()
140+
const el: any = {
141+
scrollTop: 50,
142+
scrollLeft: 0,
143+
addEventListener: (name: string, fn: any) => listeners.set(name, fn),
144+
removeEventListener: (name: string) => listeners.delete(name),
145+
}
146+
observeElementOffset(makeObserveInstance(el) as any, cb)
147+
148+
listeners.get('scroll')!({} as Event)
149+
el.scrollTop = 125
150+
vi.advanceTimersByTime(150)
151+
152+
expect(cb).toHaveBeenLastCalledWith(125, false)
153+
} finally {
154+
vi.useRealTimers()
155+
}
156+
})
157+
158+
// ─── cleanup resets the scroll flags ─────────────────────────────────────────
159+
// The cancelled debounce is the only writer of `isScrolling = false`, and
160+
// `cleanup()` also runs while the instance stays alive (element swap,
161+
// `enabled: false`), so it has to reset the flags itself.
162+
163+
const makeScrollFlagsVirtualizer = () => {
164+
const MockResizeObserver = vi.fn(function () {
165+
return { observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn() }
166+
})
167+
const mockWindow = {
168+
requestAnimationFrame: vi.fn(),
169+
cancelAnimationFrame: vi.fn(),
170+
ResizeObserver: MockResizeObserver,
171+
}
172+
const makeElement = () =>
173+
({
174+
scrollTop: 0,
175+
scrollLeft: 0,
176+
scrollWidth: 1000,
177+
scrollHeight: 5000,
178+
offsetWidth: 400,
179+
offsetHeight: 600,
180+
ownerDocument: { defaultView: mockWindow },
181+
}) as unknown as HTMLDivElement
182+
183+
const first = makeElement()
184+
const second = makeElement()
185+
let element: HTMLDivElement | null = first
186+
let emit: ((offset: number, isScrolling: boolean) => void) | null = null
187+
188+
const virtualizer = new Virtualizer({
189+
count: 100,
190+
estimateSize: () => 50,
191+
getScrollElement: () => element,
192+
scrollToFn: vi.fn(),
193+
observeElementRect: (_instance, cb) => {
194+
cb({ width: 400, height: 600 })
195+
return () => {}
196+
},
197+
observeElementOffset: (_instance, cb) => {
198+
emit = cb
199+
return () => {}
200+
},
201+
})
202+
203+
virtualizer._willUpdate()
204+
205+
// Mid-scroll: this is the state the debounce used to clear on its own.
206+
emit!(500, true)
207+
208+
return {
209+
virtualizer,
210+
swapElement: () => {
211+
element = second
212+
virtualizer._willUpdate()
213+
},
214+
disable: () => {
215+
element = null
216+
virtualizer._willUpdate()
217+
},
218+
}
219+
}
220+
221+
test('cleanup resets the scroll flags when the scroll element is swapped', () => {
222+
const { virtualizer, swapElement } = makeScrollFlagsVirtualizer()
223+
224+
expect(virtualizer.isScrolling).toBe(true)
225+
226+
swapElement()
227+
228+
expect(virtualizer.isScrolling).toBe(false)
229+
expect(virtualizer.scrollDirection).toBe(null)
230+
})
231+
232+
test('cleanup resets the scroll flags when the scroll element goes away', () => {
233+
const { virtualizer, disable } = makeScrollFlagsVirtualizer()
234+
235+
expect(virtualizer.isScrolling).toBe(true)
236+
237+
disable()
238+
239+
expect(virtualizer.isScrolling).toBe(false)
240+
expect(virtualizer.scrollDirection).toBe(null)
241+
})
242+
243+
test('cleanup resets the scroll flags on unmount', () => {
244+
const { virtualizer } = makeScrollFlagsVirtualizer()
245+
246+
expect(virtualizer.isScrolling).toBe(true)
247+
248+
virtualizer._didMount()()
249+
250+
expect(virtualizer.isScrolling).toBe(false)
251+
expect(virtualizer.scrollDirection).toBe(null)
252+
})
253+
254+
test('observeElementOffset: cleanup drops the queued isScrolling reset', () => {
255+
vi.useFakeTimers()
256+
try {
257+
const cb = vi.fn()
258+
const listeners = new Map<string, EventListener>()
259+
const el: any = {
260+
scrollTop: 50,
261+
scrollLeft: 0,
262+
addEventListener: (name: string, fn: any) => listeners.set(name, fn),
263+
removeEventListener: (name: string) => listeners.delete(name),
264+
}
265+
const cleanup = observeElementOffset(makeObserveInstance(el) as any, cb)
266+
267+
// Each scroll arms a debounce that resets isScrolling to false.
268+
listeners.get('scroll')!({} as Event)
269+
expect(cb).toHaveBeenCalledWith(50, true)
270+
cb.mockClear()
271+
272+
// Tearing down inside that window must not leave the reset queued —
273+
// it would arrive after the consumer stopped listening.
274+
cleanup?.()
275+
vi.advanceTimersByTime(1000)
276+
277+
expect(cb).not.toHaveBeenCalled()
278+
} finally {
279+
vi.useRealTimers()
280+
}
281+
})
282+
283+
test('observeElementOffset: reads scrollLeft + applies isRtl when horizontal', () => {
284+
const cb = vi.fn()
285+
const listeners = new Map<string, EventListener>()
286+
const el: any = {
287+
scrollTop: 0,
288+
scrollLeft: 80,
289+
addEventListener: (name: string, fn: any) => listeners.set(name, fn),
290+
removeEventListener: (name: string) => listeners.delete(name),
291+
}
292+
observeElementOffset(
293+
makeObserveInstance(el, { horizontal: true, isRtl: true }) as any,
294+
cb,
295+
)
296+
listeners.get('scroll')!({} as Event)
297+
// isRtl flips sign
298+
expect(cb).toHaveBeenCalledWith(-80, true)
299+
})
300+
301+
test('observeWindowOffset: returns undefined when scrollElement is null', () => {
302+
const cb = vi.fn()
303+
expect(
304+
observeWindowOffset(makeObserveInstance(null) as any, cb),
305+
).toBeUndefined()
306+
})
307+
308+
test('observeWindowOffset: attaches scroll listener and fires callback with scrollY', () => {
309+
const cb = vi.fn()
310+
const listeners = new Map<string, EventListener>()
311+
const win: any = {
312+
scrollX: 0,
313+
scrollY: 120,
314+
addEventListener: (name: string, fn: any) => listeners.set(name, fn),
315+
removeEventListener: (name: string) => listeners.delete(name),
316+
}
317+
const cleanup = observeWindowOffset(makeObserveInstance(win) as any, cb)
318+
expect(listeners.has('scroll')).toBe(true)
319+
listeners.get('scroll')!({} as Event)
320+
expect(cb).toHaveBeenCalledWith(120, true)
321+
cleanup?.()
322+
expect(listeners.has('scroll')).toBe(false)
323+
})
324+
325+
test('observeWindowOffset: reads scrollX when horizontal', () => {
326+
const cb = vi.fn()
327+
const listeners = new Map<string, EventListener>()
328+
const win: any = {
329+
scrollX: 75,
330+
scrollY: 0,
331+
addEventListener: (name: string, fn: any) => listeners.set(name, fn),
332+
removeEventListener: (name: string) => listeners.delete(name),
333+
}
334+
observeWindowOffset(makeObserveInstance(win, { horizontal: true }) as any, cb)
335+
listeners.get('scroll')!({} as Event)
336+
expect(cb).toHaveBeenCalledWith(75, true)
337+
})

0 commit comments

Comments
 (0)