Files
isparkclaw-webui/tests/test_issue856_pinned_indicator_layout.py
nesquena-hermes b82954ee70 feat(ui): session attention indicators — streaming spinner, unread dot, timestamps (#856)
Closes #856. Co-authored-by: Frank Song <138988108+franksong2702@users.noreply.github.com>
Reviewed-by: nesquena (709bd37 — test isolation fix also included)
2026-04-23 09:05:57 -07:00

69 lines
3.7 KiB
Python

"""Regression checks for #856 pinned-star layout in the session list."""
from pathlib import Path
SESSIONS_JS = (Path(__file__).resolve().parent.parent / "static" / "sessions.js").read_text()
STYLE_CSS = (Path(__file__).resolve().parent.parent / "static" / "style.css").read_text()
def test_pinned_indicator_renders_inside_title_row():
title_row_idx = SESSIONS_JS.find("titleRow.className='session-title-row';")
assert title_row_idx != -1, "session title row construction not found"
pin_idx = SESSIONS_JS.find("pinInd.className='session-pin-indicator';", title_row_idx)
assert pin_idx != -1, "pinned indicator creation not found after title row"
append_to_title_row_idx = SESSIONS_JS.find("titleRow.appendChild(pinInd);", pin_idx)
assert append_to_title_row_idx != -1, "pinned indicator should be appended to titleRow"
append_to_el_idx = SESSIONS_JS.find("el.appendChild(pinInd);", pin_idx)
assert append_to_el_idx == -1, (
"pinned indicator should not be appended to the outer session row; "
"it must align inside the title row with the spinner/unread indicator"
)
def test_pinned_indicator_uses_fixed_indicator_box():
assert ".session-pin-indicator{" in STYLE_CSS, "session pin indicator CSS block missing"
css_block = STYLE_CSS[STYLE_CSS.find(".session-pin-indicator{"):STYLE_CSS.find(".session-pin-indicator svg{")]
assert "width:10px;" in css_block, "pin indicator should reserve a fixed 10px width"
assert "height:10px;" in css_block, "pin indicator should reserve a fixed 10px height"
assert "justify-content:center;" in css_block, "pin indicator should center the star inside its box"
def test_state_indicator_always_appended_to_prevent_layout_shift():
"""State span is always added to the DOM (visibility:hidden when inactive) to prevent
titles shifting left/right when the spinner or unread dot appears/disappears."""
title_row_idx = SESSIONS_JS.find("titleRow.className='session-title-row';")
assert title_row_idx != -1, "title row construction not found"
# state span must be appended unconditionally (no surrounding if-check)
append_idx = SESSIONS_JS.find("titleRow.appendChild(state);", title_row_idx)
assert append_idx != -1, "state span must always be appended to titleRow"
# Verify CSS uses visibility:hidden to reserve the slot
assert "session-state-indicator{" in STYLE_CSS, "session-state-indicator CSS rule missing"
base_block_start = STYLE_CSS.find("session-state-indicator{")
base_block_end = STYLE_CSS.find("}", base_block_start)
base_block = STYLE_CSS[base_block_start:base_block_end]
assert "visibility:hidden;" in base_block, (
"session-state-indicator should default to visibility:hidden so it reserves slot "
"without being visible — prevents title layout shift on state changes"
)
def test_apperror_path_calls_render_session_list():
"""apperror handler must call renderSessionList() to clear the streaming indicator
immediately rather than waiting for the 5s streaming poll interval."""
messages_js = (Path(__file__).resolve().parent.parent / "static" / "messages.js").read_text()
apperror_idx = messages_js.find("source.addEventListener('apperror'")
assert apperror_idx != -1, "apperror handler not found in messages.js"
warning_idx = messages_js.find("source.addEventListener('warning'", apperror_idx)
assert warning_idx != -1, "warning handler not found after apperror handler"
apperror_block = messages_js[apperror_idx:warning_idx]
assert "renderSessionList()" in apperror_block, (
"apperror handler must call renderSessionList() so the streaming indicator "
"clears immediately on server errors, not after a 5s poll delay"
)