""" Integration tests for the AUTOPARTES console application. Uses a MockRenderer that records draw calls instead of painting to a real terminal, allowing end-to-end testing of the screen -> renderer pipeline without curses. """ import pytest from console.renderers.base import BaseRenderer from console.core.navigation import Navigation from console.db import Database # ========================================================================= # MockRenderer # ========================================================================= class MockRenderer(BaseRenderer): """A renderer that records all draw calls for later assertion. Pre-load ``self.keys`` with a sequence of key codes; ``get_key()`` pops from the front and returns ESC (27) when the list is exhausted. """ def __init__(self): self.calls = [] # list of (method_name, args, kwargs) self.keys = [] # pre-loaded key presses self._size = (24, 80) # rows, cols # -- Lifecycle ---------------------------------------------------------- def init_screen(self): self.calls.append(('init_screen', (), {})) def cleanup(self): self.calls.append(('cleanup', (), {})) # -- Screen queries ----------------------------------------------------- def get_size(self): return self._size # -- Primitive operations ----------------------------------------------- def clear(self): self.calls.append(('clear', (), {})) def refresh(self): self.calls.append(('refresh', (), {})) def get_key(self): if self.keys: return self.keys.pop(0) return 27 # ESC to exit # -- High-level widgets ------------------------------------------------- def draw_header(self, title, subtitle=''): self.calls.append(('draw_header', (title, subtitle), {})) def draw_footer(self, key_labels): self.calls.append(('draw_footer', (key_labels,), {})) def draw_menu(self, items, selected_index=0, title=''): self.calls.append(('draw_menu', (items, selected_index), {})) def draw_table(self, headers, rows, widths, page_info=None, selected_row=-1): self.calls.append(('draw_table', (headers, rows, widths), {})) def draw_detail(self, fields, title=''): self.calls.append(('draw_detail', (fields,), {})) def draw_form(self, fields, focused_index=0, title=''): self.calls.append(('draw_form', (fields, focused_index), {})) def draw_filter_list(self, items, filter_text, selected_index, title=''): self.calls.append(('draw_filter_list', (items, filter_text, selected_index), {})) def draw_comparison(self, columns, title=''): self.calls.append(('draw_comparison', (columns,), {})) # -- Low-level drawing -------------------------------------------------- def draw_text(self, row, col, text, style='normal'): self.calls.append(('draw_text', (row, col, text, style), {})) def draw_box(self, top, left, height, width, title=''): self.calls.append(('draw_box', (top, left, height, width), {})) # -- Dialogs ------------------------------------------------------------ def show_message(self, text, msg_type='info'): self.calls.append(('show_message', (text, msg_type), {})) if msg_type == 'confirm': return True # auto-confirm return True def show_input(self, prompt, max_len=40): self.calls.append(('show_input', (prompt, max_len), {})) return None # cancel by default # -- Helpers for assertions --------------------------------------------- def method_names(self): """Return a list of just the method names from recorded calls.""" return [name for name, _args, _kwargs in self.calls] def calls_for(self, method_name): """Return only the calls matching *method_name*.""" return [(args, kwargs) for name, args, kwargs in self.calls if name == method_name] # ========================================================================= # Fixtures # ========================================================================= @pytest.fixture def mock_renderer(): """Provide a fresh MockRenderer for each test.""" return MockRenderer() @pytest.fixture(scope="module") def db(): """Provide a shared Database instance for integration tests.""" return Database() # ========================================================================= # Test 1: App creates with screens # ========================================================================= class TestAppCreatesWithScreens: def test_app_creates_with_screens(self, mock_renderer, db): """App should register at least 'menu' and 'estadisticas' screens.""" from console.core.app import App app = App(renderer=mock_renderer, db=db) assert 'menu' in app.screens assert 'estadisticas' in app.screens assert len(app.screens) >= 2 # ========================================================================= # Test 2: App runs and quits # ========================================================================= class TestAppRunsAndQuits: def test_app_runs_and_quits(self, mock_renderer, db): """Pre-load ESC + confirm-yes (ord('s')). App should exit cleanly.""" from console.core.app import App # ESC triggers quit dialog, show_message auto-confirms True mock_renderer.keys = [27] # ESC app = App(renderer=mock_renderer, db=db) app.run() # should not raise # Verify init_screen and cleanup were both called names = mock_renderer.method_names() assert 'init_screen' in names assert 'cleanup' in names # ========================================================================= # Test 3: Menu renders # ========================================================================= class TestMenuRenders: def test_menu_renders(self, mock_renderer, db): """MenuPrincipalScreen.render() should call draw_header and draw_menu.""" from console.screens.menu_principal import MenuPrincipalScreen screen = MenuPrincipalScreen() screen.render(context={}, db=db, renderer=mock_renderer) names = mock_renderer.method_names() assert 'draw_header' in names assert 'draw_menu' in names # ========================================================================= # Test 4: Estadisticas renders # ========================================================================= class TestEstadisticasRenders: def test_estadisticas_renders(self, mock_renderer, db): """EstadisticasScreen.render() should call draw_header and draw_text.""" from console.screens.estadisticas import EstadisticasScreen screen = EstadisticasScreen() screen.render(context={}, db=db, renderer=mock_renderer) names = mock_renderer.method_names() assert 'draw_header' in names # EstadisticasScreen uses draw_text for its detail fields assert 'draw_text' in names # ========================================================================= # Test 5: Navigation integration # ========================================================================= class TestNavigationIntegration: def test_navigation_push_pop_breadcrumb(self): """Push menu, push estadisticas, verify breadcrumb, pop, verify current.""" nav = Navigation() nav.push('menu', {}, label='Menu') nav.push('estadisticas', {}, label='Estadisticas') # Breadcrumb should show both assert nav.breadcrumb() == ['Menu', 'Estadisticas'] assert nav.depth() == 2 # Current should be estadisticas current = nav.current() assert current is not None assert current[0] == 'estadisticas' # Pop estadisticas popped = nav.pop() assert popped[0] == 'estadisticas' # Now current should be menu current = nav.current() assert current is not None assert current[0] == 'menu' assert nav.breadcrumb() == ['Menu'] # ========================================================================= # Test 6: All screens instantiate # ========================================================================= class TestAllScreensInstantiate: """Import and instantiate all 13 screen classes, verifying each has name and title attributes.""" # (module_path, class_name) _SCREEN_CLASSES = [ ("console.screens.menu_principal", "MenuPrincipalScreen"), ("console.screens.estadisticas", "EstadisticasScreen"), ("console.screens.vehiculo_nav", "VehiculoNavScreen"), ("console.screens.buscar_parte", "BuscarParteScreen"), ("console.screens.buscar_texto", "BuscarTextoScreen"), ("console.screens.vin_decoder", "VinDecoderScreen"), ("console.screens.catalogo", "CatalogoScreen"), ("console.screens.parte_detalle", "ParteDetalleScreen"), ("console.screens.comparador", "ComparadorScreen"), ("console.screens.admin_partes", "AdminPartesScreen"), ("console.screens.admin_fabricantes", "AdminFabricantesScreen"), ("console.screens.admin_crossref", "AdminCrossrefScreen"), ("console.screens.admin_import", "AdminImportScreen"), ] def test_all_13_screens_exist(self): """All 13 screen modules should be importable.""" assert len(self._SCREEN_CLASSES) == 13 @pytest.mark.parametrize("module_path,class_name", _SCREEN_CLASSES) def test_screen_instantiates(self, module_path, class_name): """Each screen class should instantiate and have name + title.""" import importlib mod = importlib.import_module(module_path) cls = getattr(mod, class_name) instance = cls() assert hasattr(instance, 'name') assert hasattr(instance, 'title') assert isinstance(instance.name, str) assert isinstance(instance.title, str) assert len(instance.name) > 0 assert len(instance.title) > 0