642 lines
23 KiB
Python
642 lines
23 KiB
Python
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
from typing import Any
|
|
|
|
from PySide6.QtCore import QCoreApplication, QSettings
|
|
from PySide6.QtGui import QFont, QPageSize, QRawFont
|
|
from PySide6.QtWidgets import (
|
|
QApplication,
|
|
QDialog,
|
|
QDialogButtonBox,
|
|
QLabel,
|
|
QMessageBox,
|
|
QPushButton,
|
|
QVBoxLayout,
|
|
)
|
|
|
|
from doctor_workstation import app as app_module
|
|
from doctor_workstation.app import ApplicationController
|
|
from doctor_workstation.config import AppConfig
|
|
from doctor_workstation.core.errors import AuthenticationExpiredError
|
|
from doctor_workstation.services import DemoDoctorRepository
|
|
from doctor_workstation.ui import login as login_module
|
|
from doctor_workstation.ui import widgets as widget_module
|
|
from doctor_workstation.ui.login import LoginWindow
|
|
from doctor_workstation.ui.pages.consultations import _video_payload
|
|
from doctor_workstation.ui.shell import NAVIGATION
|
|
from doctor_workstation.ui.theme import apply_theme
|
|
from doctor_workstation.ui.widgets import (
|
|
friendly_error,
|
|
gender_text,
|
|
invoke,
|
|
set_authentication_expired_handler,
|
|
)
|
|
|
|
|
|
def test_consultation_video_payload_keeps_appointment_and_diagnosis_ids_distinct() -> None:
|
|
record = SimpleNamespace(
|
|
id=501,
|
|
appointment_id=101,
|
|
patient_id=301,
|
|
patient_name="林晓岚",
|
|
)
|
|
|
|
payload = _video_payload(record)
|
|
|
|
assert payload["appointment_id"] == 101
|
|
assert payload["diagnosis_id"] == 501
|
|
assert payload["patient_id"] == 301
|
|
|
|
|
|
def test_gender_text_maps_legacy_codes_and_preserves_labels() -> None:
|
|
assert gender_text(1) == "男"
|
|
assert gender_text("2") == "女"
|
|
assert gender_text(0) == "未知"
|
|
assert gender_text("未知标签") == "未知标签"
|
|
|
|
|
|
def test_theme_resolves_real_chinese_glyphs() -> None:
|
|
"""Guard packaged/offscreen builds against rendering every CJK glyph as tofu."""
|
|
|
|
application = QApplication.instance() or QApplication([])
|
|
apply_theme(application)
|
|
raw_font = QRawFont.fromFont(application.font())
|
|
assert raw_font.familyName() == "Noto Sans SC"
|
|
assert not application.font().styleStrategy() & QFont.StyleStrategy.NoSubpixelAntialias
|
|
assert application.font().hintingPreference() == QFont.HintingPreference.PreferDefaultHinting
|
|
glyphs = raw_font.glyphIndexesForString("甄养堂医生工作站")
|
|
|
|
assert glyphs
|
|
assert all(glyph > 0 for glyph in glyphs)
|
|
assert len(set(glyphs)) > 1
|
|
|
|
# QSS must not override the resolved platform face on real controls.
|
|
for widget in (QLabel("甄养堂医生工作站"), QPushButton("确认接诊")):
|
|
widget.ensurePolished()
|
|
resolved = QRawFont.fromFont(widget.font())
|
|
assert resolved.familyName() == raw_font.familyName()
|
|
assert all(glyph > 0 for glyph in resolved.glyphIndexesForString(widget.text()))
|
|
assert not widget.font().styleStrategy() & QFont.StyleStrategy.NoSubpixelAntialias
|
|
assert widget.font().hintingPreference() == QFont.HintingPreference.PreferDefaultHinting
|
|
widget.close()
|
|
|
|
|
|
def test_theme_marks_dynamic_business_dialogs_and_semantic_buttons() -> None:
|
|
application = QApplication.instance() or QApplication([])
|
|
apply_theme(application)
|
|
dialog = QDialog()
|
|
buttons = QDialogButtonBox(
|
|
QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel,
|
|
parent=dialog,
|
|
)
|
|
QVBoxLayout(dialog).addWidget(buttons)
|
|
|
|
dialog.show()
|
|
application.processEvents()
|
|
|
|
assert dialog.property("businessDialog") is True
|
|
assert buttons.button(QDialogButtonBox.StandardButton.Save).property("variant") == "primary"
|
|
assert buttons.button(QDialogButtonBox.StandardButton.Cancel).property("variant") == "secondary"
|
|
dialog.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_navigation_requires_each_pages_actual_list_capability() -> None:
|
|
assert {item.key: item.permissions for item in NAVIGATION} == {
|
|
"reception": ("doctor.appointment/lists",),
|
|
"appointments": ("doctor.appointment/lists",),
|
|
"prescription_library": ("tcm.prescriptionLibrary/lists",),
|
|
"prescriptions": ("tcm.prescription/lists",),
|
|
"patients": ("firstvisit.myPatient/lists",),
|
|
"consultations": ("tcm.diagnosis/lists",),
|
|
}
|
|
|
|
|
|
def test_video_release_does_not_remove_a_newer_call() -> None:
|
|
older = object()
|
|
newer = object()
|
|
# _release_video_call also clears the preview slot, so the double needs the
|
|
# same attributes the real controller sets up in __init__.
|
|
controller = SimpleNamespace(
|
|
video_calls={"501": newer},
|
|
_video_preview_state=None,
|
|
_video_preview_generation=0,
|
|
)
|
|
|
|
ApplicationController._release_video_call(controller, "501", older)
|
|
assert controller.video_calls == {"501": newer}
|
|
|
|
ApplicationController._release_video_call(controller, "501", newer)
|
|
assert controller.video_calls == {}
|
|
|
|
|
|
def test_invoke_leaves_prescription_filters_for_repository_mapping() -> None:
|
|
"""The UI adapter only normalises pagination, not API-specific DTO fields."""
|
|
|
|
class Repository:
|
|
def list_prescriptions(self, **filters: Any) -> dict[str, Any]:
|
|
return filters
|
|
|
|
assert invoke(
|
|
Repository(),
|
|
"prescriptions",
|
|
keyword="CF-2026-8",
|
|
status=2,
|
|
page=3,
|
|
page_size=15,
|
|
) == {
|
|
"keyword": "CF-2026-8",
|
|
"status": 2,
|
|
"page_no": 3,
|
|
"page_size": 15,
|
|
}
|
|
|
|
|
|
def test_async_error_dispatch_consumes_active_session_expiry_globally() -> None:
|
|
"""A consumed authentication expiry does not also reach a stale page."""
|
|
|
|
global_errors: list[Exception] = []
|
|
local_errors: list[Exception] = []
|
|
error = AuthenticationExpiredError("expired", code=-1)
|
|
try:
|
|
set_authentication_expired_handler(lambda caught: global_errors.append(caught) is None)
|
|
widget_module._dispatch_async_error(error, local_errors.append)
|
|
finally:
|
|
set_authentication_expired_handler(None)
|
|
|
|
assert global_errors == [error]
|
|
assert local_errors == []
|
|
|
|
|
|
def test_controller_session_expiry_returns_to_login_once() -> None:
|
|
"""The composition root owns the single transition out of an active shell."""
|
|
|
|
messages: list[str] = []
|
|
controller = SimpleNamespace(
|
|
shell_window=object(),
|
|
current_repository=object(),
|
|
_authentication_expiry_in_progress=False,
|
|
_logout=lambda *, message="": messages.append(message),
|
|
)
|
|
error = AuthenticationExpiredError("expired", code=-1)
|
|
|
|
assert ApplicationController._on_authentication_expired(controller, error)
|
|
assert ApplicationController._on_authentication_expired(controller, error)
|
|
assert messages == ["登录状态已失效,请重新登录。"]
|
|
|
|
|
|
def test_persisted_session_restore_blocks_manual_submit_before_worker_start(
|
|
monkeypatch: Any,
|
|
) -> None:
|
|
"""Login controls are locked before the asynchronous restore is dispatched."""
|
|
|
|
pending_states: list[bool] = []
|
|
worker_calls: list[tuple[Any, dict[str, Any]]] = []
|
|
worker = object()
|
|
repository = SimpleNamespace(restore_session=lambda: None)
|
|
login_window = SimpleNamespace(set_session_restore_pending=pending_states.append)
|
|
controller = SimpleNamespace(
|
|
remote_repository=repository,
|
|
_shutting_down=False,
|
|
config=SimpleNamespace(demo_mode=False),
|
|
current_repository=None,
|
|
_restore_generation=0,
|
|
_restore_in_progress=False,
|
|
_restore_worker=None,
|
|
login_window=login_window,
|
|
_on_restore_success=lambda *args: None,
|
|
_on_restore_error=lambda *args: None,
|
|
_on_restore_finished=lambda *args: None,
|
|
)
|
|
|
|
def fake_run_async(function: Any, **callbacks: Any) -> object:
|
|
assert pending_states == [True]
|
|
worker_calls.append((function, callbacks))
|
|
return worker
|
|
|
|
monkeypatch.setattr(app_module, "run_async", fake_run_async)
|
|
ApplicationController._begin_session_restore(controller)
|
|
|
|
assert controller._restore_in_progress
|
|
assert controller._restore_worker is worker
|
|
assert worker_calls[0][0] == repository.restore_session
|
|
|
|
|
|
def test_login_restore_pending_uses_existing_submit_guard() -> None:
|
|
"""A manual submit is a no-op for the whole persisted-token validation window."""
|
|
|
|
class Banner:
|
|
def clear(self) -> None:
|
|
pass
|
|
|
|
class LoginDouble:
|
|
def __init__(self) -> None:
|
|
self._loading = False
|
|
self.error_banner = Banner()
|
|
self.loading_options: dict[str, Any] = {}
|
|
|
|
def _set_loading(self, loading: bool, **options: Any) -> None:
|
|
self._loading = loading
|
|
self.loading_options = options
|
|
|
|
login = LoginDouble()
|
|
LoginWindow.set_session_restore_pending(login, True) # type: ignore[arg-type]
|
|
LoginWindow.submit(login) # type: ignore[arg-type]
|
|
|
|
assert login._loading
|
|
assert login.loading_options["button_text"] == "正在恢复登录…"
|
|
|
|
|
|
def test_real_demo_login_reaches_success_without_widget_adapter(
|
|
monkeypatch: Any,
|
|
tmp_path: Any,
|
|
) -> None:
|
|
"""Exercise the actual login widgets and demo repository as one contract."""
|
|
|
|
application = QApplication.instance() or QApplication([])
|
|
repository = DemoDoctorRepository()
|
|
settings = QSettings(str(tmp_path / "login.ini"), QSettings.Format.IniFormat)
|
|
config = SimpleNamespace(
|
|
api_base_url="https://127.0.0.1:9",
|
|
request_timeout=30,
|
|
demo_mode=True,
|
|
debug_mode=True,
|
|
remembered_account="",
|
|
)
|
|
payloads: list[dict[str, Any]] = []
|
|
|
|
def run_immediately(function: Any, **callbacks: Any) -> object:
|
|
try:
|
|
callbacks["on_success"](function())
|
|
except Exception as error: # pragma: no cover - assertion output is more useful
|
|
callbacks["on_error"](error)
|
|
finally:
|
|
callbacks["on_finished"]()
|
|
return object()
|
|
|
|
monkeypatch.setattr(login_module, "run_async", run_immediately)
|
|
window = LoginWindow(
|
|
object(),
|
|
config=config,
|
|
demo_repository=repository,
|
|
settings=settings,
|
|
)
|
|
window.login_succeeded.connect(payloads.append)
|
|
|
|
window.submit()
|
|
|
|
assert len(payloads) == 1
|
|
assert payloads[0]["repository"] is repository
|
|
assert payloads[0]["demo_mode"] is True
|
|
assert window.busy_overlay.label.text() == "正在验证账号…"
|
|
assert not window._loading
|
|
window.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_remembered_password_survives_a_new_login_window(tmp_path: Any) -> None:
|
|
application = QApplication.instance() or QApplication([])
|
|
settings_path = tmp_path / "remember-password.ini"
|
|
secrets: dict[tuple[str, str], str] = {}
|
|
|
|
def load_password(*, account: str, scope: str) -> str | None:
|
|
return secrets.get((account, scope.rstrip("/")))
|
|
|
|
def save_password(password: str, *, account: str, scope: str) -> bool:
|
|
secrets[(account, scope.rstrip("/"))] = password
|
|
return True
|
|
|
|
def clear_password(*, account: str, scope: str) -> None:
|
|
secrets.pop((account, scope.rstrip("/")), None)
|
|
|
|
credentials = SimpleNamespace(
|
|
load_password=load_password,
|
|
save_password=save_password,
|
|
clear_password=clear_password,
|
|
)
|
|
config = SimpleNamespace(
|
|
api_base_url="https://example.test/adminapi",
|
|
request_timeout=30,
|
|
verify_ssl=True,
|
|
demo_mode=False,
|
|
remembered_account="",
|
|
)
|
|
first = LoginWindow(
|
|
object(),
|
|
config=config,
|
|
settings=QSettings(str(settings_path), QSettings.Format.IniFormat),
|
|
credential_store=credentials,
|
|
)
|
|
first._on_login_success({}, "admin", True, "secret-value")
|
|
first.close()
|
|
application.processEvents()
|
|
|
|
restored = LoginWindow(
|
|
object(),
|
|
config=config,
|
|
settings=QSettings(str(settings_path), QSettings.Format.IniFormat),
|
|
credential_store=credentials,
|
|
)
|
|
assert restored.account_edit.text() == "admin"
|
|
assert restored.password_edit.text() == "secret-value"
|
|
assert restored.remember_check.text() == "记住密码"
|
|
assert restored.remember_check.isChecked()
|
|
|
|
restored._on_login_success({}, "admin", False, "secret-value")
|
|
restored.close()
|
|
application.processEvents()
|
|
|
|
forgotten = LoginWindow(
|
|
object(),
|
|
config=config,
|
|
settings=QSettings(str(settings_path), QSettings.Format.IniFormat),
|
|
credential_store=credentials,
|
|
)
|
|
assert forgotten.password_edit.text() == ""
|
|
assert not forgotten.remember_check.isChecked()
|
|
forgotten.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_server_settings_panel_keeps_controls_separated_at_minimum_window(
|
|
tmp_path: Any,
|
|
) -> None:
|
|
application = QApplication.instance() or QApplication([])
|
|
settings = QSettings(str(tmp_path / "server-panel.ini"), QSettings.Format.IniFormat)
|
|
config = SimpleNamespace(
|
|
api_base_url="",
|
|
request_timeout=30,
|
|
demo_mode=False,
|
|
debug_mode=True,
|
|
remembered_account="",
|
|
)
|
|
window = LoginWindow(object(), config=config, settings=settings)
|
|
window.resize(860, 590)
|
|
window.show()
|
|
window.server_toggle.setChecked(True)
|
|
window._toggle_server_panel(True)
|
|
application.processEvents()
|
|
|
|
assert window.server_panel.isVisible()
|
|
assert window.server_panel.height() >= window.server_panel.minimumSizeHint().height()
|
|
assert window.server_url_label.geometry().bottom() < window.server_url_edit.geometry().top()
|
|
assert window.server_url_edit.geometry().bottom() < window.timeout_label.geometry().top()
|
|
assert window.timeout_spin.geometry().right() < window.save_server_button.geometry().left()
|
|
assert (
|
|
window.timeout_label.geometry().bottom() < window.allow_self_signed_check.geometry().top()
|
|
)
|
|
assert window.allow_self_signed_check.geometry().bottom() < window.server_hint.geometry().top()
|
|
|
|
window.allow_self_signed_check.setChecked(True)
|
|
application.processEvents()
|
|
assert window.ssl_warning.isVisible()
|
|
assert window.allow_self_signed_check.geometry().bottom() < window.ssl_warning.geometry().top()
|
|
assert window.ssl_warning.geometry().bottom() < window.server_hint.geometry().top()
|
|
|
|
window.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_production_login_hides_and_blocks_debug_controls(tmp_path: Any) -> None:
|
|
application = QApplication.instance() or QApplication([])
|
|
settings = QSettings(str(tmp_path / "production.ini"), QSettings.Format.IniFormat)
|
|
settings.setValue("server/base_url", "https://stale.example.test")
|
|
settings.setValue("server/verify_ssl", False)
|
|
remote_repository = object()
|
|
demo_repository = DemoDoctorRepository()
|
|
config = SimpleNamespace(
|
|
api_base_url="https://prod.example.test/adminapi",
|
|
request_timeout=30,
|
|
verify_ssl=True,
|
|
demo_mode=True,
|
|
debug_mode=False,
|
|
remembered_account="",
|
|
)
|
|
window = LoginWindow(
|
|
remote_repository,
|
|
config=config,
|
|
demo_repository=demo_repository,
|
|
settings=settings,
|
|
)
|
|
window.show()
|
|
application.processEvents()
|
|
|
|
assert not window.demo_check.isVisible()
|
|
assert not window.debug_settings_section.isVisible()
|
|
assert not window.server_toggle.isVisible()
|
|
assert not window.server_panel.isVisible()
|
|
assert not window.demo_check.isChecked()
|
|
assert window.active_repository is remote_repository
|
|
assert window.server_url_edit.text() == "https://prod.example.test/adminapi"
|
|
assert window._credential_scope() == "https://prod.example.test/adminapi"
|
|
|
|
window._on_demo_toggled(True)
|
|
window._toggle_server_panel(True)
|
|
|
|
assert not window.demo_check.isChecked()
|
|
assert window.active_repository is remote_repository
|
|
assert window.server_panel.isHidden()
|
|
window.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_server_settings_can_persist_self_signed_debug_mode(tmp_path: Any) -> None:
|
|
application = QApplication.instance() or QApplication([])
|
|
settings = QSettings(str(tmp_path / "self-signed.ini"), QSettings.Format.IniFormat)
|
|
config = SimpleNamespace(
|
|
api_base_url="https://internal.example.test",
|
|
request_timeout=30,
|
|
verify_ssl=True,
|
|
demo_mode=False,
|
|
debug_mode=True,
|
|
remembered_account="",
|
|
)
|
|
window = LoginWindow(object(), config=config, settings=settings)
|
|
emitted: list[dict[str, Any]] = []
|
|
window.server_settings_changed.connect(emitted.append)
|
|
window.allow_self_signed_check.setChecked(True)
|
|
|
|
window._save_server_settings()
|
|
|
|
assert emitted[-1]["verify_ssl"] is False
|
|
assert settings.value("server/verify_ssl", type=bool) is False
|
|
assert "证书校验已关闭" in window.error_banner.label.text()
|
|
window.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_login_applies_self_signed_setting_before_authentication(
|
|
monkeypatch: Any,
|
|
tmp_path: Any,
|
|
) -> None:
|
|
application = QApplication.instance() or QApplication([])
|
|
settings = QSettings(str(tmp_path / "self-signed-login.ini"), QSettings.Format.IniFormat)
|
|
config = AppConfig(
|
|
api_base_url="https://internal.example.test/adminapi",
|
|
demo_mode=False,
|
|
debug_mode=True,
|
|
verify_ssl=True,
|
|
)
|
|
calls: list[str] = []
|
|
|
|
class Repository:
|
|
def __init__(self, name: str) -> None:
|
|
self.name = name
|
|
|
|
def login(self, **_payload: Any) -> object:
|
|
calls.append(self.name)
|
|
return object()
|
|
|
|
def get_current_user(self) -> object:
|
|
return object()
|
|
|
|
old_repository = Repository("old")
|
|
rebuilt_repository = Repository("rebuilt-without-verification")
|
|
window = LoginWindow(old_repository, config=config, settings=settings)
|
|
|
|
def rebuild_on_config_change(updated: AppConfig) -> None:
|
|
assert updated.verify_ssl is False
|
|
window.repository = rebuilt_repository
|
|
window.active_repository = rebuilt_repository
|
|
|
|
def run_immediately(function: Any, **callbacks: Any) -> object:
|
|
try:
|
|
callbacks["on_success"](function())
|
|
except Exception as error: # pragma: no cover - assertion output is more useful
|
|
callbacks["on_error"](error)
|
|
finally:
|
|
callbacks["on_finished"]()
|
|
return object()
|
|
|
|
monkeypatch.setattr(login_module, "run_async", run_immediately)
|
|
window.config_changed.connect(rebuild_on_config_change)
|
|
window.account_edit.setText("admin")
|
|
window.password_edit.setText("secret")
|
|
window.allow_self_signed_check.setChecked(True)
|
|
|
|
window.submit()
|
|
|
|
assert calls == ["rebuilt-without-verification"]
|
|
assert settings.value("server/verify_ssl", type=bool) is False
|
|
window.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_certificate_error_explains_self_signed_server_setting() -> None:
|
|
message = friendly_error(
|
|
RuntimeError("[SSL: CERTIFICATE_VERIFY_FAILED] self-signed certificate")
|
|
)
|
|
assert "信任自签名证书" in message
|
|
assert "服务器设置" in message
|
|
|
|
|
|
def test_qt_standard_dialog_buttons_are_localized_to_chinese() -> None:
|
|
application = QApplication.instance() or QApplication([])
|
|
app_module._install_chinese_translations(application)
|
|
|
|
question = QMessageBox()
|
|
question.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel)
|
|
assert question.button(QMessageBox.StandardButton.Yes).text() == "是"
|
|
assert question.button(QMessageBox.StandardButton.Cancel).text() == "取消"
|
|
|
|
buttons = QDialogButtonBox(
|
|
QDialogButtonBox.StandardButton.Ok
|
|
| QDialogButtonBox.StandardButton.Save
|
|
| QDialogButtonBox.StandardButton.Close
|
|
)
|
|
assert buttons.button(QDialogButtonBox.StandardButton.Ok).text() == "确定"
|
|
assert buttons.button(QDialogButtonBox.StandardButton.Save).text() == "保存"
|
|
assert buttons.button(QDialogButtonBox.StandardButton.Close).text() == "关闭"
|
|
assert QCoreApplication.translate("QPageSize", "A4") == "A4"
|
|
assert QPageSize(QPageSize.PageSizeId.A4).isValid()
|
|
|
|
|
|
def test_friendly_error_hides_english_technical_messages() -> None:
|
|
message = friendly_error(TypeError("invoke() takes 2 positional arguments but 3 were given"))
|
|
assert message == "程序执行失败,请重试;若问题持续出现,请联系管理员。"
|
|
assert friendly_error(RuntimeError("API response envelope must be an object")) == (
|
|
"服务器返回的数据格式不正确,请联系管理员检查接口。"
|
|
)
|
|
|
|
|
|
def test_certificate_error_opens_server_settings(tmp_path: Any) -> None:
|
|
application = QApplication.instance() or QApplication([])
|
|
settings = QSettings(str(tmp_path / "certificate-error.ini"), QSettings.Format.IniFormat)
|
|
config = SimpleNamespace(
|
|
api_base_url="https://internal.example.test",
|
|
request_timeout=30,
|
|
verify_ssl=True,
|
|
demo_mode=False,
|
|
debug_mode=True,
|
|
remembered_account="",
|
|
)
|
|
window = LoginWindow(object(), config=config, settings=settings)
|
|
|
|
window._on_login_error(RuntimeError("[SSL: CERTIFICATE_VERIFY_FAILED] self-signed certificate"))
|
|
|
|
assert window.server_toggle.isChecked()
|
|
assert not window.server_panel.isHidden()
|
|
assert "信任自签名证书" in window.error_banner.label.text()
|
|
window.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_certificate_error_does_not_reveal_production_server_settings(tmp_path: Any) -> None:
|
|
application = QApplication.instance() or QApplication([])
|
|
settings = QSettings(str(tmp_path / "production-certificate.ini"), QSettings.Format.IniFormat)
|
|
config = SimpleNamespace(
|
|
api_base_url="https://prod.example.test/adminapi",
|
|
request_timeout=30,
|
|
verify_ssl=True,
|
|
demo_mode=False,
|
|
debug_mode=False,
|
|
remembered_account="",
|
|
)
|
|
window = LoginWindow(object(), config=config, settings=settings)
|
|
window.show()
|
|
|
|
window._on_login_error(RuntimeError("[SSL: CERTIFICATE_VERIFY_FAILED]"))
|
|
application.processEvents()
|
|
|
|
assert not window.server_toggle.isChecked()
|
|
assert not window.debug_settings_section.isVisible()
|
|
assert window.server_panel.isHidden()
|
|
assert "联系管理员" in window.error_banner.label.text()
|
|
window.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_business_dialogs_can_be_maximized_but_prompts_cannot() -> None:
|
|
"""Dense AI panels and editors were stuck at their constructed size."""
|
|
|
|
from PySide6.QtCore import Qt
|
|
from PySide6.QtWidgets import QApplication, QDialog, QMessageBox
|
|
|
|
from doctor_workstation.ui.theme import allow_dialog_resize
|
|
|
|
application = QApplication.instance() or QApplication([])
|
|
assert application is not None
|
|
|
|
dialog = QDialog()
|
|
dialog.resize(600, 400)
|
|
allow_dialog_resize(dialog)
|
|
flags = dialog.windowFlags()
|
|
assert flags & Qt.WindowType.WindowMaximizeButtonHint
|
|
assert flags & Qt.WindowType.WindowMinimizeButtonHint
|
|
assert dialog.isSizeGripEnabled()
|
|
dialog.deleteLater()
|
|
|
|
# Transient prompts keep their plain frame.
|
|
prompt = QMessageBox()
|
|
allow_dialog_resize(prompt)
|
|
assert not (prompt.windowFlags() & Qt.WindowType.WindowMaximizeButtonHint)
|
|
prompt.deleteLater()
|
|
|
|
# A dialog that pinned itself to a fixed size keeps that decision.
|
|
fixed = QDialog()
|
|
fixed.setFixedSize(420, 300)
|
|
allow_dialog_resize(fixed)
|
|
assert not (fixed.windowFlags() & Qt.WindowType.WindowMaximizeButtonHint)
|
|
fixed.deleteLater()
|