201 lines
6.1 KiB
TypeScript
201 lines
6.1 KiB
TypeScript
import { expect, test } from "bun:test"
|
|
import { type Virtualizer } from "@tanstack/solid-virtual"
|
|
import { mutationNodesContainElement, observeElementOffsetReconnectAware } from "./observe-element-offset"
|
|
|
|
test("matches only the scroll element or an ancestor containing it", () => {
|
|
const route = document.createElement("section")
|
|
const viewport = document.createElement("div")
|
|
const child = document.createElement("div")
|
|
const sibling = document.createElement("div")
|
|
route.append(viewport)
|
|
viewport.append(child)
|
|
|
|
expect(mutationNodesContainElement([viewport], viewport)).toBe(true)
|
|
expect(mutationNodesContainElement([route], viewport)).toBe(true)
|
|
expect(mutationNodesContainElement([child, sibling], viewport)).toBe(false)
|
|
})
|
|
|
|
test("reports a divergent native offset once and ignores equal offsets and unrelated mutations", async () => {
|
|
const route = document.createElement("section")
|
|
const viewport = document.createElement("div")
|
|
const unrelated = document.createElement("div")
|
|
route.append(viewport)
|
|
document.body.append(route)
|
|
const instance = {
|
|
scrollElement: viewport,
|
|
targetWindow: window,
|
|
scrollOffset: 79_400,
|
|
options: {
|
|
horizontal: false,
|
|
isRtl: false,
|
|
isScrollingResetDelay: 0,
|
|
useScrollendEvent: false,
|
|
},
|
|
} as unknown as Virtualizer<HTMLDivElement, HTMLDivElement>
|
|
const calls: [number, boolean][] = []
|
|
const cleanup = observeElementOffsetReconnectAware(instance, (offset, isScrolling) => {
|
|
calls.push([offset, isScrolling])
|
|
instance.scrollOffset = offset
|
|
})
|
|
|
|
route.remove()
|
|
document.body.append(route)
|
|
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
await frames(3)
|
|
expect(calls).toEqual([[0, false]])
|
|
|
|
instance.scrollOffset = 79_400
|
|
document.body.append(unrelated)
|
|
unrelated.remove()
|
|
await frames(2)
|
|
expect(calls).toEqual([[0, false]])
|
|
|
|
instance.scrollOffset = 0
|
|
route.remove()
|
|
document.body.append(route)
|
|
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
await frames(3)
|
|
expect(calls).toEqual([[0, false]])
|
|
|
|
cleanup?.()
|
|
route.remove()
|
|
})
|
|
|
|
test("keeps checking until stale reset-delay callbacks can no longer win", async () => {
|
|
const route = document.createElement("section")
|
|
const viewport = document.createElement("div")
|
|
route.append(viewport)
|
|
document.body.append(route)
|
|
const instance = {
|
|
scrollElement: viewport,
|
|
targetWindow: window,
|
|
scrollOffset: 79_400,
|
|
options: {
|
|
horizontal: false,
|
|
isRtl: false,
|
|
isScrollingResetDelay: 20,
|
|
useScrollendEvent: false,
|
|
},
|
|
} as unknown as Virtualizer<HTMLDivElement, HTMLDivElement>
|
|
const calls: number[] = []
|
|
const cleanup = observeElementOffsetReconnectAware(instance, (offset) => {
|
|
calls.push(offset)
|
|
instance.scrollOffset = offset
|
|
})
|
|
|
|
route.remove()
|
|
document.body.append(route)
|
|
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
await frames(1)
|
|
expect(instance.scrollOffset).toBe(0)
|
|
|
|
instance.scrollOffset = 79_400
|
|
await new Promise((resolve) => setTimeout(resolve, 25))
|
|
await frames(3)
|
|
|
|
expect(instance.scrollOffset).toBe(0)
|
|
expect(calls).toEqual([0, 0])
|
|
cleanup?.()
|
|
route.remove()
|
|
})
|
|
|
|
test.each([
|
|
{ name: "LTR", isRtl: false, expected: 240 },
|
|
{ name: "RTL", isRtl: true, expected: -240 },
|
|
])("reports the TanStack horizontal $name offset after reconnect", async ({ isRtl, expected }) => {
|
|
const route = document.createElement("section")
|
|
const viewport = document.createElement("div")
|
|
route.append(viewport)
|
|
document.body.append(route)
|
|
viewport.scrollLeft = 240
|
|
const instance = {
|
|
scrollElement: viewport,
|
|
targetWindow: window,
|
|
scrollOffset: 0,
|
|
options: {
|
|
horizontal: true,
|
|
isRtl,
|
|
isScrollingResetDelay: 0,
|
|
useScrollendEvent: false,
|
|
},
|
|
} as unknown as Virtualizer<HTMLDivElement, HTMLDivElement>
|
|
const calls: [number, boolean][] = []
|
|
const cleanup = observeElementOffsetReconnectAware(instance, (offset, isScrolling) => {
|
|
calls.push([offset, isScrolling])
|
|
instance.scrollOffset = offset
|
|
})
|
|
|
|
route.remove()
|
|
document.body.append(route)
|
|
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
await frames(3)
|
|
|
|
expect(calls).toEqual([[expected, false]])
|
|
cleanup?.()
|
|
route.remove()
|
|
})
|
|
|
|
test("cleanup suppresses an already queued delegated offset callback", async () => {
|
|
const viewport = document.createElement("div")
|
|
document.body.append(viewport)
|
|
viewport.scrollTop = 100
|
|
const instance = {
|
|
scrollElement: viewport,
|
|
targetWindow: window,
|
|
scrollOffset: 0,
|
|
options: {
|
|
horizontal: false,
|
|
isRtl: false,
|
|
isScrollingResetDelay: 10,
|
|
useScrollendEvent: false,
|
|
},
|
|
} as unknown as Virtualizer<HTMLDivElement, HTMLDivElement>
|
|
const calls: [number, boolean][] = []
|
|
const cleanup = observeElementOffsetReconnectAware(instance, (offset, isScrolling) =>
|
|
calls.push([offset, isScrolling]),
|
|
)
|
|
|
|
viewport.dispatchEvent(new Event("scroll"))
|
|
cleanup?.()
|
|
await new Promise((resolve) => setTimeout(resolve, 25))
|
|
|
|
expect(calls).toEqual([[100, true]])
|
|
viewport.remove()
|
|
})
|
|
|
|
test("cleanup cancels reconnect checks and delegated offset observation", async () => {
|
|
const route = document.createElement("section")
|
|
const viewport = document.createElement("div")
|
|
route.append(viewport)
|
|
document.body.append(route)
|
|
const instance = {
|
|
scrollElement: viewport,
|
|
targetWindow: window,
|
|
scrollOffset: 0,
|
|
options: {
|
|
horizontal: false,
|
|
isRtl: false,
|
|
isScrollingResetDelay: 50,
|
|
useScrollendEvent: false,
|
|
},
|
|
} as unknown as Virtualizer<HTMLDivElement, HTMLDivElement>
|
|
const calls: number[] = []
|
|
const cleanup = observeElementOffsetReconnectAware(instance, (offset) => calls.push(offset))
|
|
|
|
route.remove()
|
|
document.body.append(route)
|
|
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
cleanup?.()
|
|
instance.scrollOffset = 100
|
|
viewport.dispatchEvent(new Event("scroll"))
|
|
await frames(4)
|
|
|
|
expect(calls).toEqual([])
|
|
route.remove()
|
|
})
|
|
|
|
async function frames(count: number) {
|
|
for (let index = 0; index < count; index++) {
|
|
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
|
|
}
|
|
}
|