Skip to content

Commit b2ae8cc

Browse files
committed
test(cli): drive a real wheel event at the scrollbox
The review on #1274 flagged the one gap in that PR: the delta -> multiplier -> scrollTop path was checked by reading OpenTUI's source, not by an actual wheel event, so nothing proved the scrollAcceleration JSX prop reaches the constructor field the wheel handler reads. This renders a scrollbox through @opentui/react's reconciler and scrolls it with the mock mouse from @opentui/core/testing, which emits the same SGR sequence a terminal does. One notch moves three lines; the same scrollbox without the prop still moves one, so the assertion cannot pass for any reason other than the accelerator. The line counts are written out rather than read from WHEEL_SCROLL_LINES -- a test that reads the constant it pins follows it anywhere. Verified red with the constant set to 1 (3 of 4 fail, the no-prop control correctly unaffected) and green at 3. Uses flushSync rather than @opentui/react's testRender helper: that helper wraps the render in React's act(), which is stripped from React's production build, and the suite runs under NODE_ENV=production. Claude-Session: https://claude.ai/code/session_01QNL5SiuLLyRHZcFgtUN5Yp
1 parent 9b67a13 commit b2ae8cc

1 file changed

Lines changed: 118 additions & 0 deletions

File tree

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { createTestRenderer } from '@opentui/core/testing'
2+
import { createRoot, flushSync } from '@opentui/react'
3+
import { describe, expect, test } from 'bun:test'
4+
5+
import { wheelScrollAcceleration } from '../wheel-scroll-acceleration'
6+
7+
import type { ScrollBoxRenderable } from '@opentui/core'
8+
9+
// The unit tests pin what ConstantScrollAccel returns. They cannot show that
10+
// the JSX prop reaches the field OpenTUI's wheel handler reads -- that path
11+
// runs through @opentui/react's reconciler and the terminal's mouse parser,
12+
// neither of which is ours. This drives real wheel events at a rendered
13+
// scrollbox and watches scrollTop, so the whole chain is covered by a test
14+
// rather than by reading the dependency's source.
15+
//
16+
// The line counts below are written out rather than taken from
17+
// WHEEL_SCROLL_LINES: three is what issue #1268 asked for, and a test that
18+
// reads the constant it is meant to pin would follow it anywhere.
19+
20+
const WIDTH = 40
21+
const HEIGHT = 10
22+
const CONTENT_LINES = 200
23+
24+
// Somewhere inside the scrollbox, so the renderer routes the event to it.
25+
const CURSOR_X = 5
26+
const CURSOR_Y = 5
27+
28+
// @opentui/react's own testRender helper wraps the render in React's act(),
29+
// which the tests cannot use: they run under NODE_ENV=production, and act is
30+
// stripped from React's production build. flushSync commits the tree just as
31+
// synchronously, and without a dev-only import.
32+
const renderTranscript = async (
33+
scrollAcceleration?: ScrollBoxRenderable['scrollAcceleration'],
34+
) => {
35+
let box: ScrollBoxRenderable | null = null
36+
37+
const setup = await createTestRenderer({ width: WIDTH, height: HEIGHT })
38+
const root = createRoot(setup.renderer)
39+
40+
flushSync(() => {
41+
root.render(
42+
<scrollbox
43+
ref={(instance: ScrollBoxRenderable | null) => {
44+
box = instance
45+
}}
46+
scrollX={false}
47+
scrollAcceleration={scrollAcceleration}
48+
style={{ width: WIDTH, height: HEIGHT }}
49+
>
50+
{Array.from({ length: CONTENT_LINES }, (_, i) => (
51+
<text key={i}>line {i}</text>
52+
))}
53+
</scrollbox>,
54+
)
55+
})
56+
await setup.flush()
57+
58+
if (!box) throw new Error('scrollbox never mounted')
59+
return { ...setup, box: box as ScrollBoxRenderable }
60+
}
61+
62+
describe('wheel scrolling the transcript', () => {
63+
test('one notch moves three lines', async () => {
64+
const { box, mockMouse, flush } = await renderTranscript(
65+
wheelScrollAcceleration,
66+
)
67+
68+
const before = box.scrollTop
69+
await mockMouse.scroll(CURSOR_X, CURSOR_Y, 'down')
70+
await flush()
71+
72+
expect(box.scrollTop - before).toBe(3)
73+
})
74+
75+
test('without the prop a notch still moves one line', async () => {
76+
// Guards the assertion above against passing for some reason other than
77+
// our accelerator -- e.g. if OpenTUI ever changed its own default.
78+
const { box, mockMouse, flush } = await renderTranscript()
79+
80+
const before = box.scrollTop
81+
await mockMouse.scroll(CURSOR_X, CURSOR_Y, 'down')
82+
await flush()
83+
84+
expect(box.scrollTop - before).toBe(1)
85+
})
86+
87+
test('three notches move nine lines, not more', async () => {
88+
// MacOSScrollAccel would ramp across a burst like this.
89+
const { box, mockMouse, flush } = await renderTranscript(
90+
wheelScrollAcceleration,
91+
)
92+
93+
const before = box.scrollTop
94+
for (let i = 0; i < 3; i++) {
95+
await mockMouse.scroll(CURSOR_X, CURSOR_Y, 'down')
96+
}
97+
await flush()
98+
99+
expect(box.scrollTop - before).toBe(9)
100+
})
101+
102+
test('scrolling back up moves three lines a notch too', async () => {
103+
const { box, mockMouse, flush } = await renderTranscript(
104+
wheelScrollAcceleration,
105+
)
106+
107+
for (let i = 0; i < 5; i++) {
108+
await mockMouse.scroll(CURSOR_X, CURSOR_Y, 'down')
109+
}
110+
await flush()
111+
112+
const before = box.scrollTop
113+
await mockMouse.scroll(CURSOR_X, CURSOR_Y, 'up')
114+
await flush()
115+
116+
expect(before - box.scrollTop).toBe(3)
117+
})
118+
})

0 commit comments

Comments
 (0)