Files
Fenris/tests/test_dashboard_design.py
T

178 lines
7.2 KiB
Python

"""User-facing navigation, zoom, and plotted volume in the redesigned dashboard."""
from datetime import datetime, timedelta, timezone
from xml.etree import ElementTree
import pytest
from test_tui import (
_insert_baseline,
_insert_day,
_insert_local_day,
_insert_segment,
_open_period,
)
from fenris.store import init_store
from fenris.tui import FenrisTuiApp
NOW = datetime(2026, 9, 19, 12, tzinfo=timezone.utc)
@pytest.fixture
def dashboard(tmp_path, monkeypatch):
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "prefs"))
class Clock(datetime):
@classmethod
def now(cls, tz=None):
return NOW
monkeypatch.setattr("fenris.tui.datetime", Clock)
monkeypatch.setattr("fenris.status.query_service_state", lambda: {
"boot_enabled": True, "timer_active": True, "last_collect_ok": True,
"last_collect_age_s": 0, "last_collect_reason": None,
})
conn = init_store(tmp_path / "test.db")
_insert_segment(conn)
_insert_baseline(conn)
_open_period(conn)
for offset in range(18):
date = (NOW - timedelta(days=offset)).date().isoformat()
_insert_day(conn, date, bw=(offset + 1) * 1_000_000_000)
_insert_local_day(conn, date, bw=(offset + 1) * 1_000_000_000, br=2_000_000_000)
for index in range(61):
conn.execute(
"INSERT INTO samples (ts, device, bytes_written, bytes_read, segment_id) VALUES (?, ?, ?, ?, ?)",
((NOW - timedelta(minutes=(60 - index) * 3)).isoformat(),
"/dev/test", index * 1_000_000, index * 2_000_000, 1),
)
conn.execute(
"INSERT INTO hour_observations (hour, bytes_written_delta, bytes_read_delta, coverage, sample_count) "
"VALUES (?, ?, ?, ?, ?)", ("2026-09-18T12:00:00+00:00", 4_000_000, 8_000_000, 1, 20),
)
conn.commit()
conn.close()
return FenrisTuiApp(store_path=tmp_path / "test.db", refresh_interval_s=999)
def visible(app):
return " ".join("".join(ElementTree.fromstring(app.export_screenshot()).itertext()).split())
@pytest.mark.asyncio
@pytest.mark.parametrize("size", [(140, 44), (80, 24)])
async def test_live_plot_inspection_zoom_refresh_and_resize(dashboard, size):
app = dashboard
async with app.run_test(size=size) as pilot:
app.on_refresh_tick()
await pilot.pause()
assert app.theme == "fenris-chalktone"
graph = app.query_one("#live-activity")
assert app.focused is graph
await pilot.press("left", "w")
selected = str(app.query_one("#live-readout").render())
assert "11:54" in selected and "W 0.001 GB" in selected and "R 0.002 GB" in selected
assert "Reads" in str(app.query_one("#live-legend").render())
await pilot.press("z")
app.on_refresh_tick()
await pilot.pause()
assert str(app.query_one("#live-readout").render()) == selected
assert app.query_one("#activity-panel").region.width == size[0]
for fact in ("Freshness:", "Last collect:", "Boot:", "Timer:", "q Quit TUI"):
assert fact in visible(app)
await pilot.resize_terminal(100, 30)
await pilot.press("escape")
assert str(app.query_one("#live-readout").render()) == selected
assert app._zoomed_panel is None
assert app.query_one("#action-rail").region.bottom <= 30
@pytest.mark.asyncio
async def test_tabs_date_entry_and_hourly_inspection_keep_context(dashboard):
app = dashboard
async with app.run_test(size=(100, 36)) as pilot:
await pilot.click("#view-history")
await pilot.press("left", "enter")
assert app.query_one("#activity-tabs").active == "view-day"
assert "2026-09-18" in str(app.query_one("#local-day").render())
# Hour 23 is selected initially; move to the known hour 12.
await pilot.press(*(["left"] * 11))
readout = str(app.query_one("#bar-readout").render())
assert "12:00 UTC" in readout and "W 0.004 GB" in readout
await pilot.press("z", "w")
app.on_refresh_tick()
await pilot.pause()
assert str(app.query_one("#bar-readout").render()) == readout
assert "Reads" in str(app.query_one("#bar-legend").render())
await pilot.press("g")
await pilot.press(*list("2026-09-17"))
await pilot.press("escape")
assert str(app.query_one("#bar-readout").render()) == readout
assert app._zoomed_panel == "activity-panel"
await pilot.press("escape", "t")
assert app.query_one("#activity-tabs").active == "view-live"
@pytest.mark.asyncio
async def test_mouse_inspection_matches_time_axis(dashboard):
app = dashboard
async with app.run_test(size=(100, 36)) as pilot:
await pilot.pause()
graph = app.query_one("#live-activity")
x = graph._point_columns[0]
await pilot.click("#live-render", offset=(x, 1))
text = str(app.query_one("#live-readout").render())
assert "09:00 → 09:03 UTC" in text
@pytest.mark.asyncio
async def test_theme_control_persists_choice_and_quit_never_pauses(dashboard, monkeypatch):
calls = []
monkeypatch.setattr(dashboard, "_run_helper", lambda *args: calls.append(args))
async with dashboard.run_test(size=(80, 24)) as pilot:
await pilot.press("s")
assert dashboard.theme == "fenris-amber"
await pilot.press("q")
assert calls == []
from fenris.preferences import load_preferences
assert load_preferences()["theme"] == "amber"
@pytest.mark.asyncio
@pytest.mark.parametrize("size", [(100, 36), (70, 20)])
async def test_unallocated_volume_survives_day_with_missing_coverage(dashboard, size):
async with dashboard.run_test(size=size) as pilot:
await pilot.click("#view-history")
graph = dashboard.query_one("#usage-history")
graph.set_data([{
"day": "2026-09-18", "total_bytes": 2_000_000_000,
"unallocated_bytes": 2_000_000_000, "is_gap": True,
"is_partial": True,
}])
readout = str(dashboard.query_one("#bar-readout").render())
assert "W 2.000 GB unallocated" in readout
assert "R unavailable" in readout and "gap" in readout
if size[0] >= 80:
plot = str(dashboard.query_one("#bar-render").render())
assert any(0x2801 <= ord(c) <= 0x28ff for c in plot)
graph.measure = "read"
graph._refresh()
plot = str(dashboard.query_one("#bar-render").render())
assert not any(0x2801 <= ord(c) <= 0x28ff for c in plot)
@pytest.mark.asyncio
@pytest.mark.parametrize("state", ["gap", "future"])
async def test_small_terminal_hour_readout_does_not_invent_zero(dashboard, state):
async with dashboard.run_test(size=(70, 20)) as pilot:
await pilot.click("#view-day")
graph = dashboard.query_one("#usage-history")
graph.set_hour_data([{
"hour": "2026-09-18T12:00:00+00:00", "local_label": "12",
"is_gap": state == "gap", "is_future": state == "future",
}])
graph._hourly_selected = 0
graph._refresh_hourly()
readout = str(dashboard.query_one("#bar-readout").render())
assert "W unavailable · R unavailable" in readout
assert state in readout and "12:00 UTC" in readout