mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-07-19 06:00:46 +00:00
505465a81e
The profile-switch skeleton (#4671) always rendered a fixed content shape; for a brand-new/empty profile that flashed a misleading 'loading conversations' skeleton that resolved to an empty list. Track each profile's last-seen session count (localStorage, per profile name, recorded after each non-all-profiles render) and, when switching into a profile KNOWN to have zero sessions, render a quiet empty-state placeholder instead of the content skeleton. Unknown profiles keep the content skeleton (safe default); skipped under an active project/source filter (the count is an unfiltered total). prefers-reduced-motion + light/dark aware. Frontend-only. 7 source-structural tests; Phase-1 skeleton tests still green.
84 lines
3.8 KiB
Python
84 lines
3.8 KiB
Python
"""Phase 1.5 (#4717): empty/zero-conversation profile shows a quiet empty-state
|
|
skeleton on switch instead of a misleading content skeleton.
|
|
|
|
These are source-level structural guards (the skeleton logic is browser JS; the
|
|
behavioural check is a manual browser pass documented in the PR). They pin that
|
|
the per-profile count cache + the empty-branch wiring exist and are connected, so
|
|
a refactor can't silently drop the empty-profile affordance.
|
|
"""
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import api.routes as routes # noqa: F401 (ensures repo import path is set up)
|
|
|
|
SESSIONS_JS = Path(__file__).resolve().parent.parent / "static" / "sessions.js"
|
|
PANELS_JS = Path(__file__).resolve().parent.parent / "static" / "panels.js"
|
|
STYLE_CSS = Path(__file__).resolve().parent.parent / "static" / "style.css"
|
|
|
|
|
|
def _sessions():
|
|
return SESSIONS_JS.read_text(encoding="utf-8")
|
|
|
|
|
|
def test_per_profile_count_cache_helpers_exist():
|
|
src = _sessions()
|
|
assert "SESSION_PROFILE_COUNTS_KEY" in src
|
|
assert "function _recordSessionProfileCount(" in src
|
|
assert "function _knownSessionProfileCount(" in src
|
|
|
|
|
|
def test_count_is_recorded_after_render():
|
|
"""The render path must persist the active profile's session count so the
|
|
next switch can consult it — only for an unfiltered, single-profile view."""
|
|
src = _sessions()
|
|
assert "_recordSessionProfileCount(_allSessionsScope.profile, _allSessions.length)" in src
|
|
# Gated on !all-profiles AND !filter-active so only an unfiltered total is cached
|
|
# (mirrors the read-side gate; a filtered subset must not cache a misleading 0).
|
|
record_idx = src.index("_recordSessionProfileCount(_allSessionsScope.profile")
|
|
window = src[record_idx - 400:record_idx]
|
|
assert "_showAllProfiles" in window, "count record must be gated on !_showAllProfiles"
|
|
assert "_recordFilterActive" in window, "count record must be gated on !filterActive (write/read parity)"
|
|
|
|
|
|
def test_skeleton_accepts_target_profile_and_has_empty_branch():
|
|
src = _sessions()
|
|
assert "function showSessionListSkeleton(targetProfile)" in src
|
|
# Empty branch keys off a KNOWN zero count (null/unknown keeps content skeleton).
|
|
assert "_knownSessionProfileCount(targetProfile)" in src
|
|
assert "knownCount === 0" in src
|
|
assert "skeleton-list-empty" in src
|
|
|
|
|
|
def test_empty_branch_skipped_when_filter_active():
|
|
"""A project/source filter makes the unfiltered per-profile count unreliable,
|
|
so the empty branch must be suppressed when a filter is active."""
|
|
src = _sessions()
|
|
assert "filterActive" in src
|
|
# The guard combines the known-zero count with !filterActive.
|
|
assert "knownCount === 0 && !filterActive" in src
|
|
|
|
|
|
def test_switch_call_site_passes_target_profile():
|
|
src = PANELS_JS.read_text(encoding="utf-8")
|
|
assert "showSessionListSkeleton(name)" in src, "profile switch must pass the target profile to the skeleton"
|
|
|
|
|
|
def test_empty_skeleton_css_respects_reduced_motion():
|
|
css = STYLE_CSS.read_text(encoding="utf-8")
|
|
assert ".skeleton-empty-hint" in css
|
|
# The empty hint must have a reduced-motion override (no animation).
|
|
rm_blocks = re.findall(r"@media\s*\(prefers-reduced-motion:reduce\)\s*\{[^}]*skeleton-empty-hint[^}]*\}", css)
|
|
assert rm_blocks, "skeleton-empty-hint must have a prefers-reduced-motion:reduce override"
|
|
|
|
|
|
def test_virtual_scroll_teardown_runs_for_skeleton():
|
|
"""The skeleton builder must tear down virtual-scroll state (the #4662
|
|
Codex-gate guard against stale-row repaint) — done once up front so it
|
|
applies to both the content and empty-state branches."""
|
|
src = _sessions()
|
|
idx = src.index("function showSessionListSkeleton(")
|
|
body = src[idx: idx + 2000]
|
|
assert "cancelAnimationFrame(_sessionVirtualScrollRaf)" in body
|
|
assert "delete list.dataset.sessionVirtualTotal" in body
|
|
assert "delete list.dataset.sessionVirtualActiveAnchor" in body
|