forked from vhaudiquet/weekly-activity
Add arrow-key menus and report loading status
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.
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user