Skip to content

Commit 3746c53

Browse files
authored
Merge pull request #29 from HS-Teams/codex/add-tests-for-tuimenu-functionality
Add unit tests for TUIMenu prompt and builders
2 parents 679344a + 93c9d53 commit 3746c53

File tree

1 file changed

+205
-0
lines changed

1 file changed

+205
-0
lines changed
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
import sys
5+
import unittest
6+
from pathlib import Path
7+
from typing import Optional
8+
from types import SimpleNamespace
9+
from unittest.mock import Mock, patch
10+
11+
TEST_DIR = Path(__file__).resolve().parent
12+
if str(TEST_DIR) not in sys.path:
13+
sys.path.insert(0, str(TEST_DIR))
14+
15+
from _test_setup import setup_test_environment
16+
17+
setup_test_environment()
18+
19+
from clitt.core.tui.menu.tui_menu import TUIMenu
20+
from clitt.core.tui.menu.tui_menu_action import TUIMenuAction
21+
from clitt.core.tui.menu.tui_menu_factory import TUIMenuFactory
22+
from clitt.core.tui.menu.tui_menu_item import TUIMenuItem
23+
from clitt.core.tui.menu.tui_menu_view import TUIMenuView
24+
from clitt.core.tui.minput.input_validator import InputValidator
25+
from clitt.core.tui.menu.tui_menu_ui import TUIMenuUi
26+
from clitt.core.tui.tui_preferences import TUIPreferences
27+
from clitt.core.term.terminal import Terminal
28+
29+
30+
def _install_terminal_stub() -> None:
31+
cursor = SimpleNamespace(
32+
erase=lambda *_, **__: None,
33+
writeln=lambda *_, **__: None,
34+
write=lambda *_, **__: None,
35+
save=lambda *_, **__: None,
36+
restore=lambda *_, **__: None,
37+
move=lambda *_, **__: None,
38+
track=lambda *_, **__: None,
39+
end=lambda *_, **__: None,
40+
reset_mode=lambda *_, **__: None,
41+
home=lambda *_, **__: None,
42+
)
43+
preferences = TUIPreferences()
44+
screen = SimpleNamespace(
45+
lines=25,
46+
columns=80,
47+
cursor=cursor,
48+
preferences=preferences,
49+
add_watcher=lambda *_, **__: None,
50+
clear=lambda: None,
51+
)
52+
53+
class _TerminalStub:
54+
def __init__(self, screen_ref):
55+
self.screen = screen_ref
56+
57+
def echo(self, *_, **__):
58+
pass
59+
60+
Terminal.INSTANCE = _TerminalStub(screen)
61+
TUIMenuUi.SCREEN = screen
62+
TUIMenuUi.PREFS = preferences
63+
TUIMenuUi.MENU_LINE = f"{'--' * TUIMenuUi.PREFS.title_line_length}"
64+
TUIMenuUi.MENU_TITLE_FMT = (
65+
f"{TUIMenuUi.PREFS.title_color}"
66+
f"+{TUIMenuUi.MENU_LINE}+%EOL%"
67+
"|{title:^" + str(2 * TUIMenuUi.PREFS.title_line_length) + "s}|%EOL%"
68+
f"+{TUIMenuUi.MENU_LINE}+%EOL%%NC%"
69+
)
70+
71+
72+
_install_terminal_stub()
73+
74+
75+
class TestTuiMenu(unittest.TestCase):
76+
77+
def test_prompt_uses_default_dest_and_validator(self):
78+
captured_fields: list = []
79+
80+
def fake_minput(form_fields):
81+
captured_fields.append(form_fields)
82+
return "result"
83+
84+
with patch("clitt.core.tui.menu.tui_menu.minput", side_effect=fake_minput) as mock_minput:
85+
result = TUIMenu.prompt("Full Name")
86+
87+
self.assertEqual("result", result)
88+
mock_minput.assert_called_once()
89+
self.assertEqual(1, len(captured_fields))
90+
self.assertEqual(1, len(captured_fields[0]))
91+
field = captured_fields[0][0]
92+
self.assertEqual("Full Name", field.label)
93+
self.assertEqual("Full Name", field.dest)
94+
self.assertEqual(InputValidator.words().pattern_type, field.input_validator.pattern_type)
95+
self.assertEqual(1, field.min_length)
96+
self.assertEqual(32, field.max_length)
97+
98+
def test_prompt_accepts_custom_validator_and_dest(self):
99+
captured_fields: list = []
100+
validator = InputValidator.letters()
101+
102+
def fake_minput(form_fields):
103+
captured_fields.append(form_fields)
104+
return "custom"
105+
106+
with patch("clitt.core.tui.menu.tui_menu.minput", side_effect=fake_minput) as mock_minput:
107+
result = TUIMenu.prompt("User", dest="username", min_length=2, max_length=10, validator=validator)
108+
109+
self.assertEqual("custom", result)
110+
mock_minput.assert_called_once()
111+
field = captured_fields[0][0]
112+
self.assertEqual("username", field.dest)
113+
self.assertIs(field.input_validator, validator)
114+
self.assertEqual(2, field.min_length)
115+
self.assertEqual(10, field.max_length)
116+
117+
def test_factory_builders_create_nested_menu_tree(self):
118+
callbacks: list[Optional[TUIMenu]] = []
119+
120+
def save_cb(menu: TUIMenu) -> Optional[TUIMenu]:
121+
callbacks.append(menu)
122+
return menu
123+
124+
def reset_cb(menu: TUIMenu) -> Optional[TUIMenu]:
125+
callbacks.append(None)
126+
return None
127+
128+
def quit_cb(menu: TUIMenu) -> Optional[TUIMenu]:
129+
callbacks.append(menu.parent)
130+
return menu.parent
131+
132+
factory = (
133+
TUIMenuFactory.create_main_menu("Main")
134+
.with_item("Settings", "Adjust settings")
135+
.with_action("Save", "Persist changes").on_trigger(save_cb)
136+
.with_view("About", "About view").on_render("About content")
137+
.with_item("Advanced", "Advanced settings")
138+
.with_action("Reset", "Reset to defaults").on_trigger(reset_cb)
139+
.then()
140+
.then()
141+
.with_action("Quit", "Exit").on_trigger(quit_cb)
142+
.then()
143+
)
144+
145+
main_menu = factory.build()
146+
147+
self.assertIsInstance(main_menu, TUIMenuItem)
148+
self.assertEqual("Main", main_menu.title)
149+
self.assertEqual(2, len(main_menu.items))
150+
151+
settings, quit_action = main_menu.items
152+
self.assertIsInstance(settings, TUIMenuItem)
153+
self.assertEqual("Settings", settings.title)
154+
self.assertEqual(3, len(settings.items))
155+
156+
save_action, about_view, advanced_item = settings.items
157+
self.assertIsInstance(save_action, TUIMenuAction)
158+
self.assertIs(save_action._on_trigger, save_cb)
159+
160+
self.assertIsInstance(about_view, TUIMenuView)
161+
self.assertEqual("About content", about_view._content)
162+
self.assertEqual(about_view._display_content, about_view._on_render)
163+
164+
self.assertIsInstance(advanced_item, TUIMenuItem)
165+
self.assertEqual("Advanced", advanced_item.title)
166+
self.assertEqual(1, len(advanced_item.items))
167+
reset_action = advanced_item.items[0]
168+
self.assertIsInstance(reset_action, TUIMenuAction)
169+
self.assertIs(reset_action._on_trigger, reset_cb)
170+
171+
self.assertIsInstance(quit_action, TUIMenuAction)
172+
self.assertIs(quit_action._on_trigger, quit_cb)
173+
174+
def test_view_render_switches_between_content_and_callbacks(self):
175+
parent_menu = TUIMenuItem(title="Parent")
176+
view = TUIMenuView(parent_menu, title="Details")
177+
178+
with patch.object(TUIMenuUi, "render_app_title") as mock_title, \
179+
patch.object(view, "draw_navbar") as mock_navbar:
180+
display_mock = Mock()
181+
view._display_content = display_mock
182+
view.on_render("Custom text")
183+
184+
view.render()
185+
186+
mock_title.assert_called_once()
187+
display_mock.assert_called_once_with()
188+
mock_navbar.assert_called_once_with(view.navbar())
189+
self.assertEqual("Custom text", view._content)
190+
191+
with patch.object(TUIMenuUi, "render_app_title") as mock_title, \
192+
patch.object(view, "draw_navbar") as mock_navbar:
193+
custom_hook = Mock()
194+
view.on_render(custom_hook)
195+
196+
view.render()
197+
198+
mock_title.assert_called_once()
199+
custom_hook.assert_called_once_with()
200+
mock_navbar.assert_called_once_with(view.navbar())
201+
self.assertEqual(f"This is a view: {view.title}", view._content)
202+
203+
204+
if __name__ == "__main__":
205+
unittest.main()

0 commit comments

Comments
 (0)