387 lines
13 KiB
Python
387 lines
13 KiB
Python
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 (
|
|
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",
|
|
"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_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_are_rejected_for_templates_and_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)
|
|
editor.accept()
|
|
assert editor.result() == QDialog.DialogCode.Rejected
|
|
assert "药材不可重复:黄芪" in editor.validation.label.text()
|
|
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 _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) -> 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()
|