251 lines
7.7 KiB
Python
251 lines
7.7 KiB
Python
"""Update dialog contract for optional and forced desktop upgrades."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from dataclasses import replace
|
|
from types import SimpleNamespace
|
|
|
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
|
|
|
from PySide6.QtCore import QObject, Qt
|
|
from PySide6.QtTest import QTest
|
|
from PySide6.QtWidgets import QApplication
|
|
|
|
from doctor_workstation.services.app_update import (
|
|
PACKAGE_TYPE_INNO_SETUP,
|
|
UpdateOffer,
|
|
UpdatePackage,
|
|
)
|
|
from doctor_workstation.ui.dialogs.app_update import AppUpdateDialog, AppUpdateSession
|
|
from doctor_workstation.ui.theme import apply_theme
|
|
|
|
|
|
def _offer(
|
|
*,
|
|
force: bool,
|
|
can_install: bool = True,
|
|
install_unavailable_reason: str = "",
|
|
) -> UpdateOffer:
|
|
package = (
|
|
UpdatePackage(
|
|
url="https://cdn.example.com/DoctorWorkstation.zip",
|
|
sha256="a" * 64,
|
|
size=1024,
|
|
filename="DoctorWorkstation.zip",
|
|
)
|
|
if can_install
|
|
else None
|
|
)
|
|
return UpdateOffer(
|
|
has_update=True,
|
|
force=force,
|
|
enabled=True,
|
|
current_version="0.1.0",
|
|
latest_version="0.2.0",
|
|
min_version="",
|
|
title="医生工作站 0.2.0",
|
|
notes="修复若干问题",
|
|
platform="windows",
|
|
arch="x64",
|
|
package=package,
|
|
can_install=can_install,
|
|
install_unavailable_reason=install_unavailable_reason,
|
|
)
|
|
|
|
|
|
def test_optional_update_dialog_allows_later(application: QApplication | None = None) -> None:
|
|
app = application or QApplication.instance() or QApplication([])
|
|
apply_theme(app)
|
|
dialog = AppUpdateDialog(_offer(force=False))
|
|
dialog.show()
|
|
app.processEvents()
|
|
deferred: list[bool] = []
|
|
dialog.update_deferred.connect(lambda: deferred.append(True))
|
|
assert dialog.later_button.isVisible()
|
|
assert dialog.later_button.isEnabled()
|
|
assert dialog.later_button.text() == "稍后提醒"
|
|
assert not dialog.exit_button.isVisible()
|
|
assert dialog.update_button.text() == "立即更新"
|
|
assert dialog.notes.toPlainText() == "修复若干问题"
|
|
dialog.later_button.click()
|
|
assert deferred == [True]
|
|
assert not dialog.isVisible()
|
|
dialog.deleteLater()
|
|
app.processEvents()
|
|
|
|
|
|
def test_forced_update_dialog_has_explicit_exit_and_blocks_implicit_close(
|
|
application: QApplication | None = None,
|
|
) -> None:
|
|
app = application or QApplication.instance() or QApplication([])
|
|
apply_theme(app)
|
|
dialog = AppUpdateDialog(_offer(force=True))
|
|
dialog.show()
|
|
app.processEvents()
|
|
assert not dialog.later_button.isVisible()
|
|
assert dialog.exit_button.isVisible()
|
|
assert dialog.exit_button.isEnabled()
|
|
assert dialog.exit_button.text() == "退出软件"
|
|
assert "必须更新" in dialog.badge.text()
|
|
|
|
dialog.close()
|
|
app.processEvents()
|
|
assert dialog.isVisible()
|
|
|
|
QTest.keyClick(dialog, Qt.Key.Key_Escape)
|
|
app.processEvents()
|
|
assert dialog.isVisible()
|
|
|
|
dialog.set_busy(True)
|
|
dialog.show_download_progress(256, 1024)
|
|
app.processEvents()
|
|
assert dialog.exit_button.isVisible()
|
|
assert dialog.exit_button.isEnabled()
|
|
assert not dialog.update_button.isEnabled()
|
|
assert not dialog.cancel_button.isVisible()
|
|
|
|
dialog.close()
|
|
app.processEvents()
|
|
assert dialog.isVisible()
|
|
QTest.keyClick(dialog, Qt.Key.Key_Escape)
|
|
app.processEvents()
|
|
assert dialog.isVisible()
|
|
|
|
dialog.allow_application_exit()
|
|
dialog.close()
|
|
app.processEvents()
|
|
assert not dialog.isVisible()
|
|
dialog.deleteLater()
|
|
app.processEvents()
|
|
|
|
|
|
def test_forced_update_exit_button_emits_dedicated_request(
|
|
application: QApplication | None = None,
|
|
) -> None:
|
|
app = application or QApplication.instance() or QApplication([])
|
|
dialog = AppUpdateDialog(_offer(force=True))
|
|
requests: list[bool] = []
|
|
dialog.exit_requested.connect(lambda: requests.append(True))
|
|
dialog.show()
|
|
dialog.set_busy(True)
|
|
app.processEvents()
|
|
|
|
dialog.exit_button.click()
|
|
|
|
assert requests == [True]
|
|
assert dialog.isVisible()
|
|
dialog.hide()
|
|
dialog.deleteLater()
|
|
app.processEvents()
|
|
|
|
|
|
def test_session_quits_immediately_when_update_has_not_started(
|
|
application: QApplication | None = None,
|
|
) -> None:
|
|
app = application or QApplication.instance() or QApplication([])
|
|
host = QObject()
|
|
quit_requests: list[bool] = []
|
|
host.request_quit = lambda: quit_requests.append(True) # type: ignore[attr-defined]
|
|
session = AppUpdateSession(host)
|
|
dialog = AppUpdateDialog(_offer(force=True))
|
|
session.dialog = dialog
|
|
dialog.show()
|
|
app.processEvents()
|
|
|
|
session._request_exit(dialog)
|
|
|
|
assert session._cancel_event.is_set()
|
|
assert quit_requests == [True]
|
|
assert not dialog.exit_button.isEnabled()
|
|
assert not dialog.isVisible()
|
|
|
|
dialog.hide()
|
|
dialog.deleteLater()
|
|
app.processEvents()
|
|
|
|
|
|
def test_session_waits_for_update_worker_before_quitting(
|
|
application: QApplication | None = None,
|
|
) -> None:
|
|
app = application or QApplication.instance() or QApplication([])
|
|
host = QObject()
|
|
quit_requests: list[bool] = []
|
|
host.request_quit = lambda: quit_requests.append(True) # type: ignore[attr-defined]
|
|
session = AppUpdateSession(host)
|
|
dialog = AppUpdateDialog(_offer(force=True))
|
|
session.dialog = dialog
|
|
active_signals = QObject()
|
|
session._signals = active_signals # type: ignore[assignment]
|
|
session._active_install_signals = active_signals # type: ignore[assignment]
|
|
dialog.show()
|
|
app.processEvents()
|
|
|
|
session._request_exit(dialog)
|
|
|
|
assert session._cancel_event.is_set()
|
|
assert quit_requests == []
|
|
assert not dialog.exit_button.isEnabled()
|
|
assert "退出软件" in dialog.status_label.text()
|
|
assert dialog.isVisible()
|
|
|
|
session._finish_install(active_signals, dialog, object()) # type: ignore[arg-type]
|
|
assert quit_requests == []
|
|
|
|
session._on_install_finished(active_signals) # type: ignore[arg-type]
|
|
assert quit_requests == [True]
|
|
assert session._active_install_signals is None
|
|
assert not dialog.isVisible()
|
|
|
|
dialog.hide()
|
|
dialog.deleteLater()
|
|
app.processEvents()
|
|
|
|
|
|
def test_unavailable_update_dialog_shows_policy_reason(
|
|
application: QApplication | None = None,
|
|
) -> None:
|
|
app = application or QApplication.instance() or QApplication([])
|
|
apply_theme(app)
|
|
reason = "无法自动安装:自动安装 Windows 更新必须开启 HTTPS 证书校验。"
|
|
dialog = AppUpdateDialog(
|
|
_offer(
|
|
force=False,
|
|
can_install=False,
|
|
install_unavailable_reason=reason,
|
|
)
|
|
)
|
|
dialog.show()
|
|
app.processEvents()
|
|
assert dialog.status_label.text() == reason
|
|
assert not dialog.update_button.isEnabled()
|
|
dialog.close()
|
|
|
|
|
|
def test_session_explains_disabled_certificate_verification() -> None:
|
|
host = QObject()
|
|
host.config = SimpleNamespace(verify_ssl=False) # type: ignore[attr-defined]
|
|
session = AppUpdateSession(host)
|
|
session._generation = 1
|
|
presented: list[UpdateOffer] = []
|
|
session._present = presented.append # type: ignore[method-assign]
|
|
offer = replace(
|
|
_offer(force=True),
|
|
package=UpdatePackage(
|
|
url="https://cdn.example.com/DoctorWorkstation-Setup.exe",
|
|
sha256="a" * 64,
|
|
size=1024,
|
|
filename="DoctorWorkstation-Setup.exe",
|
|
type=PACKAGE_TYPE_INNO_SETUP,
|
|
),
|
|
)
|
|
|
|
session._on_offer(offer, interactive=True, generation=1)
|
|
|
|
assert len(presented) == 1
|
|
assert presented[0].can_install is False
|
|
assert presented[0].force is False
|
|
assert presented[0].package is None
|
|
assert "开启 HTTPS 证书校验" in presented[0].install_unavailable_reason
|
|
assert "取消勾选“信任自签名证书" in presented[0].install_unavailable_reason
|