153 lines
6.9 KiB
Python
153 lines
6.9 KiB
Python
"""Regression coverage for #3479: iOS Safari must not see a top-scroll frame.
|
|
|
|
The live token path writes into an existing streaming-markdown DOM node. The
|
|
jump came from discrete transcript rebuilds and card replacement paths, so these
|
|
tests pin those call sites rather than the per-token renderer.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
|
|
SESSIONS_JS = (ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
|
|
STYLE_CSS = (ROOT / "static" / "style.css").read_text(encoding="utf-8")
|
|
|
|
|
|
def _function_body(src: str, name: str) -> str:
|
|
marker = f"function {name}"
|
|
start = src.find(marker)
|
|
assert start >= 0, f"{name} not found"
|
|
brace = src.find("{", start)
|
|
assert brace >= 0, f"{name} body not found"
|
|
depth = 0
|
|
for pos in range(brace, len(src)):
|
|
ch = src[pos]
|
|
if ch == "{":
|
|
depth += 1
|
|
elif ch == "}":
|
|
depth -= 1
|
|
if depth == 0:
|
|
return src[start : pos + 1]
|
|
raise AssertionError(f"{name} body did not terminate")
|
|
|
|
|
|
def _compact(text: str) -> str:
|
|
return "".join(text.split())
|
|
|
|
|
|
def test_refresh_session_uses_same_frame_scroll_snapshot_restore():
|
|
body = _function_body(UI_JS, "refreshSession")
|
|
|
|
assert "syncTopbar(); _renderMessagesWithScrollSnapshot();" in body
|
|
assert "syncTopbar(); renderMessages();" not in body
|
|
|
|
|
|
def test_handoff_rebuilds_use_same_frame_scroll_snapshot_restore():
|
|
clear_body = _function_body(UI_JS, "clearHandoffUi")
|
|
set_body = _function_body(UI_JS, "setHandoffUi")
|
|
|
|
assert "_renderMessagesWithScrollSnapshot();" in clear_body
|
|
assert "_renderMessagesWithScrollSnapshot();" in set_body
|
|
assert "renderMessages();" not in clear_body
|
|
assert "renderMessages();" not in set_body
|
|
|
|
|
|
def test_live_compression_card_replacement_restores_snapshot_before_follow_settle():
|
|
body = _function_body(UI_JS, "appendLiveCompressionCard")
|
|
|
|
capture_idx = body.index("const scrollSnapshot=_captureMessageScrollSnapshot();")
|
|
replace_idx = body.index("if(existing) existing.replaceWith(node);")
|
|
restore_idx = body.index("_restoreMessageScrollSnapshotSameFrame(scrollSnapshot);")
|
|
settle_idx = body.index("if(typeof scrollIfPinned==='function') scrollIfPinned();")
|
|
|
|
assert capture_idx < replace_idx < restore_idx < settle_idx
|
|
|
|
|
|
def test_live_anchor_worklog_rebuild_restores_snapshot_before_follow_settle():
|
|
body = _function_body(UI_JS, "renderLiveAnchorActivityScene")
|
|
|
|
capture_idx = body.index("const scrollSnapshot=_captureMessageScrollSnapshot();")
|
|
guard_idx = body.index("const scrollRebuildGuard=_prepareLiveAnchorScrollRebuildGuard(scrollSnapshot);")
|
|
remove_idx = body.index("blocks.querySelectorAll('[data-anchor-scene-owner=\"1\"],[data-anchor-scene-row=\"1\"]')")
|
|
restore_detail_idx = body.index("_restoreWorklogDetailDisclosureState(blocks, liveDisclosureState);")
|
|
dedupe_idx = body.index("_dedupeLiveProcessedWorklogAnchors(turn);")
|
|
move_status_idx = body.index("_moveLiveRunStatusToTurnEnd();")
|
|
restore_idx = body.index("_restoreMessageScrollSnapshotSameFrame(scrollSnapshot);")
|
|
release_idx = body.index("_restoreLiveAnchorScrollSnapshotAfterRebuild(scrollSnapshot,scrollRebuildGuard);")
|
|
settle_idx = body.index("if(!scrollRebuildGuard.readerAwayFromBottom&&typeof scrollIfPinned==='function') scrollIfPinned();")
|
|
|
|
assert capture_idx < guard_idx < remove_idx < restore_detail_idx < dedupe_idx < move_status_idx < restore_idx < release_idx < settle_idx
|
|
|
|
|
|
def test_live_anchor_worklog_rebuild_guards_height_for_unpinned_reader():
|
|
guard = _function_body(UI_JS, "_prepareLiveAnchorScrollRebuildGuard")
|
|
compact = _compact(guard)
|
|
|
|
assert "constbeforeBottomDistance=Math.max(0,messagesEl.scrollHeight-messagesEl.scrollTop-messagesEl.clientHeight);" in compact
|
|
assert "beforeBottomDistance>250&&(_messageUserUnpinned||_scrollPinned===false)" in compact
|
|
assert "scrollSnapshot.pinned=false;" in compact
|
|
assert "scrollSnapshot.userUnpinned=true;" in compact
|
|
assert "scrollSnapshot.bottom=beforeBottomDistance;" in compact
|
|
assert "_messageUserUnpinned=true;" in compact
|
|
assert "_scrollPinned=false;" in compact
|
|
assert "_nearBottomCount=0;" in compact
|
|
assert "msgInner.style.minHeight=`${guardHeight}px`;" in compact
|
|
|
|
|
|
def test_same_frame_snapshot_preserves_bottom_distance_and_unpinned_state():
|
|
capture = _function_body(UI_JS, "_captureMessageScrollSnapshot")
|
|
restore = _function_body(UI_JS, "_restoreMessageScrollSnapshotSameFrame")
|
|
wrapper = _function_body(UI_JS, "_renderMessagesWithScrollSnapshot")
|
|
|
|
assert "bottom" in capture
|
|
assert "readerAwayFromBottom?false:_shouldFollowMessagesOnDomReplace()" in _compact(capture)
|
|
assert "readerAwayFromBottom?true:_messageUserUnpinned" in _compact(capture)
|
|
assert "maxTop-Math.max(0,bottom)" in restore
|
|
assert "_messageUserUnpinned=true" in restore
|
|
assert "_scrollPinned=false" in restore
|
|
assert "renderMessages({...(options||{}),preserveScroll:true});" in wrapper
|
|
assert "_restoreMessageScrollSnapshotSameFrame(scrollSnapshot);" in wrapper
|
|
|
|
|
|
def test_preserve_scroll_restores_reader_away_from_bottom_before_following():
|
|
body = _function_body(UI_JS, "_scrollAfterMessageRender")
|
|
compact = _compact(body)
|
|
|
|
reader_idx = compact.index("constreaderAwayFromBottom=")
|
|
follow_idx = compact.index("if(!readerAwayFromBottom&&!_messageUserUnpinned&&_followMessagesAfterDomReplace())return;")
|
|
restore_idx = compact.index("_restoreMessageScrollSnapshot(scrollSnapshot);")
|
|
|
|
assert "Number(scrollSnapshot.bottom)>250" in compact
|
|
assert reader_idx < follow_idx < restore_idx
|
|
|
|
|
|
def test_scroll_snapshot_restore_reinstates_unpinned_state_when_reader_is_mid_answer():
|
|
restore = _function_body(UI_JS, "_restoreMessageScrollSnapshot")
|
|
compact = _compact(restore)
|
|
|
|
assert "constbottomDistance=el.scrollHeight-el.scrollTop-el.clientHeight;" in compact
|
|
assert "if(bottomDistance>250)" in compact
|
|
assert "_messageUserUnpinned=true" in compact
|
|
assert "_scrollPinned=false" in compact
|
|
|
|
|
|
def test_same_session_force_refresh_does_not_reset_scroll_direction_tracker():
|
|
body = _function_body(SESSIONS_JS, "loadSession")
|
|
compact = _compact(body)
|
|
|
|
assert "constcurrentSid=S.session?S.session.session_id:null;" in compact
|
|
assert "if(currentSid!==sid&&typeofwindow!=='undefined'&&typeofwindow._resetScrollDirectionTracker==='function')" in compact
|
|
|
|
|
|
def test_clarify_card_is_height_clamped_and_scrollable_on_mobile_viewports():
|
|
compact = _compact(STYLE_CSS)
|
|
|
|
assert ".clarify-card" in STYLE_CSS
|
|
assert "max-height:clamp(180px,min(68vh,calc(100vh-220px)),420px)" in compact
|
|
assert "@supports(height:100dvh)" in compact
|
|
assert "max-height:clamp(180px,min(62dvh,calc(100dvh-180px)),360px)" in compact
|
|
assert "overflow-y:auto" in compact
|
|
assert "-webkit-overflow-scrolling:touch" in compact
|
|
assert "overscroll-behavior:contain" in compact
|