"""Tests for the shared full-screen view plumbing (ui/viewkit.py). ScreenView is the base behind TaskView and RunView; those views' suites cover it end to end. These tests pin the template-loop contract itself: worker start-up, event draining, terminal/key handling, and the getch mode switching. """ import sys import threading import unittest from unittest.mock import patch from tests.test_tui import FakeCurses, FakeScreen from ui import viewkit from ui.viewkit import ScreenView class _SyncThread: """Thread stand-in that runs its target synchronously on start().""" def __init__(self, target, daemon=None): self._target = target def start(self): self._target() def join(self, timeout=None): pass class EchoView(ScreenView): """Minimal concrete view: records keys, ends after N events.""" def __init__(self, scr, *, terminal_after=None, clock=lambda: 1000.0): super().__init__(scr, clock=clock) self.keys = [] self.terminal_after = terminal_after def work(): for index in range(self.terminal_after or 0): self._queue.put({"kind": "tick", "index": index}) if self.terminal_after is not None: self._finish("done") # Created here; the base template starts it. self._worker = _SyncThread(work) def handle_event(self, event): pass def render(self): pass def _terminal_result(self): return "terminal" class TemplateLoopTests(unittest.TestCase): def setUp(self): self.curses = FakeCurses() patcher = patch.dict(sys.modules, {"curses": self.curses}) patcher.start() self.addCleanup(patcher.stop) viewkit.tui._THEME.clear() self.addCleanup(viewkit.tui._THEME.clear) def _view(self, keys, **kwargs): screen = FakeScreen(keys=keys) return EchoView(screen, **kwargs), screen def test_worker_runs_and_terminal_key_ends_the_view(self): view, screen = self._view(keys=[ord("x")], terminal_after=2) self.assertEqual(view.run(), "terminal") # Both queued events were drained before the key was read. self.assertEqual(view.phase, "done") def test_timeout_getch_keeps_the_loop_going(self): # -1 (redraw timeout) events never end the loop. view, screen = self._view(keys=[-1, ord("x")], terminal_after=1) self.assertEqual(view.run(), "terminal") def test_esc_confirms_cancel_then_ends_via_after_cancel(self): view, screen = self._view(keys=[27]) with patch.object(view, "_prompt_cancel", return_value=True) as mk, \ patch.object(view, "_after_cancel", return_value="cancelled") as mk_after: self.assertEqual(view.run(), "cancelled") mk.assert_called_once() mk_after.assert_called_once() def test_esc_declined_keeps_the_view_running(self): view, screen = self._view(keys=[27, ord("x")], terminal_after=1) with patch.object(view, "_prompt_cancel", return_value=False): self.assertEqual(view.run(), "terminal") def test_early_exit_wins_before_render(self): view, screen = self._view(keys=[], terminal_after=1) with patch.object(view, "_early_exit", return_value="early"), \ patch.object(view, "render") as mk_render: self.assertEqual(view.run(), "early") mk_render.assert_not_called() def test_stop_hook_restores_blocking_getch(self): view, screen = self._view(keys=[ord("x")], terminal_after=1) view.run() self.assertEqual(screen.timeouts[-1], -1) class DefaultPromptCancelTests(unittest.TestCase): def setUp(self): self.curses = FakeCurses() patcher = patch.dict(sys.modules, {"curses": self.curses}) patcher.start() self.addCleanup(patcher.stop) viewkit.tui._THEME.clear() self.addCleanup(viewkit.tui._THEME.clear) def test_confirm_sets_cancel_and_joins_the_worker(self): screen = FakeScreen(keys=[]) view = EchoView(screen) joined = threading.Event() class Worker: def join(self, timeout=None): joined.set() view._worker = Worker() with patch.object(viewkit.tui, "confirm", return_value=True): self.assertTrue(view._prompt_cancel()) self.assertTrue(joined.is_set()) self.assertTrue(view.cancelling) self.assertTrue(view._cancel.is_set()) def test_declined_cancel_leaves_the_run_alone(self): screen = FakeScreen(keys=[]) view = EchoView(screen) with patch.object(viewkit.tui, "confirm", return_value=False): self.assertFalse(view._prompt_cancel()) self.assertFalse(view.cancelling) self.assertFalse(view._cancel.is_set()) if __name__ == "__main__": unittest.main()