""" Tests for the core framework: keybindings, navigation, and screen base class. """ import pytest from console.core.keybindings import Key, KeyBindings from console.core.navigation import Navigation from console.core.screens import Screen # ========================================================================= # Key constants # ========================================================================= class TestKeyConstants: def test_escape_is_27(self): assert Key.ESCAPE == 27 def test_enter_is_10(self): assert Key.ENTER == 10 def test_tab_is_9(self): assert Key.TAB == 9 def test_backspace_is_127(self): assert Key.BACKSPACE == 127 def test_arrow_keys_are_not_none(self): assert Key.UP is not None assert Key.DOWN is not None assert Key.LEFT is not None assert Key.RIGHT is not None def test_page_keys_are_not_none(self): assert Key.PGUP is not None assert Key.PGDN is not None def test_home_end_are_not_none(self): assert Key.HOME is not None assert Key.END is not None def test_f1_is_not_none(self): assert Key.F1 is not None def test_f10_is_not_none(self): assert Key.F10 is not None def test_f_keys_are_sequential(self): """F1 through F10 should be sequential curses key codes.""" for i in range(1, 10): f_current = getattr(Key, f"F{i}") f_next = getattr(Key, f"F{i + 1}") assert f_next == f_current + 1 # ========================================================================= # KeyBindings # ========================================================================= class TestKeyBindings: def test_bind_and_handle_calls_callback(self): kb = KeyBindings() called = [] kb.bind(Key.ENTER, lambda: called.append(True)) result = kb.handle(Key.ENTER) assert result is True assert len(called) == 1 def test_handle_returns_false_for_unbound_key(self): kb = KeyBindings() result = kb.handle(Key.ESCAPE) assert result is False def test_bind_overwrites_previous(self): kb = KeyBindings() called_a = [] called_b = [] kb.bind(Key.ENTER, lambda: called_a.append(True)) kb.bind(Key.ENTER, lambda: called_b.append(True)) kb.handle(Key.ENTER) assert len(called_a) == 0 assert len(called_b) == 1 def test_multiple_bindings(self): kb = KeyBindings() results = {} kb.bind(Key.ENTER, lambda: results.update(enter=True)) kb.bind(Key.ESCAPE, lambda: results.update(escape=True)) kb.handle(Key.ENTER) kb.handle(Key.ESCAPE) assert results == {"enter": True, "escape": True} def test_set_footer_and_get_footer_labels(self): kb = KeyBindings() labels = [("F1", "Help"), ("F10", "Quit")] kb.set_footer(labels) assert kb.get_footer_labels() == labels def test_get_footer_labels_empty_by_default(self): kb = KeyBindings() assert kb.get_footer_labels() == [] # ========================================================================= # Navigation # ========================================================================= class TestNavigation: def test_initial_state_is_empty(self): nav = Navigation() assert nav.current() is None assert nav.depth() == 0 def test_push_and_current(self): nav = Navigation() nav.push("brands", context={"page": 1}, label="Brands") result = nav.current() assert result is not None screen_name, context = result assert screen_name == "brands" assert context == {"page": 1} def test_push_increases_depth(self): nav = Navigation() nav.push("brands", label="Brands") assert nav.depth() == 1 nav.push("models", label="Models") assert nav.depth() == 2 def test_pop_returns_previous_screen(self): nav = Navigation() nav.push("brands", context={"page": 1}, label="Brands") nav.push("models", context={"brand": "TOYOTA"}, label="Models") popped = nav.pop() assert popped is not None screen_name, context = popped assert screen_name == "models" assert context == {"brand": "TOYOTA"} # Current should now be brands current = nav.current() assert current[0] == "brands" def test_pop_on_empty_returns_none(self): nav = Navigation() result = nav.pop() assert result is None def test_pop_on_single_item_returns_it_and_empties(self): nav = Navigation() nav.push("home", label="Home") popped = nav.pop() assert popped is not None assert popped[0] == "home" assert nav.current() is None assert nav.depth() == 0 def test_breadcrumb_returns_label_list(self): nav = Navigation() nav.push("brands", label="Brands") nav.push("models", label="Toyota Models") nav.push("years", label="2020") assert nav.breadcrumb() == ["Brands", "Toyota Models", "2020"] def test_breadcrumb_empty_when_no_items(self): nav = Navigation() assert nav.breadcrumb() == [] def test_breadcrumb_uses_screen_name_as_fallback(self): nav = Navigation() nav.push("brands") assert nav.breadcrumb() == ["brands"] def test_clear_empties_stack(self): nav = Navigation() nav.push("brands", label="Brands") nav.push("models", label="Models") nav.clear() assert nav.depth() == 0 assert nav.current() is None assert nav.breadcrumb() == [] def test_context_defaults_to_none(self): nav = Navigation() nav.push("home", label="Home") screen_name, context = nav.current() assert context is None # ========================================================================= # Screen base class # ========================================================================= class TestScreen: def test_has_name_and_title(self): screen = Screen("brands", "Select Brand") assert screen.name == "brands" assert screen.title == "Select Brand" def test_on_enter_is_callable(self): screen = Screen("test", "Test") # Should not raise screen.on_enter(context=None, db=None, renderer=None) def test_on_key_is_callable(self): screen = Screen("test", "Test") # Should not raise, returns None by default result = screen.on_key(key=10, context=None, db=None, renderer=None, nav=None) assert result is None def test_render_is_callable(self): screen = Screen("test", "Test") # Should not raise screen.render(context=None, db=None, renderer=None)