from __future__ import annotations import os from typing import Any os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") import pytest from PySide6.QtCore import QPoint, QSize from PySide6.QtGui import QFont, QFontInfo, QIcon from PySide6.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget from doctor_workstation.ui import shell as shell_module from doctor_workstation.ui.reception_style import ( TECH_BLUE, body_family, heading_family, number_family, ) from doctor_workstation.ui.shell import NavigationItem, ShellWindow from doctor_workstation.ui.theme import COLORS, apply_theme class _Page(QWidget): def __init__(self, _repository: Any, *, parent: QWidget, **_kwargs: Any) -> None: super().__init__(parent) layout = QVBoxLayout(self) self.label = QLabel("原有页面正文", self) layout.addWidget(self.label) @pytest.fixture(scope="module") def application() -> QApplication: app = QApplication.instance() or QApplication([]) apply_theme(app) return app @pytest.fixture def window(application: QApplication, monkeypatch: pytest.MonkeyPatch): navigation = [ NavigationItem(key, title, "", _Page, ("doctor.appointment/lists",)) for key, title in ( ("appointments", "挂号列表"), ("consultations", "问诊列表"), ("reception", "接诊台"), ("patients", "我的患者"), ("prescriptions", "已开处方"), ("prescription_library", "我的处方库"), ("legacy_reference", "原版框架参照"), ) ] monkeypatch.setattr( shell_module, "_resolve_navigation", lambda *_args, **_kwargs: [(item, item.title) for item in navigation], ) monkeypatch.setattr(shell_module.motion, "reduced_motion", lambda: True) shell = ShellWindow( object(), {"user": {"name": "陈医生"}, "demo_mode": True}, permissions={"doctor.appointment/lists", "tcm.diagnosis/aiAssistant"}, ) shell.resize(1536, 960) shell.show() application.processEvents() yield shell shell.close() application.processEvents() def _pixel(widget: QWidget, x: int, y: int) -> str: image = widget.grab().toImage() ratio = image.devicePixelRatio() return image.pixelColor(round(x * ratio), round(y * ratio)).name().upper() def test_approved_chrome_restores_styles_fonts_icons_and_geometry( application: QApplication, window: ShellWindow ) -> None: assert window.navigate("legacy_reference") application.processEvents() original_app_qss = application.styleSheet() original_app_font = application.font().toString() original_colors = dict(COLORS) original_page_fonts = {key: page.label.font().toString() for key, page in window.pages.items()} original_styles = dict(window._legacy_chrome_styles) original_avatar = window.user_menu_button.icon().pixmap(34, 34).toImage() original_ai = window.ai_top_button.icon().pixmap(18, 18).toImage() original_nav = ( window.nav_buttons["reception"] .icon() .pixmap(18, 18, QIcon.Mode.Normal, QIcon.State.On) .toImage() ) for key in ("appointments", "consultations", "reception", "patients", "prescriptions", "prescription_library", "consultations", "appointments"): assert window.navigate(key) application.processEvents() assert window.sidebar.width() == 208 assert _pixel(window, 6, 300) == "#F3F7FD" assert _pixel(window.sidebar, 5, 300) == "#F3F7FD" assert _pixel(window.workspace, 600, 10) == TECH_BLUE["surface"] assert window.nav_buttons[key].font().family() == body_family() assert window.nav_buttons[key].font().weight() == QFont.Weight.Normal assert window.brand_name.font().family() == heading_family() assert window.brand_name.font().weight() == QFont.Weight.DemiBold assert window.assistant_glyph.property("receptionTechBlue") is True assert window.assistant_status.property("receptionTechBlue") is True assert original_avatar != window.user_menu_button.icon().pixmap(34, 34).toImage() assert original_ai != window.ai_top_button.icon().pixmap(18, 18).toImage() assert ( original_nav != window.nav_buttons["reception"] .icon() .pixmap(18, 18, QIcon.Mode.Normal, QIcon.State.On) .toImage() ) assert { key: page.label.font().toString() for key, page in window.pages.items() } == original_page_fonts assert window.navigate("legacy_reference") application.processEvents() assert window.sidebar.width() == 190 assert _pixel(window, 6, 300) == COLORS["canvas"].upper() assert _pixel(window, 18, 313) == COLORS["canvas"].upper() assert {widget: widget.styleSheet() for widget in original_styles} == original_styles assert window.user_menu_button.icon().pixmap(34, 34).toImage() == original_avatar assert window.ai_top_button.icon().pixmap(18, 18).toImage() == original_ai assert ( window.nav_buttons["reception"] .icon() .pixmap(18, 18, QIcon.Mode.Normal, QIcon.State.On) .toImage() == original_nav ) assert window.assistant_glyph.property("receptionTechBlue") is False assert window.assistant_status.property("receptionTechBlue") is False assert application.styleSheet() == original_app_qss assert application.font().toString() == original_app_font assert original_colors == COLORS assert { key: page.label.font().toString() for key, page in window.pages.items() } == original_page_fonts @pytest.mark.parametrize("key", ["appointments", "consultations", "reception", "patients", "prescriptions", "prescription_library"]) def test_selected_navigation_and_ai_tile_use_approved_blue( application: QApplication, window: ShellWindow, key: str ) -> None: window.navigate(key) application.processEvents() button = window.nav_buttons[key] button.clearFocus() application.processEvents() assert _pixel(button, 1, button.height() // 2) == TECH_BLUE["accent"] assert _pixel(button, button.width() - 12, button.height() // 2) == TECH_BLUE["selection"] assert _pixel(window.assistant_glyph, 10, 25) == TECH_BLUE["accent"] assert _pixel(window.assistant_status, 3, 9) == TECH_BLUE["accent"] @pytest.mark.parametrize("key", ["appointments", "consultations", "reception", "patients", "prescriptions", "prescription_library"]) def test_approved_brand_mark_uses_blue_medical_cross_and_restores_original( application: QApplication, window: ShellWindow, key: str ) -> None: assert window.navigate("legacy_reference") application.processEvents() mark = window.brand_mark original = mark.grab().toImage() assert window.navigate(key) application.processEvents() assert mark.property("receptionTechBlue") is True assert mark.width() == mark.height() == 38 assert _pixel(mark, 5, 18) == TECH_BLUE["accent"] assert _pixel(mark, 19, 19) == TECH_BLUE["accent"] assert _pixel(mark, 18, 9) == TECH_BLUE["surface"] assert original != mark.grab().toImage() assert window.navigate("legacy_reference") application.processEvents() assert mark.property("receptionTechBlue") is False assert mark.grab().toImage() == original def test_collapsed_rail_keeps_state_across_approved_and_original_pages( application: QApplication, window: ShellWindow ) -> None: window.toggle_sidebar() assert window.sidebar.width() == 68 window.navigate("reception") application.processEvents() assert window.sidebar.width() == 68 assert window.nav_buttons["reception"].isChecked() assert not window.assistant_card.isVisible() assert window.nav_buttons["reception"].toolTip() == "接诊台" window.toggle_sidebar() application.processEvents() assert window.sidebar.width() == 208 assert window.assistant_card.isVisible() window.toggle_sidebar() window.navigate("appointments") application.processEvents() assert window.sidebar.width() == 68 assert window.nav_buttons["appointments"].isChecked() assert window.nav_buttons["appointments"].toolTip() == "挂号列表" assert not window.assistant_card.isVisible() window.navigate("consultations") application.processEvents() assert window.sidebar.width() == 68 assert window.nav_buttons["consultations"].isChecked() assert window.nav_buttons["consultations"].toolTip() == "问诊列表" assert not window.assistant_card.isVisible() window.navigate("patients") application.processEvents() assert window.sidebar.width() == 68 assert window.nav_buttons["patients"].isChecked() window.toggle_sidebar() application.processEvents() assert window.sidebar.width() == 208 window.toggle_sidebar() window.navigate("prescriptions") application.processEvents() assert window.sidebar.width() == 68 assert window.nav_buttons["prescriptions"].isChecked() assert window.nav_buttons["prescriptions"].toolTip() == "已开处方" assert not window.assistant_card.isVisible() window.toggle_sidebar() application.processEvents() assert window.sidebar.width() == 208 window.toggle_sidebar() window.navigate("legacy_reference") application.processEvents() assert window.sidebar.width() == 68 window.toggle_sidebar() application.processEvents() assert window.sidebar.width() == 190 assert window.nav_buttons["legacy_reference"].text() == "原版框架参照" @pytest.mark.parametrize("key", ["appointments", "consultations", "reception", "patients", "prescriptions", "prescription_library"]) def test_approved_shell_geometry_and_function_entries_restore( application: QApplication, window: ShellWindow, key: str ) -> None: assert window.navigate("legacy_reference") application.processEvents() original_geometry = { widget: (widget.size(), widget.iconSize() if hasattr(widget, "iconSize") else None) for widget in ( window.sidebar, window.topbar, window.search_host, window.brand_mark, window.assistant_card, window.assistant_button, window.assistant_status, window.upload_settings_button, window.user_menu_button, window.refresh_button, window.notification_button, window.settings_button, *window.nav_buttons.values(), ) } assert window.navigate(key) application.processEvents() assert window.workspace.pos() == QPoint(208, 0) assert window.topbar.height() == 76 assert window.centralWidget().layout().contentsMargins().isNull() assert window.search_host.mapTo(window, QPoint()) == QPoint(234, 15) assert window.search_host.size() == QSize(384, 44) assert window.assistant_card.mapTo(window, QPoint()) == QPoint(20, 695) assert window.assistant_card.size() == QSize(168, 171) assert window.assistant_button.height() == 46 assert window.brand_name.font().pixelSize() == 18 assert window.brand_subtitle.font().pixelSize() == 13 for index, button in enumerate(window.nav_buttons.values()): assert button.mapTo(window, QPoint()).y() == 99 + 58 * index assert button.height() == 52 assert button.font().pixelSize() == 15 assert button.iconSize() == QSize(20, 20) assert not window.fold_button.isVisible() assert not window.ai_top_button.isVisible() assert window.menu_sidebar_action.isVisible() assert window.menu_ai_action.isVisible() window.menu_sidebar_action.trigger() application.processEvents() assert window.sidebar.width() == 68 assert window.menu_sidebar_action.isVisible() window.menu_sidebar_action.trigger() application.processEvents() assert window.sidebar.width() == 208 assert window.navigate("legacy_reference") application.processEvents() assert window.workspace.pos() == QPoint(203, 13) assert window.fold_button.isVisible() assert window.ai_top_button.isVisible() assert not window.menu_sidebar_action.isVisible() assert not window.menu_ai_action.isVisible() assert not window.user_separator.isVisible() for widget, (size, icon_size) in original_geometry.items(): assert widget.size() == size if icon_size is not None: assert widget.iconSize() == icon_size def test_reception_fonts_resolve_real_requested_weights(application: QApplication) -> None: for family, weights in ( (body_family(), (QFont.Weight.Normal,)), (heading_family(), (QFont.Weight.Medium, QFont.Weight.DemiBold)), (number_family(), (QFont.Weight.Normal, QFont.Weight.DemiBold)), ): for weight in weights: font = QFont(family) font.setPixelSize(15) font.setWeight(weight) resolved = QFontInfo(font) assert resolved.family() == family assert resolved.weight() == weight @pytest.mark.parametrize("key", ["appointments", "consultations", "reception", "patients", "prescriptions", "prescription_library"]) def test_minimum_window_keeps_search_and_all_chrome_actions_separate( application: QApplication, window: ShellWindow, key: str ) -> None: window.navigate(key) window.resize(1024, 640) application.processEvents() assert window.width() == 1024 assert 220 <= window.search_host.width() < 384 controls = ( window.search_host, window.refresh_button, window.notification_button, window.settings_button, window.user_menu_button, window.minimize_button, window.fullscreen_button, window.close_button, ) for left, right in zip(controls, controls[1:], strict=False): assert left.x() + left.width() <= right.x() assert right.x() + right.width() <= window.topbar.width() window.resize(1536, 960) application.processEvents() assert window.search_host.size() == QSize(384, 44) def test_refresh_animation_keeps_running_and_restores_palette_on_page_switch( application: QApplication, window: ShellWindow ) -> None: window.navigate("legacy_reference") application.processEvents() button = window.refresh_button original = button.icon().pixmap(18, 18).toImage() button.start_spin() for key in ("appointments", "consultations", "reception", "patients", "prescriptions", "prescription_library"): window.navigate(key) application.processEvents() assert button._timer.isActive() assert button.property("receptionTechBlue") is True window.navigate("legacy_reference") application.processEvents() assert button._timer.isActive() assert button.property("receptionTechBlue") is False assert button._resting.pixmap(18, 18).toImage() == original button._timer.stop() def test_navigation_between_approved_pages_keeps_existing_chrome_assets( application: QApplication, window: ShellWindow ) -> None: assert window.navigate("appointments") application.processEvents() styles = {widget: widget.styleSheet() for widget in window._legacy_chrome_styles} icons = { widget: widget.icon().cacheKey() for widget in ( window.user_menu_button, window.refresh_button, window.ai_top_button, *window.nav_buttons.values(), ) } window.global_search.setText("患者查询") for key in ("consultations", "reception", "patients", "prescriptions", "prescription_library", "appointments", "consultations"): assert window.navigate(key) application.processEvents() assert window.nav_buttons[key].isChecked() assert window.global_search.text() == "患者查询" assert window._reception_chrome_active assert {widget: widget.styleSheet() for widget in styles} == styles assert {widget: widget.icon().cacheKey() for widget in icons} == icons