renderFileTree() clears its scroll container with box.innerHTML='' and rebuilds every row. Detaching the rows collapses scrollHeight, so the browser clamps scrollTop to 0 — every folder expand/collapse, breadcrumb nav, refresh, and hidden-files toggle that re-runs the renderer teleported the reader to the top of a long tree, losing their place (the dominant interaction in the panel). Fix: capture box.scrollTop before the innerHTML='' wipe and restore it after the normal render tail (after _renderTreeItems repaints and the container is tall again). A plain scrollTop restore is sufficient and correct here — expand/collapse insert or remove rows BELOW the clicked disclosure, so the clicked row keeps its offset from the top; no getBoundingClientRect anchor delta is needed (that's only for the prepend-above case in the chat messages list). The reporter's data-path anchor sketch is deliberately NOT used: _renderTreeItems rows carry no data-path, so that selector would match nothing. The two early-return paths (no-workspace hides the box; empty-dir has nothing to scroll) are left untouched — they legitimately reset. The restore guards only the normal render tail. loadDir's two-render sequence (initial + post-expanded-dirs prefetch) is handled correctly: each renderFileTree() call independently captures then restores its own pre-wipe scrollTop across the await. Test: tests/test_issue5657_filetree_scroll_preserve.py — static guards that the capture precedes the wipe and the restore follows _renderTreeItems (per the maintainer test plan; drops the getBoundingClientRect anchor assertion since the fix deliberately doesn't use it). Fail-without-fix verified; 355 workspace-tree regression tests green. Closes #5657.
65 lines
2.8 KiB
Python
65 lines
2.8 KiB
Python
"""Regression coverage for #5657 — workspace file-tree preserves scroll on re-render.
|
|
|
|
`renderFileTree()` clears its scroll container with ``box.innerHTML=''`` and
|
|
rebuilds every row. That detaches the rows, collapses ``scrollHeight``, and the
|
|
browser clamps ``scrollTop`` to 0 — so every folder expand/collapse, breadcrumb
|
|
nav, refresh, and hidden-files toggle that re-runs the renderer teleported the
|
|
reader to the top of a long tree.
|
|
|
|
Fix: capture ``scrollTop`` before the wipe and restore it after the normal
|
|
render tail. A plain scrollTop restore is sufficient (expand/collapse insert or
|
|
remove rows BELOW the clicked disclosure, so the clicked row keeps its offset) —
|
|
the reporter's getBoundingClientRect anchor sketch is deliberately NOT used, and
|
|
the ``.file-item`` rows carry no ``data-path`` for it anyway.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
UI_JS = (REPO_ROOT / "static" / "ui.js").read_text(encoding="utf-8")
|
|
|
|
|
|
def _render_file_tree_body() -> str:
|
|
start = UI_JS.index("function renderFileTree()")
|
|
end = UI_JS.index("\nfunction ", start + 1)
|
|
return UI_JS[start:end]
|
|
|
|
|
|
def _render_file_tree_code() -> str:
|
|
"""Body with // comment lines stripped, so assertions anchor on real code."""
|
|
lines = [ln for ln in _render_file_tree_body().splitlines() if not ln.strip().startswith("//")]
|
|
return "\n".join(lines)
|
|
|
|
|
|
def test_render_file_tree_captures_and_restores_scrolltop():
|
|
body = _render_file_tree_code()
|
|
# It must still wipe the container (the behavior that loses scroll)...
|
|
assert "box.innerHTML=''" in body or 'box.innerHTML = ""' in body
|
|
# ...and it must capture scrollTop BEFORE the wipe and restore it after.
|
|
assert "scrollTop" in body, (
|
|
"renderFileTree must capture + restore scrollTop around the innerHTML wipe (#5657)"
|
|
)
|
|
capture_idx = body.index("prevScrollTop=box")
|
|
wipe_idx = body.index("box.innerHTML=''")
|
|
assert capture_idx < wipe_idx, (
|
|
"scrollTop must be captured BEFORE box.innerHTML='' so it isn't already clamped to 0"
|
|
)
|
|
render_idx = body.index("_renderTreeItems(box")
|
|
restore_idx = body.rindex("box.scrollTop=")
|
|
assert restore_idx > render_idx, (
|
|
"scrollTop must be restored AFTER _renderTreeItems repaints the tree"
|
|
)
|
|
|
|
|
|
def test_early_return_paths_still_reset_scroll():
|
|
# The no-workspace and empty-dir early returns legitimately want scroll reset
|
|
# (box hidden / nothing to scroll) — the restore must live on the normal tail
|
|
# only, i.e. AFTER _renderTreeItems, not before the early returns.
|
|
body = _render_file_tree_code()
|
|
render_idx = body.index("_renderTreeItems(box")
|
|
tail = body[render_idx:]
|
|
assert "box.scrollTop=" in tail or "box.scrollTop =" in tail, (
|
|
"the scrollTop restore must be on the normal render tail, after _renderTreeItems"
|
|
)
|