summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/pages/session/scroll-spy.test.ts
blob: f3e6775cb48ca6d2c61cbc152fb0728c4c2ec636 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import { describe, expect, test } from "bun:test"
import { createScrollSpy, pickOffsetId, pickVisibleId } from "./scroll-spy"

const rect = (top: number, height = 80): DOMRect =>
  ({
    x: 0,
    y: top,
    top,
    left: 0,
    right: 800,
    bottom: top + height,
    width: 800,
    height,
    toJSON: () => ({}),
  }) as DOMRect

const setRect = (el: Element, top: number, height = 80) => {
  Object.defineProperty(el, "getBoundingClientRect", {
    configurable: true,
    value: () => rect(top, height),
  })
}

describe("pickVisibleId", () => {
  test("prefers higher intersection ratio", () => {
    const id = pickVisibleId(
      [
        { id: "a", ratio: 0.2, top: 100 },
        { id: "b", ratio: 0.8, top: 300 },
      ],
      120,
    )

    expect(id).toBe("b")
  })

  test("breaks ratio ties by nearest line", () => {
    const id = pickVisibleId(
      [
        { id: "a", ratio: 0.5, top: 90 },
        { id: "b", ratio: 0.5, top: 140 },
      ],
      130,
    )

    expect(id).toBe("b")
  })
})

describe("pickOffsetId", () => {
  test("uses binary search cutoff", () => {
    const id = pickOffsetId(
      [
        { id: "a", top: 0 },
        { id: "b", top: 200 },
        { id: "c", top: 400 },
      ],
      350,
    )

    expect(id).toBe("b")
  })
})

describe("createScrollSpy fallback", () => {
  test("tracks active id from offsets and dirty refresh", () => {
    const active: string[] = []
    const root = document.createElement("div") as HTMLDivElement
    const one = document.createElement("div")
    const two = document.createElement("div")
    const three = document.createElement("div")

    root.append(one, two, three)
    document.body.append(root)

    Object.defineProperty(root, "scrollTop", { configurable: true, writable: true, value: 250 })
    setRect(root, 0, 800)
    setRect(one, -250)
    setRect(two, -50)
    setRect(three, 150)

    const queue: FrameRequestCallback[] = []
    const flush = () => {
      const run = [...queue]
      queue.length = 0
      for (const cb of run) cb(0)
    }

    const spy = createScrollSpy({
      onActive: (id) => active.push(id),
      raf: (cb) => (queue.push(cb), queue.length),
      caf: () => {},
      IntersectionObserver: undefined,
      ResizeObserver: undefined,
      MutationObserver: undefined,
    })

    spy.setContainer(root)
    spy.register(one, "a")
    spy.register(two, "b")
    spy.register(three, "c")
    spy.onScroll()
    flush()

    expect(spy.getActiveId()).toBe("b")
    expect(active.at(-1)).toBe("b")

    root.scrollTop = 450
    setRect(one, -450)
    setRect(two, -250)
    setRect(three, -50)
    spy.onScroll()
    flush()
    expect(spy.getActiveId()).toBe("c")

    root.scrollTop = 250
    setRect(one, -250)
    setRect(two, 250)
    setRect(three, 150)
    spy.markDirty()
    spy.onScroll()
    flush()
    expect(spy.getActiveId()).toBe("a")

    spy.destroy()
  })
})