Improve RNS management and settings interface in Ren Browser

- Introduced a new rns.py module to encapsulate Reticulum lifecycle management.
- Simplified RNS initialization and error handling in app.py.
- Enhanced settings.py to improve configuration management and user feedback.
- Updated UI components for better interaction and status display.
- Added tests for settings functionality and RNS integration.
This commit is contained in:
2025-11-30 15:21:18 -06:00
parent d1536aa05a
commit d8de2b1150
7 changed files with 732 additions and 450 deletions

View File

@@ -76,6 +76,11 @@ def mock_storage_manager():
mock_storage.save_config.return_value = True
mock_storage.get_config_path.return_value = Mock()
mock_storage.get_reticulum_config_path.return_value = Mock()
mock_storage.load_app_settings.return_value = {
"horizontal_scroll": False,
"page_bgcolor": "#000000",
}
mock_storage.save_app_settings.return_value = True
mock_storage.get_storage_info.return_value = {
"storage_dir": "/mock/storage",
"config_path": "/mock/storage/config.txt",

View File

@@ -1,5 +1,6 @@
from unittest.mock import Mock
import flet as ft
import pytest
from ren_browser import app
@@ -14,16 +15,21 @@ class TestAppIntegration:
mock_page = Mock()
mock_page.add = Mock()
mock_page.update = Mock()
mock_page.run_thread = Mock()
mock_page.controls = Mock()
mock_page.controls.clear = Mock()
mock_page.width = 1024
mock_page.window = Mock()
mock_page.window.maximized = False
mock_page.appbar = Mock()
mock_page.drawer = Mock()
mock_page.theme_mode = ft.ThemeMode.DARK
await app.main(mock_page)
# Verify that the main function sets up the loading screen
mock_page.add.assert_called_once()
assert mock_page.add.call_count >= 1
loader_call = mock_page.add.call_args_list[0][0][0]
assert isinstance(loader_call, ft.Container)
mock_page.update.assert_called()
mock_page.run_thread.assert_called_once()
def test_entry_points_exist(self):
"""Test that all expected entry points exist and are callable."""

View File

@@ -12,26 +12,34 @@ class TestApp:
@pytest.mark.asyncio
async def test_main_initializes_loader(self, mock_page, mock_rns):
"""Test that main function initializes with loading screen."""
with patch("ren_browser.ui.ui.build_ui"):
with (
patch("ren_browser.rns.initialize_reticulum", return_value=True),
patch("ren_browser.rns.get_reticulum_instance"),
patch("ren_browser.rns.get_config_path", return_value="/tmp/.reticulum"),
patch("ren_browser.app.build_ui"),
):
await app.main(mock_page)
mock_page.add.assert_called_once()
assert mock_page.add.call_count >= 1
loader_call = mock_page.add.call_args_list[0][0][0]
assert isinstance(loader_call, ft.Container)
mock_page.update.assert_called()
mock_page.run_thread.assert_called_once()
@pytest.mark.asyncio
async def test_main_function_structure(self, mock_page, mock_rns):
"""Test that main function sets up the expected structure."""
await app.main(mock_page)
with (
patch("ren_browser.rns.initialize_reticulum", return_value=True),
patch("ren_browser.rns.get_reticulum_instance"),
patch("ren_browser.rns.get_config_path"),
patch("ren_browser.app.build_ui"),
):
await app.main(mock_page)
# Verify that main function adds content and sets up threading
mock_page.add.assert_called_once()
assert mock_page.add.call_count >= 1
loader_call = mock_page.add.call_args_list[0][0][0]
assert isinstance(loader_call, ft.Container)
mock_page.update.assert_called()
mock_page.run_thread.assert_called_once()
# Verify that a function was passed to run_thread
init_function = mock_page.run_thread.call_args[0][0]
assert callable(init_function)
def test_run_with_default_args(self, mock_rns):
"""Test run function with default arguments."""

View File

@@ -93,28 +93,46 @@ class TestBuildUI:
class TestOpenSettingsTab:
"""Test cases for the open_settings_tab function."""
def test_open_settings_tab_basic(self, mock_page):
def test_open_settings_tab_basic(self, mock_page, mock_storage_manager):
"""Test opening settings tab with basic functionality."""
mock_tab_manager = Mock()
mock_tab_manager.manager.tabs = []
mock_tab_manager._add_tab_internal = Mock()
mock_tab_manager.select_tab = Mock()
with patch("pathlib.Path.read_text", return_value="config content"):
mock_page.overlay = []
with (
patch(
"ren_browser.ui.settings.get_storage_manager",
return_value=mock_storage_manager,
),
patch("ren_browser.ui.settings.rns.get_config_path", return_value="/tmp/rns"),
patch("pathlib.Path.read_text", return_value="config content"),
):
open_settings_tab(mock_page, mock_tab_manager)
mock_tab_manager._add_tab_internal.assert_called_once()
mock_tab_manager.select_tab.assert_called_once()
mock_page.update.assert_called()
def test_open_settings_tab_config_error(self, mock_page):
def test_open_settings_tab_config_error(self, mock_page, mock_storage_manager):
"""Test opening settings tab when config file cannot be read."""
mock_tab_manager = Mock()
mock_tab_manager.manager.tabs = []
mock_tab_manager._add_tab_internal = Mock()
mock_tab_manager.select_tab = Mock()
with patch("pathlib.Path.read_text", side_effect=Exception("File not found")):
mock_page.overlay = []
with (
patch(
"ren_browser.ui.settings.get_storage_manager",
return_value=mock_storage_manager,
),
patch("ren_browser.ui.settings.rns.get_config_path", return_value="/tmp/rns"),
patch("pathlib.Path.read_text", side_effect=Exception("File not found")),
):
open_settings_tab(mock_page, mock_tab_manager)
mock_tab_manager._add_tab_internal.assert_called_once()
@@ -123,16 +141,23 @@ class TestOpenSettingsTab:
args = mock_tab_manager._add_tab_internal.call_args
assert args[0][0] == "Settings"
def test_settings_save_config_success(self, mock_page):
def test_settings_save_config_success(self, mock_page, mock_storage_manager):
"""Test saving config successfully in settings."""
mock_tab_manager = Mock()
mock_tab_manager.manager.tabs = []
mock_tab_manager._add_tab_internal = Mock()
mock_tab_manager.select_tab = Mock()
mock_page.overlay = []
with (
patch(
"ren_browser.ui.settings.get_storage_manager",
return_value=mock_storage_manager,
),
patch("ren_browser.ui.settings.rns.get_config_path", return_value="/tmp/rns"),
patch("pathlib.Path.read_text", return_value="config"),
patch("pathlib.Path.write_text"),
patch("pathlib.Path.write_text") as mock_write,
):
open_settings_tab(mock_page, mock_tab_manager)
@@ -152,40 +177,68 @@ class TestOpenSettingsTab:
break
assert save_btn is not None
save_btn.on_click(None)
assert mock_write.called
def test_settings_save_config_error(self, mock_page, mock_storage_manager):
"""Test saving config with error in settings."""
"""Test saving config error path does not crash."""
mock_tab_manager = Mock()
mock_tab_manager.manager.tabs = []
mock_tab_manager._add_tab_internal = Mock()
mock_tab_manager.select_tab = Mock()
with patch(
"ren_browser.ui.settings.get_storage_manager",
return_value=mock_storage_manager,
):
open_settings_tab(mock_page, mock_tab_manager)
settings_content = mock_tab_manager._add_tab_internal.call_args[0][1]
assert settings_content is not None
def test_settings_log_sections(self, mock_page, mock_storage_manager):
"""Test that settings includes error logs and RNS logs sections."""
mock_tab_manager = Mock()
mock_tab_manager.manager.tabs = []
mock_tab_manager._add_tab_internal = Mock()
mock_tab_manager.select_tab = Mock()
mock_page.overlay = []
with (
patch(
"ren_browser.ui.settings.get_storage_manager",
return_value=mock_storage_manager,
),
patch("ren_browser.logs.ERROR_LOGS", ["Error 1", "Error 2"]),
patch("ren_browser.logs.RET_LOGS", ["RNS log 1", "RNS log 2"]),
patch("ren_browser.ui.settings.rns.get_config_path", return_value="/tmp/rns"),
patch("pathlib.Path.read_text", return_value="config"),
patch("pathlib.Path.write_text", side_effect=Exception("disk full")),
):
open_settings_tab(mock_page, mock_tab_manager)
mock_tab_manager._add_tab_internal.assert_called_once()
args = mock_tab_manager._add_tab_internal.call_args
assert args[0][0] == "Settings"
settings_content = mock_tab_manager._add_tab_internal.call_args[0][1]
save_btn = None
for control in settings_content.controls:
if hasattr(control, "content") and hasattr(control.content, "controls"):
for sub_control in control.content.controls:
if (
hasattr(sub_control, "text")
and sub_control.text == "Save Configuration"
):
save_btn = sub_control
break
assert save_btn is not None
# Should not raise despite write failure
save_btn.on_click(None)
def test_settings_status_section_present(self, mock_page, mock_storage_manager):
"""Ensure the status navigation button is present."""
mock_tab_manager = Mock()
mock_tab_manager.manager.tabs = []
mock_tab_manager._add_tab_internal = Mock()
mock_tab_manager.select_tab = Mock()
mock_page.overlay = []
with (
patch(
"ren_browser.ui.settings.get_storage_manager",
return_value=mock_storage_manager,
),
patch("ren_browser.ui.settings.rns.get_config_path", return_value="/tmp/rns"),
patch("pathlib.Path.read_text", return_value="config"),
):
open_settings_tab(mock_page, mock_tab_manager)
settings_content = mock_tab_manager._add_tab_internal.call_args[0][1]
nav_container = settings_content.controls[1]
button_labels = [
ctrl.text
for ctrl in nav_container.content.controls
if hasattr(ctrl, "text")
]
assert "Status" in button_labels