aboutsummaryrefslogtreecommitdiff
path: root/tests/test_tui.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_tui.py')
-rw-r--r--tests/test_tui.py661
1 files changed, 0 insertions, 661 deletions
diff --git a/tests/test_tui.py b/tests/test_tui.py
deleted file mode 100644
index c121d55..0000000
--- a/tests/test_tui.py
+++ /dev/null
@@ -1,661 +0,0 @@
-"""Tests for the DOS-style curses TUI widgets in tui.py.
-
-The widget module imports curses lazily, so these tests swap the
-curses module for a small fake (patched into sys.modules) and drive
-the widgets with scripted keys against a recording screen. That works
-without a terminal and lets the tests assert exact drawing
-coordinates: theme colors, the left margin of list rows, and that no
-row ever paints over the dialog border.
-"""
-
-import sys
-import tempfile
-import unittest
-from pathlib import Path
-from unittest.mock import patch
-
-from ui import tui
-
-
-class FakeCurses:
- """Minimal curses stand-in: attributes, key codes, color pairs."""
-
- A_BOLD = 1
- A_DIM = 2
- A_REVERSE = 4
-
- COLOR_BLACK = 0
- COLOR_RED = 1
- COLOR_GREEN = 2
- COLOR_YELLOW = 3
- COLOR_BLUE = 4
- COLOR_MAGENTA = 5
- COLOR_CYAN = 6
- COLOR_WHITE = 7
-
- KEY_DOWN = 0x101
- KEY_UP = 0x102
- KEY_LEFT = 0x103
- KEY_RIGHT = 0x104
- KEY_HOME = 0x105
- KEY_END = 0x106
- KEY_PPAGE = 0x107
- KEY_NPAGE = 0x108
- KEY_BACKSPACE = 0x109
- KEY_BTAB = 0x10A
-
- ACS_ULCORNER = "ul"
- ACS_URCORNER = "ur"
- ACS_LLCORNER = "ll"
- ACS_LRCORNER = "lr"
- ACS_VLINE = "v"
- ACS_HLINE = "h"
-
- class error(Exception):
- pass
-
- def __init__(self):
- self.pairs = {} # pair number -> (fg, bg)
- self.colors = True
-
- def has_colors(self):
- return self.colors
-
- def start_color(self):
- pass
-
- def init_pair(self, number, fg, bg):
- self.pairs[number] = (fg, bg)
-
- def color_pair(self, number):
- return number << 8
-
- def curs_set(self, visibility):
- pass
-
- def endwin(self):
- pass
-
-
-class FakeScreen:
- """Recording curses window; getch() replays scripted keys."""
-
- def __init__(self, keys=(), width=80, height=24):
- self.keys = list(keys)
- self.width = width
- self.height = height
- self.strings = [] # (y, x, text, attr) from addstr
- self.chars = [] # (y, x, ch, attr) from addch
-
- def getmaxyx(self):
- return self.height, self.width
-
- def erase(self):
- pass
-
- def refresh(self):
- pass
-
- def bkgd(self, ch, attr):
- pass
-
- def addstr(self, y, x, text, attr=0):
- self.strings.append((y, x, text, attr))
-
- def addch(self, y, x, ch, attr=0):
- self.chars.append((y, x, ch, attr))
-
- def hline(self, y, x, ch, n, attr=0):
- pass
-
- def redrawwin(self):
- pass
-
- def getch(self):
- if not self.keys:
- raise AssertionError("the script ran out of keys")
- return self.keys.pop(0)
-
-
-class TuiTestCase(unittest.TestCase):
- """Base class: fresh theme + fake curses module for every test."""
-
- def setUp(self):
- tui._THEME.clear()
- self.curses = FakeCurses()
- self.screen = FakeScreen()
- patcher = patch.dict(sys.modules, {"curses": self.curses})
- patcher.start()
- self.addCleanup(patcher.stop)
- self.addCleanup(tui._THEME.clear)
-
- def dialog_box(self, screen=None):
- """(x0, x_right) border columns of the drawn dialog."""
- screen = screen or self.screen
- corners = screen.chars[:2]
- self.assertEqual(corners[0][2], FakeCurses.ACS_ULCORNER)
- self.assertEqual(corners[1][2], FakeCurses.ACS_URCORNER)
- return corners[0][1], corners[1][1]
-
- def assert_inside_border(self, screen=None):
- """No drawn string may reach the right border column."""
- screen = screen or self.screen
- _, x_right = self.dialog_box(screen)
- for _, x, text, _ in screen.strings:
- self.assertLessEqual(
- x + len(text), x_right,
- f"{text!r} painted over the border at x={x}")
-
-
-class ThemeTests(TuiTestCase):
- def test_desktop_and_message_backgrounds_are_black(self):
- frame = tui.Frame(self.screen, "Title", "footer")
- theme, pairs = frame.theme, self.curses.pairs
- for name in ("desktop", "border", "title", "ok", "warn", "err",
- "info", "input", "check", "accent"):
- fg, bg = pairs[theme[name] >> 8]
- self.assertEqual(bg, FakeCurses.COLOR_BLACK, name)
-
- def test_cursor_bar_and_active_button_stand_out(self):
- frame = tui.Frame(self.screen, "Title", "footer")
- theme, pairs = frame.theme, self.curses.pairs
- fg, bg = pairs[theme["bar"] >> 8]
- self.assertEqual((fg, bg),
- (FakeCurses.COLOR_BLACK, FakeCurses.COLOR_CYAN))
- fg, bg = pairs[theme["btn_on"] >> 8]
- self.assertEqual((fg, bg),
- (FakeCurses.COLOR_BLACK, FakeCurses.COLOR_GREEN))
-
- def test_without_colors_theme_uses_plain_attributes(self):
- self.curses.colors = False
- frame = tui.Frame(self.screen, "Title", "footer")
- self.assertEqual(self.curses.pairs, {})
- self.assertEqual(frame.theme["desktop"], 0)
- self.assertEqual(frame.theme["title"], FakeCurses.A_BOLD)
-
-
-class MenuTests(TuiTestCase):
- OPTIONS = [("first option", "one"), ("second option", "two")]
-
- def test_option_rows_left_justified_help_centered(self):
- screen = FakeScreen(keys=[10])
- value = tui.menu(screen, "Pick one", self.OPTIONS,
- help_lines=["Help text"])
- self.assertEqual(value, "one")
- x0, x_right = self.dialog_box(screen)
- margin = x0 + 1 + tui.Frame.LIST_MARGIN
- for label in ("first option", "second option"):
- x = next(x for _, x, text, _ in screen.strings if text == label)
- self.assertEqual(x, margin, label)
- inner_w = x_right - x0 - 1
- help_x = next(x for _, x, text, _ in screen.strings
- if text == "Help text")
- self.assertEqual(help_x, x0 + 1 + (inner_w - len("Help text")) // 2)
- self.assertGreater(help_x, margin)
- self.assert_inside_border(screen)
-
- def test_up_wraps_around_to_last_option(self):
- screen = FakeScreen(keys=[FakeCurses.KEY_UP, 10])
- value = tui.menu(screen, "Pick one", self.OPTIONS)
- self.assertEqual(value, "two")
-
- def test_no_option_painted_over_the_border(self):
- screen = FakeScreen(keys=[FakeCurses.KEY_END, 10])
- tui.menu(screen, "Pick", [("a" * 60, "a"), ("b", "b")])
- self.assert_inside_border(screen)
-
- def test_empty_options_rejected(self):
- with self.assertRaises(ValueError):
- tui.menu(self.screen, "Pick", [])
-
- def test_esc_returns_back_value(self):
- marker = object()
- screen = FakeScreen(keys=[27])
- self.assertIs(
- tui.menu(screen, "Pick", self.OPTIONS, back_value=marker),
- marker)
-
- def test_q_still_aborts_with_back_value(self):
- marker = object()
- screen = FakeScreen(keys=[ord("q")])
- with self.assertRaises(tui.WizardCancelled):
- tui.menu(screen, "Pick", self.OPTIONS, back_value=marker)
-
-
-class MenuTableTests(TuiTestCase):
- """The optional status table: aligned columns and colored statuses."""
-
- ROWS = [("audio.cpp", "not installed", "err"),
- ("qwen-tts", "installed", "warn"),
- ("faster-qwen3-tts", "running", "ok")]
-
- def test_name_column_left_aligned_at_margin(self):
- screen = FakeScreen(keys=[10])
- tui.menu(screen, "Hub", [("Quit", "quit")],
- table_title="Backend status", table_rows=self.ROWS)
- x0, _ = self.dialog_box(screen)
- margin = x0 + 1 + tui.Frame.LIST_MARGIN
- for name, _, _ in self.ROWS:
- x = next(x for _, x, text, _ in screen.strings
- if text.rstrip() == name)
- self.assertEqual(x, margin, name)
-
- def test_status_column_aligned_at_one_fixed_offset(self):
- screen = FakeScreen(keys=[10])
- tui.menu(screen, "Hub", [("Quit", "quit")],
- table_title="Backend status", table_rows=self.ROWS)
- x0, _ = self.dialog_box(screen)
- margin = x0 + 1 + tui.Frame.LIST_MARGIN
- name_w = max(len(name) for name, _, _ in self.ROWS)
- expected_x = margin + name_w # the " status" segment starts here
- for _, status, _ in self.ROWS:
- x = next(x for _, x, text, _ in screen.strings
- if text.strip() == status)
- self.assertEqual(x, expected_x, status)
-
- def test_status_text_uses_the_theme_kind_color(self):
- screen = FakeScreen(keys=[10])
- tui.menu(screen, "Hub", [("Quit", "quit")],
- table_title="Backend status", table_rows=self.ROWS)
- want = {"err": tui._THEME["err"], "warn": tui._THEME["warn"],
- "ok": tui._THEME["ok"]}
- for _, status, kind in self.ROWS:
- attr = next(a for _, _, text, a in screen.strings
- if text.strip() == status)
- self.assertEqual(attr, want[kind], status)
-
- def test_optional_name_kind_colors_the_name_column(self):
- # 4-element rows: the 4th value is a theme kind for the name.
- rows = [("gone", "unavailable", "err", "dim"),
- ("here", "running", "ok", "body")]
- screen = FakeScreen(keys=[10])
- tui.menu(screen, "Hub", [("Quit", "quit")], table_rows=rows)
- drawn = {text.rstrip(): attr for _, _, text, attr in screen.strings}
- self.assertEqual(drawn["gone"], tui._THEME["dim"])
- self.assertEqual(drawn["here"], tui._THEME["body"])
-
- def test_three_element_rows_default_to_body_names(self):
- screen = FakeScreen(keys=[10])
- tui.menu(screen, "Hub", [("Quit", "quit")],
- table_title="Backend status", table_rows=self.ROWS)
- for name, _, _ in self.ROWS:
- attr = next(a for _, _, text, a in screen.strings
- if text.rstrip() == name)
- self.assertEqual(attr, tui._THEME["body"], name)
-
- def test_table_title_is_dim_and_left_aligned(self):
- screen = FakeScreen(keys=[10])
- tui.menu(screen, "Hub", [("Quit", "quit")],
- table_title="Backend status", table_rows=self.ROWS)
- x0, _ = self.dialog_box(screen)
- margin = x0 + 1 + tui.Frame.LIST_MARGIN
- x, attr = next((x, a) for _, x, text, a in screen.strings
- if text == "Backend status")
- self.assertEqual(x, margin)
- self.assertEqual(attr, tui._THEME["dim"])
-
- def test_table_does_not_paint_over_the_border(self):
- screen = FakeScreen(keys=[10])
- tui.menu(screen, "Hub", [("Quit", "quit")],
- table_title="Backend status", table_rows=self.ROWS)
- self.assert_inside_border(screen)
-
-
-class ConfirmTests(TuiTestCase):
- def test_tab_switches_and_enter_activates(self):
- screen = FakeScreen(keys=[9, 10])
- self.assertFalse(tui.confirm(screen, "Overwrite?", default=True))
-
- def test_y_answers_directly(self):
- screen = FakeScreen(keys=[ord("y")])
- self.assertTrue(tui.confirm(screen, "Overwrite?", default=False))
-
- def test_enter_takes_the_default(self):
- screen = FakeScreen(keys=[10])
- self.assertTrue(tui.confirm(screen, "Overwrite?", default=True))
-
- def test_esc_aborts_without_cancel_value(self):
- screen = FakeScreen(keys=[27])
- with self.assertRaises(tui.WizardCancelled):
- tui.confirm(screen, "Overwrite?", default=True)
-
- def test_esc_returns_cancel_value(self):
- marker = object()
- screen = FakeScreen(keys=[27])
- self.assertIs(
- tui.confirm(screen, "Overwrite?", default=True,
- cancel_value=marker),
- marker)
-
- def test_q_returns_cancel_value(self):
- marker = object()
- screen = FakeScreen(keys=[ord("q")])
- self.assertIs(
- tui.confirm(screen, "Overwrite?", default=True,
- cancel_value=marker),
- marker)
-
-
-class LineEditTests(TuiTestCase):
- def test_typing_backspace_and_enter(self):
- keys = [ord("c"), ord("d"), FakeCurses.KEY_BACKSPACE, 10]
- screen = FakeScreen(keys=keys)
- value = tui.line_edit(screen, "Edit", "ab")
- self.assertEqual(value, "abc")
-
- def test_validation_error_keeps_editing(self):
- keys = [ord("x"), 10, FakeCurses.KEY_BACKSPACE, 10]
- screen = FakeScreen(keys=keys)
- value = tui.line_edit(
- screen, "Edit", "5",
- validate=lambda s: None if s.isdigit() else "digits only")
- self.assertEqual(value, "5")
-
- def test_esc_returns_back_value(self):
- marker = object()
- screen = FakeScreen(keys=[27])
- self.assertIs(
- tui.line_edit(screen, "Edit", "text", back_value=marker),
- marker)
-
- def test_q_stays_typeable_with_back_value(self):
- marker = object()
- screen = FakeScreen(keys=[ord("q"), 10])
- value = tui.line_edit(screen, "Edit", "te", back_value=marker)
- self.assertEqual(value, "teq")
-
-
-class FormTests(TuiTestCase):
- def _fields(self):
- # Fresh dicts each call: form() edits field values in place, and a
- # shared class attribute would leak edits between tests.
- return [
- {"key": "fmt", "label": "Format", "kind": "choice",
- "value": "m4b", "choices": ["mp3", "m4b", "ogg"]},
- {"key": "chunk", "label": "Chunk", "kind": "text",
- "value": "250"},
- ]
-
- def test_save_returns_current_values(self):
- screen = FakeScreen(keys=[9, 10]) # Tab -> Save, Enter
- result = tui.form(screen, "Settings", self._fields())
- self.assertEqual(result, {"fmt": "m4b", "chunk": "250"})
-
- def test_cancel_returns_back_value(self):
- marker = object()
- # Tab -> buttons, Left -> Cancel, Enter.
- screen = FakeScreen(keys=[9, FakeCurses.KEY_LEFT, 10])
- result = tui.form(screen, "Settings", self._fields(),
- back_value=marker)
- self.assertIs(result, marker)
-
- def test_esc_returns_back_value(self):
- marker = object()
- screen = FakeScreen(keys=[27])
- self.assertIs(
- tui.form(screen, "Settings", self._fields(), back_value=marker),
- marker)
-
- def test_q_still_aborts_with_back_value(self):
- marker = object()
- screen = FakeScreen(keys=[ord("q")])
- with self.assertRaises(tui.WizardCancelled):
- tui.form(screen, "Settings", self._fields(), back_value=marker)
-
- def test_choice_field_picks_another_value(self):
- # Enter opens the choice menu on 'm4b' (index 1); Down moves to
- # 'ogg', Enter accepts; Tab -> Save, Enter.
- screen = FakeScreen(keys=[10, FakeCurses.KEY_DOWN, 10, 9, 10])
- result = tui.form(screen, "Settings", self._fields())
- self.assertEqual(result, {"fmt": "ogg", "chunk": "250"})
-
- def test_text_field_edits_then_saves(self):
- # Down to the text row, Enter opens the editor, type 'x', Enter,
- # then Tab -> Save, Enter.
- screen = FakeScreen(
- keys=[FakeCurses.KEY_DOWN, 10, ord("x"), 10, 9, 10])
- result = tui.form(screen, "Settings", self._fields())
- self.assertEqual(result, {"fmt": "m4b", "chunk": "250x"})
-
- def test_validation_error_refuses_save_then_recovers(self):
- fields = [
- {"key": "chunk", "label": "Chunk", "kind": "text", "value": "bad",
- "validate": lambda s: None if s.isdigit() else "digits only"},
- {"key": "fmt", "label": "Format", "kind": "text", "value": "x"},
- ]
- # Tab->Save(Enter) fails, the flash consumes the next key; Enter
- # reopens the editor, Ctrl-U clears 'bad', type '120', Enter; then
- # Tab->Save(Enter).
- keys = [9, 10, 10, 10, 21, ord("1"), ord("2"), ord("0"), 10, 9, 10]
- screen = FakeScreen(keys=keys)
- result = tui.form(screen, "Settings", fields)
- self.assertEqual(result, {"chunk": "120", "fmt": "x"})
-
- def test_rows_left_justified_inside_the_border(self):
- screen = FakeScreen(keys=[9, 10])
- tui.form(screen, "Settings", self._fields())
- self.assert_inside_border(screen)
-
- def test_empty_fields_rejected(self):
- with self.assertRaises(ValueError):
- tui.form(self.screen, "Settings", [])
-
-
-def _accept_audio_cpp(entry: Path):
- """auto_select callback that accepts an 'audio.cpp' checkout root."""
- if entry.name == "audio.cpp" and (entry / "model_specs").is_dir():
- return entry
- return None
-
-
-class BrowseDirectoryTests(TuiTestCase):
- def setUp(self):
- super().setUp()
- tmp = tempfile.TemporaryDirectory()
- self.addCleanup(tmp.cleanup)
- self.root = Path(tmp.name)
- for name in ("alpha", "beta", "zulu"):
- (self.root / name).mkdir()
- (self.root / "noise.txt").write_text("x", encoding="utf-8")
-
- def _checkout_tree(self):
- """A temp dir containing an 'audio.cpp' checkout + a sibling dir."""
- tmp = tempfile.TemporaryDirectory()
- self.addCleanup(tmp.cleanup)
- root = Path(tmp.name)
- (root / "audio.cpp").mkdir()
- (root / "audio.cpp" / "model_specs").mkdir()
- (root / "other").mkdir()
- return root
-
- def test_listing_rows_left_justified(self):
- screen = FakeScreen(keys=[10])
- chosen = tui.browse_directory(screen, "Pick", start=self.root)
- self.assertEqual(chosen, self.root.resolve())
- x0, x_right = self.dialog_box(screen)
- margin = x0 + 1 + tui.Frame.LIST_MARGIN
- for label in ("[ Use this directory ]", "..",
- "alpha/", "beta/", "zulu/"):
- x = next(x for _, x, text, _ in screen.strings if text == label)
- self.assertEqual(x, margin, label)
- drawn = " ".join(text for _, _, text, _ in screen.strings)
- self.assertNotIn("noise.txt", drawn)
- self.assert_inside_border(screen)
-
- def test_enter_opens_highlighted_subdirectory(self):
- keys = [FakeCurses.KEY_DOWN, FakeCurses.KEY_DOWN, 10, 10]
- screen = FakeScreen(keys=keys)
- chosen = tui.browse_directory(screen, "Pick", start=self.root)
- self.assertEqual(chosen, (self.root / "alpha").resolve())
- self.assert_inside_border(screen)
-
- def test_enter_auto_accepts_matching_subdir(self):
- root = self._checkout_tree()
- # sel 0 = [ Use this directory ], 1 = .., 2 = audio.cpp/
- keys = [FakeCurses.KEY_DOWN, FakeCurses.KEY_DOWN, 10]
- screen = FakeScreen(keys=keys)
- chosen = tui.browse_directory(screen, "Pick", start=root,
- auto_select=_accept_audio_cpp)
- self.assertEqual(chosen, (root / "audio.cpp").resolve())
-
- def test_right_auto_accepts_matching_subdir(self):
- root = self._checkout_tree()
- keys = [FakeCurses.KEY_DOWN, FakeCurses.KEY_DOWN,
- FakeCurses.KEY_RIGHT]
- screen = FakeScreen(keys=keys)
- chosen = tui.browse_directory(screen, "Pick", start=root,
- auto_select=_accept_audio_cpp)
- self.assertEqual(chosen, (root / "audio.cpp").resolve())
-
- def test_use_this_directory_ignores_auto_select(self):
- # Enter on '[ Use this directory ]' must accept the listed dir
- # without ever consulting auto_select.
- root = self._checkout_tree()
- calls = []
-
- def callback(entry):
- calls.append(entry)
- return entry # would auto-accept any subdir if consulted
-
- screen = FakeScreen(keys=[10])
- chosen = tui.browse_directory(screen, "Pick", start=root,
- auto_select=callback)
- self.assertEqual(chosen, root.resolve())
- self.assertEqual(calls, [])
-
- def test_auto_select_returning_none_descends_normally(self):
- # A non-matching subdir (or a None reply) keeps browsing: Enter
- # descends into it, then '[ Use this directory ]' accepts it.
- root = self._checkout_tree()
- calls = []
-
- def callback(entry):
- calls.append(entry)
- return None
-
- # sel 0 = use, 1 = .., 2 = audio.cpp/, 3 = other/
- keys = [FakeCurses.KEY_DOWN, FakeCurses.KEY_DOWN,
- FakeCurses.KEY_DOWN, 10, 10]
- screen = FakeScreen(keys=keys)
- chosen = tui.browse_directory(screen, "Pick", start=root,
- auto_select=callback)
- self.assertEqual(chosen, (root / "other").resolve())
- # auto_select was consulted only for the highlighted 'other/' row.
- self.assertEqual([p.name for p in calls], ["other"])
-
- def test_esc_returns_back_value(self):
- marker = object()
- screen = FakeScreen(keys=[27])
- self.assertIs(
- tui.browse_directory(screen, "Pick", start=self.root,
- back_value=marker),
- marker)
-
- def test_q_still_aborts_with_back_value(self):
- marker = object()
- screen = FakeScreen(keys=[ord("q")])
- with self.assertRaises(tui.WizardCancelled):
- tui.browse_directory(screen, "Pick", start=self.root,
- back_value=marker)
-
-
-class CheckboxTreeTests(TuiTestCase):
- FAMILIES = [
- {"label": "Family one", "detail": "tts",
- "options": [{"key": "pkg-a", "label": "pkg-a", "recommended": True},
- {"key": "pkg-b", "label": "pkg-b", "recommended": False}]},
- {"label": "Family two", "detail": "tts, cloning",
- "options": [{"key": "pkg-c", "label": "pkg-c", "recommended": True}]},
- ]
-
- def test_nothing_selected_by_default(self):
- # Nothing is pre-checked: Enter alone flashes and waits, and a
- # selection only happens after Space checks an option. The first
- # Enter and the flash each consume a key.
- screen = FakeScreen(keys=[10, 10, ord(" "), 10])
- picked = tui.checkbox_tree(screen, "Pick models", self.FAMILIES)
- self.assertEqual(picked, [(0, "pkg-a")])
-
- def test_rows_left_justified_inside_the_border(self):
- screen = FakeScreen(keys=[ord(" "), 10])
- tui.checkbox_tree(screen, "Pick models", self.FAMILIES)
- x0, x_right = self.dialog_box(screen)
- margin = x0 + 1 + tui.Frame.LIST_MARGIN
- strings = sorted((y, x, text) for y, x, text, _ in screen.strings)
- # Family row: its checkbox starts at the margin.
- y_family = next(y for y, _, text in strings if text == "- Family one")
- family_box_x = next(x for y, x, text in strings
- if y == y_family and text == "[x] ")
- self.assertEqual(family_box_x, margin)
- # Option row: its checkbox sits one indent (2 columns) deeper.
- option_box_x = next(x for y, x, text in strings
- if text == "[x] " and y != y_family)
- self.assertEqual(option_box_x, margin + 4)
- self.assert_inside_border(screen)
-
- def test_long_indented_options_do_not_paint_over_the_border(self):
- # A wide screen keeps the dialog width driven by the option row
- # itself (not the footer), the geometry where the old centered
- # segments drawing could paint over the right border.
- families = [{"label": "F", "detail": "tts",
- "options": [{"key": "long", "label": "x" * 60,
- "recommended": True}]}]
- screen = FakeScreen(keys=[ord(" "), 10], width=120)
- picked = tui.checkbox_tree(screen, "Pick", families)
- self.assertEqual(picked, [(0, "long")])
- self.assert_inside_border(screen)
-
- def test_space_checks_then_enter_accepts(self):
- screen = FakeScreen(keys=[ord(" "), 10])
- picked = tui.checkbox_tree(screen, "Pick models", self.FAMILIES)
- self.assertEqual(picked, [(0, "pkg-a")])
-
- def test_empty_families_rejected(self):
- with self.assertRaises(ValueError):
- tui.checkbox_tree(self.screen, "Pick", [])
-
- def test_esc_returns_back_value(self):
- marker = object()
- screen = FakeScreen(keys=[27])
- self.assertIs(
- tui.checkbox_tree(screen, "Pick models", self.FAMILIES,
- back_value=marker),
- marker)
-
- def test_q_still_aborts_with_back_value(self):
- marker = object()
- screen = FakeScreen(keys=[ord("q")])
- with self.assertRaises(tui.WizardCancelled):
- tui.checkbox_tree(screen, "Pick models", self.FAMILIES,
- back_value=marker)
-
-
-class SuspendTests(TuiTestCase):
- """tui.suspend leaves curses, runs code, then repaints."""
-
- def test_suspend_runs_block_and_restores(self):
- ran = []
- with tui.suspend(self.screen):
- ran.append("inside")
- self.assertEqual(ran, ["inside"])
-
- def test_suspend_always_restores_on_exception(self):
- class Boom(Exception):
- pass
- with self.assertRaises(Boom):
- with tui.suspend(self.screen):
- raise Boom()
-
-
-class FlashTests(TuiTestCase):
- """tui.flash shows a notice until any key is pressed."""
-
- def test_notice_dismissed_by_any_key(self):
- screen = FakeScreen(keys=[10])
- # Should return (None) after consuming one key; not raise.
- tui.flash(screen, "a notice", kind="warn")
- self.assertEqual(screen.keys, [])
-
-
-if __name__ == "__main__":
- unittest.main()