From 83f63ddf234afe60c245eeaea98aa3a16b2ec69d Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Mon, 16 Mar 2026 13:08:59 -0400 Subject: [PATCH] test: add menu test module --- tests/test_menu.py | 116 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 tests/test_menu.py diff --git a/tests/test_menu.py b/tests/test_menu.py new file mode 100644 index 0000000..31ebc6c --- /dev/null +++ b/tests/test_menu.py @@ -0,0 +1,116 @@ +from unittest.mock import MagicMock, patch + +from hate_crack.menu import ( + _arrow_menu, + _numbered_menu, + _use_arrow_menu, + interactive_menu, +) + +SAMPLE_ITEMS = [ + ("1", "Quick Crack"), + ("2", "Brute Force"), + ("99", "Quit"), +] + + +class TestUseArrowMenu: + def test_falls_back_without_library(self, monkeypatch): + import hate_crack.menu as mod + + monkeypatch.setattr(mod, "_HAS_TERM_MENU", False) + monkeypatch.delenv("HATE_CRACK_PLAIN_MENU", raising=False) + assert _use_arrow_menu() is False + + def test_falls_back_on_non_tty(self, monkeypatch): + import hate_crack.menu as mod + + monkeypatch.setattr(mod, "_HAS_TERM_MENU", True) + monkeypatch.delenv("HATE_CRACK_PLAIN_MENU", raising=False) + monkeypatch.setattr("sys.stdout.isatty", lambda: False) + assert _use_arrow_menu() is False + + def test_falls_back_with_env_var(self, monkeypatch): + import hate_crack.menu as mod + + monkeypatch.setattr(mod, "_HAS_TERM_MENU", True) + monkeypatch.setenv("HATE_CRACK_PLAIN_MENU", "1") + assert _use_arrow_menu() is False + + def test_enabled_when_all_conditions_met(self, monkeypatch): + import hate_crack.menu as mod + + monkeypatch.setattr(mod, "_HAS_TERM_MENU", True) + monkeypatch.delenv("HATE_CRACK_PLAIN_MENU", raising=False) + monkeypatch.setattr("sys.stdout.isatty", lambda: True) + assert _use_arrow_menu() is True + + +class TestNumberedMenu: + def test_returns_correct_key(self, monkeypatch): + monkeypatch.setattr("builtins.input", lambda _: "2") + result = _numbered_menu(SAMPLE_ITEMS, "\nSelect: ") + assert result == "2" + + def test_prints_all_labels(self, monkeypatch, capsys): + monkeypatch.setattr("builtins.input", lambda _: "1") + _numbered_menu(SAMPLE_ITEMS, "\nSelect: ") + captured = capsys.readouterr().out + for key, label in SAMPLE_ITEMS: + assert f"({key}) {label}" in captured + + def test_returns_none_on_empty_input(self, monkeypatch): + monkeypatch.setattr("builtins.input", lambda _: "") + result = _numbered_menu(SAMPLE_ITEMS, "\nSelect: ") + assert result is None + + +class TestArrowMenu: + def test_returns_correct_key(self): + mock_menu_instance = MagicMock() + mock_menu_instance.show.return_value = 1 # index 1 -> key "2" + with patch( + "hate_crack.menu.TerminalMenu", + create=True, + return_value=mock_menu_instance, + ): + result = _arrow_menu(SAMPLE_ITEMS, "Title") + assert result == "2" + + def test_returns_none_on_escape(self): + mock_menu_instance = MagicMock() + mock_menu_instance.show.return_value = None + with patch( + "hate_crack.menu.TerminalMenu", + create=True, + return_value=mock_menu_instance, + ): + result = _arrow_menu(SAMPLE_ITEMS, "Title") + assert result is None + + +class TestInteractiveMenu: + def test_uses_numbered_when_no_tty(self, monkeypatch): + import hate_crack.menu as mod + + monkeypatch.setattr(mod, "_HAS_TERM_MENU", False) + monkeypatch.delenv("HATE_CRACK_PLAIN_MENU", raising=False) + monkeypatch.setattr("builtins.input", lambda _: "99") + result = interactive_menu(SAMPLE_ITEMS) + assert result == "99" + + def test_uses_arrow_when_available(self, monkeypatch): + import hate_crack.menu as mod + + monkeypatch.setattr(mod, "_HAS_TERM_MENU", True) + monkeypatch.delenv("HATE_CRACK_PLAIN_MENU", raising=False) + monkeypatch.setattr("sys.stdout.isatty", lambda: True) + mock_menu_instance = MagicMock() + mock_menu_instance.show.return_value = 0 + with patch( + "hate_crack.menu.TerminalMenu", + create=True, + return_value=mock_menu_instance, + ): + result = interactive_menu(SAMPLE_ITEMS, title="Test") + assert result == "1"