Files
Autoparts-DB/console/tests/test_core.py
consultoria-as ceacab789b feat(console): add core framework - keybindings, navigation, screen base
Add the three core modules that all screens depend on:
- keybindings.py: Key constants (curses codes) and KeyBindings registry
- navigation.py: Stack-based screen navigation with breadcrumbs
- screens.py: Screen base class with on_enter/on_key/render lifecycle

Includes 31 tests covering all public APIs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 01:38:02 +00:00

215 lines
6.7 KiB
Python

"""
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)