feat: Implement asynchronous update check on startup with UI integration

This commit is contained in:
claudi 2026-01-29 08:44:03 +01:00
parent 5b28c931d8
commit 41549848ed
3 changed files with 246 additions and 2 deletions

View file

@ -54,6 +54,9 @@ def main() -> int:
logger.info("Main window opened successfully") logger.info("Main window opened successfully")
# Check for updates on startup (non-blocking, async)
window.check_for_updates_startup()
# Run event loop # Run event loop
return app.exec() return app.exec()

View file

@ -1,16 +1,20 @@
"""Main application window with web engine integration.""" """Main application window with web engine integration."""
import asyncio
import logging
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
from PySide6.QtCore import QSize, Qt, QUrl, Signal from PySide6.QtCore import QSize, Qt, QThread, QUrl, Signal
from PySide6.QtWidgets import QMainWindow, QToolBar, QVBoxLayout, QWidget, QLabel, QStatusBar from PySide6.QtWidgets import QLabel, QMainWindow, QStatusBar, QToolBar, QVBoxLayout, QWidget
from webdrop_bridge.config import Config from webdrop_bridge.config import Config
from webdrop_bridge.core.drag_interceptor import DragInterceptor from webdrop_bridge.core.drag_interceptor import DragInterceptor
from webdrop_bridge.core.validator import PathValidator from webdrop_bridge.core.validator import PathValidator
from webdrop_bridge.ui.restricted_web_view import RestrictedWebEngineView from webdrop_bridge.ui.restricted_web_view import RestrictedWebEngineView
logger = logging.getLogger(__name__)
# Default welcome page HTML when no webapp is configured # Default welcome page HTML when no webapp is configured
DEFAULT_WELCOME_PAGE = """ DEFAULT_WELCOME_PAGE = """
<!DOCTYPE html> <!DOCTYPE html>
@ -175,6 +179,7 @@ class MainWindow(QMainWindow):
# Signals # Signals
check_for_updates = Signal() check_for_updates = Signal()
update_available = Signal(object) # Emits Release object
def __init__( def __init__(
self, self,
@ -424,3 +429,111 @@ class MainWindow(QMainWindow):
True if drag was initiated successfully True if drag was initiated successfully
""" """
return self.drag_interceptor.initiate_drag(file_paths) return self.drag_interceptor.initiate_drag(file_paths)
def check_for_updates_startup(self) -> None:
"""Check for updates on application startup.
Runs asynchronously in background without blocking UI.
Uses 24h cache so won't hammer the API.
"""
from webdrop_bridge.core.updater import UpdateManager
try:
# Create update manager
cache_dir = Path.home() / ".webdrop-bridge"
manager = UpdateManager(
current_version=self.config.app_version,
config_dir=cache_dir
)
# Run async check in background
self._run_async_check(manager)
except Exception as e:
logger.error(f"Failed to initialize update check: {e}")
def _run_async_check(self, manager) -> None:
"""Run update check in background thread.
Args:
manager: UpdateManager instance
"""
# Create and start background thread
thread = QThread()
worker = UpdateCheckWorker(manager, self.config.app_version)
# Connect signals
worker.update_available.connect(self._on_update_available)
worker.update_status.connect(self._on_update_status)
worker.finished.connect(thread.quit)
# Start thread
worker.moveToThread(thread)
thread.started.connect(worker.run)
thread.start()
def _on_update_status(self, status: str, emoji: str) -> None:
"""Handle update status changes.
Args:
status: Status text
emoji: Status emoji
"""
self.set_update_status(status, emoji)
def _on_update_available(self, release) -> None:
"""Handle update available notification.
Args:
release: Release object with update info
"""
# Update status to show update available
self.set_update_status(f"Update available: v{release.version}", emoji="")
# Emit signal for main app to show dialog
self.update_available.emit(release)
class UpdateCheckWorker:
"""Worker for running update check asynchronously."""
def __init__(self, manager, current_version: str):
"""Initialize worker.
Args:
manager: UpdateManager instance
current_version: Current app version
"""
self.manager = manager
self.current_version = current_version
# Create signals
from PySide6.QtCore import Signal
self.update_available = Signal(object)
self.update_status = Signal(str, str)
self.finished = Signal()
def run(self) -> None:
"""Run the update check."""
try:
# Notify checking status
self.update_status.emit("Checking for updates", "🔄")
# Run async check
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
release = loop.run_until_complete(self.manager.check_for_updates())
loop.close()
# Emit result
if release:
self.update_available.emit(release)
else:
# No update available - show ready status
self.update_status.emit("Ready", "")
except Exception as e:
logger.error(f"Update check failed: {e}")
self.update_status.emit("Update check failed", "⚠️")
finally:
self.finished.emit()

View file

@ -0,0 +1,128 @@
"""Tests for update startup check functionality."""
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from webdrop_bridge.config import Config
from webdrop_bridge.ui.main_window import UpdateCheckWorker
@pytest.fixture
def sample_config(tmp_path):
"""Create a sample config for testing."""
return Config(
app_name="Test WebDrop",
app_version="1.0.0",
log_level="INFO",
log_file=None,
allowed_roots=[tmp_path],
allowed_urls=[],
webapp_url="file:///./webapp/index.html",
window_width=800,
window_height=600,
enable_logging=False,
)
class TestUpdateCheckWorker:
"""Tests for UpdateCheckWorker."""
def test_worker_initialization(self):
"""Test worker can be initialized."""
manager = MagicMock()
worker = UpdateCheckWorker(manager, "0.0.1")
assert worker.manager is manager
assert worker.current_version == "0.0.1"
def test_worker_has_signals(self):
"""Test worker has required signals."""
manager = MagicMock()
worker = UpdateCheckWorker(manager, "0.0.1")
assert hasattr(worker, "update_available")
assert hasattr(worker, "update_status")
assert hasattr(worker, "finished")
def test_worker_run_method_exists(self):
"""Test worker has run method."""
manager = MagicMock()
worker = UpdateCheckWorker(manager, "0.0.1")
assert hasattr(worker, "run")
assert callable(worker.run)
class TestMainWindowStartupCheck:
"""Test startup check integration in MainWindow."""
def test_window_has_startup_check_method(self, qtbot, sample_config):
"""Test MainWindow has check_for_updates_startup method."""
from webdrop_bridge.ui.main_window import MainWindow
window = MainWindow(sample_config)
qtbot.addWidget(window)
assert hasattr(window, "check_for_updates_startup")
assert callable(window.check_for_updates_startup)
def test_window_has_update_available_signal(self, qtbot, sample_config):
"""Test MainWindow has update_available signal."""
from webdrop_bridge.ui.main_window import MainWindow
window = MainWindow(sample_config)
qtbot.addWidget(window)
assert hasattr(window, "update_available")
def test_startup_check_initializes_without_error(self, qtbot, sample_config):
"""Test startup check can be called without raising."""
from webdrop_bridge.ui.main_window import MainWindow
window = MainWindow(sample_config)
qtbot.addWidget(window)
# Should not raise
window.check_for_updates_startup()
def test_on_update_status_updates_status_bar(self, qtbot, sample_config):
"""Test _on_update_status updates the status bar."""
from webdrop_bridge.ui.main_window import MainWindow
window = MainWindow(sample_config)
qtbot.addWidget(window)
window._on_update_status("Testing", "")
assert "Testing" in window.update_status_label.text()
assert "" in window.update_status_label.text()
def test_on_update_available_emits_signal(self, qtbot, sample_config):
"""Test _on_update_available emits update_available signal."""
from webdrop_bridge.ui.main_window import MainWindow
window = MainWindow(sample_config)
qtbot.addWidget(window)
# Create mock release
mock_release = MagicMock()
mock_release.version = "0.0.2"
with qtbot.waitSignal(window.update_available):
window._on_update_available(mock_release)
def test_on_update_available_updates_status(self, qtbot, sample_config):
"""Test _on_update_available updates status bar."""
from webdrop_bridge.ui.main_window import MainWindow
window = MainWindow(sample_config)
qtbot.addWidget(window)
# Create mock release
mock_release = MagicMock()
mock_release.version = "0.0.2"
window._on_update_available(mock_release)
assert "0.0.2" in window.update_status_label.text()
assert "" in window.update_status_label.text()