mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-25 03:00:23 +00:00
4e9ec6f191
PR #1669 added DOM virtualization to renderSessionListFromCache() with two issues for lists below the virtualization threshold (≤80 rows): 1. The unconditional scroll listener triggered renderSessionListFromCache() on every rAF, rebuilding the entire list DOM on every scroll event. 2. After each rebuild, scrollTop was only restored when virtualWindow.virtualized was true (i.e. total > 80). For lists ≤ 80 rows, scrollTop dropped to 0 on every scroll event, producing a 'scroll keeps jumping back' feel. Fix: - Always restore scrollTop after re-render when listScrollTopBeforeRender > 0 (regardless of virtualized flag). - Short-circuit _scheduleSessionVirtualizedRender when total <= SESSION_VIRTUAL_THRESHOLD_ROWS (saves wasteful rebuild on small lists). Live verified on a 56-session sidebar: scrollTop holds across animation frames. 3 regression tests pin the fix shape.
88 lines
3.8 KiB
Python
88 lines
3.8 KiB
Python
"""Regression test for #1669 follow-up — sidebar scroll jump fix.
|
|
|
|
The original PR #1669 added DOM virtualization to renderSessionListFromCache,
|
|
which:
|
|
|
|
1. Attached an unconditional scroll listener to the session list
|
|
2. The scroll listener triggers renderSessionListFromCache() on every rAF
|
|
3. The render rebuilds the list DOM via list.innerHTML='' / appendChild loop
|
|
4. After the rebuild, scrollTop was only restored when virtualWindow.virtualized
|
|
was true (i.e. total > 80 rows)
|
|
5. For lists ≤ 80 rows, the scrollTop reset to 0 on every scroll event,
|
|
producing a "scroll keeps jumping back" feel.
|
|
|
|
This test pins:
|
|
- The non-virtualized branch always restores scrollTop after a rebuild
|
|
- The scroll handler short-circuits when total <= threshold (prevents the
|
|
rebuild churn entirely on small lists)
|
|
"""
|
|
from pathlib import Path
|
|
|
|
SESSIONS_JS = Path(__file__).parent.parent / "static" / "sessions.js"
|
|
|
|
|
|
def _read_source():
|
|
return SESSIONS_JS.read_text()
|
|
|
|
|
|
def test_render_restores_scroll_top_for_non_virtualized_lists():
|
|
"""The bug: virtualWindow.virtualized=false skipped the scrollTop restore.
|
|
|
|
The fix: restore scrollTop whenever listScrollTopBeforeRender > 0,
|
|
regardless of virtualized flag. Otherwise small lists (≤80 rows) reset
|
|
to scrollTop=0 on every render.
|
|
"""
|
|
src = _read_source()
|
|
# The new branch must include listScrollTopBeforeRender>0 as the guard
|
|
# rather than virtualWindow.virtualized
|
|
assert "}else if(listScrollTopBeforeRender>0){" in src, (
|
|
"Expected the scrollTop-restore guard to use listScrollTopBeforeRender>0, "
|
|
"not virtualWindow.virtualized — without this fix, small lists drop "
|
|
"scrollTop to 0 on every scroll event."
|
|
)
|
|
|
|
|
|
def test_scroll_handler_short_circuits_below_virtualization_threshold():
|
|
"""The bug: the rAF re-render fired on every scroll event regardless of
|
|
whether virtualization was actually needed. For ≤80-row lists this caused
|
|
full DOM rebuild on every scroll tick.
|
|
|
|
The fix: _scheduleSessionVirtualizedRender skips the rebuild when
|
|
total <= SESSION_VIRTUAL_THRESHOLD_ROWS — there's no virtual window to
|
|
recompute on small lists, and the rebuild was wasteful (and bug-prone).
|
|
"""
|
|
src = _read_source()
|
|
# Locate the function body
|
|
start = src.find("function _scheduleSessionVirtualizedRender()")
|
|
end = src.find("function _ensureSessionVirtualScrollHandler", start)
|
|
body = src[start:end]
|
|
# The fix introduces an early-return when total <= SESSION_VIRTUAL_THRESHOLD_ROWS
|
|
assert "SESSION_VIRTUAL_THRESHOLD_ROWS" in body, (
|
|
"Expected _scheduleSessionVirtualizedRender to read the threshold; "
|
|
"without this guard, the rAF re-render fires on every scroll event "
|
|
"even when there's nothing to virtualize."
|
|
)
|
|
assert "total<=SESSION_VIRTUAL_THRESHOLD_ROWS" in body or "total <= SESSION_VIRTUAL_THRESHOLD_ROWS" in body, (
|
|
"Expected explicit total<=THRESHOLD comparison to short-circuit the re-render."
|
|
)
|
|
# The early return must be BEFORE the rAF schedule (else it's dead code)
|
|
early_return_idx = body.find("return")
|
|
raf_idx = body.find("requestAnimationFrame")
|
|
assert early_return_idx > 0 and early_return_idx < raf_idx, (
|
|
"The total<=THRESHOLD short-circuit must return BEFORE scheduling the rAF."
|
|
)
|
|
|
|
|
|
def test_virtualization_still_active_for_large_lists():
|
|
"""Regression: ensure the threshold + virtualWindow logic is still in place
|
|
for large lists. The fix must not break the original virtualization path.
|
|
"""
|
|
src = _read_source()
|
|
assert "SESSION_VIRTUAL_THRESHOLD_ROWS = 80" in src, (
|
|
"Threshold constant must remain at 80 rows."
|
|
)
|
|
# _sessionVirtualWindow function still defined
|
|
assert "function _sessionVirtualWindow" in src
|
|
# virtualWindow.virtualized branch still drives spacer rendering
|
|
assert "virtualWindow.virtualized" in src
|