one
Version:
One is a new React Framework that makes Vite serve both native and web.
121 lines (96 loc) • 2.91 kB
text/typescript
import { describe, expect, it, beforeEach, vi } from 'vitest'
import { createPrefetchViewport } from './prefetchViewport'
// mock IntersectionObserver
let mockIOCallback: IntersectionObserverCallback
const mockObserved = new Set<Element>()
vi.stubGlobal(
'IntersectionObserver',
class {
constructor(callback: IntersectionObserverCallback) {
mockIOCallback = callback
}
observe(el: Element) {
mockObserved.add(el)
}
unobserve(el: Element) {
mockObserved.delete(el)
}
disconnect() {
mockObserved.clear()
}
}
)
function simulateIntersect(el: Element, isIntersecting: boolean) {
mockIOCallback(
[{ target: el, isIntersecting } as IntersectionObserverEntry],
{} as IntersectionObserver
)
}
describe('prefetchViewport', () => {
let prefetched: string[]
let vp: ReturnType<typeof createPrefetchViewport>
beforeEach(() => {
prefetched = []
mockObserved.clear()
vp = createPrefetchViewport()
vp.start((href) => prefetched.push(href))
})
it('prefetches when link enters viewport', () => {
const el = {} as HTMLElement
vp.observe(el, '/about')
simulateIntersect(el, true)
expect(prefetched).toEqual(['/about'])
})
it('does not prefetch when link exits viewport', () => {
const el = {} as HTMLElement
vp.observe(el, '/about')
simulateIntersect(el, false)
expect(prefetched).toEqual([])
})
it('only prefetches each href once', () => {
const el = {} as HTMLElement
vp.observe(el, '/about')
simulateIntersect(el, true)
simulateIntersect(el, false)
simulateIntersect(el, true)
expect(prefetched).toEqual(['/about'])
})
it('cleanup re-enables prefetch for href', () => {
const el = {} as HTMLElement
const cleanup = vp.observe(el, '/about')
simulateIntersect(el, true)
expect(prefetched).toEqual(['/about'])
cleanup()
prefetched.length = 0
// re-observe same href after cleanup
vp.observe(el, '/about')
simulateIntersect(el, true)
expect(prefetched).toEqual(['/about'])
})
describe('memory and performance', () => {
it('does not leak elements after cleanup', () => {
const elements: HTMLElement[] = []
const cleanups: (() => void)[] = []
// add 100 elements
for (let i = 0; i < 100; i++) {
const el = {} as HTMLElement
elements.push(el)
cleanups.push(vp.observe(el, `/page-${i}`))
}
expect(vp.nodes.size).toBe(100)
// cleanup all
cleanups.forEach((c) => c())
expect(vp.nodes.size).toBe(0)
expect(vp.done.size).toBe(0)
})
it('handles rapid observe/unobserve cycles', () => {
const el = {} as HTMLElement
for (let i = 0; i < 100; i++) {
const cleanup = vp.observe(el, '/test')
cleanup()
}
// should not leak
expect(vp.nodes.size).toBe(0)
})
})
})