forked from vhaudiquet/weekly-activity
wizard: when stdin and stdout are both TTYs, menus render inline and are driven by keys: up/down/j/k/Home/End move the bold '>' highlight, Enter accepts, Esc/Ctrl-C/q back out of cancellable menus while Ctrl-C on fixed menus still aborts. Rows redraw with erase+CR across the option block only and clear before returning so prompts stay aligned. Non-TTY stdio keeps the original numbered prompt verbatim. report: each configured source shows a transient "# Generating report: pulling <name> data..." line that is erased on completion, only when stdout is a TTY; piped output remains exactly the report. tests: cover the numbered fallback, row rendering, status suppression and show+clear parity; reformat stray test files to satisfy ruff.
59 lines
2.3 KiB
Python
59 lines
2.3 KiB
Python
"""Unit tests for the wizard menus — scripted input, no raw terminal involved."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
import sys
|
|
import unittest
|
|
from unittest import mock
|
|
|
|
from weekly_activity.wizard import _menu, _option_line
|
|
|
|
|
|
class NumberedMenuFallbackTest(unittest.TestCase):
|
|
"""When stdin/stdout are not a TTY the numbered prompt stays as it was."""
|
|
|
|
def run_numbered(self, answers: list[str], options: list[str], *, cancellable: bool = False):
|
|
"""Run _menu with forced non-TTY stdio and scripted interactive answers."""
|
|
fake_stdin, fake_stdout = io.StringIO(), io.StringIO()
|
|
with (
|
|
mock.patch.object(sys, "stdin", fake_stdin),
|
|
mock.patch.object(sys, "stdout", fake_stdout),
|
|
mock.patch("builtins.input", side_effect=iter(answers)),
|
|
):
|
|
picked = _menu("Pick one", options, cancellable=cancellable)
|
|
return picked, fake_stdout.getvalue()
|
|
|
|
def test_returns_zero_based_index_of_chosen_option(self) -> None:
|
|
picked, output = self.run_numbered(["2"], ["Alpha", "Beta", "Gamma"])
|
|
self.assertEqual(picked, 1)
|
|
self.assertIn("Pick one", output)
|
|
self.assertIn(" 2) Beta", output)
|
|
|
|
def test_empty_answer_backs_out_of_cancellable_menu(self) -> None:
|
|
picked, _ = self.run_numbered([""], ["Alpha", "Beta"], cancellable=True)
|
|
self.assertIsNone(picked)
|
|
|
|
def test_back_number_returns_none(self) -> None:
|
|
picked, _ = self.run_numbered(["3"], ["Alpha", "Beta"], cancellable=True)
|
|
self.assertIsNone(picked)
|
|
|
|
def test_invalid_answers_loop_until_valid_one(self) -> None:
|
|
picked, output = self.run_numbered(["9", "nope", "3"], ["Alpha", "Beta", "Gamma"])
|
|
self.assertEqual(picked, 2)
|
|
self.assertEqual(output.count("Please enter a number between 1 and 3."), 2)
|
|
|
|
|
|
class OptionLineRenderingTest(unittest.TestCase):
|
|
"""Selected rows carry the '>' marker and bold codes; others stay plain."""
|
|
|
|
def test_selected_line_is_bold_and_marked(self) -> None:
|
|
self.assertEqual(_option_line("Exit", True), "\x1b[1m> Exit\x1b[0m")
|
|
|
|
def test_unselected_line_is_indented_and_plain(self) -> None:
|
|
self.assertEqual(_option_line("Remove a source", False), " Remove a source")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|