From 2a9a78dd1caec0811ed2640b28375890ee96e4cc Mon Sep 17 00:00:00 2001 From: historia Date: Thu, 27 Aug 2026 17:46:02 -0400 Subject: feat: help menu in tui --- app/tests/test_hub.py | 109 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 75 insertions(+), 34 deletions(-) (limited to 'app/tests/test_hub.py') diff --git a/app/tests/test_hub.py b/app/tests/test_hub.py index 4186c92..798c27f 100644 --- a/app/tests/test_hub.py +++ b/app/tests/test_hub.py @@ -143,9 +143,11 @@ class HubMenuTests(unittest.TestCase): return BackendStatus(key, label, installed=False, configured=False) def test_quit_returns_none_when_no_backend(self): - # No backends installed/running: menu is [Configure backends, - # Settings, Quit]. Quit is the 3rd option (Down twice) then Enter. - screen = FakeScreen(keys=[FakeCurses.KEY_DOWN, FakeCurses.KEY_DOWN, 10]) + # No backends installed/running: menu is [Configure Backends, + # Settings, Help, Quit]. Quit is the 4th option (Down x3) then + # 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(screen).run() self.assertIsNone(result) @@ -190,7 +192,8 @@ class HubMenuTests(unittest.TestCase): patch.object(hub, "detect_all", return_value=[]): hub._Hub(screen).run() labels = [label for label, _ in captured["options"]] - self.assertEqual(labels, ["Configure backends", "Settings", "Quit"]) + self.assertEqual(labels, + ["Configure Backends", "Settings", "Help", "Quit"]) def test_menu_has_all_five_when_one_installed(self): captured = {} @@ -209,8 +212,8 @@ class HubMenuTests(unittest.TestCase): labels = [label for label, _ in captured["options"]] self.assertEqual( labels, - ["Generate audiobooks", "Configure backends", - "Start/Stop Backend Servers", "Settings", "Quit"]) + ["Generate Audiobooks", "Configure Backends", + "Start/Stop Backend Servers", "Settings", "Help", "Quit"]) # The status table is passed through, one row per backend. self.assertEqual(captured["rows"], [("qwen-tts", "installed", "ok", "body")]) @@ -256,8 +259,8 @@ class HubMenuTests(unittest.TestCase): labels = [label for label, _ in captured["options"]] self.assertEqual( labels, - ["Generate audiobooks", "Configure backends", "Settings", - "Quit"]) + ["Generate Audiobooks", "Configure Backends", "Settings", + "Help", "Quit"]) def test_ffmpeg_warning_shown_when_missing(self): # ffmpeg not on PATH → a red notice is passed above the table. @@ -293,8 +296,8 @@ class HubMenuTests(unittest.TestCase): def test_convert_with_no_available_backend_flashes(self): # Installed-but-not-ready backends → Convert is offered, but the # convert flow has nothing to list: it flashes a hint (no "Configure - # backends" detour anymore) and returns to the main menu. Then - # quit: 5 main-menu options, Quit is the 5th (Down x4). + # Backends" detour anymore) and returns to the main menu. Then + # quit: 6 main-menu options, Quit is the 6th (Down x5). from backends import BackendStatus statuses = [BackendStatus("audiocpp", "audio.cpp", installed=True, configured=False), @@ -309,16 +312,54 @@ class HubMenuTests(unittest.TestCase): with patch.object(hub, "detect_all", return_value=statuses), \ patch.object(hub.tui, "flash", fake_flash): - # Convert(Enter) → flash → main menu; Down x4 -> Quit, Enter. + # Convert(Enter) → flash → main menu; Down x5 -> Quit, Enter. screen = FakeScreen(keys=[10, FakeCurses.KEY_DOWN, FakeCurses.KEY_DOWN, FakeCurses.KEY_DOWN, FakeCurses.KEY_DOWN, + FakeCurses.KEY_DOWN, 10]) result = hub._Hub(screen).run() self.assertIsNone(result) self.assertEqual(len(flashed), 1) self.assertIn("No backend is ready", flashed[0]) + def test_help_opens_viewer_and_backs_out(self): + # Selecting Help opens the text viewer with the quick-start text + # (real folder paths); closing it lands back on the main menu. + calls = [] + + def fake_viewer(stdscr, title, lines, **kwargs): + calls.append((title, list(lines), kwargs)) + return kwargs.get("back_value") + + with patch.object(hub, "detect_all", return_value=[]), \ + patch.object(hub.tui, "text_viewer", fake_viewer): + # Help is the 3rd main-menu option (Down x2), then Enter; + # back on the main menu Quit is the 4th (Down x3), Enter. + screen = FakeScreen(keys=[FakeCurses.KEY_DOWN, FakeCurses.KEY_DOWN, + 10, + FakeCurses.KEY_DOWN, FakeCurses.KEY_DOWN, + FakeCurses.KEY_DOWN, + 10]) + result = hub._Hub(screen).run() + self.assertIsNone(result) + self.assertEqual(len(calls), 1) + title, lines, kwargs = calls[0] + self.assertEqual(title, "Help") + self.assertIs(kwargs.get("back_value"), tui.Wizard.BACK) + text = "\n".join(lines) + self.assertIn("1. Put your ebooks (epub, txt, or pdf) here:", text) + self.assertIn(str(hub.BOOKS_FOLDER), text) + self.assertIn("2. Put any .wavs of voices to clone here:", text) + self.assertIn(str(hub.common.VOICES_DIR), text) + self.assertIn("Install Backend and install audio.cpp.", text) + self.assertIn("qwen3_tts_1_7b_base_q8_0", text) + self.assertIn("qwen3_tts_1_7b_customvoice_q8_0", text) + self.assertIn("5. Go to Generate Audiobooks.", text) + self.assertIn("6. Generated audiobooks (m4b, mp3, etc.) will " + "output here:", text) + self.assertIn(str(hub.AUDIOBOOKS_FOLDER), text) + class SubmenuStatusTableTests(unittest.TestCase): """First picker screen of every flow repeats the backend status table. @@ -375,7 +416,7 @@ class SubmenuStatusTableTests(unittest.TestCase): self.assertEqual([label for label, _ in captured["options"]], ["Install Backend", "Uninstall Backend"]) # ...the shared status table carries the states instead. - self.assertEqual(captured["table_title"], "Backend status") + self.assertEqual(captured["table_title"], "Backend Status") self.assertEqual( captured["table_rows"], [("qwen-tts", "installed", "ok", "body"), @@ -421,7 +462,7 @@ class SubmenuStatusTableTests(unittest.TestCase): # the whole set of installed backends (faster has nothing on disk # and so contributes nothing). self.assertEqual([label for label, _ in captured["options"]], - ["Install Backend", "Update backends", + ["Install Backend", "Update Backends", "Uninstall Backend"]) def test_configure_backends_menu_audiocpp_model_actions(self): @@ -492,9 +533,9 @@ class SubmenuStatusTableTests(unittest.TestCase): # model download stays hidden until the binary exists — Build and # Download never coexist. Configure needs an installed (built) # backend. - self.assertEqual(labels, ["Build audio.cpp server", "Uninstall Backend"]) + self.assertEqual(labels, ["Build audio.cpp Server", "Uninstall Backend"]) self.assertEqual(captured["options"][0], - ("Build audio.cpp server", "build_audiocpp", + ("Build audio.cpp Server", "build_audiocpp", ("[recommended]", "warn"))) self.assertIs(captured["options"][1], tui.MENU_SEPARATOR) @@ -520,7 +561,7 @@ class SubmenuStatusTableTests(unittest.TestCase): result = hub._Hub(None).screen_configure() self.assertIs(result, tui.Wizard.BACK) labels = self._labels(captured["options"]) - self.assertNotIn("Build audio.cpp server", labels) + self.assertNotIn("Build audio.cpp Server", labels) def test_configure_backends_menu_configure_only_when_built_unconfigured(self): captured = {} @@ -607,7 +648,7 @@ class SubmenuStatusTableTests(unittest.TestCase): def test_bare_qwen_without_configure_screen_still_has_no_entry(self): # Without a dedicated configure screen, plain qwen stays excluded - # from Configure backends (its wizard asks nothing to configure). + # from Configure Backends (its wizard asks nothing to configure). captured = {} infos = [BackendInfo("qwen", "qwen-tts", lambda: None, lambda: 0, uninstall=lambda **kwargs: 0)] @@ -633,7 +674,7 @@ class SubmenuStatusTableTests(unittest.TestCase): patch.object(hub.shutil, "which", return_value="/x"): result = hub._Hub(None).screen_convert() self.assertIs(result, tui.Wizard.BACK) - self.assertEqual(captured["title"], "Generate audiobooks") + self.assertEqual(captured["title"], "Generate Audiobooks") # One form, no picker menu: the first field is the Backend picker, # and only convertible backends are offered in it. self.assertEqual(captured["fields"][0]["key"], "backend") @@ -677,7 +718,7 @@ class SubmenuStatusTableTests(unittest.TestCase): patch.object(hub.shutil, "which", return_value="/x"): result = hub._Hub(None).screen_configure() self.assertIs(result, tui.Wizard.BACK) - self.assertEqual(captured["table_title"], "Backend status") + self.assertEqual(captured["table_title"], "Backend Status") self.assertEqual( captured["table_rows"], [("qwen-tts", "installed", "ok", "body")]) @@ -706,7 +747,7 @@ class SubmenuStatusTableTests(unittest.TestCase): ["audio.cpp"]) # The running/stopped state lives in the status table above the # menu (not on the entries, whose colors the selection bar covers). - self.assertEqual(captured["table_title"], "Server status") + self.assertEqual(captured["table_title"], "Server Status") self.assertEqual(captured["table_rows"], [("audio.cpp", "stopped", "err", "body")]) @@ -863,7 +904,7 @@ class ConvertFlowTests(unittest.TestCase): # One form, not a cascade of menus/editors. self.assertEqual(len(self.tui.forms_seen), 1) title, fields, form_kwargs = self.tui.forms_seen[0] - self.assertEqual(title, "Generate audiobooks") + self.assertEqual(title, "Generate Audiobooks") self.assertEqual([f["key"] for f in fields], ["backend", "audiocpp-remote.model_id", "audiocpp-remote.audiocpp_voice", @@ -991,7 +1032,7 @@ class ConvertFlowTests(unittest.TestCase): def test_audiocpp_local_without_voice_dir_points_at_configure(self): # The managed entry's server.json has no voice_dir: clone-capable # models get an empty Voice picker whose hint sends the user to - # Configure backends instead of crashing on menu(). + # Configure Backends instead of crashing on menu(). with tempfile.TemporaryDirectory() as td: root = Path(td) (root / "server.json").write_text(json.dumps({ @@ -1012,7 +1053,7 @@ class ConvertFlowTests(unittest.TestCase): error = voice_field["validate"]("") self.assertIsNotNone(error) self.assertIn(".wav", error) - self.assertIn("Configure backends", error) + self.assertIn("Configure Backends", error) def test_audiocpp_remote_missing_family_is_clone_capable(self): # A missing family is unknown — not guessed as qwen3_tts — so the @@ -2213,7 +2254,7 @@ class SettingsTests(unittest.TestCase): self.assertIsNotNone(hub._validate_port("abc")) def test_language_fields_are_pickers_with_edit_hint(self): - # Both Language fields (Settings and Generate audiobooks) are + # Both Language fields (Settings and Generate Audiobooks) are # static pickers over the audio.cpp-menu languages, with a dim # hint inside their edit dialog. Common languages lead. expected_choices = ["English", "Spanish", "Chinese", "French", @@ -2820,12 +2861,12 @@ class ConfigureBackendsDispatchTests(unittest.TestCase): return_value=0) as mk_run, \ patch_flash: hub._update_backends_action(None) - # One task-view run titled "Update backends", one step per + # One task-view run titled "Update Backends", one step per # updatable backend in registry order; executing a step # forwards emit/cancel to that backend's update. mk_run.assert_called_once() self.assertEqual(mk_run.call_args[0][0], None) - self.assertEqual(mk_run.call_args[0][1], "Update backends") + self.assertEqual(mk_run.call_args[0][1], "Update Backends") steps = mk_run.call_args[0][2] self.assertEqual([step.title for step in steps], ["Update audio.cpp", "Update qwen-tts"]) @@ -2908,7 +2949,7 @@ class ConfigureBackendsDispatchTests(unittest.TestCase): self.assertEqual(invalidated, [True]) # An inline action: the same menu re-shows (second title) with a # freshly detected status table. - self.assertEqual(titles, ["Configure backends", "Configure backends"]) + self.assertEqual(titles, ["Configure Backends", "Configure Backends"]) def test_pick_backend_install_lists_uninstalled_only(self): captured = {} @@ -3067,7 +3108,7 @@ class HubNavigationTests(unittest.TestCase): def test_esc_on_wizard_first_screen_returns_to_configure(self): # The reported bug: Esc on the audio.cpp "Select TTS model # families" tree (the wizard's first screen) must land back on - # "Configure backends", not the main menu. + # "Configure Backends", not the main menu. info = self._info() with patch.object(info, "setup_screen", return_value=1): titles = self._drive( @@ -3076,8 +3117,8 @@ class HubNavigationTests(unittest.TestCase): [self._status()], [info]) self.assertEqual( titles, - ["tts-audiobook-generator", "Configure backends", - "Configure backends", "tts-audiobook-generator"]) + ["tts-audiobook-generator", "Configure Backends", + "Configure Backends", "tts-audiobook-generator"]) def test_esc_on_install_picker_returns_to_configure(self): registry = [self._info("audiocpp", "audio.cpp"), @@ -3091,8 +3132,8 @@ class HubNavigationTests(unittest.TestCase): statuses, registry) self.assertEqual( titles, - ["tts-audiobook-generator", "Configure backends", - "Install Backend", "Configure backends", + ["tts-audiobook-generator", "Configure Backends", + "Install Backend", "Configure Backends", "tts-audiobook-generator"]) def test_esc_on_server_action_returns_one_screen_at_a_time(self): @@ -3122,8 +3163,8 @@ class HubNavigationTests(unittest.TestCase): # Esc steps back one screen at a time to the server list and main. self.assertEqual( titles, - ["tts-audiobook-generator", "Start / Stop a server", - "Start / Stop a server", "tts-audiobook-generator"]) + ["tts-audiobook-generator", "Start / Stop A Server", + "Start / Stop A Server", "tts-audiobook-generator"]) def test_esc_on_main_menu_quits(self): titles = self._drive([tui.Wizard.BACK], [], []) -- cgit v1.2.3