1. Add basic Micron parser and link support
2. Improve styling/layout
3. Add hot reloading for RNS
This commit is contained in:
2025-11-16 00:34:51 -06:00
parent e36bfec4a0
commit 3cddaeb2b9
15 changed files with 1079 additions and 207 deletions

View File

@@ -62,7 +62,7 @@ def sample_page_request():
from ren_browser.pages.page_request import PageRequest
return PageRequest(
destination_hash="1234567890abcdef", page_path="/page/index.mu", field_data=None
destination_hash="1234567890abcdef", page_path="/page/index.mu", field_data=None,
)

View File

@@ -19,7 +19,7 @@ class TestAnnounce:
def test_announce_with_none_display_name(self):
"""Test Announce creation with None display name."""
announce = Announce(
destination_hash="1234567890abcdef", display_name=None, timestamp=1234567890
destination_hash="1234567890abcdef", display_name=None, timestamp=1234567890,
)
assert announce.destination_hash == "1234567890abcdef"

View File

@@ -59,7 +59,7 @@ class TestLogsModule:
assert len(logs.RET_LOGS) == 1
assert logs.RET_LOGS[0] == "[2023-01-01T12:00:00] Test RNS message"
logs._original_rns_log.assert_called_once_with(
"Test RNS message", "arg1", kwarg1="value1"
"Test RNS message", "arg1", kwarg1="value1",
)
assert result == "original_result"

View File

@@ -7,7 +7,7 @@ class TestPageRequest:
def test_page_request_creation(self):
"""Test basic PageRequest creation."""
request = PageRequest(
destination_hash="1234567890abcdef", page_path="/page/index.mu"
destination_hash="1234567890abcdef", page_path="/page/index.mu",
)
assert request.destination_hash == "1234567890abcdef"

View File

@@ -63,66 +63,58 @@ class TestMicronRenderer:
"""
def test_render_micron_basic(self):
"""Test basic micron rendering (currently displays raw content)."""
"""Test basic micron rendering."""
content = "# Heading\n\nSome content"
result = render_micron(content)
assert isinstance(result, ft.Text)
assert result.value == "# Heading\n\nSome content"
assert result.selectable is True
assert result.font_family == "monospace"
assert isinstance(result, ft.Column)
assert result.expand is True
assert result.scroll == ft.ScrollMode.AUTO
def test_render_micron_empty(self):
"""Test micron rendering with empty content."""
content = ""
result = render_micron(content)
assert isinstance(result, ft.Text)
assert result.value == ""
assert result.selectable is True
assert isinstance(result, ft.Column)
assert len(result.controls) >= 0
def test_render_micron_unicode(self):
"""Test micron rendering with Unicode characters."""
content = "Unicode content: 你好 🌍 αβγ"
result = render_micron(content)
assert isinstance(result, ft.Text)
assert result.value == content
assert result.selectable is True
assert isinstance(result, ft.Column)
assert len(result.controls) > 0
class TestRendererComparison:
"""Test cases comparing both renderers."""
def test_renderers_return_same_type(self):
"""Test that both renderers return the same control type."""
"""Test that both renderers return Flet controls."""
content = "Test content"
plaintext_result = render_plaintext(content)
micron_result = render_micron(content)
assert type(plaintext_result) is type(micron_result)
assert isinstance(plaintext_result, ft.Text)
assert isinstance(micron_result, ft.Text)
assert isinstance(micron_result, ft.Column)
def test_renderers_preserve_content(self):
"""Test that both renderers preserve the original content."""
"""Test that plaintext renderer preserves content."""
content = "Test content with\nmultiple lines"
plaintext_result = render_plaintext(content)
micron_result = render_micron(content)
assert plaintext_result.value == content
assert micron_result.value == content
def test_renderers_same_properties(self):
"""Test that both renderers set the same basic properties."""
"""Test that both renderers have expand property."""
content = "Test content"
plaintext_result = render_plaintext(content)
micron_result = render_micron(content)
assert plaintext_result.selectable == micron_result.selectable
assert plaintext_result.font_family == micron_result.font_family
assert plaintext_result.expand == micron_result.expand
assert plaintext_result.expand is True
assert micron_result.expand is True

View File

@@ -18,7 +18,7 @@ class TestStorageManager:
def test_storage_manager_init_without_page(self):
"""Test StorageManager initialization without a page."""
with patch(
"ren_browser.storage.storage.StorageManager._get_storage_directory"
"ren_browser.storage.storage.StorageManager._get_storage_directory",
) as mock_get_dir:
mock_dir = Path("/mock/storage")
mock_get_dir.return_value = mock_dir
@@ -35,7 +35,7 @@ class TestStorageManager:
mock_page = Mock()
with patch(
"ren_browser.storage.storage.StorageManager._get_storage_directory"
"ren_browser.storage.storage.StorageManager._get_storage_directory",
) as mock_get_dir:
mock_dir = Path("/mock/storage")
mock_get_dir.return_value = mock_dir
@@ -51,17 +51,16 @@ class TestStorageManager:
with (
patch("os.name", "posix"),
patch.dict(
"os.environ", {"XDG_CONFIG_HOME": "/home/user/.config"}, clear=True
"os.environ", {"XDG_CONFIG_HOME": "/home/user/.config"}, clear=True,
),
patch("pathlib.Path.mkdir"),
patch("pathlib.Path.mkdir"),patch(
"ren_browser.storage.storage.StorageManager._ensure_storage_directory",
),
):
with patch(
"ren_browser.storage.storage.StorageManager._ensure_storage_directory"
):
storage = StorageManager()
storage._storage_dir = storage._get_storage_directory()
expected_dir = Path("/home/user/.config") / "ren_browser"
assert storage._storage_dir == expected_dir
storage = StorageManager()
storage._storage_dir = storage._get_storage_directory()
expected_dir = Path("/home/user/.config") / "ren_browser"
assert storage._storage_dir == expected_dir
def test_get_storage_directory_windows(self):
"""Test storage directory detection for Windows."""
@@ -76,7 +75,7 @@ class TestStorageManager:
patch("pathlib.Path.mkdir"),
):
with patch(
"ren_browser.storage.storage.StorageManager._ensure_storage_directory"
"ren_browser.storage.storage.StorageManager._ensure_storage_directory",
):
storage = StorageManager()
storage._storage_dir = storage._get_storage_directory()
@@ -91,7 +90,7 @@ class TestStorageManager:
patch("pathlib.Path.mkdir"),
):
with patch(
"ren_browser.storage.storage.StorageManager._ensure_storage_directory"
"ren_browser.storage.storage.StorageManager._ensure_storage_directory",
):
storage = StorageManager()
storage._storage_dir = storage._get_storage_directory()
@@ -103,15 +102,14 @@ class TestStorageManager:
with (
patch("os.name", "posix"),
patch.dict("os.environ", {"ANDROID_ROOT": "/system"}, clear=True),
patch("pathlib.Path.mkdir"),
patch("pathlib.Path.mkdir"),patch(
"ren_browser.storage.storage.StorageManager._ensure_storage_directory",
),
):
with patch(
"ren_browser.storage.storage.StorageManager._ensure_storage_directory"
):
storage = StorageManager()
storage._storage_dir = storage._get_storage_directory()
expected_dir = Path("/data/local/tmp/ren_browser")
assert storage._storage_dir == expected_dir
storage = StorageManager()
storage._storage_dir = storage._get_storage_directory()
expected_dir = Path("/data/local/tmp/ren_browser")
assert storage._storage_dir == expected_dir
def test_get_config_path(self):
"""Test getting config file path."""
@@ -171,7 +169,7 @@ class TestStorageManager:
assert result is True
mock_page.client_storage.set.assert_called_with(
"ren_browser_config", config_content
"ren_browser_config", config_content,
)
def test_save_config_fallback(self):
@@ -188,19 +186,18 @@ class TestStorageManager:
storage,
"get_reticulum_config_path",
return_value=Path(temp_dir) / "reticulum",
), patch(
"pathlib.Path.write_text",
side_effect=PermissionError("Access denied"),
):
with patch(
"pathlib.Path.write_text",
side_effect=PermissionError("Access denied"),
):
config_content = "test config content"
result = storage.save_config(config_content)
config_content = "test config content"
result = storage.save_config(config_content)
assert result is True
# Check that the config was set to client storage
mock_page.client_storage.set.assert_any_call(
"ren_browser_config", config_content
)
assert result is True
# Check that the config was set to client storage
mock_page.client_storage.set.assert_any_call(
"ren_browser_config", config_content,
)
# Verify that client storage was called at least once
assert mock_page.client_storage.set.call_count >= 1
@@ -270,7 +267,7 @@ class TestStorageManager:
bookmarks_path = storage._storage_dir / "bookmarks.json"
assert bookmarks_path.exists()
with open(bookmarks_path, "r", encoding="utf-8") as f:
with open(bookmarks_path, encoding="utf-8") as f:
loaded_bookmarks = json.load(f)
assert loaded_bookmarks == bookmarks
@@ -311,7 +308,7 @@ class TestStorageManager:
history_path = storage._storage_dir / "history.json"
assert history_path.exists()
with open(history_path, "r", encoding="utf-8") as f:
with open(history_path, encoding="utf-8") as f:
loaded_history = json.load(f)
assert loaded_history == history
@@ -360,12 +357,11 @@ class TestStorageManager:
with patch(
"pathlib.Path.mkdir",
side_effect=[PermissionError("Access denied"), None],
):
with patch("tempfile.gettempdir", return_value="/tmp"):
storage = StorageManager()
), patch("tempfile.gettempdir", return_value="/tmp"):
storage = StorageManager()
expected_fallback = Path("/tmp") / "ren_browser"
assert storage._storage_dir == expected_fallback
expected_fallback = Path("/tmp") / "ren_browser"
assert storage._storage_dir == expected_fallback
class TestStorageGlobalFunctions:
@@ -448,7 +444,7 @@ class TestStorageManagerEdgeCases:
storage = StorageManager()
with patch(
"pathlib.Path.write_text", side_effect=PermissionError("Access denied")
"pathlib.Path.write_text", side_effect=PermissionError("Access denied"),
):
test_path = Path("/mock/path")
result = storage._is_writable(test_path)

View File

@@ -34,8 +34,8 @@ class TestTabsManager:
assert isinstance(manager.manager, SimpleNamespace)
assert len(manager.manager.tabs) == 1
assert manager.manager.index == 0
assert isinstance(manager.tab_bar, ft.Row)
assert manager.tab_bar.scroll is None
assert isinstance(manager.tab_bar, ft.Container)
assert isinstance(manager.tab_bar.content, ft.Row)
assert manager.overflow_menu is None
assert isinstance(manager.content_container, ft.Container)
@@ -105,12 +105,12 @@ class TestTabsManager:
"""Test that selecting a tab updates background colors correctly."""
tabs_manager._add_tab_internal("Tab 2", Mock())
tab_controls = tabs_manager.tab_bar.controls[:-2] # Exclude add/close buttons
tab_controls = tabs_manager.tab_bar.content.controls[:-2] # Exclude add/close buttons
tabs_manager.select_tab(1)
assert tab_controls[0].bgcolor == ft.Colors.SURFACE_CONTAINER_HIGHEST
assert tab_controls[1].bgcolor == ft.Colors.PRIMARY_CONTAINER
assert tab_controls[0].bgcolor == ft.Colors.GREY_800
assert tab_controls[1].bgcolor == ft.Colors.BLUE_900
def test_on_tab_go_empty_url(self, tabs_manager):
"""Test tab go with empty URL."""
@@ -146,12 +146,12 @@ class TestTabsManager:
def test_tab_container_properties(self, tabs_manager):
"""Test that tab container has correct properties."""
assert tabs_manager.content_container.expand is True
assert tabs_manager.content_container.bgcolor == ft.Colors.BLACK
assert tabs_manager.content_container.padding == ft.padding.all(5)
assert tabs_manager.content_container.bgcolor in (ft.Colors.BLACK, "#000000")
assert tabs_manager.content_container.padding == ft.padding.all(16)
def test_tab_bar_controls(self, tabs_manager):
"""Test that tab bar has correct controls."""
controls = tabs_manager.tab_bar.controls
controls = tabs_manager.tab_bar.content.controls
# Should have: home tab, add button, close button (and potentially overflow menu)
assert len(controls) >= 3
@@ -180,7 +180,7 @@ class TestTabsManager:
url_field = tab["url_field"]
assert url_field.expand is True
assert url_field.text_style.size == 12
assert url_field.text_style.size == 14
assert url_field.content_padding is not None
def test_go_button_properties(self, tabs_manager):
@@ -188,14 +188,14 @@ class TestTabsManager:
tab = tabs_manager.manager.tabs[0]
go_btn = tab["go_btn"]
assert go_btn.icon == ft.Icons.OPEN_IN_BROWSER
assert go_btn.tooltip == "Load URL"
assert go_btn.icon == ft.Icons.ARROW_FORWARD
assert go_btn.tooltip == "Go"
def test_tab_click_handlers(self, tabs_manager):
"""Test that tab click handlers are properly set."""
tabs_manager._add_tab_internal("Tab 2", Mock())
tab_controls = tabs_manager.tab_bar.controls[:-2] # Exclude add/close buttons
tab_controls = tabs_manager.tab_bar.content.controls[:-2] # Exclude add/close buttons
for i, control in enumerate(tab_controls):
assert control.on_click is not None
@@ -242,20 +242,20 @@ class TestTabsManager:
# With page width at 800, add enough tabs that some should overflow.
for i in range(10): # Total 11 tabs
tabs_manager._add_tab_internal(f"Tab {i + 2}", Mock())
# Check that an overflow menu exists
assert tabs_manager.overflow_menu is not None
# Simulate a smaller screen, expecting more tabs to overflow
tabs_manager.page.width = 400
tabs_manager._update_tab_visibility()
visible_tabs_small = sum(1 for c in tabs_manager.tab_bar.controls if isinstance(c, ft.Container) and c.visible)
visible_tabs_small = sum(1 for c in tabs_manager.tab_bar.content.controls if isinstance(c, ft.Container) and c.visible)
assert visible_tabs_small < 11
# Simulate a larger screen, expecting all tabs to be visible
tabs_manager.page.width = 1600
tabs_manager._update_tab_visibility()
visible_tabs_large = sum(1 for c in tabs_manager.tab_bar.controls if isinstance(c, ft.Container) and c.visible)
visible_tabs_large = sum(1 for c in tabs_manager.tab_bar.content.controls if isinstance(c, ft.Container) and c.visible)
assert visible_tabs_large == 11
assert tabs_manager.overflow_menu is None

View File

@@ -29,7 +29,7 @@ class TestBuildUI:
@patch("ren_browser.tabs.tabs.TabsManager")
@patch("ren_browser.controls.shortcuts.Shortcuts")
def test_build_ui_appbar_setup(
self, mock_shortcuts, mock_tabs, mock_fetcher, mock_announce_service, mock_page
self, mock_shortcuts, mock_tabs, mock_fetcher, mock_announce_service, mock_page,
):
"""Test that build_ui sets up the app bar correctly."""
mock_tab_manager = Mock()
@@ -51,7 +51,7 @@ class TestBuildUI:
@patch("ren_browser.tabs.tabs.TabsManager")
@patch("ren_browser.controls.shortcuts.Shortcuts")
def test_build_ui_drawer_setup(
self, mock_shortcuts, mock_tabs, mock_fetcher, mock_announce_service, mock_page
self, mock_shortcuts, mock_tabs, mock_fetcher, mock_announce_service, mock_page,
):
"""Test that build_ui sets up the drawer correctly."""
mock_tab_manager = Mock()
@@ -129,14 +129,14 @@ class TestOpenSettingsTab:
# Get the settings content that was added
settings_content = mock_tab_manager._add_tab_internal.call_args[0][1]
# Find the save button and simulate click
# Find the save button - now nested in action_row container
save_btn = None
for control in settings_content.controls:
if hasattr(control, "controls"):
for sub_control in control.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 Config"
and sub_control.text == "Save Configuration"
):
save_btn = sub_control
break