Files
hermes-webui/tests/test_1045_bfcache_layout_restore.py
T
2026-05-03 01:53:01 +08:00

125 lines
5.3 KiB
Python

"""
Tests for issue #1045 — bfcache layout broken on tab restore.
When the browser restores a page from bfcache (event.persisted === true),
the async boot IIFE does not re-run. The existing pageshow handler (added for
#822) only cleared the session search field and re-rendered the session list.
This left the rail, topbar, workspace panel, and resize handles in the stale
bfcache DOM state, producing a broken layout.
Fix: extend the pageshow handler to also call syncTopbar, syncWorkspacePanelState,
_initResizePanels, and startGatewaySSE — all guarded so missing helpers degrade.
"""
from pathlib import Path
ROOT = Path(__file__).parent.parent
def _boot_js() -> str:
return (ROOT / "static" / "boot.js").read_text(encoding="utf-8")
def _pageshow_handler(src: str) -> str:
marker = "window.addEventListener('pageshow'"
ps_idx = src.find(marker)
assert ps_idx != -1, "pageshow listener not found in boot.js"
brace = src.find("=> {", ps_idx)
assert brace != -1, "pageshow listener body not found"
brace = src.find("{", brace)
depth = 1
i = brace + 1
while i < len(src) and depth:
if src[i] == "{":
depth += 1
elif src[i] == "}":
depth -= 1
i += 1
assert depth == 0, "pageshow listener body did not close"
return src[brace + 1 : i - 1]
class TestBfcacheLayoutRestore:
def test_pageshow_calls_sync_topbar(self):
"""pageshow handler must call syncTopbar() on bfcache restore."""
src = _boot_js()
handler_body = _pageshow_handler(src)
assert "syncTopbar" in handler_body, (
"pageshow handler must call syncTopbar() to restore topbar state after bfcache"
)
def test_pageshow_calls_sync_workspace_panel_state(self):
"""pageshow handler must call syncWorkspacePanelState()."""
src = _boot_js()
handler_body = _pageshow_handler(src)
assert "syncWorkspacePanelState" in handler_body, (
"pageshow handler must call syncWorkspacePanelState() on bfcache restore"
)
def test_pageshow_calls_start_gateway_sse(self):
"""pageshow handler must call startGatewaySSE() to reconnect the dead SSE connection."""
src = _boot_js()
handler_body = _pageshow_handler(src)
assert "startGatewaySSE" in handler_body, (
"pageshow handler must restart gateway SSE (bfcache-persisted connections are dead)"
)
def test_pageshow_still_clears_session_search(self):
"""pageshow handler must still clear #sessionSearch (original #822 fix preserved)."""
src = _boot_js()
handler_body = _pageshow_handler(src)
assert "sessionSearch" in handler_body, (
"pageshow handler must still clear #sessionSearch (regression: #822 fix must be preserved)"
)
def test_pageshow_still_calls_render_session_list_from_cache(self):
"""pageshow handler must still call renderSessionListFromCache()."""
src = _boot_js()
handler_body = _pageshow_handler(src)
assert "renderSessionListFromCache" in handler_body, (
"pageshow handler must still call renderSessionListFromCache() (regression: #822 fix)"
)
def test_pageshow_does_not_call_init_resize_panels(self):
"""pageshow handler must NOT call _initResizePanels() — bfcache
preserves event listeners so re-attaching them stacks duplicates."""
src = _boot_js()
handler_body = _pageshow_handler(src)
assert "_initResizePanels" not in handler_body, (
"pageshow handler must not call _initResizePanels() — it stacks "
"duplicate mousedown listeners on every bfcache restore"
)
def test_new_calls_are_guarded_with_typeof(self):
"""New calls in the pageshow handler must be typeof-guarded for safe degradation."""
src = _boot_js()
handler_body = _pageshow_handler(src)
# Each of the new calls must be guarded
for fn in ("syncTopbar", "syncWorkspacePanelState", "startGatewaySSE",
"closeModelDropdown", "closeReasoningDropdown", "closeWsDropdown", "closeProfileDropdown"):
assert f"typeof {fn} === 'function'" in handler_body, (
f"{fn}() call in pageshow handler must be guarded with typeof === 'function'"
)
def test_pageshow_closes_all_dropdowns(self):
"""pageshow handler must close all known dropdowns to reset frozen bfcache popover state."""
src = _boot_js()
handler_body = _pageshow_handler(src)
for fn in ("closeModelDropdown", "closeReasoningDropdown", "closeWsDropdown", "closeProfileDropdown"):
assert fn in handler_body, (
f"pageshow handler must call {fn}() to dismiss any dropdown left open by bfcache"
)
def test_dropdowns_closed_before_layout_sync(self):
"""Dropdown closes must come before layout sync calls (clean state first)."""
src = _boot_js()
handler_body = _pageshow_handler(src)
close_idx = handler_body.find("closeModelDropdown")
sync_idx = handler_body.find("syncTopbar")
assert close_idx != -1 and sync_idx != -1, "Both close and sync calls must be present"
assert close_idx < sync_idx, (
"Dropdown close calls must appear before layout sync calls in the pageshow handler"
)