178 lines
7.2 KiB
Python
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
|