Skip to content

test(web): extract chat auto-scroll state machine into pure helper#365

Draft
neilhtennek wants to merge 1 commit intoOpenKnots:mainfrom
neilhtennek:extract-auto-scroll-state-machine
Draft

test(web): extract chat auto-scroll state machine into pure helper#365
neilhtennek wants to merge 1 commit intoOpenKnots:mainfrom
neilhtennek:extract-auto-scroll-state-machine

Conversation

@neilhtennek
Copy link
Copy Markdown

Summary

Closes #13.

Pulls the auto-scroll intent logic out of ChatView.onMessagesScroll and into a pure computeNextAutoScrollState helper in chat-scroll.ts, with unit-test coverage for the edge cases listed in the issue.

The previous implementation lived inline as a four-branch tower of conditionals operating on refs, which made it impossible to exercise without rendering the full chat tree. Behavior is intentionally unchanged — each branch maps 1:1 to the same outcome as before, including the subtle "wheel intent is consumed even on a no-op scroll" case.

Why a refactor instead of browser tests

The issue suggests Playwright browser tests, and the repo does have a vitest-browser setup. But the auto-scroll decisions are pure state transitions over a handful of booleans and numbers — they don't need a real DOM to verify. Extracting them to a pure function gives:

  • Fast, deterministic unit tests (no virtualizer flake)
  • A single source of truth for the rules (the inline tower previously duplicated currentScrollTop < lastKnownScrollTopRef.current - 1 three times)
  • A docstring that names the priority order of the rules, which the inline version did not

The actual DOM-level "does it scroll?" path is already covered by ChatView.browser.tsx, so this PR focuses on the part that wasn't testable.

Changes

  • apps/web/src/chat-scroll.ts — add computeNextAutoScrollState + SCROLL_UP_DETECTION_TOLERANCE_PX constant + types
  • apps/web/src/chat-scroll.test.ts — 10 new tests for the state machine, plus 3 new edge cases for isScrollContainerNearBottom (rubber-band overshoot, exact boundary, zero-size container)
  • apps/web/src/components/ChatView.tsx — replace the inline branch tower in onMessagesScroll with one call to the helper

Issue #13 checklist coverage

Scenario Test
User scrolled up, then submits a message → should scroll to bottom re-engages auto-scroll when the user returns to the bottom
Optimistic message renders → should scroll to bottom unchanged path through forceStickToBottom (existing); state machine verifies it stays sticky
Streaming response starts → should stick if user hasn't scrolled up keeps auto-scroll on for a streaming response while user is at bottom
Long message past viewport → should scroll fully to bottom covered by existing forceStickToBottom + virtualizer; state machine no longer flips off mid-scroll
Multiple rapid submissions → should scroll after each keeps auto-scroll on across rapid optimistic submits at the bottom

Plus extra coverage for paths the inline code had but no test for: pointer-drag scroll up, sub-pixel jitter during pointer drag, keyboard scroll up vs down, wheel-intent consumption on no-op scrolls.

Test plan

  • npx vitest run src/chat-scroll.test.ts → 18/18 passing
  • tsc --noEmit → clean
  • CI

Filed by @neilhtennek / @claudeloop

Pull the auto-scroll intent logic out of ChatView.onMessagesScroll and
into a pure computeNextAutoScrollState helper in chat-scroll.ts, with
unit-test coverage for the edge cases listed in OpenKnots#13.

The previous implementation lived inline as a four-branch tower of
conditionals operating on refs, which made it impossible to exercise
without rendering the full chat tree. Behavior is intentionally
unchanged: each branch maps 1:1 to the same outcome as before, including
the subtle 'wheel intent is consumed even on a no-op scroll' case.

Tests added cover:
- user scrolled up, then submits a message -> re-engages auto-scroll
- streaming response while user is at the bottom
- multiple rapid optimistic submits do not flip the flag
- wheel-flagged scroll up beyond tolerance disables auto-scroll
- wheel intent is cleared even on a no-op scroll
- pointer-drag scroll up disables auto-scroll
- sub-pixel jitter during a pointer drag is ignored
- keyboard / assistive scroll up disables auto-scroll
- downward keyboard scroll is a no-op
- baseline no-op when already off and still above the bottom

Also adds a few isScrollContainerNearBottom edge cases (rubber-band
overshoot, exact threshold boundary, zero-size container).

Closes OpenKnots#13
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 8, 2026

@neilhtennek is attempting to deploy a commit to the 0xBuns Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions bot added size:L vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Apr 8, 2026
@neilhtennek
Copy link
Copy Markdown
Author

@BunsDev

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Verify scroll-to-bottom on message submit across all edge cases

1 participant