Files
DOLPHIN/Observability/TUI/test_dolphin_tui_keyboard.py

297 lines
11 KiB
Python
Raw Normal View History

"""
Keyboard shortcut tests for DolphinTUIApp.
Validates: Requirements 10.1, 10.2, 10.3, 10.4
Uses Textual's built-in async pilot (App.run_test / pilot.press) to simulate
keypresses and assert expected behaviour. DolphinDataFetcher is mocked so no
real Hazelcast or Prefect connections are needed.
"""
from __future__ import annotations
import sys
import os
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
sys.path.insert(0, os.path.dirname(__file__))
# ---------------------------------------------------------------------------
# Compatibility shim: Textual 8.x moved VerticalScroll to textual.containers.
# Patch textual.widgets so dolphin_tui.py (which imports from textual.widgets)
# can load without error.
# ---------------------------------------------------------------------------
import textual.widgets as _tw
import textual.containers as _tc
if not hasattr(_tw, "VerticalScroll"):
_tw.VerticalScroll = _tc.VerticalScroll
# ---------------------------------------------------------------------------
# Import the real app (all deps are available in this environment)
# ---------------------------------------------------------------------------
from dolphin_tui import ( # noqa: E402
DolphinTUIApp,
DolphinDataFetcher,
DataSnapshot,
LogPanel,
)
# ---------------------------------------------------------------------------
# Shared fixture: a minimal DataSnapshot with no real data
# ---------------------------------------------------------------------------
_EMPTY_SNAP = DataSnapshot()
def _make_mock_fetcher_instance() -> MagicMock:
"""Return a mock DolphinDataFetcher with all network methods stubbed."""
fetcher = MagicMock(spec=DolphinDataFetcher)
fetcher.hz_connected = False
fetcher.connect_hz = AsyncMock(return_value=False)
fetcher.disconnect_hz = AsyncMock(return_value=None)
fetcher.fetch = AsyncMock(return_value=_EMPTY_SNAP)
fetcher.fetch_prefect = AsyncMock(return_value=(False, []))
fetcher.tail_log = MagicMock(return_value=[])
fetcher._running = True
fetcher._reconnect_task = None
return fetcher
# ---------------------------------------------------------------------------
# Test 10.1 — `q` key: action_quit called, app exits cleanly
# Validates: Requirements 10.1
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_q_key_quits_app_cleanly():
"""Pressing `q` calls action_quit which disconnects HZ and exits (Req 10.1).
Verifies that disconnect_hz is awaited before exit so the HZ client
is shut down cleanly.
"""
mock_fetcher = _make_mock_fetcher_instance()
with patch("dolphin_tui.DolphinDataFetcher", return_value=mock_fetcher):
app = DolphinTUIApp(hz_host="localhost", hz_port=5701)
async with app.run_test(size=(130, 35)) as pilot:
# Stop the poll timer to avoid background noise
try:
app._poll_timer.stop()
except Exception:
pass
await pilot.press("q")
# run_test context exits cleanly after q — app.exit() was called
# After the context exits, verify disconnect_hz was called (clean HZ shutdown)
mock_fetcher.disconnect_hz.assert_awaited_once()
# ---------------------------------------------------------------------------
# Test 10.2 — `r` key: action_force_refresh triggers an immediate poll
# Validates: Requirements 10.2
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_r_key_triggers_immediate_poll():
"""Pressing `r` calls action_force_refresh which runs _poll immediately (Req 10.2).
Verifies that fetch() is called at least once more after pressing `r`,
confirming a poll outside the normal 2s cycle.
"""
mock_fetcher = _make_mock_fetcher_instance()
with patch("dolphin_tui.DolphinDataFetcher", return_value=mock_fetcher):
app = DolphinTUIApp(hz_host="localhost", hz_port=5701)
async with app.run_test(size=(130, 35)) as pilot:
try:
app._poll_timer.stop()
except Exception:
pass
call_count_before = mock_fetcher.fetch.await_count
await pilot.press("r")
await pilot.pause(0.2)
# fetch() should have been called at least once more after `r`
assert mock_fetcher.fetch.await_count > call_count_before, (
f"Expected fetch() to be called after pressing 'r', "
f"but call count stayed at {mock_fetcher.fetch.await_count}"
)
# ---------------------------------------------------------------------------
# Test 10.3 — `l` key: LogPanel visibility toggles on/off
# Validates: Requirements 10.3
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_l_key_shows_log_panel():
"""Pressing `l` once makes the LogPanel visible (Req 10.3)."""
mock_fetcher = _make_mock_fetcher_instance()
with patch("dolphin_tui.DolphinDataFetcher", return_value=mock_fetcher):
app = DolphinTUIApp(hz_host="localhost", hz_port=5701)
async with app.run_test(size=(130, 35)) as pilot:
try:
app._poll_timer.stop()
except Exception:
pass
log_panel = app.query_one("#panel_log", LogPanel)
# Initially hidden
assert log_panel.display is False, "LogPanel should be hidden on startup"
# Press l → should become visible
await pilot.press("l")
await pilot.pause(0.05)
assert log_panel.display is True, "LogPanel should be visible after first 'l' press"
@pytest.mark.asyncio
async def test_l_key_hides_log_panel_on_second_press():
"""Pressing `l` twice returns LogPanel to hidden state (Req 10.3)."""
mock_fetcher = _make_mock_fetcher_instance()
with patch("dolphin_tui.DolphinDataFetcher", return_value=mock_fetcher):
app = DolphinTUIApp(hz_host="localhost", hz_port=5701)
async with app.run_test(size=(130, 35)) as pilot:
try:
app._poll_timer.stop()
except Exception:
pass
log_panel = app.query_one("#panel_log", LogPanel)
# Press l twice: hidden → visible → hidden
await pilot.press("l")
await pilot.pause(0.05)
assert log_panel.display is True
await pilot.press("l")
await pilot.pause(0.05)
assert log_panel.display is False, "LogPanel should be hidden after second 'l' press"
@pytest.mark.asyncio
async def test_l_key_updates_log_visible_flag():
"""Pressing `l` updates the _log_visible internal flag (Req 10.3)."""
mock_fetcher = _make_mock_fetcher_instance()
with patch("dolphin_tui.DolphinDataFetcher", return_value=mock_fetcher):
app = DolphinTUIApp(hz_host="localhost", hz_port=5701)
async with app.run_test(size=(130, 35)) as pilot:
try:
app._poll_timer.stop()
except Exception:
pass
assert app._log_visible is False
await pilot.press("l")
await pilot.pause(0.05)
assert app._log_visible is True
await pilot.press("l")
await pilot.pause(0.05)
assert app._log_visible is False
# ---------------------------------------------------------------------------
# Test 10.4 — Arrow keys: scroll actions dispatched on LogPanel
# Validates: Requirements 10.4
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_up_arrow_scrolls_log_panel():
"""Pressing ↑ dispatches action_scroll_up on LogPanel when visible (Req 10.4)."""
mock_fetcher = _make_mock_fetcher_instance()
with patch("dolphin_tui.DolphinDataFetcher", return_value=mock_fetcher):
app = DolphinTUIApp(hz_host="localhost", hz_port=5701)
async with app.run_test(size=(130, 35)) as pilot:
try:
app._poll_timer.stop()
except Exception:
pass
# Make log panel visible and focus it
await pilot.press("l")
await pilot.pause(0.05)
log_panel = app.query_one("#panel_log", LogPanel)
assert log_panel.display is True
# Focus the log panel so it receives the scroll action
log_panel.focus()
await pilot.pause(0.05)
# Track action_scroll_up calls on the log panel
scroll_up_called = []
original = log_panel.action_scroll_up
def _track(*args, **kwargs):
scroll_up_called.append(True)
return original(*args, **kwargs)
log_panel.action_scroll_up = _track
await pilot.press("up")
await pilot.pause(0.1)
assert len(scroll_up_called) > 0, (
"action_scroll_up should have been called on LogPanel after pressing 'up'"
)
@pytest.mark.asyncio
async def test_down_arrow_scrolls_log_panel():
"""Pressing ↓ dispatches action_scroll_down on LogPanel when visible (Req 10.4)."""
mock_fetcher = _make_mock_fetcher_instance()
with patch("dolphin_tui.DolphinDataFetcher", return_value=mock_fetcher):
app = DolphinTUIApp(hz_host="localhost", hz_port=5701)
async with app.run_test(size=(130, 35)) as pilot:
try:
app._poll_timer.stop()
except Exception:
pass
# Make log panel visible and focus it
await pilot.press("l")
await pilot.pause(0.05)
log_panel = app.query_one("#panel_log", LogPanel)
assert log_panel.display is True
log_panel.focus()
await pilot.pause(0.05)
# Track action_scroll_down calls on the log panel
scroll_down_called = []
original = log_panel.action_scroll_down
def _track(*args, **kwargs):
scroll_down_called.append(True)
return original(*args, **kwargs)
log_panel.action_scroll_down = _track
await pilot.press("down")
await pilot.pause(0.1)
assert len(scroll_down_called) > 0, (
"action_scroll_down should have been called on LogPanel after pressing 'down'"
)