"""Tests for the TUI hub (hub.py) menu and helpers. The hub drives the same curses widgets as tui.py, so these tests reuse the fake curses/screen from test_tui to run the menu without a terminal. """ import unittest from pathlib import Path from unittest.mock import patch import hub import tui from tests.test_tui import FakeCurses, FakeScreen class HubHelperTests(unittest.TestCase): """Pure helpers in hub.py (no curses).""" def test_is_float(self): self.assertTrue(hub._is_float("1.0")) self.assertTrue(hub._is_float("2")) self.assertFalse(hub._is_float("abc")) self.assertFalse(hub._is_float("")) def test_list_voices_from_dir(self): with __import__("tempfile").TemporaryDirectory() as td: d = Path(td) (d / "Narrator.wav").write_bytes(b"x") (d / "Alpha.WAV").write_bytes(b"x") (d / "notes.txt").write_bytes(b"x") voices = hub._list_voices(str(d)) # Stems preserve case; sorting is case-insensitive. self.assertEqual(voices, ["Alpha", "Narrator"]) def test_list_voices_missing_dir(self): self.assertEqual(hub._list_voices("/no/such/dir"), []) def test_status_mark(self): from backends import BackendStatus ready = BackendStatus("k", "l", installed=True, configured=True) half = BackendStatus("k", "l", installed=True, configured=False) none = BackendStatus("k", "l", installed=False, configured=False) self.assertEqual(hub._status_mark("k", [ready]), "ready") self.assertEqual(hub._status_mark("k", [half]), "installed") self.assertEqual(hub._status_mark("k", [none]), "not set up") self.assertEqual(hub._status_mark("missing", []), "not set up") class HubMenuTests(unittest.TestCase): """Drive _hub_menu with a fake screen (no terminal).""" def setUp(self): tui._THEME.clear() self.curses = FakeCurses() from unittest.mock import patch as _patch self._patcher = _patch.dict("sys.modules", {"curses": self.curses}) self._patcher.start() self.addCleanup(self._patcher.stop) self.addCleanup(tui._THEME.clear) def test_quit_returns_none(self): # Main menu: move to "Quit" (4th option, index 3) and press Enter. screen = FakeScreen(keys=[FakeCurses.KEY_DOWN, FakeCurses.KEY_DOWN, FakeCurses.KEY_DOWN, 10]) with patch.object(hub, "detect_all", return_value=[]): result = hub._hub_menu(screen) self.assertIsNone(result) def test_convert_with_no_ready_backend_offers_setup(self): # Convert -> "Set up a backend..." is the only entry -> Enter selects # it -> setup menu lists 3 backends; press Esc to go back -> convert # returns None -> main menu loops. Then quit (Down x3 + Enter). from backends import BackendInfo, BackendStatus none = BackendStatus("k", "l", installed=False, configured=False) infos = [BackendInfo("audiocpp", "audio.cpp", lambda: none, lambda: 0), BackendInfo("qwen", "Qwen", lambda: none, lambda: 0), BackendInfo("faster", "faster", lambda: none, lambda: 0)] with patch.object(hub, "detect_all", return_value=[none, none, none]), \ patch.object(hub, "REGISTRY", infos): # Convert(Enter), setup-entry(Enter), Esc on setup menu, # back at main menu -> Down x3 -> Enter (Quit). screen = FakeScreen(keys=[10, 10, 27, FakeCurses.KEY_DOWN, FakeCurses.KEY_DOWN, FakeCurses.KEY_DOWN, 10]) result = hub._hub_menu(screen) self.assertIsNone(result) if __name__ == "__main__": unittest.main()