"""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