fix(scroll): mount virtualized-out anchor row before same-frame restore

This commit is contained in:
Rod Boev authored and nesquena-hermes committed 2026-06-17 01:42:44 +00:00
1 parent 387af5d9c9
commit 31191a6c50
2 files changed
+108 -1

No files matched your search

+18 -1
View File
@@ -9667,9 +9667,26 @@ function _restoreMessageScrollSnapshot(snapshot){
function _restoreMessageScrollSnapshotSameFrame(snapshot){
const el=$('messages');
if(!el||!snapshot) return;
const restoredViaAnchor=(snapshot.anchor&&typeof _restoreMessageViewportAnchor==='function')
let restoredViaAnchor=(snapshot.anchor&&typeof _restoreMessageViewportAnchor==='function')
? _restoreMessageViewportAnchor(snapshot.anchor,0)
: false;
if(!restoredViaAnchor&&snapshot.anchor&&snapshot.anchor.rawIdx!=null&&
!el.querySelector(`[data-msg-idx="${snapshot.anchor.rawIdx}"]`)&&
typeof _getVisibleMessagesWithIdx==='function'&&
typeof _messageVisibleIndexForRawIdx==='function'&&
typeof _messageVirtualScrollTopForVisibleIdx==='function'){
const visWithIdx=_getVisibleMessagesWithIdx();
const visIdx=_messageVisibleIndexForRawIdx(snapshot.anchor.rawIdx,visWithIdx);
if(visIdx>=0){
_programmaticScroll=true;
el.scrollTop=_messageVirtualScrollTopForVisibleIdx(visWithIdx,visIdx,el);
_messageVirtualWindowKey='';
renderMessages({preserveScroll:true});
restoredViaAnchor=(typeof _restoreMessageViewportAnchor==='function')
? _restoreMessageViewportAnchor(snapshot.anchor,0)
: false;
}
}
if(!restoredViaAnchor){
const maxTop=Math.max(0,el.scrollHeight-el.clientHeight);
const bottom=Number(snapshot.bottom);
@@ -427,3 +427,93 @@ console.log(JSON.stringify({recovered, deletes, renderCalls}));
assert metrics["recovered"] is True
assert metrics["deletes"] == ["sid-123"]
assert metrics["renderCalls"] == [{"preserveScroll": True, "_virtualFallback": True}]
def test_same_frame_restore_nudges_virtual_window_when_anchor_row_is_missing():
js = UI_JS_PATH.read_text(encoding="utf-8")
source = _extract_func_script(js) + r"""
const ROW_HEIGHT = 120;
const TOTAL = 60;
// Rows 10-59 are mounted (tail window); rows 0-9 are virtualized out.
// Anchor points to rawIdx=5, which is in the virtualized zone.
let mountedStart = 10;
let scrollTopValue = 0;
let renderCalls = [];
let scrollTopHistory = [];
let _messageVirtualWindowKey = 'stale-key';
let _programmaticScroll = false;
let _lastScrollTop = 0;
let _messageUserUnpinned = true;
let _scrollPinned = false;
let _nearBottomCount = 0;
let _messageVirtualHeightCache = Array.from({length: TOTAL}, () => ROW_HEIGHT);
let _messageVirtualHeightCacheEntries = [];
let _messageVirtualHeightCacheLen = TOTAL;
let _messageVirtualHeightCacheSrc = null;
let _messageVirtualEstimatedRowHeight = ROW_HEIGHT;
function _clearMessageVirtualHeightCache(){}
function _syncMessageVirtualHeightCache(){}
const container = {
get scrollTop(){ return scrollTopValue; },
set scrollTop(v){ scrollTopHistory.push(v); scrollTopValue = v; },
get scrollHeight(){ return TOTAL * ROW_HEIGHT; },
get clientHeight(){ return 600; },
getBoundingClientRect(){ return {top: 0, bottom: 600}; },
querySelector(selector){
const m = selector && selector.match(/\[data-msg-idx="(\d+)"\]/);
if(!m) return null;
const idx = Number(m[1]);
if(idx < mountedStart || idx >= TOTAL) return null;
const top = (idx - mountedStart) * ROW_HEIGHT;
return { getBoundingClientRect(){ return {top, bottom: top + ROW_HEIGHT}; } };
},
};
function $(id){ return id === 'messages' ? container : null; }
function _getVisibleMessagesWithIdx(){
return Array.from({length: TOTAL}, (_, i) => ({rawIdx: i}));
}
function renderMessages(opts){
renderCalls.push(JSON.parse(JSON.stringify(opts)));
mountedStart = 0;
}
function _restoreMessageViewportAnchor(anchor, delta){
const idx = Number(anchor.rawIdx) + Number(delta||0);
const row = container.querySelector(`[data-msg-idx="${idx}"]`);
if(!row) return false;
_programmaticScroll = true;
return true;
}
eval(extractFunc('_messageVisibleIndexForRawIdx'));
eval(extractFunc('_messageVirtualScrollTopForVisibleIdx'));
eval(extractFunc('_restoreMessageScrollSnapshotSameFrame'));
const snapshot = {
anchor: {rawIdx: 5, topOffset: 50},
top: 100,
bottom: 6600,
scrollHeight: 7200,
pinned: false,
userUnpinned: true,
};
_restoreMessageScrollSnapshotSameFrame(snapshot);
console.log(JSON.stringify({renderCalls, scrollTopHistory}));
"""
metrics = json.loads(_run_node(source))
assert len(metrics["renderCalls"]) == 1, (
"_restoreMessageScrollSnapshotSameFrame must call renderMessages to mount the virtualized-out anchor row"
)
assert metrics["renderCalls"][0].get("preserveScroll") is True, (
"re-render must use preserveScroll:true to avoid scrolling to bottom"
)
assert len(metrics["scrollTopHistory"]) >= 1, (
"scrollTop must be adjusted before re-render to place anchor row in the virtual window"
)
# rawIdx=5, visIdx=5: offset=5*120=600, viewport=600, scrollTop=round(600-600*0.35)=390
assert metrics["scrollTopHistory"][0] == 390