- Renamed `initiate_drag` to `handle_drag` in MainWindow and updated related tests. - Improved drag handling logic to utilize a bridge for starting file drags. - Updated `_on_drag_started` and `_on_drag_failed` methods to match new signatures. - Modified test cases to reflect changes in drag handling and assertions. Enhance path validation and logging - Updated `PathValidator` to log warnings for nonexistent roots instead of raising errors. - Adjusted tests to verify the new behavior of skipping nonexistent roots. Update web application UI and functionality - Changed displayed text for drag items to reflect local paths and Azure Blob Storage URLs. - Added debug logging for drag operations in the web application. - Improved instructions for testing drag and drop functionality. Add configuration documentation and example files - Created `CONFIG_README.md` to provide detailed configuration instructions for WebDrop Bridge. - Added `config.example.json` and `config_test.json` for reference and testing purposes. Implement URL conversion logic - Introduced `URLConverter` class to handle conversion of Azure Blob Storage URLs to local paths. - Added unit tests for URL conversion to ensure correct functionality. Develop download interceptor script - Created `download_interceptor.js` to intercept download-related actions in the web application. - Implemented logging for fetch calls, XMLHttpRequests, and Blob URL creations. Add download test page and related tests - Created `test_download.html` for testing various download scenarios. - Implemented `test_download.py` to verify download path resolution and file construction. - Added `test_url_mappings.py` to ensure URL mappings are loaded correctly. Add unit tests for URL converter - Created `test_url_converter.py` to validate URL conversion logic and mapping behavior.
553 lines
19 KiB
Python
553 lines
19 KiB
Python
"""Unit tests for MainWindow component."""
|
|
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from PySide6.QtWidgets import QToolBar
|
|
|
|
from webdrop_bridge.config import Config
|
|
from webdrop_bridge.ui.main_window import MainWindow
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_config(tmp_path):
|
|
"""Provide a test configuration."""
|
|
# Create required directories
|
|
allowed_root = tmp_path / "allowed"
|
|
allowed_root.mkdir(exist_ok=True)
|
|
|
|
# Create webapp HTML
|
|
webapp_dir = tmp_path / "webapp"
|
|
webapp_dir.mkdir(exist_ok=True)
|
|
webapp_file = webapp_dir / "index.html"
|
|
webapp_file.write_text("<html><body>Test</body></html>")
|
|
|
|
config = Config(
|
|
app_name="Test WebDrop",
|
|
app_version="1.0.0",
|
|
log_level="INFO",
|
|
log_file=None,
|
|
allowed_roots=[allowed_root],
|
|
allowed_urls=["localhost", "127.0.0.1"],
|
|
webapp_url=str(webapp_file),
|
|
window_width=800,
|
|
window_height=600,
|
|
window_title="Test WebDrop v1.0.0",
|
|
enable_logging=False,
|
|
)
|
|
return config
|
|
|
|
|
|
class TestMainWindowInitialization:
|
|
"""Test MainWindow initialization and setup."""
|
|
|
|
def test_main_window_creation(self, qtbot, sample_config):
|
|
"""Test MainWindow can be instantiated."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
assert window is not None
|
|
assert window.config == sample_config
|
|
|
|
def test_main_window_title(self, qtbot, sample_config):
|
|
"""Test window title is set correctly."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
expected_title = f"{sample_config.app_name} v{sample_config.app_version}"
|
|
assert window.windowTitle() == expected_title
|
|
|
|
def test_main_window_geometry(self, qtbot, sample_config):
|
|
"""Test window geometry is set correctly."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
assert window.width() == sample_config.window_width
|
|
assert window.height() == sample_config.window_height
|
|
|
|
def test_main_window_has_web_view(self, qtbot, sample_config):
|
|
"""Test MainWindow has web_view attribute."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
assert hasattr(window, "web_view")
|
|
assert window.web_view is not None
|
|
|
|
def test_main_window_has_drag_interceptor(self, qtbot, sample_config):
|
|
"""Test MainWindow has drag_interceptor attribute."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
assert hasattr(window, "drag_interceptor")
|
|
assert window.drag_interceptor is not None
|
|
|
|
|
|
class TestMainWindowNavigation:
|
|
"""Test navigation toolbar and functionality."""
|
|
|
|
def test_navigation_toolbar_created(self, qtbot, sample_config):
|
|
"""Test navigation toolbar is created."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
toolbars = window.findChildren(QToolBar)
|
|
assert len(toolbars) > 0
|
|
|
|
def test_navigation_toolbar_not_movable(self, qtbot, sample_config):
|
|
"""Test navigation toolbar is not movable (locked for Kiosk-mode)."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
toolbar = window.findChild(QToolBar)
|
|
assert toolbar is not None
|
|
assert not toolbar.isMovable()
|
|
|
|
def test_navigate_home(self, qtbot, sample_config):
|
|
"""Test home button navigation."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
with patch.object(window.web_view, "load") as mock_load:
|
|
window._navigate_home()
|
|
mock_load.assert_called_once()
|
|
|
|
def test_navigate_home_with_http_url(self, qtbot, tmp_path):
|
|
"""Test home navigation with HTTP URL."""
|
|
config = Config(
|
|
app_name="Test",
|
|
app_version="1.0.0",
|
|
log_level="INFO",
|
|
log_file=None,
|
|
allowed_roots=[tmp_path],
|
|
allowed_urls=[],
|
|
webapp_url="http://localhost:8000",
|
|
window_width=800,
|
|
window_height=600,
|
|
enable_logging=False,
|
|
)
|
|
|
|
window = MainWindow(config)
|
|
qtbot.addWidget(window)
|
|
|
|
with patch.object(window.web_view, "load") as mock_load:
|
|
window._navigate_home()
|
|
|
|
# Verify load was called with HTTP URL
|
|
call_args = mock_load.call_args
|
|
url = call_args[0][0]
|
|
assert url.scheme() == "http"
|
|
|
|
def test_navigate_home_with_file_url(self, qtbot, sample_config):
|
|
"""Test home navigation with file:// URL."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
with patch.object(window.web_view, "load") as mock_load:
|
|
window._navigate_home()
|
|
|
|
call_args = mock_load.call_args
|
|
url = call_args[0][0]
|
|
assert url.scheme() == "file"
|
|
|
|
|
|
class TestMainWindowWebAppLoading:
|
|
"""Test web application loading."""
|
|
|
|
def test_load_local_webapp_file(self, qtbot, sample_config):
|
|
"""Test loading local webapp file."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
# Window should load without errors
|
|
assert window.web_view is not None
|
|
|
|
def test_load_remote_webapp_url(self, qtbot, tmp_path):
|
|
"""Test loading remote webapp URL."""
|
|
config = Config(
|
|
app_name="Test",
|
|
app_version="1.0.0",
|
|
log_level="INFO",
|
|
log_file=None,
|
|
allowed_roots=[tmp_path],
|
|
allowed_urls=["localhost"],
|
|
webapp_url="http://localhost:3000",
|
|
window_width=800,
|
|
window_height=600,
|
|
enable_logging=False,
|
|
)
|
|
|
|
window = MainWindow(config)
|
|
qtbot.addWidget(window)
|
|
|
|
assert window.web_view is not None
|
|
|
|
def test_load_nonexistent_file_shows_welcome_page(self, qtbot, tmp_path):
|
|
"""Test loading nonexistent file shows welcome page HTML."""
|
|
config = Config(
|
|
app_name="Test",
|
|
app_version="1.0.0",
|
|
log_level="INFO",
|
|
log_file=None,
|
|
allowed_roots=[tmp_path],
|
|
allowed_urls=[],
|
|
webapp_url="/nonexistent/file.html",
|
|
window_width=800,
|
|
window_height=600,
|
|
enable_logging=False,
|
|
)
|
|
|
|
with patch.object(config, "webapp_url", "/nonexistent/file.html"):
|
|
window = MainWindow(config)
|
|
qtbot.addWidget(window)
|
|
|
|
with patch.object(
|
|
window.web_view, "setHtml"
|
|
) as mock_set_html:
|
|
window._load_webapp()
|
|
mock_set_html.assert_called_once()
|
|
|
|
# Verify welcome page is shown instead of error
|
|
call_args = mock_set_html.call_args[0][0]
|
|
assert "WebDrop Bridge" in call_args
|
|
assert "Application Ready" in call_args
|
|
|
|
|
|
class TestMainWindowDragIntegration:
|
|
"""Test drag-and-drop integration."""
|
|
|
|
def test_drag_interceptor_validator_set(self, qtbot, sample_config):
|
|
"""Test drag interceptor validator is configured."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
assert window.drag_interceptor._validator is not None
|
|
|
|
def test_drag_interceptor_signals_connected(self, qtbot, sample_config):
|
|
"""Test drag interceptor signals are connected."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
# Signals should be connected
|
|
assert window.drag_interceptor.drag_started is not None
|
|
assert window.drag_interceptor.drag_failed is not None
|
|
|
|
def test_handle_drag_delegates_to_interceptor(
|
|
self, qtbot, sample_config, tmp_path
|
|
):
|
|
"""Test drag handling delegates to interceptor."""
|
|
from PySide6.QtCore import QCoreApplication
|
|
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
# Create test file
|
|
test_file = sample_config.allowed_roots[0] / "test.txt"
|
|
test_file.write_text("test")
|
|
|
|
with patch.object(
|
|
window.drag_interceptor, "handle_drag"
|
|
) as mock_drag:
|
|
mock_drag.return_value = True
|
|
# Call through bridge
|
|
window._drag_bridge.start_file_drag(str(test_file))
|
|
|
|
# Process deferred QTimer.singleShot(0, ...) call
|
|
QCoreApplication.processEvents()
|
|
|
|
mock_drag.assert_called_once_with(str(test_file))
|
|
|
|
def test_on_drag_started_called(self, qtbot, sample_config):
|
|
"""Test _on_drag_started handler can be called."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
# Should not raise - new signature has source and local_path
|
|
window._on_drag_started("https://example.com/file.png", "/local/path/file.png")
|
|
|
|
def test_on_drag_failed_called(self, qtbot, sample_config):
|
|
"""Test _on_drag_failed handler can be called."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
# Should not raise - new signature has source and error
|
|
window._on_drag_failed("https://example.com/file.png", "Test error message")
|
|
|
|
|
|
class TestMainWindowURLWhitelist:
|
|
"""Test URL whitelisting integration."""
|
|
|
|
def test_restricted_web_view_receives_allowed_urls(
|
|
self, qtbot, sample_config
|
|
):
|
|
"""Test RestrictedWebEngineView receives allowed URLs from config."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
# web_view should have allowed_urls configured
|
|
assert window.web_view.allowed_urls == sample_config.allowed_urls
|
|
|
|
def test_empty_allowed_urls_list(self, qtbot, tmp_path):
|
|
"""Test with empty allowed URLs (no restriction)."""
|
|
config = Config(
|
|
app_name="Test",
|
|
app_version="1.0.0",
|
|
log_level="INFO",
|
|
log_file=None,
|
|
allowed_roots=[tmp_path],
|
|
allowed_urls=[], # Empty = no restriction
|
|
webapp_url="http://localhost",
|
|
window_width=800,
|
|
window_height=600,
|
|
enable_logging=False,
|
|
)
|
|
|
|
window = MainWindow(config)
|
|
qtbot.addWidget(window)
|
|
|
|
assert window.web_view.allowed_urls == []
|
|
|
|
|
|
class TestMainWindowSignals:
|
|
"""Test signal connections."""
|
|
|
|
def test_drag_started_signal_connection(self, qtbot, sample_config):
|
|
"""Test drag_started signal is connected to handler."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
with patch.object(window, "_on_drag_started") as mock_handler:
|
|
window.drag_interceptor.drag_started.emit(["/path/to/file"])
|
|
mock_handler.assert_called_once()
|
|
|
|
def test_drag_failed_signal_connection(self, qtbot, sample_config):
|
|
"""Test drag_failed signal is connected to handler."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
with patch.object(window, "_on_drag_failed") as mock_handler:
|
|
window.drag_interceptor.drag_failed.emit("Error message")
|
|
mock_handler.assert_called_once()
|
|
|
|
|
|
class TestMainWindowMenuBar:
|
|
"""Test toolbar help actions integration."""
|
|
|
|
def test_navigation_toolbar_created(self, qtbot, sample_config):
|
|
"""Test navigation toolbar is created with help buttons."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
# Check that toolbar exists
|
|
assert len(window.findChildren(QToolBar)) > 0
|
|
toolbar = window.findChildren(QToolBar)[0]
|
|
assert toolbar is not None
|
|
|
|
def test_window_has_check_for_updates_signal(self, qtbot, sample_config):
|
|
"""Test window has check_for_updates signal."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
# Test that signal exists
|
|
assert hasattr(window, "check_for_updates")
|
|
|
|
# Test that signal is callable (can be emitted)
|
|
assert callable(window.check_for_updates.emit)
|
|
|
|
def test_on_check_for_updates_method_exists(self, qtbot, sample_config):
|
|
"""Test _on_manual_check_for_updates method exists."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
# Test that the method exists
|
|
assert hasattr(window, "_on_manual_check_for_updates")
|
|
assert callable(window._on_manual_check_for_updates)
|
|
|
|
def test_show_about_dialog_method_exists(self, qtbot, sample_config):
|
|
"""Test _show_about_dialog method exists."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
# Test that the method exists
|
|
assert hasattr(window, "_show_about_dialog")
|
|
assert callable(window._show_about_dialog)
|
|
|
|
|
|
class TestMainWindowStatusBar:
|
|
"""Test status bar and update status."""
|
|
|
|
def test_status_bar_created(self, qtbot, sample_config):
|
|
"""Test status bar is created."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
assert window.statusBar() is not None
|
|
assert hasattr(window, "status_bar")
|
|
|
|
def test_update_status_label_created(self, qtbot, sample_config):
|
|
"""Test update status label exists."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
assert hasattr(window, "update_status_label")
|
|
assert window.update_status_label is not None
|
|
|
|
def test_set_update_status_text_only(self, qtbot, sample_config):
|
|
"""Test setting update status with text only."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
window.set_update_status("Checking for updates")
|
|
assert "Checking for updates" in window.update_status_label.text()
|
|
|
|
def test_set_update_status_with_emoji(self, qtbot, sample_config):
|
|
"""Test setting update status with emoji."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
window.set_update_status("Checking", emoji="🔄")
|
|
assert "🔄" in window.update_status_label.text()
|
|
assert "Checking" in window.update_status_label.text()
|
|
|
|
def test_set_update_status_checking(self, qtbot, sample_config):
|
|
"""Test checking for updates status."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
window.set_update_status("Checking for updates", emoji="🔄")
|
|
assert "🔄" in window.update_status_label.text()
|
|
|
|
def test_set_update_status_available(self, qtbot, sample_config):
|
|
"""Test update available status."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
window.set_update_status("Update available v0.0.2", emoji="✅")
|
|
assert "✅" in window.update_status_label.text()
|
|
|
|
def test_set_update_status_downloading(self, qtbot, sample_config):
|
|
"""Test downloading status."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
window.set_update_status("Downloading update", emoji="⬇️")
|
|
assert "⬇️" in window.update_status_label.text()
|
|
|
|
def test_set_update_status_error(self, qtbot, sample_config):
|
|
"""Test error status."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
window.set_update_status("Update check failed", emoji="⚠️")
|
|
assert "⚠️" in window.update_status_label.text()
|
|
|
|
|
|
class TestMainWindowStylesheet:
|
|
"""Test stylesheet application."""
|
|
|
|
def test_stylesheet_loading_gracefully_handles_missing_file(
|
|
self, qtbot, sample_config
|
|
):
|
|
"""Test missing stylesheet doesn't crash application."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
# Should not raise even if stylesheet missing
|
|
window._apply_stylesheet()
|
|
|
|
def test_stylesheet_loading_with_nonexistent_file(
|
|
self, qtbot, sample_config
|
|
):
|
|
"""Test stylesheet loading with nonexistent file path."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
with patch("builtins.open", side_effect=OSError("File not found")):
|
|
# Should handle gracefully
|
|
window._apply_stylesheet()
|
|
|
|
|
|
class TestMainWindowCloseEvent:
|
|
"""Test window close handling."""
|
|
|
|
def test_close_event_accepted(self, qtbot, sample_config):
|
|
"""Test close event is accepted."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
from PySide6.QtGui import QCloseEvent
|
|
|
|
event = QCloseEvent()
|
|
window.closeEvent(event)
|
|
|
|
assert event.isAccepted()
|
|
|
|
|
|
class TestMainWindowIntegration:
|
|
"""Integration tests for MainWindow with all components."""
|
|
|
|
def test_full_initialization_flow(self, qtbot, sample_config):
|
|
"""Test complete initialization flow."""
|
|
window = MainWindow(sample_config)
|
|
qtbot.addWidget(window)
|
|
|
|
# Verify all components initialized
|
|
assert window.web_view is not None
|
|
assert window.drag_interceptor is not None
|
|
assert window.config == sample_config
|
|
|
|
# Verify toolbar exists
|
|
toolbars = window.findChildren(QToolBar)
|
|
assert len(toolbars) > 0
|
|
|
|
def test_window_with_multiple_allowed_roots(self, qtbot, tmp_path):
|
|
"""Test MainWindow with multiple allowed root directories."""
|
|
root1 = tmp_path / "root1"
|
|
root2 = tmp_path / "root2"
|
|
root1.mkdir()
|
|
root2.mkdir()
|
|
|
|
webapp_file = tmp_path / "index.html"
|
|
webapp_file.write_text("<html></html>")
|
|
|
|
config = Config(
|
|
app_name="Test",
|
|
app_version="1.0.0",
|
|
log_level="INFO",
|
|
log_file=None,
|
|
allowed_roots=[root1, root2],
|
|
allowed_urls=[],
|
|
webapp_url=str(webapp_file),
|
|
window_width=800,
|
|
window_height=600,
|
|
enable_logging=False,
|
|
)
|
|
|
|
window = MainWindow(config)
|
|
qtbot.addWidget(window)
|
|
|
|
# Verify validator has both roots
|
|
assert window.drag_interceptor._validator is not None
|
|
assert len(
|
|
window.drag_interceptor._validator.allowed_roots
|
|
) == 2
|
|
|
|
def test_window_with_url_whitelist(self, qtbot, tmp_path):
|
|
"""Test MainWindow respects URL whitelist."""
|
|
config = Config(
|
|
app_name="Test",
|
|
app_version="1.0.0",
|
|
log_level="INFO",
|
|
log_file=None,
|
|
allowed_roots=[tmp_path],
|
|
allowed_urls=["*.example.com", "localhost"],
|
|
webapp_url="http://localhost",
|
|
window_width=800,
|
|
window_height=600,
|
|
enable_logging=False,
|
|
)
|
|
|
|
window = MainWindow(config)
|
|
qtbot.addWidget(window)
|
|
|
|
# Verify whitelist is set
|
|
assert window.web_view.allowed_urls == ["*.example.com", "localhost"]
|