from __future__ import annotations import os from types import SimpleNamespace from typing import Any os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") import pytest from PySide6.QtCore import Qt from PySide6.QtWidgets import QApplication, QDialog from doctor_workstation.core import PermissionSet from doctor_workstation.ui import widgets from doctor_workstation.ui.dialogs import diagnosis as diagnosis_module from doctor_workstation.ui.dialogs import prescription as dialog_module from doctor_workstation.ui.dialogs.diagnosis import DiagnosisDialog from doctor_workstation.ui.dialogs.prescription import ( DiagnosisDetailDialog, PrescriptionEditorDialog, PrescriptionOrderDialog, PrescriptionTemplateDialog, ) from doctor_workstation.ui.pages import prescription_library as library_module from doctor_workstation.ui.pages import prescriptions as prescription_module from doctor_workstation.ui.pages.prescription_library import PrescriptionLibraryPage from doctor_workstation.ui.pages.prescriptions import PrescriptionsPage @pytest.fixture(scope="module") def application() -> QApplication: return QApplication.instance() or QApplication([]) def _immediate_async( function: Any, *args: Any, on_success: Any = None, on_error: Any = None, on_finished: Any = None, **_kwargs: Any, ) -> object: try: result = function(*args) except Exception as error: if on_error: on_error(error) else: if on_success: on_success(result) finally: if on_finished: on_finished() return object() class _DiagnosisRepository: def __init__(self) -> None: self.order_queries: list[dict[str, Any]] = [] self.phone_checks: list[dict[str, Any]] = [] self.id_card_checks: list[dict[str, Any]] = [] self.updates: list[tuple[int, dict[str, Any]]] = [] def get_diagnosis_detail(self, diagnosis_id: int, *, readonly: bool = False) -> dict[str, Any]: del readonly return { "diagnosis": { "id": diagnosis_id, "patient_id": 321, "patient_name": "林晓岚", "phone": "13812345678", "id_card": "510107199001011234", "gender": 0, "age": 36, "height": 162.5, "weight": 52.0, "fasting_blood_sugar": "6.2", "diagnosis_type": "first_visit", "local_hospital_name": "杭州市第一人民医院", "chief_complaint": "乏力", "symptoms": "口干", "appetite": ["一般", "少食"], "clinical_diagnosis": "气阴两虚", "can_edit_patient_basic": True, }, "patient": {"id": 321}, "appointment": {}, } def appointment_history(self, **_kwargs: Any) -> dict[str, Any]: return {"lists": [], "count": 0} def assign_history(self, **_kwargs: Any) -> dict[str, Any]: return {"lists": [], "count": 0} def list_prescription_orders(self, **kwargs: Any) -> dict[str, Any]: self.order_queries.append(kwargs) return { "lists": [ { "id": 8, "order_no": "ORDER-8", "prescription_id": 5, "patient_name": "林晓岚", "amount": 128, } ], "count": 1, } def check_diagnosis_phone(self, payload: dict[str, Any]) -> dict[str, Any]: self.phone_checks.append(payload) return {"exists": False} def check_diagnosis_id_card(self, payload: dict[str, Any]) -> dict[str, Any]: self.id_card_checks.append(payload) return {"duplicate": False} def update_diagnosis( self, diagnosis: int, changes: dict[str, Any] | None = None ) -> dict[str, Any]: self.updates.append((diagnosis, dict(changes or {}))) return {"id": diagnosis, **dict(changes or {})} def test_canonical_permission_helper_rejects_dot_aliases_and_accepts_wildcards() -> None: assert not widgets.has_permission( PermissionSet(["cf.prescription.edit"]), "cf.prescription/edit" ) assert widgets.has_permission(PermissionSet(["cf.prescription/*"]), "cf.prescription/edit") assert widgets.has_permission(PermissionSet(["*"]), "cf.prescription/edit") assert not widgets.has_permission({}, "cf.prescription/edit") def test_diagnosis_prescription_edit_uses_admin_edit_permission( application: QApplication, ) -> None: repository = SimpleNamespace( create_prescription=lambda **_kwargs: {}, update_prescription=lambda **_kwargs: {}, ) create_only = DiagnosisDialog( repository, permissions=PermissionSet(["tcm.diagnosis/chufang"]), ) assert create_only._can_prescribe assert not create_only._can_edit_prescription create_only.close() edit_only = DiagnosisDialog( repository, permissions=PermissionSet(["cf.prescription/edit"]), ) assert not edit_only._can_prescribe assert edit_only._can_edit_prescription edit_only.close() application.processEvents() def test_diagnosis_masks_sensitive_fields_and_loads_exact_context_orders( application: QApplication, monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setattr(diagnosis_module, "run_async", _immediate_async) repository = _DiagnosisRepository() dialog = DiagnosisDialog( repository, permissions=PermissionSet(["tcm.diagnosis/patientOrders"]), ) dialog.open_for(77, editable=False) assert dialog.summary_fields["phone"].text() == "138****5678" assert dialog.summary_fields["id_card"].text() == "510107********1234" assert dialog.edit_fields["phone"].toPlainText() == "138****5678" assert dialog.edit_fields["id_card"].toPlainText() == "510107********1234" assert dialog.edit_fields["phone"].isReadOnly() assert dialog.edit_fields["id_card"].isReadOnly() assert dialog.orders_table.rowCount() == 1 assert repository.order_queries == [ { "page_no": 1, "page_size": 10, "context_diagnosis_id": 77, "patient_id": 321, "scene": "diagnosis_edit", } ] dialog.close() application.processEvents() def test_diagnosis_edit_checks_unique_identity_and_saves_expanded_dto( application: QApplication, monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setattr(diagnosis_module, "run_async", _immediate_async) repository = _DiagnosisRepository() dialog = DiagnosisDialog( repository, permissions=PermissionSet(["tcm.diagnosis/edit", "tcm.diagnosis/phonePlain"]), ) dialog.open_for(77, editable=True) dialog.edit_fields["symptoms"].setPlainText("口干、多饮") dialog.edit_fields["appetite"].setPlainText("一般、少食") dialog._save() assert repository.phone_checks == [{"phone": "13812345678", "id": 77}] assert repository.id_card_checks == [{"id_card": "510107199001011234", "id": 77}] diagnosis_id, changes = repository.updates[-1] assert diagnosis_id == 77 assert changes["phone"] == "13812345678" assert changes["id_card"] == "510107199001011234" assert changes["symptoms"] == "口干、多饮" assert changes["appetite"] == ["一般", "少食"] dialog.close() application.processEvents() def test_duplicate_herbs_match_admin_warning_for_issued_prescriptions( application: QApplication, ) -> None: herbs = [ {"medicine_id": 11, "name": "黄芪", "dosage": 10}, {"medicine_id": 11, "name": "黄芪", "dosage": 15}, ] repository = SimpleNamespace() template = PrescriptionTemplateDialog( repository, {"id": 1, "prescription_name": "重复方", "herbs": herbs}, mode="edit", ) template.accept() assert template.result() == QDialog.DialogCode.Rejected assert "药材不可重复:黄芪" in template.validation.label.text() editor = PrescriptionEditorDialog( repository, mode="add", current_user=SimpleNamespace(id=9, name="周医生"), ) editor.patient_name.setText("林晓岚") editor.clinical_diagnosis.setPlainText("气虚") editor.signature._has_strokes = True editor.herbs.set_rows(herbs, locked=False) assert "存在重复药名" in editor.herb_summary.text() editor.accept() assert editor.result() == QDialog.DialogCode.Accepted template.close() editor.close() application.processEvents() def test_paid_order_response_is_bound_to_active_diagnosis_and_blocks_save( application: QApplication, monkeypatch: pytest.MonkeyPatch, ) -> None: callbacks: list[dict[str, Any]] = [] def queue_async(_function: Any, **options: Any) -> object: callbacks.append(options) return object() monkeypatch.setattr(dialog_module, "run_async", queue_async) monkeypatch.setattr(dialog_module.QTimer, "singleShot", staticmethod(lambda *_args: None)) dialog = PrescriptionOrderDialog( SimpleNamespace(list_paid_prescription_orders=lambda diagnosis_id: {}), {"id": 12, "diagnosis_id": 6, "patient_name": "林晓岚"}, ) dialog._load_paid_orders() assert not dialog.save_button.isEnabled() dialog.diagnosis_id.setValue(7) assert len(callbacks) == 2 callbacks[0]["on_success"]( {"lists": [{"id": 66, "order_no": "OLD"}], "deposit_min_amount": 100} ) assert dialog.paid_orders.count() == 0 assert not dialog.save_button.isEnabled() callbacks[1]["on_success"]({"lists": [{"id": 77, "order_no": "NEW"}], "deposit_min_amount": 50}) assert dialog.paid_orders.item(0).data(Qt.ItemDataRole.UserRole) == 77 assert dialog.save_button.isEnabled() assert dialog._paid_orders_diagnosis_id == 7 dialog.close() application.processEvents() def test_prescription_diagnosis_detail_uses_shared_structured_readonly_ui( application: QApplication, monkeypatch: pytest.MonkeyPatch, ) -> None: class Repository: def get_doctor_notes(self, diagnosis_id: int) -> list[dict[str, Any]]: assert diagnosis_id == 745 return [] monkeypatch.setattr(diagnosis_module, "run_async", _immediate_async) dialog = DiagnosisDetailDialog( { "diagnosis": { "id": 745, "patient_id": 745, "patient_name": "庄志芳", "phone": "13823549442", "id_card": "440305196701011234", "gender": 0, "age": 59, "chief_complaint": "睡眠不好、出汗多", "past_history": ["高血压", "高脂血症"], }, "patient": {"id": 745}, }, repository=Repository(), permissions=PermissionSet(["tcm.diagnosis/readonlyDetail"]), ) assert isinstance(dialog, DiagnosisDialog) assert dialog.view_stack.currentWidget() is dialog.readonly_page assert dialog.edit_fields["chief_complaint"].toPlainText() == "睡眠不好、出汗多" assert dialog.summary_fields["phone"].text() == "138****9442" assert dialog.case_grid.isVisibleTo(dialog) assert not dialog.findChildren(dialog_module.QTextBrowser) dialog.close() application.processEvents() @pytest.mark.parametrize("owner_width", [1024, 1440, 1710]) def test_prescription_editor_embeds_scrollable_diagnosis_beside_editable_form( application: QApplication, monkeypatch: pytest.MonkeyPatch, owner_width: int, ) -> None: class Repository: def get_diagnosis_detail( self, diagnosis_id: int, *, readonly: bool = False ) -> dict[str, Any]: assert readonly return { "diagnosis": { "id": diagnosis_id, "patient_id": 745, "patient_name": "庄志芳", "chief_complaint": "睡眠不好、出汗多", } } def get_doctor_notes(self, _diagnosis_id: int) -> list[dict[str, Any]]: return [] monkeypatch.setattr(diagnosis_module, "run_async", _immediate_async) owner = QDialog() owner.resize(owner_width, 720) owner.show() repository = Repository() editor = PrescriptionEditorDialog( repository, {"diagnosis_id": 745, "patient_name": "庄志芳"}, mode="edit", current_user=SimpleNamespace(id=9, name="周医生"), permissions=PermissionSet(["tcm.diagnosis/readonlyDetail"]), parent=owner, ) editor.show() application.processEvents() assert QApplication.activeModalWidget() is editor assert editor.width() == editor.DRAWER_WIDTH editor.diagnosis_button.click() application.processEvents() detail = editor._diagnosis_view assert detail is not None assert QApplication.activeModalWidget() is editor assert not detail.isWindow() assert detail.parentWidget() is editor.diagnosis_host assert editor.diagnosis_host.isVisibleTo(editor) assert editor.drawer_surface.isVisibleTo(editor) assert editor.width() == owner.width() assert editor.diagnosis_host.geometry().right() < editor.drawer_surface.geometry().left() scroll_bar = detail.readonly_scroll.verticalScrollBar() assert scroll_bar.maximum() > 0 scroll_bar.setValue(scroll_bar.maximum()) assert scroll_bar.value() == scroll_bar.maximum() editor.patient_name.setText("庄志芳(已核对)") assert editor.patient_name.isEnabled() assert editor.payload()["patient_name"] == "庄志芳(已核对)" detail.readonly_close_button.click() application.processEvents() assert editor.isVisible() assert not editor.diagnosis_host.isVisible() assert editor.width() == editor.DRAWER_WIDTH assert editor.patient_name.text() == "庄志芳(已核对)" editor.diagnosis_button.click() application.processEvents() assert editor.diagnosis_host.isVisibleTo(editor) editor.reject() application.processEvents() assert not editor.isVisible() assert not detail.isVisible() owner.close() application.processEvents() def _finish_queued(callback: dict[str, Any], result: Any) -> None: callback["on_success"](result) if callback.get("on_finished"): callback["on_finished"]() def test_prescription_lists_snapshot_queries_and_replay_pending_refresh( application: QApplication, monkeypatch: pytest.MonkeyPatch, ) -> None: library_callbacks: list[tuple[Any, dict[str, Any]]] = [] library_calls: list[dict[str, Any]] = [] def queue_library(function: Any, **options: Any) -> object: library_callbacks.append((function, options)) return object() class LibraryRepository: def list_prescription_templates(self, **kwargs: Any) -> dict[str, Any]: library_calls.append(kwargs) return {"lists": [], "count": 0} monkeypatch.setattr(library_module, "run_async", queue_library) library = PrescriptionLibraryPage( LibraryRepository(), PermissionSet(["wcf.prescription/*"]), SimpleNamespace(id=1) ) library.refresh() library.name_filter.setText("新条件") library.refresh() assert len(library_callbacks) == 1 first_function, first_options = library_callbacks[0] first_result = first_function() _finish_queued(first_options, first_result) assert len(library_callbacks) == 2 second_function, second_options = library_callbacks[1] second_result = second_function() _finish_queued(second_options, second_result) assert [call["prescription_name"] for call in library_calls] == ["", "新条件"] issued_callbacks: list[tuple[Any, dict[str, Any]]] = [] issued_calls: list[dict[str, Any]] = [] def queue_issued(function: Any, **options: Any) -> object: issued_callbacks.append((function, options)) return object() class IssuedRepository: def list_prescriptions(self, **kwargs: Any) -> dict[str, Any]: issued_calls.append(kwargs) return {"lists": [], "count": 0} monkeypatch.setattr(prescription_module, "run_async", queue_issued) issued = PrescriptionsPage( IssuedRepository(), PermissionSet(["cf.prescription/*"]), SimpleNamespace(id=1) ) issued.refresh() issued.patient_filter.setText("新患者") issued.refresh() assert len(issued_callbacks) == 1 first_function, first_options = issued_callbacks[0] first_result = first_function() _finish_queued(first_options, first_result) assert len(issued_callbacks) == 2 second_function, second_options = issued_callbacks[1] second_result = second_function() _finish_queued(second_options, second_result) assert [call["patient_name"] for call in issued_calls] == ["", "新患者"] library.close() issued.close() application.processEvents() def test_diagnosis_detail_requires_permission_and_ignores_stale_target( application: QApplication, monkeypatch: pytest.MonkeyPatch, ) -> None: repository_calls: list[int] = [] class Repository: def get_diagnosis_detail( self, diagnosis_id: int, *, readonly: bool = False ) -> dict[str, Any]: repository_calls.append(diagnosis_id) return {"id": diagnosis_id, "readonly": readonly} denied = PrescriptionsPage(Repository(), PermissionSet([]), SimpleNamespace(id=1)) denied._open_diagnosis(1) assert repository_calls == [] callbacks: list[tuple[Any, dict[str, Any]]] = [] def queue_async(function: Any, **options: Any) -> object: callbacks.append((function, options)) return object() shown: list[int] = [] class FakeDiagnosisDetailDialog: def __init__(self, detail: dict[str, Any], _parent: Any, **_kwargs: Any) -> None: shown.append(detail["id"]) def exec(self) -> int: return 0 monkeypatch.setattr(prescription_module, "run_async", queue_async) monkeypatch.setattr(prescription_module, "DiagnosisDetailDialog", FakeDiagnosisDetailDialog) allowed = PrescriptionsPage( Repository(), PermissionSet(["tcm.diagnosis/readonlyDetail"]), SimpleNamespace(id=1), ) allowed._open_diagnosis(11) allowed._open_diagnosis(12) assert len(callbacks) == 2 old_function, old_options = callbacks[0] old_options["on_success"](old_function()) assert shown == [] new_function, new_options = callbacks[1] new_options["on_success"](new_function()) assert shown == [12] assert repository_calls == [11, 12] denied.close() allowed.close() application.processEvents() def test_diagnosis_prescription_view_fetches_full_detail_and_ignores_stale_row( application: QApplication, monkeypatch: pytest.MonkeyPatch, ) -> None: requested: list[int] = [] callbacks: list[tuple[Any, dict[str, Any]]] = [] shown: list[int] = [] class Repository: def get_prescription(self, prescription_id: int) -> dict[str, Any]: requested.append(prescription_id) return { "id": prescription_id, "patient_name": f"患者{prescription_id}", "doctor_signature": "data:image/png;base64,detail", "herbs": [{"medicine_id": 31, "name": "黄芪", "dosage": 15}], } def queue_async(function: Any, **options: Any) -> object: callbacks.append((function, options)) return object() class FakePrescriptionDetailDialog: def __init__(self, detail: dict[str, Any], **_kwargs: Any) -> None: assert detail["doctor_signature"].endswith("detail") assert detail["herbs"][0]["name"] == "黄芪" shown.append(detail["id"]) def exec(self) -> int: return 0 monkeypatch.setattr(diagnosis_module, "run_async", queue_async) monkeypatch.setattr(dialog_module, "PrescriptionDetailDialog", FakePrescriptionDetailDialog) dialog = DiagnosisDialog(Repository(), permissions=PermissionSet(["*"])) dialog._view_prescription({"id": 11, "herbs": []}) dialog._view_prescription({"id": 12, "herbs": []}) old_function, old_options = callbacks[0] old_options["on_success"](old_function()) assert shown == [] new_function, new_options = callbacks[1] new_options["on_success"](new_function()) assert requested == [11, 12] assert shown == [12] dialog.close() application.processEvents()