Files
zyt/app/tests/test_app_update_ui.py
2026-08-28 18:24:37 +08:00

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