aboutsummaryrefslogtreecommitdiff
path: root/app/tests/test_hub.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-27 17:46:02 -0400
committerhistoria <historiavg@proton.me>2026-08-27 17:46:02 -0400
commit2a9a78dd1caec0811ed2640b28375890ee96e4cc (patch)
tree6aebbb503a98eb035015ff56e51d4ccc4f4a395e /app/tests/test_hub.py
parentcef2352a5e81b272d067c2c02eb9588e54edfcfd (diff)
downloadtts-audiobook-generator-2a9a78dd1caec0811ed2640b28375890ee96e4cc.tar.gz
feat: help menu in tui
Diffstat (limited to 'app/tests/test_hub.py')
-rw-r--r--app/tests/test_hub.py109
1 files changed, 75 insertions, 34 deletions
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], [], [])