@aiquants/virtualscroll 1.19.3 → 1.22.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +9 -6
  3. package/dist/ScrollBar.d.cts +123 -0
  4. package/dist/ScrollBar.d.ts +123 -0
  5. package/dist/ScrollBar.d.ts.map +1 -1
  6. package/dist/ScrollPane.d.cts +19 -1
  7. package/dist/ScrollPane.d.ts +19 -1
  8. package/dist/ScrollPane.d.ts.map +1 -1
  9. package/dist/VirtualScroll.d.cts +9 -0
  10. package/dist/VirtualScroll.d.ts +9 -0
  11. package/dist/VirtualScroll.d.ts.map +1 -1
  12. package/dist/_headFenwick.d.cts +450 -0
  13. package/dist/_headFenwick.d.ts +451 -0
  14. package/dist/_headFenwick.d.ts.map +1 -0
  15. package/dist/index.cjs +1 -1
  16. package/dist/index.d.cts +1 -1
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1470 -1360
  20. package/dist/styles/virtualscroll.css +1 -1
  21. package/dist/styles/virtualscroll.standalone.css +1 -1
  22. package/dist/tapScrollCircleSampleVisual.d.ts.map +1 -1
  23. package/dist/useFenwickMapTree.d.cts +43 -0
  24. package/dist/useFenwickMapTree.d.ts +43 -0
  25. package/dist/useFenwickMapTree.d.ts.map +1 -1
  26. package/package.json +4 -1
  27. package/src/ScrollBar.tsx +211 -33
  28. package/src/ScrollPane.tsx +252 -35
  29. package/src/VirtualScroll.tsx +11 -1
  30. package/src/index.ts +1 -1
  31. package/src/styles/virtualscroll.css +3 -3
  32. package/src/tapScrollCircleSampleVisual.tsx +22 -5
  33. package/src/useFenwickMapTree.ts +95 -13
  34. package/src/ScrollBar.spec.tsx +0 -620
  35. package/src/ScrollPane.spec.tsx +0 -482
  36. package/src/TapScrollCircle.spec.tsx +0 -275
  37. package/src/VirtualScroll.spec.ts +0 -641
  38. package/src/cli.server.spec.ts +0 -137
  39. package/src/logger.spec.ts +0 -128
  40. package/src/useFenwickMapTree.huge.spec.ts +0 -388
  41. package/src/useFenwickMapTree.spec.ts +0 -1518
  42. package/src/useLruCache.spec.ts +0 -382
  43. package/src/utils.spec.ts +0 -39
@@ -1,482 +0,0 @@
1
- /**
2
- * @fileoverview Regression tests for ScrollPane fixes (G4 group).
3
- * Covers negative initialScrollPosition clamping, the conditional
4
- * wheel preventDefault behavior (ctrl+wheel zoom / horizontal wheel),
5
- * pointer-drag state robustness (lost pointerup / lostpointercapture /
6
- * second-pointer takeover / synchronous lostpointercapture on release),
7
- * inertia robustness (stale velocity samples / reentrant stopInertia),
8
- * scrollTo non-finite guarding, conditional touchAction, and lazy
9
- * debug-log argument evaluation.
10
- *
11
- * ScrollPane の修正に対する回帰テスト(G4 グループ)。
12
- * 負の initialScrollPosition のクランプ、ホイールの条件付き preventDefault
13
- * (ctrl+wheel ズーム / 水平ホイール)、ポインタドラッグ状態の堅牢化
14
- * (pointerup 喪失 / lostpointercapture / 2 本目ポインタの乗っ取り /
15
- * 解放時の同期 lostpointercapture)、慣性の堅牢化 (静止後リリース /
16
- * 再入 stopInertia)、scrollTo の非有限値ガード、touchAction の条件付き適用、
17
- * デバッグログ引数の遅延評価を検証します。
18
- */
19
-
20
- import { cleanup, fireEvent, render } from "@testing-library/react"
21
- import { createRef } from "react"
22
- import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"
23
- import { Logger, LogLevel } from "./logger.ts"
24
- import { ScrollPane, type ScrollPaneHandle } from "./ScrollPane"
25
-
26
- // jsdom は PointerEvent とポインタキャプチャ API を持たないため補完する。
27
- // jsdom lacks PointerEvent and pointer-capture APIs; polyfill them for these tests.
28
- beforeAll(() => {
29
- if (typeof window.PointerEvent === "undefined") {
30
- class PointerEventPolyfill extends MouseEvent {
31
- pointerId: number
32
- pointerType: string
33
- constructor(type: string, params: PointerEventInit = {}) {
34
- super(type, params)
35
- this.pointerId = params.pointerId ?? 0
36
- this.pointerType = params.pointerType ?? "mouse"
37
- }
38
- }
39
- // @ts-expect-error jsdom への PointerEvent 補完
40
- window.PointerEvent = PointerEventPolyfill
41
- }
42
- HTMLElement.prototype.setPointerCapture ??= function setPointerCapture() {}
43
- HTMLElement.prototype.releasePointerCapture ??= function releasePointerCapture() {}
44
- HTMLElement.prototype.hasPointerCapture ??= function hasPointerCapture() {
45
- return false
46
- }
47
- })
48
-
49
- afterEach(() => {
50
- cleanup()
51
- vi.restoreAllMocks()
52
- })
53
-
54
- const dispatchWheel = (target: Element, init: WheelEventInit) => {
55
- const event = new WheelEvent("wheel", { cancelable: true, bubbles: true, ...init })
56
- target.dispatchEvent(event)
57
- return event
58
- }
59
-
60
- describe("ScrollPane - negative initialScrollPosition clamp", () => {
61
- it("clamps a negative initialScrollPosition to 0 on first render", () => {
62
- let capturedPosition = Number.NaN
63
- render(
64
- <ScrollPane contentSize={1000} viewportSize={200} initialScrollPosition={-50}>
65
- {(scrollPosition) => {
66
- capturedPosition = scrollPosition
67
- return <div>content</div>
68
- }}
69
- </ScrollPane>,
70
- )
71
- expect(capturedPosition).toBe(0)
72
- })
73
-
74
- it("keeps a valid initialScrollPosition untouched", () => {
75
- let capturedPosition = Number.NaN
76
- render(
77
- <ScrollPane contentSize={1000} viewportSize={200} initialScrollPosition={300}>
78
- {(scrollPosition) => {
79
- capturedPosition = scrollPosition
80
- return <div>content</div>
81
- }}
82
- </ScrollPane>,
83
- )
84
- expect(capturedPosition).toBe(300)
85
- })
86
- })
87
-
88
- describe("ScrollPane - conditional wheel preventDefault", () => {
89
- const setup = (onScroll = vi.fn()) => {
90
- const { container } = render(
91
- <ScrollPane contentSize={1000} viewportSize={200} onScroll={onScroll}>
92
- {() => <div>content</div>}
93
- </ScrollPane>,
94
- )
95
- const pane = container.querySelector(".aqvs-scroll-pane")
96
- if (!(pane instanceof HTMLElement)) {
97
- throw new Error("scroll pane element not found")
98
- }
99
- return { pane, onScroll }
100
- }
101
-
102
- it("prevents default and scrolls on a normal vertical wheel", () => {
103
- const { pane, onScroll } = setup()
104
- const event = dispatchWheel(pane, { deltaY: 100 })
105
- expect(event.defaultPrevented).toBe(true)
106
- expect(onScroll).toHaveBeenCalledWith(100, 0)
107
- })
108
-
109
- it("does not prevent default nor scroll on ctrl+wheel (pinch zoom)", () => {
110
- const { pane, onScroll } = setup()
111
- const event = dispatchWheel(pane, { deltaY: 100, ctrlKey: true })
112
- expect(event.defaultPrevented).toBe(false)
113
- expect(onScroll).not.toHaveBeenCalled()
114
- })
115
-
116
- it("does not prevent default nor scroll on a horizontal-only wheel", () => {
117
- const { pane, onScroll } = setup()
118
- const event = dispatchWheel(pane, { deltaX: 100, deltaY: 0 })
119
- expect(event.defaultPrevented).toBe(false)
120
- expect(onScroll).not.toHaveBeenCalled()
121
- })
122
- })
123
-
124
- /**
125
- * Builds a PointerEvent-like init for jsdom fireEvent.pointer*.
126
- *
127
- * jsdom の fireEvent.pointer* 用に PointerEvent 相当の初期化を生成。
128
- */
129
- const pointerInit = (pointerId: number, clientY: number, buttons: number) => ({
130
- pointerId,
131
- clientX: 0,
132
- clientY,
133
- buttons,
134
- button: 0,
135
- bubbles: true,
136
- cancelable: true,
137
- pointerType: "mouse",
138
- })
139
-
140
- const setupDragPane = (props: { contentSize?: number; viewportSize?: number } = {}) => {
141
- const onScroll = vi.fn()
142
- const { container } = render(
143
- <ScrollPane contentSize={props.contentSize ?? 1000} viewportSize={props.viewportSize ?? 200} onScroll={onScroll}>
144
- {() => <div>content</div>}
145
- </ScrollPane>,
146
- )
147
- const content = container.querySelector(".aqvs-scroll-pane-content")
148
- if (!(content instanceof HTMLElement)) {
149
- throw new Error("scroll pane content element not found")
150
- }
151
- return { container, content, onScroll }
152
- }
153
-
154
- describe("ScrollPane - pointer drag state robustness", () => {
155
- it("scrolls on a normal captured drag (baseline)", () => {
156
- const { content, onScroll } = setupDragPane()
157
- fireEvent.pointerDown(content, pointerInit(1, 100, 1))
158
- fireEvent.pointerMove(window, pointerInit(1, 80, 1))
159
- expect(onScroll).toHaveBeenCalledWith(20, 0)
160
- })
161
-
162
- it("discards stale drag state when a pointermove arrives with no buttons pressed (lost pointerup)", () => {
163
- const { content, onScroll } = setupDragPane()
164
- // pointerdown 後に pointerup が失われたと想定 (iframe 上での解放や Alt+Tab 等)
165
- fireEvent.pointerDown(content, pointerInit(1, 100, 1))
166
- // ボタン非押下の hover 移動はドラッグ化してはならない
167
- fireEvent.pointerMove(window, pointerInit(1, 50, 0))
168
- fireEvent.pointerMove(window, pointerInit(1, 20, 0))
169
- expect(onScroll).not.toHaveBeenCalled()
170
- // その後の click も握り潰されない
171
- const click = new MouseEvent("click", { bubbles: true, cancelable: true })
172
- content.dispatchEvent(click)
173
- expect(click.defaultPrevented).toBe(false)
174
- })
175
-
176
- it("ends the drag when pointer capture is lost (lostpointercapture)", () => {
177
- const { content, onScroll } = setupDragPane()
178
- fireEvent.pointerDown(content, pointerInit(1, 100, 1))
179
- fireEvent.pointerMove(window, pointerInit(1, 80, 1))
180
- expect(onScroll).toHaveBeenCalledTimes(1)
181
- // OS/ブラウザ都合の強制キャプチャ解放をシミュレート
182
- fireEvent.lostPointerCapture(content, { pointerId: 1, bubbles: true })
183
- fireEvent.pointerMove(window, pointerInit(1, 40, 1))
184
- expect(onScroll).toHaveBeenCalledTimes(1)
185
- })
186
-
187
- it("keeps click suppression when releasePointerCapture fires lostpointercapture synchronously", () => {
188
- const { content, onScroll } = setupDragPane()
189
- // releasePointerCapture が lostpointercapture を同期発火するブラウザを模倣する
190
- let captured = false
191
- content.setPointerCapture = () => {
192
- captured = true
193
- }
194
- content.hasPointerCapture = () => captured
195
- content.releasePointerCapture = () => {
196
- captured = false
197
- content.dispatchEvent(new window.PointerEvent("lostpointercapture", { pointerId: 1, bubbles: true }))
198
- }
199
-
200
- fireEvent.pointerDown(content, pointerInit(1, 100, 1))
201
- fireEvent.pointerMove(window, pointerInit(1, 80, 1))
202
- expect(onScroll).toHaveBeenCalledWith(20, 0)
203
- fireEvent.pointerUp(window, pointerInit(1, 80, 1))
204
-
205
- // 同期 lostpointercapture (意図的解放) で abortDrag が click 抑止を消してはならない
206
- const click = new MouseEvent("click", { bubbles: true, cancelable: true })
207
- content.dispatchEvent(click)
208
- expect(click.defaultPrevented).toBe(true)
209
- })
210
-
211
- it("ignores a second pointerdown while another pointer is actively dragging", () => {
212
- const { content, onScroll } = setupDragPane()
213
- fireEvent.pointerDown(content, pointerInit(1, 100, 1))
214
- fireEvent.pointerMove(window, pointerInit(1, 80, 1))
215
- expect(onScroll).toHaveBeenCalledWith(20, 0)
216
-
217
- // 2 本目の pointerdown はドラッグを乗っ取らない (先勝ち)
218
- fireEvent.pointerDown(content, pointerInit(2, 300, 1))
219
- fireEvent.pointerMove(window, pointerInit(2, 200, 1))
220
- expect(onScroll).toHaveBeenCalledTimes(1)
221
-
222
- // 1 本目のドラッグは継続する
223
- fireEvent.pointerMove(window, pointerInit(1, 60, 1))
224
- expect(onScroll).toHaveBeenLastCalledWith(40, 20)
225
-
226
- // 1 本目解放時の click 抑止も保持されている
227
- fireEvent.pointerUp(window, pointerInit(1, 60, 1))
228
- const click = new MouseEvent("click", { bubbles: true, cancelable: true })
229
- content.dispatchEvent(click)
230
- expect(click.defaultPrevented).toBe(true)
231
- })
232
-
233
- it("does not throw when setPointerCapture fails at drag activation", () => {
234
- const { content, onScroll } = setupDragPane()
235
- content.setPointerCapture = () => {
236
- throw new DOMException("pointer not found", "NotFoundError")
237
- }
238
- fireEvent.pointerDown(content, pointerInit(1, 100, 1))
239
- expect(() => {
240
- fireEvent.pointerMove(window, pointerInit(1, 80, 1))
241
- }).not.toThrow()
242
- // キャプチャ失敗後もドラッグ (スクロール) は継続する
243
- expect(onScroll).toHaveBeenCalledWith(20, 0)
244
- })
245
- })
246
-
247
- describe("ScrollPane - horizontal wheel delegation vs ctrl+wheel zoom", () => {
248
- const setupHorizontal = () => {
249
- const onWheelHorizontal = vi.fn()
250
- const onScroll = vi.fn()
251
- const { container } = render(
252
- <ScrollPane contentSize={1000} viewportSize={200} onScroll={onScroll} onWheelHorizontal={onWheelHorizontal}>
253
- {() => <div>content</div>}
254
- </ScrollPane>,
255
- )
256
- const pane = container.querySelector(".aqvs-scroll-pane")
257
- if (!(pane instanceof HTMLElement)) {
258
- throw new Error("scroll pane element not found")
259
- }
260
- return { pane, onScroll, onWheelHorizontal }
261
- }
262
-
263
- it("delegates shift+wheel to onWheelHorizontal (baseline)", () => {
264
- const { pane, onScroll, onWheelHorizontal } = setupHorizontal()
265
- const event = dispatchWheel(pane, { deltaY: 100, shiftKey: true })
266
- expect(event.defaultPrevented).toBe(true)
267
- expect(onWheelHorizontal).toHaveBeenCalledWith(100)
268
- expect(onScroll).not.toHaveBeenCalled()
269
- })
270
-
271
- it("does not intercept ctrl+shift+wheel (browser zoom passes through)", () => {
272
- const { pane, onScroll, onWheelHorizontal } = setupHorizontal()
273
- // ctrl+shift+縦ホイールはブラウザズーム: 横委譲に横取りされてはならない
274
- const event = dispatchWheel(pane, { deltaY: 100, shiftKey: true, ctrlKey: true })
275
- expect(event.defaultPrevented).toBe(false)
276
- expect(onWheelHorizontal).not.toHaveBeenCalled()
277
- expect(onScroll).not.toHaveBeenCalled()
278
- })
279
-
280
- it("does not intercept ctrl + horizontal trackpad pan", () => {
281
- const { pane, onScroll, onWheelHorizontal } = setupHorizontal()
282
- // ctrl を押しながらの横パンもズーム系操作として素通しする
283
- const event = dispatchWheel(pane, { deltaX: 100, deltaY: 0, ctrlKey: true })
284
- expect(event.defaultPrevented).toBe(false)
285
- expect(onWheelHorizontal).not.toHaveBeenCalled()
286
- expect(onScroll).not.toHaveBeenCalled()
287
- })
288
- })
289
-
290
- describe("ScrollPane - conditional touchAction", () => {
291
- it("applies touchAction none while the pane is scrollable", () => {
292
- const { content } = setupDragPane({ contentSize: 1000, viewportSize: 200 })
293
- expect(content.style.touchAction).toBe("none")
294
- })
295
-
296
- it("keeps the default touchAction when the pane cannot scroll (contentSize <= viewportSize)", () => {
297
- const { content } = setupDragPane({ contentSize: 100, viewportSize: 200 })
298
- expect(content.style.touchAction).not.toBe("none")
299
- })
300
- })
301
-
302
- describe("ScrollPane - inertia robustness", () => {
303
- it("does not start inertia when the pointer was held still before release", () => {
304
- // 速度サンプルの時刻を制御するため performance.now をモックする
305
- let now = 0
306
- vi.spyOn(performance, "now").mockImplementation(() => now)
307
- const rafSpy = vi.spyOn(window, "requestAnimationFrame").mockImplementation(() => 1)
308
- const { content } = setupDragPane()
309
-
310
- fireEvent.pointerDown(content, pointerInit(1, 200, 1))
311
- now = 16
312
- fireEvent.pointerMove(window, pointerInit(1, 150, 1))
313
- now = 32
314
- fireEvent.pointerMove(window, pointerInit(1, 100, 1))
315
- // 150ms 静止したまま離す: 古いサンプルによる慣性の誤発火があってはならない
316
- now = 182
317
- fireEvent.pointerUp(window, pointerInit(1, 100, 1))
318
-
319
- expect(rafSpy).not.toHaveBeenCalled()
320
- })
321
-
322
- it("starts inertia when released immediately after a fast drag (baseline)", () => {
323
- let now = 0
324
- vi.spyOn(performance, "now").mockImplementation(() => now)
325
- const rafSpy = vi.spyOn(window, "requestAnimationFrame").mockImplementation(() => 1)
326
- const { content } = setupDragPane()
327
-
328
- fireEvent.pointerDown(content, pointerInit(1, 200, 1))
329
- now = 16
330
- fireEvent.pointerMove(window, pointerInit(1, 150, 1))
331
- now = 32
332
- fireEvent.pointerMove(window, pointerInit(1, 100, 1))
333
- // 素早いドラッグ直後の解放では慣性が発火する
334
- now = 48
335
- fireEvent.pointerUp(window, pointerInit(1, 100, 1))
336
-
337
- expect(rafSpy).toHaveBeenCalled()
338
- })
339
-
340
- it("does not re-arm the inertia loop when stopInertia is called synchronously from onScroll", () => {
341
- let now = 0
342
- vi.spyOn(performance, "now").mockImplementation(() => now)
343
- // rAF をキュー化して手動でフレームを進められるようにする
344
- const rafCallbacks = new Map<number, FrameRequestCallback>()
345
- let rafId = 0
346
- vi.spyOn(window, "requestAnimationFrame").mockImplementation((callback) => {
347
- rafId += 1
348
- rafCallbacks.set(rafId, callback)
349
- return rafId
350
- })
351
- vi.spyOn(window, "cancelAnimationFrame").mockImplementation((id) => {
352
- rafCallbacks.delete(id)
353
- })
354
- const runFrame = (timestamp: number) => {
355
- const entry = [...rafCallbacks.entries()][0]
356
- if (!entry) {
357
- throw new Error("no pending animation frame")
358
- }
359
- rafCallbacks.delete(entry[0])
360
- entry[1](timestamp)
361
- }
362
-
363
- const ref = createRef<ScrollPaneHandle>()
364
- let stopOnNextScroll = false
365
- // 消費者が onScroll から同期的に handle.scrollTo (内部で stopInertia) を呼ぶケース (クランプ/スナップ実装等)
366
- const onScroll = vi.fn((position: number) => {
367
- if (stopOnNextScroll) {
368
- stopOnNextScroll = false
369
- ref.current?.scrollTo(position)
370
- }
371
- })
372
- const { container } = render(
373
- <ScrollPane ref={ref} contentSize={1000} viewportSize={200} onScroll={onScroll}>
374
- {() => <div>content</div>}
375
- </ScrollPane>,
376
- )
377
- const content = container.querySelector(".aqvs-scroll-pane-content")
378
- if (!(content instanceof HTMLElement)) {
379
- throw new Error("scroll pane content element not found")
380
- }
381
-
382
- fireEvent.pointerDown(content, pointerInit(1, 200, 1))
383
- now = 16
384
- fireEvent.pointerMove(window, pointerInit(1, 150, 1))
385
- now = 32
386
- fireEvent.pointerMove(window, pointerInit(1, 100, 1))
387
- now = 48
388
- fireEvent.pointerUp(window, pointerInit(1, 100, 1))
389
- // 慣性ループがアームされている
390
- expect(rafCallbacks.size).toBe(1)
391
-
392
- // 初回フレームは lastTimestamp の初期化のみで再アームされる
393
- runFrame(100)
394
- expect(rafCallbacks.size).toBe(1)
395
-
396
- // このフレームの scrollTo → onScroll → handle.scrollTo → stopInertia (同期再入)
397
- stopOnNextScroll = true
398
- runFrame(116)
399
-
400
- // 停止指示が尊重され、frame は再アームされない (世代トークンによる自壊)
401
- expect(rafCallbacks.size).toBe(0)
402
- })
403
- })
404
-
405
- describe("ScrollPane - scrollTo non-finite guard", () => {
406
- it("keeps the current position and skips onScroll when scrollTo receives NaN", () => {
407
- const warnImpl = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }
408
- Logger.setImplementation(warnImpl)
409
- try {
410
- const ref = createRef<ScrollPaneHandle>()
411
- const onScroll = vi.fn()
412
- const { container } = render(
413
- <ScrollPane ref={ref} contentSize={1000} viewportSize={200} onScroll={onScroll} initialScrollPosition={100}>
414
- {() => <div>content</div>}
415
- </ScrollPane>,
416
- )
417
- // NaN は現位置維持 + 警告ログ。onScroll は発火しない
418
- expect(ref.current?.scrollTo(Number.NaN)).toBe(100)
419
- expect(onScroll).not.toHaveBeenCalled()
420
- expect(warnImpl.warn).toHaveBeenCalled()
421
-
422
- // 関数形式の更新でも同様にガードされる
423
- expect(ref.current?.scrollTo(() => Number.NaN)).toBe(100)
424
- expect(onScroll).not.toHaveBeenCalled()
425
-
426
- // NaN 汚染が無いため、その後の相対スクロール (wheel の prev + delta) は正常に動く
427
- const pane = container.querySelector(".aqvs-scroll-pane")
428
- if (!(pane instanceof HTMLElement)) {
429
- throw new Error("scroll pane element not found")
430
- }
431
- dispatchWheel(pane, { deltaY: 50 })
432
- expect(onScroll).toHaveBeenCalledWith(150, 100)
433
- } finally {
434
- Logger.setImplementation(console)
435
- }
436
- })
437
- })
438
-
439
- describe("ScrollPane - lazy debug logging", () => {
440
- it("does not read scrollTop in the wheel handler while debug logging is suppressed", () => {
441
- const { container, content } = setupDragPane()
442
- const pane = container.querySelector(".aqvs-scroll-pane")
443
- if (!(pane instanceof HTMLElement)) {
444
- throw new Error("scroll pane element not found")
445
- }
446
- // 抑制時 (既定 WARN) はログ引数のサンクが評価されず、DOM 読み取りも発生しないことを検証する
447
- let scrollTopReads = 0
448
- Object.defineProperty(content, "scrollTop", {
449
- configurable: true,
450
- get() {
451
- scrollTopReads++
452
- return 0
453
- },
454
- set() {},
455
- })
456
- dispatchWheel(pane, { deltaY: 100 })
457
- expect(scrollTopReads).toBe(0)
458
- })
459
-
460
- it("resolves thunk arguments into plain objects when debug logging is enabled", () => {
461
- const impl = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }
462
- Logger.setImplementation(impl)
463
- Logger.setLevel(LogLevel.DEBUG)
464
- try {
465
- const { container } = setupDragPane()
466
- const pane = container.querySelector(".aqvs-scroll-pane")
467
- if (!(pane instanceof HTMLElement)) {
468
- throw new Error("scroll pane element not found")
469
- }
470
- impl.debug.mockClear()
471
- dispatchWheel(pane, { deltaY: 100 })
472
- const wheelCall = impl.debug.mock.calls.find((call) => typeof call[0] === "string" && call[0].includes("wheel event"))
473
- expect(wheelCall).toBeDefined()
474
- // サンクは Logger 内で解決され、実装には生のオブジェクトが渡る
475
- expect(typeof wheelCall?.[1]).toBe("object")
476
- expect((wheelCall?.[1] as { deltaY: number }).deltaY).toBe(100)
477
- } finally {
478
- Logger.setLevel(LogLevel.WARN)
479
- Logger.setImplementation(console)
480
- }
481
- })
482
- })