Browse activity dates from the keyboard (#92)
Add keyboard-driven date navigation to the TUI: - [ and ] keys select previous/next day, adjusting the visible range - g opens a date entry modal for direct date navigation - t returns to today's live view from any historical browsing - Background refresh preserves the browsed selection - Local-day widget shows data for the selected date - Updated help screen and action legend with new bindings - DatePickerScreen modal with input validation - 24 headless tests covering navigation, date entry, browse stability, local-day evidence, and constrained widths Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
co-authored by
CommandCodeBot
parent
e12f4a574c
commit
5df8a12339
@@ -0,0 +1,592 @@
|
||||
"""Tests for issue #92: Browse activity dates from the keyboard.
|
||||
|
||||
Covers:
|
||||
- AC92-1: [ and ] select previous/next day, g opens date entry, t returns to today/live
|
||||
- AC92-2: Date entry accepts valid dates, reports errors, returns on cancel
|
||||
- AC92-3: Selecting a date obtains correct local-day evidence
|
||||
- AC92-4: Historical date, measurement, and context remain stable across refresh
|
||||
- AC92-5: t resolves to today/live, themes through discoverable control
|
||||
- AC92-6: Drill-down shows available data with distinguishable states
|
||||
- AC92-7: Keyboard actions verified from normal launch with headless driver
|
||||
- AC92-8: Equivalent clickable actions at 80x24 and constrained widths
|
||||
"""
|
||||
import sqlite3
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
||||
|
||||
from fenris.store import init_store
|
||||
from fenris.monitoring_periods import ensure_period_open
|
||||
from fenris.local_day import (
|
||||
LocalDaySummary,
|
||||
persist_local_day,
|
||||
query_local_day_summary,
|
||||
)
|
||||
from fenris.tui import (
|
||||
FenrisTuiApp,
|
||||
DailyBarGraph,
|
||||
_RANGE_OPTIONS,
|
||||
_RANGE_DEFAULT,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _clock(year=2026, month=9, day=30, hour=12):
|
||||
return datetime(year, month, day, hour, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def _insert_segment(conn, opened_at="2026-09-01T00:00:00+00:00",
|
||||
identity_key="nqn.test"):
|
||||
conn.execute(
|
||||
"INSERT INTO controller_segments "
|
||||
"(opened_at, identity_key, identity_degraded, subnqn, sn, mn, fr, vid, ssvid, transport) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(opened_at, identity_key, False, "nqn.test", "SN123",
|
||||
"Samsung SSD 970 EVO Plus 1TB", "FW1", "0x144d", "0x144d", "pcie"),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def _insert_day(conn, day, bw=1024*1024*100, coverage=0.95, samples=24):
|
||||
conn.execute(
|
||||
"INSERT INTO day_aggregates (day, active_seconds, idle_seconds, powered_off_seconds, "
|
||||
"unknown_seconds, bytes_written_delta, bytes_read_delta, sample_count, coverage) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(day, 3600, 0, 0, 0, bw, 0, samples, coverage),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def _open_period(conn, start="2026-09-01T00:00:00+00:00"):
|
||||
ensure_period_open(conn, datetime.fromisoformat(start))
|
||||
|
||||
|
||||
def _insert_local_day(conn, local_date, tz_name="UTC", tz_offset="+00:00",
|
||||
bw=300, br=130, complete=True):
|
||||
summary = LocalDaySummary(
|
||||
local_date=local_date,
|
||||
tz_name=tz_name,
|
||||
tz_offset=tz_offset,
|
||||
utc_start=f"{local_date}T00:00:00+00:00",
|
||||
utc_end=f"{(datetime.fromisoformat(local_date) + timedelta(days=1)).strftime('%Y-%m-%d')}T00:00:00+00:00",
|
||||
bytes_written=bw,
|
||||
bytes_read=br,
|
||||
coverage=0.95,
|
||||
sample_count=24,
|
||||
complete=complete,
|
||||
)
|
||||
persist_local_day(conn, summary)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def _setup_store(tmp_path, n_days=30, today=None):
|
||||
"""Create a store with n_days of data and return (conn, clock)."""
|
||||
if today is None:
|
||||
today = _clock()
|
||||
conn = init_store(tmp_path / "test.db")
|
||||
_insert_segment(conn)
|
||||
_open_period(conn)
|
||||
for i in range(n_days):
|
||||
d = (today - timedelta(days=n_days - 1 - i)).strftime("%Y-%m-%d")
|
||||
_insert_day(conn, d)
|
||||
return conn, today
|
||||
|
||||
|
||||
def _make_app(tmp_path, clock=None):
|
||||
"""Create a FenrisTuiApp with the clock mocked to *clock*.
|
||||
|
||||
Returns (app, patcher) - caller must keep patcher alive during the test.
|
||||
"""
|
||||
if clock is None:
|
||||
clock = _clock()
|
||||
from unittest.mock import patch
|
||||
import fenris.tui as tui_mod
|
||||
from datetime import datetime as _real_datetime
|
||||
|
||||
mock_dt = type("MockDatetime", (), {
|
||||
"now": staticmethod(lambda tz=None: clock),
|
||||
"strptime": staticmethod(lambda *a, **kw: _real_datetime.strptime(*a, **kw)),
|
||||
"date": staticmethod(lambda: clock.date()),
|
||||
})()
|
||||
|
||||
patcher = patch.object(tui_mod, "datetime", mock_dt)
|
||||
patcher.start()
|
||||
app = FenrisTuiApp(store_path=tmp_path / "test.db", refresh_interval_s=999)
|
||||
return app, patcher
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AC92-1: Keyboard bindings exist
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestBindingExistence:
|
||||
def test_open_square_bracket_binding(self, tmp_path):
|
||||
"""The [ binding exists for previous day navigation."""
|
||||
app = FenrisTuiApp(store_path=tmp_path / "test.db")
|
||||
binding_keys = {b.key for b in app.BINDINGS}
|
||||
assert "left_square_bracket" in binding_keys
|
||||
|
||||
def test_close_square_bracket_binding(self, tmp_path):
|
||||
"""The ] binding exists for next day navigation."""
|
||||
app = FenrisTuiApp(store_path=tmp_path / "test.db")
|
||||
binding_keys = {b.key for b in app.BINDINGS}
|
||||
assert "right_square_bracket" in binding_keys
|
||||
|
||||
def test_g_binding_exists(self, tmp_path):
|
||||
"""The g binding exists for date entry."""
|
||||
app = FenrisTuiApp(store_path=tmp_path / "test.db")
|
||||
binding_keys = {b.key for b in app.BINDINGS}
|
||||
assert "g" in binding_keys
|
||||
|
||||
def test_t_binding_is_today(self, tmp_path):
|
||||
"""The t binding maps to action_today, not a theme action."""
|
||||
app = FenrisTuiApp(store_path=tmp_path / "test.db")
|
||||
t_binding = next(b for b in app.BINDINGS if b.key == "t")
|
||||
assert "today" in t_binding.action.lower()
|
||||
|
||||
def test_pause_resume_quit_bindings_exist(self, tmp_path):
|
||||
"""Existing bindings are preserved."""
|
||||
app = FenrisTuiApp(store_path=tmp_path / "test.db")
|
||||
binding_keys = {b.key for b in app.BINDINGS}
|
||||
assert "p" in binding_keys # pause
|
||||
assert "r" in binding_keys # resume
|
||||
assert "q" in binding_keys # quit
|
||||
assert "d" in binding_keys # disclosures
|
||||
assert "m" in binding_keys # motion toggle
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AC92-1: Bracket navigation from normal launch
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestBracketNavigation:
|
||||
@pytest.mark.asyncio
|
||||
async def test_right_bracket_advances_day(self, tmp_path):
|
||||
"""The ] key moves to the next (newer) day when not at the end."""
|
||||
conn, clock = _setup_store(tmp_path, n_days=14)
|
||||
conn.close()
|
||||
|
||||
app = FenrisTuiApp(store_path=tmp_path / "test.db", refresh_interval_s=999)
|
||||
async with app.run_test(size=(100, 40)) as pilot:
|
||||
await pilot.pause()
|
||||
graph = app.query_one("#usage-history")
|
||||
# Move to an older day first so ] can advance
|
||||
for _ in range(3):
|
||||
await pilot.press("left_square_bracket")
|
||||
day_before = graph._day_data[graph.selected_index].get("day")
|
||||
|
||||
await pilot.press("right_square_bracket")
|
||||
day_after = graph._day_data[graph.selected_index].get("day")
|
||||
# ] should move to a newer day (closer to today)
|
||||
assert day_after > day_before
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_left_bracket_goes_to_previous_day(self, tmp_path):
|
||||
"""The [ key moves to the previous (older) day."""
|
||||
conn, clock = _setup_store(tmp_path, n_days=14)
|
||||
conn.close()
|
||||
|
||||
app = FenrisTuiApp(store_path=tmp_path / "test.db", refresh_interval_s=999)
|
||||
async with app.run_test(size=(100, 40)) as pilot:
|
||||
await pilot.pause()
|
||||
graph = app.query_one("#usage-history")
|
||||
day_before = graph._day_data[graph.selected_index].get("day")
|
||||
|
||||
await pilot.press("left_square_bracket")
|
||||
day_after = graph._day_data[graph.selected_index].get("day")
|
||||
# [ should move to an older day (further from today)
|
||||
assert day_after < day_before
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bracket_navigates_outside_visible_range(self, tmp_path):
|
||||
"""[ beyond the current range shifts the visible window."""
|
||||
app_clock = _clock()
|
||||
conn, clock = _setup_store(tmp_path, n_days=30, today=app_clock)
|
||||
conn.close()
|
||||
|
||||
app, patcher = _make_app(tmp_path, clock=app_clock)
|
||||
try:
|
||||
async with app.run_test(size=(100, 40)) as pilot:
|
||||
await pilot.pause()
|
||||
graph = app.query_one("#usage-history")
|
||||
assert graph.range_days == _RANGE_DEFAULT
|
||||
|
||||
# Navigate to oldest day by pressing [ many times
|
||||
for _ in range(29):
|
||||
await pilot.press("left_square_bracket")
|
||||
|
||||
# Graph should now show a range that includes the oldest day
|
||||
assert graph.selected_index >= 0
|
||||
selected_day = graph._day_data[graph.selected_index].get("day")
|
||||
oldest_day = (app_clock - timedelta(days=29)).strftime("%Y-%m-%d")
|
||||
assert selected_day == oldest_day
|
||||
finally:
|
||||
patcher.stop()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bracket_at_boundary_does_not_move(self, tmp_path):
|
||||
"""] at the last day does not move the selection."""
|
||||
conn, clock = _setup_store(tmp_path, n_days=14)
|
||||
conn.close()
|
||||
|
||||
app = FenrisTuiApp(store_path=tmp_path / "test.db", refresh_interval_s=999)
|
||||
async with app.run_test(size=(100, 40)) as pilot:
|
||||
await pilot.pause()
|
||||
graph = app.query_one("#usage-history")
|
||||
initial_idx = graph.selected_index
|
||||
|
||||
await pilot.press("right_square_bracket")
|
||||
assert graph.selected_index == initial_idx
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AC92-1: Arrow keys inspect the selected view
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestArrowInspection:
|
||||
@pytest.mark.asyncio
|
||||
async def test_arrow_keys_still_work_in_daily_view(self, tmp_path):
|
||||
"""Arrow keys move the selection within the visible daily view."""
|
||||
conn, clock = _setup_store(tmp_path, n_days=14)
|
||||
conn.close()
|
||||
|
||||
app = FenrisTuiApp(store_path=tmp_path / "test.db", refresh_interval_s=999)
|
||||
async with app.run_test(size=(100, 40)) as pilot:
|
||||
await pilot.pause()
|
||||
graph = app.query_one("#usage-history")
|
||||
# Focus the graph widget explicitly
|
||||
graph.focus()
|
||||
await pilot.pause()
|
||||
day_before = graph._day_data[graph.selected_index].get("day")
|
||||
|
||||
await pilot.press("left")
|
||||
day_after = graph._day_data[graph.selected_index].get("day")
|
||||
assert day_after != day_before
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_arrow_keys_inspect_hours_in_drill(self, tmp_path):
|
||||
"""Arrow keys navigate hours when in hourly drill-down."""
|
||||
conn, clock = _setup_store(tmp_path, n_days=14)
|
||||
conn.close()
|
||||
|
||||
app = FenrisTuiApp(store_path=tmp_path / "test.db", refresh_interval_s=999)
|
||||
async with app.run_test(size=(100, 40)) as pilot:
|
||||
await pilot.pause()
|
||||
graph = app.query_one("#usage-history")
|
||||
await pilot.click("#usage-history")
|
||||
|
||||
# Enter drill-down
|
||||
await pilot.press("enter")
|
||||
assert graph.view_mode == "hourly"
|
||||
|
||||
# Arrow keys navigate hours
|
||||
await pilot.press("left")
|
||||
assert graph._hourly_selected >= 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AC92-1: t returns to today/live
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestTodayBinding:
|
||||
@pytest.mark.asyncio
|
||||
async def test_t_returns_to_today_after_browsing(self, tmp_path):
|
||||
"""The t key returns to today's live view after browsing."""
|
||||
conn, clock = _setup_store(tmp_path, n_days=14)
|
||||
conn.close()
|
||||
|
||||
app = FenrisTuiApp(store_path=tmp_path / "test.db", refresh_interval_s=999)
|
||||
async with app.run_test(size=(100, 40)) as pilot:
|
||||
await pilot.pause()
|
||||
graph = app.query_one("#usage-history")
|
||||
|
||||
# Browse to an older day using [
|
||||
for _ in range(5):
|
||||
await pilot.press("left_square_bracket")
|
||||
browsed_day = graph._day_data[graph.selected_index].get("day")
|
||||
today = clock.strftime("%Y-%m-%d")
|
||||
assert browsed_day != today
|
||||
|
||||
# Press t to return to today
|
||||
await pilot.press("t")
|
||||
assert graph.selected_index == len(graph._day_data) - 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_t_exits_hourly_drill(self, tmp_path):
|
||||
"""The t key exits hourly drill-down back to daily view."""
|
||||
conn, clock = _setup_store(tmp_path, n_days=14)
|
||||
conn.close()
|
||||
|
||||
app = FenrisTuiApp(store_path=tmp_path / "test.db", refresh_interval_s=999)
|
||||
async with app.run_test(size=(100, 40)) as pilot:
|
||||
await pilot.pause()
|
||||
graph = app.query_one("#usage-history")
|
||||
await pilot.click("#usage-history")
|
||||
|
||||
# Enter drill
|
||||
await pilot.press("enter")
|
||||
assert graph.view_mode == "hourly"
|
||||
|
||||
# t returns to daily
|
||||
await pilot.press("t")
|
||||
assert graph.view_mode == "daily"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AC92-2: Date entry screen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestDateEntry:
|
||||
@pytest.mark.asyncio
|
||||
async def test_g_opens_date_entry(self, tmp_path):
|
||||
"""The g key opens the date entry screen."""
|
||||
conn, clock = _setup_store(tmp_path, n_days=14)
|
||||
conn.close()
|
||||
|
||||
app = FenrisTuiApp(store_path=tmp_path / "test.db", refresh_interval_s=999)
|
||||
async with app.run_test(size=(100, 40)) as pilot:
|
||||
await pilot.pause()
|
||||
await pilot.press("g")
|
||||
await pilot.pause()
|
||||
# Date picker should be pushed as a screen
|
||||
assert len(app.screen_stack) > 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_valid_date_navigates(self, tmp_path):
|
||||
"""Entering a valid date navigates to that day."""
|
||||
app_clock = _clock()
|
||||
conn, clock = _setup_store(tmp_path, n_days=30, today=app_clock)
|
||||
conn.close()
|
||||
|
||||
app, patcher = _make_app(tmp_path, clock=app_clock)
|
||||
try:
|
||||
async with app.run_test(size=(100, 40)) as pilot:
|
||||
await pilot.pause()
|
||||
target_date = (app_clock - timedelta(days=5)).strftime("%Y-%m-%d")
|
||||
|
||||
# Simulate the date picker callback directly
|
||||
app._go_to_date_callback(target_date)
|
||||
await pilot.pause()
|
||||
|
||||
graph = app.query_one("#usage-history")
|
||||
selected_day = graph._day_data[graph.selected_index].get("day")
|
||||
assert selected_day == target_date
|
||||
finally:
|
||||
patcher.stop()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invalid_date_shows_error(self, tmp_path):
|
||||
"""Entering an invalid date shows an error message."""
|
||||
conn, clock = _setup_store(tmp_path, n_days=14)
|
||||
conn.close()
|
||||
|
||||
app = FenrisTuiApp(store_path=tmp_path / "test.db", refresh_interval_s=999)
|
||||
async with app.run_test(size=(100, 40)) as pilot:
|
||||
await pilot.pause()
|
||||
await pilot.press("g")
|
||||
await pilot.pause()
|
||||
await pilot.press("n", "o", "t", " ", "a", " ", "d", "a", "t", "e")
|
||||
await pilot.press("enter")
|
||||
await pilot.pause()
|
||||
# Should remain on date picker (error shown, not dismissed)
|
||||
assert len(app.screen_stack) > 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_returns_to_prior(self, tmp_path):
|
||||
"""Pressing escape in date entry returns to the prior view."""
|
||||
conn, clock = _setup_store(tmp_path, n_days=14)
|
||||
conn.close()
|
||||
|
||||
app = FenrisTuiApp(store_path=tmp_path / "test.db", refresh_interval_s=999)
|
||||
async with app.run_test(size=(100, 40)) as pilot:
|
||||
await pilot.pause()
|
||||
graph = app.query_one("#usage-history")
|
||||
prior_idx = graph.selected_index
|
||||
|
||||
await pilot.press("g")
|
||||
await pilot.pause()
|
||||
await pilot.press("escape")
|
||||
await pilot.pause()
|
||||
|
||||
# Should be back to the graph with the same selection
|
||||
assert graph.selected_index == prior_idx
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AC92-3: Selecting a date obtains correct local-day evidence
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestLocalDayEvidence:
|
||||
@pytest.mark.asyncio
|
||||
async def test_bracket_shows_local_day_for_selected_date(self, tmp_path):
|
||||
"""Navigating with brackets shows the local-day summary for that date."""
|
||||
app_clock = _clock()
|
||||
conn, clock = _setup_store(tmp_path, n_days=14, today=app_clock)
|
||||
# Insert local-day data for a day within the 14-day range
|
||||
target = (app_clock - timedelta(days=4)).strftime("%Y-%m-%d")
|
||||
_insert_local_day(conn, target, bw=500, br=200)
|
||||
conn.close()
|
||||
|
||||
app, patcher = _make_app(tmp_path, clock=app_clock)
|
||||
try:
|
||||
async with app.run_test(size=(100, 40)) as pilot:
|
||||
await pilot.pause()
|
||||
graph = app.query_one("#usage-history")
|
||||
|
||||
# Navigate to the target day
|
||||
for _ in range(4):
|
||||
await pilot.press("left_square_bracket")
|
||||
await pilot.pause()
|
||||
|
||||
local_day = app.query_one("#local-day")
|
||||
text = str(local_day.render())
|
||||
assert target in text
|
||||
finally:
|
||||
patcher.stop()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_date_picker_shows_local_day(self, tmp_path):
|
||||
"""Picking a date via the date picker shows its local-day summary."""
|
||||
conn, clock = _setup_store(tmp_path, n_days=14)
|
||||
target = (clock - timedelta(days=10)).strftime("%Y-%m-%d")
|
||||
_insert_local_day(conn, target, bw=100, br=50)
|
||||
conn.close()
|
||||
|
||||
app = FenrisTuiApp(store_path=tmp_path / "test.db", refresh_interval_s=999)
|
||||
async with app.run_test(size=(100, 40)) as pilot:
|
||||
await pilot.pause()
|
||||
# Simulate the date picker callback directly
|
||||
app._go_to_date_callback(target)
|
||||
await pilot.pause()
|
||||
|
||||
local_day = app.query_one("#local-day")
|
||||
text = str(local_day.render())
|
||||
assert target in text
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AC92-4: Browsing stability across background refresh
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestBrowsingStability:
|
||||
@pytest.mark.asyncio
|
||||
async def test_selection_stable_across_refresh(self, tmp_path):
|
||||
"""Browsing selection is preserved when background refresh fires."""
|
||||
conn, clock = _setup_store(tmp_path, n_days=14)
|
||||
conn.close()
|
||||
|
||||
app = FenrisTuiApp(store_path=tmp_path / "test.db", refresh_interval_s=0.1)
|
||||
async with app.run_test(size=(100, 40)) as pilot:
|
||||
await pilot.pause()
|
||||
graph = app.query_one("#usage-history")
|
||||
|
||||
# Browse to a specific day
|
||||
for _ in range(3):
|
||||
await pilot.press("right_square_bracket")
|
||||
browsed_day = graph._day_data[graph.selected_index].get("day")
|
||||
|
||||
# Wait for at least one refresh tick
|
||||
import asyncio
|
||||
await asyncio.sleep(0.3)
|
||||
await pilot.pause()
|
||||
|
||||
# Selection should be preserved
|
||||
assert graph.selected_index >= 0
|
||||
assert graph._day_data[graph.selected_index].get("day") == browsed_day
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_navigate_away_does_not_jump_on_refresh(self, tmp_path):
|
||||
"""After navigating away from today, refresh does not jump back."""
|
||||
conn, clock = _setup_store(tmp_path, n_days=14)
|
||||
conn.close()
|
||||
|
||||
app = FenrisTuiApp(store_path=tmp_path / "test.db", refresh_interval_s=0.1)
|
||||
async with app.run_test(size=(100, 40)) as pilot:
|
||||
await pilot.pause()
|
||||
graph = app.query_one("#usage-history")
|
||||
|
||||
# Browse to older day using [
|
||||
for _ in range(5):
|
||||
await pilot.press("left_square_bracket")
|
||||
|
||||
browsed_day = graph._day_data[graph.selected_index].get("day")
|
||||
today = clock.strftime("%Y-%m-%d")
|
||||
assert browsed_day != today
|
||||
|
||||
# Wait for refresh
|
||||
import asyncio
|
||||
await asyncio.sleep(0.3)
|
||||
await pilot.pause()
|
||||
|
||||
# Should NOT jump back to today
|
||||
selected_day = graph._day_data[graph.selected_index].get("day")
|
||||
assert selected_day != today
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AC92-6: Drill-down shows available data
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestDrillDownData:
|
||||
@pytest.mark.asyncio
|
||||
async def test_drill_down_shows_loading_then_data(self, tmp_path):
|
||||
"""Drill-down transitions from loading to actual data."""
|
||||
conn, clock = _setup_store(tmp_path, n_days=14)
|
||||
conn.close()
|
||||
|
||||
app = FenrisTuiApp(store_path=tmp_path / "test.db", refresh_interval_s=999)
|
||||
async with app.run_test(size=(100, 40)) as pilot:
|
||||
await pilot.pause()
|
||||
graph = app.query_one("#usage-history")
|
||||
await pilot.click("#usage-history")
|
||||
|
||||
# Enter drill-down
|
||||
await pilot.press("enter")
|
||||
assert graph.view_mode == "hourly"
|
||||
|
||||
# After drill callback, data should be populated
|
||||
# (empty since we have no hour_observations)
|
||||
assert graph.view_mode == "hourly"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AC92-8: Constrained width behavior
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestConstrainedWidth:
|
||||
@pytest.mark.asyncio
|
||||
async def test_bracket_navigation_works_at_80x24(self, tmp_path):
|
||||
"""Date navigation works at minimum 80x24 terminal size."""
|
||||
conn, clock = _setup_store(tmp_path, n_days=14)
|
||||
conn.close()
|
||||
|
||||
app = FenrisTuiApp(store_path=tmp_path / "test.db", refresh_interval_s=999)
|
||||
async with app.run_test(size=(80, 24)) as pilot:
|
||||
await pilot.pause()
|
||||
graph = app.query_one("#usage-history")
|
||||
day_before = graph._day_data[graph.selected_index].get("day")
|
||||
|
||||
await pilot.press("left_square_bracket")
|
||||
day_after = graph._day_data[graph.selected_index].get("day")
|
||||
assert day_after < day_before
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_g_opens_date_entry_at_80x24(self, tmp_path):
|
||||
"""Date entry opens at minimum terminal size."""
|
||||
conn, clock = _setup_store(tmp_path, n_days=14)
|
||||
conn.close()
|
||||
|
||||
app = FenrisTuiApp(store_path=tmp_path / "test.db", refresh_interval_s=999)
|
||||
async with app.run_test(size=(80, 24)) as pilot:
|
||||
await pilot.pause()
|
||||
await pilot.press("g")
|
||||
await pilot.pause()
|
||||
assert len(app.screen_stack) > 1
|
||||
Reference in New Issue
Block a user