feat: Implement asynchronous update check on startup with UI integration
This commit is contained in:
parent
5b28c931d8
commit
41549848ed
3 changed files with 246 additions and 2 deletions
|
|
@ -54,6 +54,9 @@ def main() -> int:
|
|||
|
||||
logger.info("Main window opened successfully")
|
||||
|
||||
# Check for updates on startup (non-blocking, async)
|
||||
window.check_for_updates_startup()
|
||||
|
||||
# Run event loop
|
||||
return app.exec()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,20 @@
|
|||
"""Main application window with web engine integration."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from PySide6.QtCore import QSize, Qt, QUrl, Signal
|
||||
from PySide6.QtWidgets import QMainWindow, QToolBar, QVBoxLayout, QWidget, QLabel, QStatusBar
|
||||
from PySide6.QtCore import QSize, Qt, QThread, QUrl, Signal
|
||||
from PySide6.QtWidgets import QLabel, QMainWindow, QStatusBar, QToolBar, QVBoxLayout, QWidget
|
||||
|
||||
from webdrop_bridge.config import Config
|
||||
from webdrop_bridge.core.drag_interceptor import DragInterceptor
|
||||
from webdrop_bridge.core.validator import PathValidator
|
||||
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 = """
|
||||
<!DOCTYPE html>
|
||||
|
|
@ -175,6 +179,7 @@ class MainWindow(QMainWindow):
|
|||
|
||||
# Signals
|
||||
check_for_updates = Signal()
|
||||
update_available = Signal(object) # Emits Release object
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -424,3 +429,111 @@ class MainWindow(QMainWindow):
|
|||
True if drag was initiated successfully
|
||||
"""
|
||||
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()
|
||||
|
|
|
|||
128
tests/unit/test_startup_check.py
Normal file
128
tests/unit/test_startup_check.py
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue