import sys from unittest.mock import patch, MagicMock import pytest from pathlib import Path # Mock tkinter and other GUI/macOS specific imports sys.modules['tkinter'] = MagicMock() sys.modules['tkinter.ttk'] = MagicMock() sys.modules['tkinter.scrolledtext'] = MagicMock() sys.modules['tkinter.messagebox'] = MagicMock() sys.modules['Foundation'] = MagicMock() sys.modules['AppKit'] = MagicMock() sys.modules['PIL'] = MagicMock() sys.modules['PIL.Image'] = MagicMock() sys.modules['PIL.ImageTk'] = MagicMock() import install from install import ProleInstaller @pytest.fixture def mock_installer(tmp_path): test_cfg_src = Path(__file__).parent / "fixtures" / "test-prole.cfg" cfg_path = tmp_path / "prole.cfg" cfg_path.write_text(test_cfg_src.read_text()) with patch('install.tk.Tk'), \ patch('install.tk.Frame'), \ patch('install.tk.Canvas'), \ patch('install.tk.Label'), \ patch('install.tk.Button'), \ patch('install.ttk.Style'), \ patch('install.Path.exists', return_value=True): root = MagicMock() # Mock winfo methods needed for centering root.winfo_screenwidth.return_value = 1920 root.winfo_screenheight.return_value = 1080 app = ProleInstaller(root, config_path=str(cfg_path)) return app def test_initial_page(mock_installer): # Should start at index 0 (welcome) assert mock_installer.page_index == 0 assert mock_installer.pages[mock_installer.page_index][0] == 'welcome' def test_show_page_by_id(mock_installer): mock_installer.show_page('network_scan') assert mock_installer.pages[mock_installer.page_index][0] == 'network_scan' def test_show_page_invalid_id(mock_installer): # Current page is welcome (0) mock_installer.page_index = 0 with patch('install.messagebox.showerror') as mock_error: mock_installer.show_page('non_existent_page') # Should stay at current index and NOT fallback to 0 if it was elsewhere, # or just stay at 0 if it was at 0. # The key fix was NOT defaulting to 0 when index_or_id is a string and not found. assert mock_installer.page_index == 0 mock_error.assert_called_once() def test_navigation_flow_standard(mock_installer): # Welcome -> Dependencies mock_installer.on_next() assert mock_installer.pages[mock_installer.page_index][0] == 'deps_summary' # Mock all dependencies installed to skip dep pages with patch.object(mock_installer, 'all_dependencies_installed', return_value=True): mock_installer.on_next() assert mock_installer.pages[mock_installer.page_index][0] == 'network_scan' mock_installer.on_next() assert mock_installer.pages[mock_installer.page_index][0] == 'env_setup' mock_installer.on_next() assert mock_installer.pages[mock_installer.page_index][0] == 'init_cluster' with patch.object(mock_installer, '_cluster_ready_for_navigation', return_value=True): mock_installer.on_next() assert mock_installer.pages[mock_installer.page_index][0] == 'init_password' def test_navigation_flow_missing_deps(mock_installer): # Welcome -> Dependencies mock_installer.show_page('welcome') mock_installer.on_next() assert mock_installer.pages[mock_installer.page_index][0] == 'deps_summary' # Mock one dependency missing mock_dep_id = mock_installer.dependencies[0]['id'] with patch.object(mock_installer, 'all_dependencies_installed', return_value=False), \ patch.object(mock_installer, '_dep_navigation_sequence', return_value=[f'dep_{mock_dep_id}']): mock_installer.on_next() assert mock_installer.pages[mock_installer.page_index][0] == f'dep_{mock_dep_id}' # Next from dep page with all installed now with patch.object(mock_installer, 'all_dependencies_installed', return_value=True): mock_installer.on_next() assert mock_installer.pages[mock_installer.page_index][0] == 'network_scan' def test_on_prev(mock_installer): # network_scan -> deps_summary mock_installer.show_page('network_scan') with patch.object(mock_installer, 'all_dependencies_installed', return_value=True): mock_installer.on_prev() assert mock_installer.pages[mock_installer.page_index][0] == 'deps_summary' # deps_summary -> welcome mock_installer.on_prev() assert mock_installer.pages[mock_installer.page_index][0] == 'welcome' def test_infinite_loop_prevention(mock_installer): # This specifically tests the scenario that caused the bug: # trying to navigate to a missing ID should NOT reset to welcome (0) mock_installer.show_page('network_scan') current_idx = mock_installer.page_index mock_installer.show_page('invalid_id') assert mock_installer.page_index == current_idx assert mock_installer.pages[mock_installer.page_index][0] == 'network_scan' def test_on_next_finish_loop(mock_installer): # From last page, on_next should not crash or loop last_idx = len(mock_installer.pages) - 1 mock_installer.show_page(last_idx) mock_installer.on_next() assert mock_installer.page_index == last_idx def test_on_prev_first_page(mock_installer): # From first page, on_prev should not crash or loop mock_installer.show_page(0) mock_installer.on_prev() assert mock_installer.page_index == 0 def test_navigation_to_create_installer(mock_installer): # From build page, if successful, next should go to create_installer mock_installer.show_page('build') mock_installer._built_success = True mock_installer.on_next() assert mock_installer.pages[mock_installer.page_index][0] == 'create_installer' # Last page Finish label mock_installer.update_footer() mock_installer.next_button.configure.assert_any_call(text='Finish') def test_all_pages_render(mock_installer): with patch.object(mock_installer, '_verify_k3s_services', return_value=None), \ patch.object(mock_installer, 'check_cluster_status_async', return_value=None), \ patch.object(mock_installer, '_ensure_db_build_registry_async', return_value=None), \ patch.object(mock_installer, '_get_kubectx_list', return_value=['default']), \ patch.object(mock_installer, '_apply_k3s_defaults', return_value=None): for page_id, _ in mock_installer.pages: mock_installer.show_page(page_id) # Exercise init_cluster conditional branches for service/prod modes mock_installer.cluster_env.get.return_value = 'prole-service-cluster' with patch.object(mock_installer, '_cluster_env_key', return_value='service'): mock_installer.show_page('init_cluster') mock_installer.cluster_env.get.return_value = 'prole-prod-cluster' with patch.object(mock_installer, '_cluster_env_key', return_value='prod'): mock_installer.show_page('init_cluster')