from __future__ import annotations import os from types import SimpleNamespace os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") import pytest from PySide6.QtCore import QPoint from PySide6.QtGui import QPalette from PySide6.QtWidgets import QApplication, QDialog, QLabel, QVBoxLayout, QWidget from doctor_workstation.core import PermissionSet from doctor_workstation.ui import apply_theme from doctor_workstation.ui.appointment_drawer import APPOINTMENT_DRAWER_QSS, _SlotCard from doctor_workstation.ui.diagnosis_drawer import DIAGNOSIS_QSS, DiagnosisTabWidget from doctor_workstation.ui.dialogs import prescription_ai as ai_module from doctor_workstation.ui.dialogs.prescription import PrescriptionEditorDialog from doctor_workstation.ui.dialogs.prescription_ai import PrescriptionAiReportDialog @pytest.fixture(scope="module") def application() -> QApplication: app = QApplication.instance() or QApplication([]) apply_theme(app) return app def settle(application: QApplication) -> None: for _ in range(10): application.processEvents() @pytest.mark.parametrize("width", [600, 760, 860]) def test_prescription_form_keeps_date_gender_and_controls_readable( application: QApplication, width: int ) -> None: host = QWidget() host.resize(width, 640) host.show() editor = PrescriptionEditorDialog( SimpleNamespace(list_medicines=lambda **kwargs: {"lists": [], "count": 0}), {"diagnosis_id": 501, "patient_name": "林晓岚", "prescription_type": "浓缩水丸"}, parent=host, ) editor.show() settle(application) try: display = editor.date_edit._display assert display.width() >= display.fontMetrics().horizontalAdvance(display.text()) + 15 for button in (editor.gender_male, editor.gender_female): assert button.width() >= button.sizeHint().width() controls = [editor.patient_name, editor.age, editor.date_edit, editor.prescription_type, editor.times_per_day] assert len({control.height() for control in controls}) == 1 assert editor.body_content.width() == editor.body_scroll.viewport().width() assert editor.body_scroll.horizontalScrollBar().maximum() == 0 footer_position = editor.footer.mapToGlobal(QPoint()) editor.body_scroll.verticalScrollBar().setValue(editor.body_scroll.verticalScrollBar().maximum()) settle(application) assert editor.footer.mapToGlobal(QPoint()) == footer_position assert editor.save_button.visibleRegion().boundingRect() == editor.save_button.rect() # Changing prescription type keeps its conditional fields reachable after reflow. editor.prescription_type.setCurrentText("饮片") settle(application) assert not editor._main_decoction_field.isHidden() assert editor._main_bag_field.isHidden() if width == 860: for resized_width in (600, 860): editor.resize(resized_width, 640) settle(application) assert display.width() >= display.fontMetrics().horizontalAdvance(display.text()) + 15 assert editor._main_decoction_field.width() >= editor.need_decoction.minimumSizeHint().width() + 104 finally: editor.close() host.close() def test_diagnosis_overflow_rail_leaves_selected_tab_underline_visible(application: QApplication) -> None: tabs = DiagnosisTabWidget() tabs.setObjectName("DiagnosisDrawerTabs") tabs.setStyleSheet(DIAGNOSIS_QSS) for title in ("病历", "医生备注", "日常记录", "处方", "业务订单", "视频录制回放", "聊天", "指派医助记录", "挂号记录"): tabs.addTab(QWidget(), title) tabs.resize(614, 480) tabs.show() settle(application) try: selected = tabs.tabBar().tabRect(tabs.currentIndex()) selected.moveTopLeft(tabs.tabBar().pos() + selected.topLeft()) assert tabs.tab_scrollbar.isVisible() assert not selected.intersects(tabs.tab_scrollbar.geometry()) tabs.tab_scrollbar.setValue(tabs.count() - 1) assert tabs.currentIndex() == tabs.count() - 1 finally: tabs.close() def test_appointment_slot_labels_follow_checked_and_disabled_states(application: QApplication) -> None: dialog = QDialog() dialog.setObjectName("AppointmentDrawerOverlay") dialog.setStyleSheet(APPOINTMENT_DRAWER_QSS) layout = QVBoxLayout(dialog) card = _SlotCard("09:00-09:30", "可约") card.setProperty("appointmentSlot", True) card.setProperty("availability", "available") card.setCheckable(True) layout.addWidget(card) dialog.show() settle(application) try: for checked, expected in ((True, "#ffffff"), (False, "#1a1c1f")): card.setChecked(checked) settle(application) assert card.time_label.palette().color(QPalette.ColorRole.WindowText).name() == expected card.setChecked(True) card.setEnabled(False) settle(application) assert card.time_label.palette().color(QPalette.ColorGroup.Disabled, QPalette.ColorRole.WindowText).name() == "#8e8f90" finally: dialog.close() def test_ai_report_columns_keep_headings_at_the_same_text_baseline( application: QApplication, monkeypatch: pytest.MonkeyPatch ) -> None: def immediate(function, **kwargs): kwargs["on_success"](function()) if kwargs.get("on_finished"): kwargs["on_finished"]() monkeypatch.setattr(ai_module, "run_async", immediate) report = {"summary": "复核参考", "possible_symptoms": ["乏力", "纳差", "睡眠不安"], "main_indications": "脾气不足", "efficacy": ["健脾", "益气", "养阴"], "suitable_people": ["需由医师辨证确认"]} repository = SimpleNamespace(list_prescription_template_ai_reports=lambda template_id: {"prescription_id": template_id, "reports": [{"report_id": 1, "model_key": "qwen", "report": report}]}) dialog = PrescriptionAiReportDialog(repository, PermissionSet(["*"])) dialog.open_for({"id": 1, "prescription_name": "测试方", "herbs": []}) dialog.show() settle(application) try: labels = {label.text(): label for label in dialog.findChildren(QLabel) if label.objectName() == "PrescriptionAiSectionTitle"} for left, right in (("可能症状与证候", "主治方向"), ("主要功效", "可能适用人群")): # QLabel vertically centers text: compare the centers, not just widget origins. centers = [labels[text].mapTo(dialog, labels[text].rect().center()).y() for text in (left, right)] assert abs(centers[0] - centers[1]) <= 1 finally: dialog.close()