1603 lines
51 KiB
Python
1603 lines
51 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 QDate, Signal
|
|
from PySide6.QtWidgets import (
|
|
QApplication,
|
|
QDialog,
|
|
QDialogButtonBox,
|
|
QInputDialog,
|
|
QLabel,
|
|
QMessageBox,
|
|
QToolButton,
|
|
QWidget,
|
|
)
|
|
|
|
from doctor_workstation.core import PermissionSet
|
|
from doctor_workstation.ui.dialogs import diagnosis as diagnosis_module
|
|
from doctor_workstation.ui.pages import consultations as consultations_module
|
|
from doctor_workstation.ui.pages.consultations import (
|
|
ConsultationsPage,
|
|
_AppointmentLogDialog,
|
|
_DiagnosisCreateDialog,
|
|
_DiagnosisOrderDialog,
|
|
_DiagnosisOrderQrDialog,
|
|
_QrResultDialog,
|
|
_video_payload,
|
|
appointment_rows,
|
|
is_diagnosis_confirmed,
|
|
is_valid_id_card,
|
|
is_video_available,
|
|
prescription_action_label,
|
|
)
|
|
|
|
|
|
class _ListDiagnosisDialog(QWidget):
|
|
saved = Signal()
|
|
|
|
def __init__(self, _repository: Any, parent: QWidget | None = None) -> None:
|
|
super().__init__(parent)
|
|
|
|
def open_for(self, *_args: Any, **_kwargs: Any) -> None:
|
|
return None
|
|
|
|
def open_view_only(self, *_args: Any, **_kwargs: Any) -> None:
|
|
return None
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def application() -> QApplication:
|
|
return QApplication.instance() or QApplication([])
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def isolate_list_from_detail_dialog(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(consultations_module, "DiagnosisDialog", _ListDiagnosisDialog)
|
|
|
|
|
|
@pytest.fixture
|
|
def immediate_async(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
def run_immediately(
|
|
function: Any,
|
|
*args: Any,
|
|
on_success: Any = None,
|
|
on_error: Any = None,
|
|
on_finished: Any = None,
|
|
**kwargs: Any,
|
|
) -> object:
|
|
try:
|
|
result = function(*args, **kwargs)
|
|
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()
|
|
|
|
monkeypatch.setattr(consultations_module, "run_async", run_immediately)
|
|
monkeypatch.setattr(diagnosis_module, "run_async", run_immediately)
|
|
|
|
|
|
def _row(**changes: Any) -> dict[str, Any]:
|
|
row = {
|
|
"id": 501,
|
|
"diagnosis_id": 501,
|
|
"patient_id": 301,
|
|
"patient_name": "林晓岚",
|
|
"gender": 2,
|
|
"age": 36,
|
|
"status": 4,
|
|
"has_appointment": 1,
|
|
"appointment_id": 101,
|
|
"appointment_status": 1,
|
|
"appointments": [
|
|
{
|
|
"id": 101,
|
|
"status": 1,
|
|
"doctor_name": "陈医生",
|
|
"appointment_date": "2026-08-10",
|
|
"time_text": "09:00",
|
|
}
|
|
],
|
|
"DiagnosisViewRecord": [{"is_confirmed": 1}],
|
|
"has_prescription": 0,
|
|
}
|
|
row.update(changes)
|
|
return row
|
|
|
|
|
|
def test_consultation_business_dialogs_expose_shared_visual_contract(
|
|
application: QApplication,
|
|
) -> None:
|
|
record = _row(patient_name="鹿立核", patient_id=301)
|
|
dialogs = (
|
|
_DiagnosisCreateDialog(),
|
|
_DiagnosisOrderDialog(record),
|
|
_AppointmentLogDialog(record, []),
|
|
_QrResultDialog("视频二维码", record, "https://example.invalid/video.png"),
|
|
_DiagnosisOrderQrDialog(record, "ORDER-301"),
|
|
)
|
|
|
|
assert [dialog.objectName() for dialog in dialogs] == [
|
|
"DiagnosisCreateDialog",
|
|
"DiagnosisOrderDialog",
|
|
"AppointmentLogDialog",
|
|
"DiagnosisQrResultDialog",
|
|
"DiagnosisOrderQrDialog",
|
|
]
|
|
assert all(dialog.property("businessDialog") is True for dialog in dialogs)
|
|
assert all(
|
|
any(label.property("dialogRole") == "title" for label in dialog.findChildren(QLabel))
|
|
for dialog in dialogs
|
|
)
|
|
assert dialogs[0].findChild(QDialogButtonBox).button(
|
|
QDialogButtonBox.StandardButton.Save
|
|
).property("variant") == "primary"
|
|
assert dialogs[1].findChild(QDialogButtonBox).button(
|
|
QDialogButtonBox.StandardButton.Ok
|
|
).property("variant") == "primary"
|
|
assert next(
|
|
button
|
|
for button in dialogs[3].findChild(QDialogButtonBox).buttons()
|
|
if button.text() == "打开二维码"
|
|
).property("variant") == "primary"
|
|
assert dialogs[4].open_button.property("variant") == "primary"
|
|
|
|
for dialog in dialogs:
|
|
dialog.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_video_condition_never_uses_diagnosis_status_or_missed_status() -> None:
|
|
assert is_video_available(_row(status=4, appointment_status=1))
|
|
assert not is_video_available(_row(status=1, appointment_status=4))
|
|
assert not is_video_available(_row(status=1, appointment_status=1, has_appointment=0))
|
|
|
|
payload = _video_payload(_row(id=777, diagnosis_id=777, appointment_id=222))
|
|
assert payload["appointment_id"] == 222
|
|
assert payload["diagnosis_id"] == 777
|
|
assert payload["patient_id"] == 301
|
|
|
|
|
|
def test_nested_appointments_confirmation_and_prescription_labels() -> None:
|
|
row = _row(
|
|
appointment_id=None,
|
|
appointment_status=None,
|
|
DiagnosisViewRecord=[{"is_confirmed": 0}, {"is_confirmed": "1"}],
|
|
)
|
|
assert appointment_rows(row)[0]["id"] == 101
|
|
assert is_diagnosis_confirmed(row)
|
|
assert (
|
|
prescription_action_label({"prescription_audit_status": 1, "prescription_void_status": 0})
|
|
== "查看处方"
|
|
)
|
|
assert (
|
|
prescription_action_label({"prescription_audit_status": 1, "prescription_void_status": 1})
|
|
== "编辑处方"
|
|
)
|
|
assert (
|
|
prescription_action_label(
|
|
{
|
|
"has_prescription": 1,
|
|
"prescription_audit_status": 0,
|
|
"prescription_void_status": 0,
|
|
}
|
|
)
|
|
== "编辑处方"
|
|
)
|
|
assert (
|
|
prescription_action_label(
|
|
{
|
|
"has_prescription": 1,
|
|
"current_has_prescription": 0,
|
|
"prescription_audit_status": 1,
|
|
"prescription_void_status": 0,
|
|
}
|
|
)
|
|
== "开方"
|
|
)
|
|
|
|
|
|
def test_view_prescription_intent_is_readonly_and_fails_closed_on_state_change(
|
|
application: QApplication,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
row = _row(
|
|
has_prescription=1,
|
|
current_has_prescription=1,
|
|
current_prescription_id=701,
|
|
prescription_audit_status=1,
|
|
prescription_void_status=0,
|
|
)
|
|
page = ConsultationsPage(SimpleNamespace(), permissions=PermissionSet(["*"]))
|
|
page.table_host.set_rows([row])
|
|
page.table.selectRow(0)
|
|
|
|
requested: list[tuple[Any, str]] = []
|
|
monkeypatch.setattr(
|
|
page,
|
|
"_begin_prescription_load",
|
|
lambda record, *, mode: requested.append((record, mode)),
|
|
)
|
|
page._open_prescription()
|
|
assert requested == [(row, "view")]
|
|
|
|
monkeypatch.setattr(
|
|
page,
|
|
"_open_existing_prescription_editor",
|
|
lambda _existing: pytest.fail("view intent must never open the editor"),
|
|
)
|
|
page._prescription_loaded(
|
|
{
|
|
"id": 701,
|
|
"appointment_id": 101,
|
|
"audit_status": 0,
|
|
"void_status": 0,
|
|
},
|
|
row,
|
|
"view",
|
|
page._prescription_generation,
|
|
)
|
|
assert "状态已变化" in page.banner.label.text()
|
|
page.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_default_query_matches_admin_today_and_page_size_contract(
|
|
application: QApplication,
|
|
immediate_async: None,
|
|
) -> None:
|
|
calls: list[dict[str, Any]] = []
|
|
|
|
class Repository:
|
|
def list_consultations(self, **kwargs: Any) -> dict[str, Any]:
|
|
calls.append(kwargs)
|
|
return {"lists": [], "count": 0}
|
|
|
|
page = ConsultationsPage(Repository(), permissions=PermissionSet(["*"]))
|
|
page.refresh(silent=True)
|
|
|
|
assert len(calls) == 1
|
|
query = calls[0]
|
|
assert query["page_no"] == 1
|
|
assert query["page_size"] == 15
|
|
assert query["appointment_date"] == QDate.currentDate().toString("yyyy-MM-dd")
|
|
assert query["appointment_status"] == "1"
|
|
assert "status" not in query
|
|
assert query["has_appointment"] == ""
|
|
assert query["diagnosis_confirmed"] == ""
|
|
assert {
|
|
"diagnosis_type",
|
|
"syndrome_type",
|
|
"assistant_id",
|
|
"latest_appointment_start_date",
|
|
"latest_appointment_end_date",
|
|
"latest_appointment_channel_source",
|
|
"latest_assign_start_date",
|
|
"latest_assign_end_date",
|
|
"sort_unserved_days",
|
|
}.issubset(query)
|
|
assert "consultation_type" not in query
|
|
page.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_double_click_opens_readonly_and_never_emits_video(
|
|
application: QApplication,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
page = ConsultationsPage(
|
|
SimpleNamespace(),
|
|
permissions=PermissionSet(["tcm.diagnosis/readonlyDetail", "tcm.diagnosis/videoQr"]),
|
|
)
|
|
page.table.set_rows([_row()])
|
|
page.table.selectRow(0)
|
|
opened: list[int] = []
|
|
videos: list[dict[str, Any]] = []
|
|
monkeypatch.setattr(
|
|
page._diagnosis_dialog,
|
|
"open_view_only",
|
|
lambda diagnosis_id, *, seed=None: opened.append(diagnosis_id),
|
|
)
|
|
page.video_requested.connect(videos.append)
|
|
|
|
page.table.itemDoubleClicked.emit(page.table.item(0, 0))
|
|
application.processEvents()
|
|
|
|
assert opened == [501]
|
|
assert videos == []
|
|
page.close()
|
|
|
|
|
|
def test_action_visibility_requires_exact_canonical_permissions(
|
|
application: QApplication,
|
|
) -> None:
|
|
aliases = PermissionSet(
|
|
[
|
|
"tcm.diagnosis.readonlyDetail",
|
|
"tcm.diagnosis.edit",
|
|
"tcm.diagnosis.add",
|
|
"tcm.diagnosis.delete",
|
|
]
|
|
)
|
|
page = ConsultationsPage(SimpleNamespace(), permissions=aliases)
|
|
assert not page.view_button.isVisible()
|
|
assert not page.edit_button.isVisible()
|
|
assert not page.add_button.isVisible()
|
|
assert not page.delete_button.isVisible()
|
|
page.close()
|
|
|
|
exact = PermissionSet(
|
|
[
|
|
"tcm.diagnosis/readonlyDetail",
|
|
"tcm.diagnosis/edit",
|
|
"tcm.diagnosis/add",
|
|
"tcm.diagnosis/delete",
|
|
]
|
|
)
|
|
page = ConsultationsPage(SimpleNamespace(), permissions=exact)
|
|
assert not page.view_button.isHidden()
|
|
assert not page.edit_button.isHidden()
|
|
assert not page.add_button.isHidden()
|
|
assert not page.delete_button.isHidden()
|
|
page.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_refresh_generation_ignores_late_results(
|
|
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(consultations_module, "run_async", queue_async)
|
|
page = ConsultationsPage(SimpleNamespace(), permissions=PermissionSet(["*"]))
|
|
page.refresh(silent=True)
|
|
page.refresh(silent=True)
|
|
|
|
callbacks[1]["on_success"]({"lists": [_row(id=902, diagnosis_id=902)], "count": 1})
|
|
callbacks[0]["on_success"]({"lists": [_row(id=901, diagnosis_id=901)], "count": 1})
|
|
application.processEvents()
|
|
|
|
assert page.table.rowCount() == 1
|
|
assert page.table.item(0, 0).text().startswith("902")
|
|
page.close()
|
|
|
|
|
|
def test_identical_silent_refresh_has_zero_model_reset_and_fixed_widget_budget(
|
|
application: QApplication,
|
|
immediate_async: None,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
class Repository:
|
|
calls = 0
|
|
|
|
def list_consultations(self, **_kwargs: Any) -> dict[str, Any]:
|
|
self.calls += 1
|
|
return {"lists": [_row()], "count": 1}
|
|
|
|
repository = Repository()
|
|
page = ConsultationsPage(repository, permissions=PermissionSet(["*"]))
|
|
page.refresh(silent=True)
|
|
page.table.selectRow(0)
|
|
model = page.table_host.model
|
|
action_widget = page.table_host.fixed.indexWidget(model.index(0, 11))
|
|
video_widget = page.table_host.fixed.indexWidget(model.index(0, 10))
|
|
resets: list[str] = []
|
|
model.modelAboutToBeReset.connect(lambda: resets.append("begin"))
|
|
model.modelReset.connect(lambda: resets.append("end"))
|
|
install_calls: list[None] = []
|
|
original_install = page.table_host._install_fixed_widgets
|
|
|
|
def count_install() -> None:
|
|
install_calls.append(None)
|
|
original_install()
|
|
|
|
monkeypatch.setattr(page.table_host, "_install_fixed_widgets", count_install)
|
|
page.refresh(silent=True)
|
|
|
|
assert repository.calls == 2
|
|
assert resets == []
|
|
assert install_calls == []
|
|
assert page.table_host.fixed.indexWidget(model.index(0, 11)) is action_widget
|
|
assert page.table_host.fixed.indexWidget(model.index(0, 10)) is video_widget
|
|
assert page.table.currentIndex().row() == 0
|
|
page.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_timer_poll_has_one_request_budget_while_refresh_is_in_flight(
|
|
application: QApplication,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
jobs: list[dict[str, Any]] = []
|
|
|
|
def queue_async(_function: Any, **options: Any) -> object:
|
|
jobs.append(options)
|
|
return object()
|
|
|
|
monkeypatch.setattr(consultations_module, "run_async", queue_async)
|
|
page = ConsultationsPage(SimpleNamespace(), permissions=PermissionSet(["*"]))
|
|
monkeypatch.setattr(page, "isVisible", lambda: True)
|
|
monkeypatch.setattr(page, "_refresh_counts", lambda: None)
|
|
|
|
page._poll_refresh()
|
|
page._poll_refresh()
|
|
page._poll_refresh()
|
|
|
|
assert len(jobs) == 1
|
|
assert page._loading
|
|
jobs[0]["on_success"]({"lists": [_row()], "count": 1})
|
|
jobs[0]["on_finished"]()
|
|
assert not page._loading
|
|
page.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_current_appointment_is_the_only_prescription_authority(
|
|
application: QApplication,
|
|
) -> None:
|
|
calls: list[tuple[str, int]] = []
|
|
|
|
class Repository:
|
|
def get_prescription_by_appointment(self, appointment_id: int) -> None:
|
|
calls.append(("appointment", appointment_id))
|
|
return None
|
|
|
|
def list_prescriptions_by_diagnosis(self, diagnosis_id: int) -> list[dict[str, Any]]:
|
|
raise AssertionError(f"diagnosis fallback is forbidden: {diagnosis_id}")
|
|
|
|
page = ConsultationsPage(Repository(), permissions=PermissionSet(["*"]))
|
|
assert page._load_context_prescription(_row(appointment_id=202)) is None
|
|
assert calls == [("appointment", 202)]
|
|
page.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_empty_appointment_wrapper_is_treated_as_a_new_prescription(
|
|
application: QApplication,
|
|
) -> None:
|
|
class Repository:
|
|
def get_prescription_by_appointment(self, appointment_id: int) -> dict[str, Any]:
|
|
assert appointment_id == 202
|
|
return {"data": {}}
|
|
|
|
page = ConsultationsPage(Repository(), permissions=PermissionSet(["*"]))
|
|
assert page._load_context_prescription(_row(appointment_id=202)) is None
|
|
page.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_prescription_query_error_is_fail_closed_without_diagnosis_fallback(
|
|
application: QApplication,
|
|
) -> None:
|
|
class Repository:
|
|
def get_prescription_by_appointment(self, appointment_id: int) -> None:
|
|
raise RuntimeError(f"appointment {appointment_id} unavailable")
|
|
|
|
def list_prescriptions_by_diagnosis(self, diagnosis_id: int) -> list[dict[str, Any]]:
|
|
raise AssertionError(f"diagnosis fallback is forbidden: {diagnosis_id}")
|
|
|
|
page = ConsultationsPage(Repository(), permissions=PermissionSet(["*"]))
|
|
with pytest.raises(RuntimeError, match="appointment 202 unavailable"):
|
|
page._load_context_prescription(_row(appointment_id=202))
|
|
page.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_new_prescription_uses_authoritative_case_snapshot_and_exact_ids(
|
|
application: QApplication,
|
|
immediate_async: None,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
calls: list[tuple[str, int]] = []
|
|
created: list[dict[str, Any]] = []
|
|
dialog_seeds: list[dict[str, Any]] = []
|
|
case_record = {
|
|
"diagnosis": {
|
|
"id": 501,
|
|
"patient_name": "林晓岚",
|
|
"chief_complaint": "咳嗽",
|
|
"tongue": "面象哨兵",
|
|
"tongue_image": "舌象哨兵",
|
|
"pulse": "脉象哨兵",
|
|
"pulse_condition": "脉象详情哨兵",
|
|
},
|
|
"patient": {"id": 301, "gender": 2, "age": 36},
|
|
}
|
|
|
|
class Repository:
|
|
def get_prescription_by_appointment(self, appointment_id: int) -> None:
|
|
calls.append(("appointment", appointment_id))
|
|
return None
|
|
|
|
def list_prescriptions_by_diagnosis(self, diagnosis_id: int) -> list[dict[str, Any]]:
|
|
raise AssertionError(f"diagnosis fallback is forbidden: {diagnosis_id}")
|
|
|
|
def get_diagnosis_detail(self, diagnosis_id: int, **_kwargs: Any) -> dict[str, Any]:
|
|
calls.append(("diagnosis_detail", diagnosis_id))
|
|
return case_record
|
|
|
|
def create_prescription(self, prescription: Any) -> dict[str, Any]:
|
|
created.append(dict(prescription))
|
|
return {"id": 901}
|
|
|
|
def list_consultations(self, **_kwargs: Any) -> dict[str, Any]:
|
|
return {"lists": [], "count": 0}
|
|
|
|
class AcceptedEditor:
|
|
def __init__(
|
|
self,
|
|
_repository: Any,
|
|
seed: dict[str, Any],
|
|
**_kwargs: Any,
|
|
) -> None:
|
|
dialog_seeds.append(seed)
|
|
|
|
def exec(self) -> QDialog.DialogCode:
|
|
return QDialog.DialogCode.Accepted
|
|
|
|
def payload(self) -> dict[str, Any]:
|
|
return {"formula_name": "止咳方", "medicines": []}
|
|
|
|
monkeypatch.setattr(consultations_module, "PrescriptionEditorDialog", AcceptedEditor)
|
|
page = ConsultationsPage(Repository(), permissions=PermissionSet(["*"]))
|
|
page._begin_prescription_load(_row(appointment_id=202), mode="open")
|
|
|
|
assert calls[:2] == [("appointment", 202), ("diagnosis_detail", 501)]
|
|
assert len(created) == 1
|
|
assert created[0]["diagnosis_id"] == 501
|
|
assert created[0]["appointment_id"] == 202
|
|
assert created[0]["case_record"] == case_record
|
|
assert created[0]["case_record"] is not case_record
|
|
assert dialog_seeds[0]["case_record"] == case_record
|
|
assert dialog_seeds[0]["case_record"] is not case_record
|
|
assert dialog_seeds[0]["tongue"] == "面象哨兵"
|
|
assert dialog_seeds[0]["tongue_image"] == "舌象哨兵"
|
|
assert dialog_seeds[0]["pulse"] == "脉象哨兵"
|
|
assert dialog_seeds[0]["pulse_condition"] == "脉象详情哨兵"
|
|
page.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_existing_pending_prescription_is_edited_in_place_not_duplicated(
|
|
application: QApplication,
|
|
immediate_async: None,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
updated: list[tuple[int, dict[str, Any]]] = []
|
|
created: list[dict[str, Any]] = []
|
|
existing = {
|
|
"id": 901,
|
|
"diagnosis_id": 501,
|
|
"patient_name": "林晓岚",
|
|
"audit_status": 0,
|
|
"void_status": 0,
|
|
"clinical_diagnosis": "气虚",
|
|
"herbs": [{"medicine_id": 31, "name": "黄芪", "dosage": 15}],
|
|
}
|
|
|
|
class Repository:
|
|
def get_prescription_by_appointment(self, appointment_id: int) -> dict[str, Any]:
|
|
assert appointment_id == 202
|
|
return existing
|
|
|
|
def update_prescription(
|
|
self, prescription: int, changes: dict[str, Any]
|
|
) -> dict[str, Any]:
|
|
updated.append((prescription, dict(changes)))
|
|
return {"id": prescription}
|
|
|
|
def create_prescription(self, prescription: dict[str, Any]) -> dict[str, Any]:
|
|
created.append(dict(prescription))
|
|
return {"id": 999}
|
|
|
|
def list_consultations(self, **_kwargs: Any) -> dict[str, Any]:
|
|
return {"lists": [], "count": 0}
|
|
|
|
class AcceptedEditor:
|
|
def __init__(self, _repository: Any, source: Any, **kwargs: Any) -> None:
|
|
assert source is existing
|
|
assert kwargs["mode"] == "edit"
|
|
|
|
def exec(self) -> QDialog.DialogCode:
|
|
return QDialog.DialogCode.Accepted
|
|
|
|
def payload(self) -> dict[str, Any]:
|
|
return {"id": 901, "clinical_diagnosis": "气血两虚"}
|
|
|
|
monkeypatch.setattr(consultations_module, "PrescriptionEditorDialog", AcceptedEditor)
|
|
page = ConsultationsPage(Repository(), permissions=PermissionSet(["*"]))
|
|
page._begin_prescription_load(_row(appointment_id=202), mode="open")
|
|
|
|
assert updated == [(901, {"id": 901, "clinical_diagnosis": "气血两虚"})]
|
|
assert created == []
|
|
page.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_switching_rows_invalidates_prescription_worker_and_clears_busy(
|
|
application: QApplication,
|
|
immediate_async: None,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
page = ConsultationsPage(SimpleNamespace(), permissions=PermissionSet(["*"]))
|
|
callbacks: list[dict[str, Any]] = []
|
|
|
|
def queue_async(_function: Any, **options: Any) -> object:
|
|
callbacks.append(options)
|
|
return object()
|
|
|
|
monkeypatch.setattr(consultations_module, "run_async", queue_async)
|
|
page.table.set_rows(
|
|
[
|
|
_row(id=501, diagnosis_id=501, appointment_id=101),
|
|
_row(id=502, diagnosis_id=502, appointment_id=202),
|
|
]
|
|
)
|
|
page.table.selectRow(0)
|
|
page._begin_prescription_load(page.table.current_data(), mode="open")
|
|
assert page._prescription_busy
|
|
assert not page.prescription_button.isEnabled()
|
|
|
|
page.table.selectRow(1)
|
|
application.processEvents()
|
|
assert not page._prescription_busy
|
|
assert page.prescription_button.isEnabled()
|
|
|
|
callbacks[0]["on_success"]({"id": 88, "appointment_id": 101})
|
|
callbacks[0]["on_finished"]()
|
|
assert page.table.current_data()["appointment_id"] == 202
|
|
assert not page._prescription_busy
|
|
page.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_native_call_does_not_reuse_video_qr_permission(
|
|
application: QApplication,
|
|
) -> None:
|
|
class VideoRepository:
|
|
def get_call_ticket(self, patient_id: int, diagnosis_id: int) -> None:
|
|
pass
|
|
|
|
def start_call(self, diagnosis_id: int, patient_id: int, *, call_type: int = 2) -> None:
|
|
pass
|
|
|
|
def bind_call_room(self, diagnosis_id: int, room_id: str) -> None:
|
|
pass
|
|
|
|
def end_call(self, diagnosis_id: int) -> None:
|
|
pass
|
|
|
|
live_row = _row(
|
|
video_call_hint={
|
|
"state": "live",
|
|
"label": "视频通话进行中",
|
|
"start_time": 1787102100,
|
|
}
|
|
)
|
|
page = ConsultationsPage(VideoRepository(), permissions=PermissionSet([]))
|
|
emitted: list[dict[str, Any]] = []
|
|
page.video_requested.connect(emitted.append)
|
|
page.table.set_rows([live_row])
|
|
page.table.selectRow(0)
|
|
application.processEvents()
|
|
|
|
assert not page.video_button.isHidden()
|
|
assert page.video_button.isEnabled()
|
|
video_cell = page.table_host.fixed.indexWidget(page.table_host.model.index(0, 10))
|
|
video_action = next(button for button in video_cell.findChildren(QToolButton))
|
|
assert video_action.text() == "进入视频问诊"
|
|
assert emitted == []
|
|
assert "摄像头和麦克风" in video_action.toolTip()
|
|
page._request_video()
|
|
assert emitted == [_video_payload(live_row)]
|
|
assert emitted[0]["mode"] == "im"
|
|
page.close()
|
|
application.processEvents()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("hint", "status_text"),
|
|
[
|
|
({"state": "none", "label": ""}, "暂无通话"),
|
|
({"state": "pending_room", "label": "通话发起中,待同步房间"}, "等待接通"),
|
|
],
|
|
)
|
|
def test_video_join_action_is_hidden_until_doctor_session_is_live(
|
|
application: QApplication,
|
|
hint: dict[str, Any],
|
|
status_text: str,
|
|
) -> None:
|
|
class VideoRepository:
|
|
def get_call_ticket(self, patient_id: int, diagnosis_id: int) -> None:
|
|
pass
|
|
|
|
def start_call(self, diagnosis_id: int, patient_id: int, *, call_type: int = 2) -> None:
|
|
pass
|
|
|
|
def bind_call_room(self, diagnosis_id: int, room_id: str) -> None:
|
|
pass
|
|
|
|
def end_call(self, diagnosis_id: int) -> None:
|
|
pass
|
|
|
|
page = ConsultationsPage(VideoRepository(), permissions=PermissionSet([]))
|
|
emitted: list[dict[str, Any]] = []
|
|
page.video_requested.connect(emitted.append)
|
|
page.table.set_rows([_row(video_call_hint=hint)])
|
|
page.table.selectRow(0)
|
|
application.processEvents()
|
|
|
|
video_cell = page.table_host.fixed.indexWidget(page.table_host.model.index(0, 10))
|
|
assert video_cell.findChildren(QToolButton) == []
|
|
assert status_text in " ".join(label.text() for label in video_cell.findChildren(QLabel))
|
|
page._request_video()
|
|
assert emitted == []
|
|
page.close()
|
|
application.processEvents()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"value",
|
|
[
|
|
"11010519491231002X",
|
|
"110105491231002",
|
|
"44052420000101001x",
|
|
],
|
|
)
|
|
def test_id_card_preflight_accepts_admin_15_and_18_digit_shapes(value: str) -> None:
|
|
assert is_valid_id_card(value)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"value",
|
|
[
|
|
"",
|
|
"11010519491331002X",
|
|
"11010519491232002X",
|
|
"01010519491231002X",
|
|
"110105491331002",
|
|
"not-an-id",
|
|
],
|
|
)
|
|
def test_id_card_preflight_rejects_invalid_shapes(value: str) -> None:
|
|
assert not is_valid_id_card(value)
|
|
|
|
|
|
def test_invalid_id_card_never_reaches_mutation(
|
|
application: QApplication,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
page = ConsultationsPage(SimpleNamespace(), permissions=PermissionSet(["tcm.diagnosis/edit"]))
|
|
page.table.set_rows([_row()])
|
|
page.table.selectRow(0)
|
|
mutations: list[tuple[Any, str]] = []
|
|
messages: list[str] = []
|
|
monkeypatch.setattr(QInputDialog, "getText", lambda *_args, **_kwargs: ("123456", True))
|
|
monkeypatch.setattr(
|
|
page, "_run_mutation", lambda operation, message: mutations.append((operation, message))
|
|
)
|
|
monkeypatch.setattr(
|
|
consultations_module,
|
|
"show_toast",
|
|
lambda _owner, message, *_args, **_kwargs: messages.append(message),
|
|
)
|
|
|
|
page._fill_selected_id_card()
|
|
|
|
assert mutations == []
|
|
assert messages == ["请输入15或18位有效身份证号。"]
|
|
page.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_valid_id_card_reaches_the_existing_repository_dto(
|
|
application: QApplication,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
calls: list[tuple[int, str]] = []
|
|
|
|
class Repository:
|
|
def fill_diagnosis_id_card(self, diagnosis_id: int, id_card: str) -> None:
|
|
calls.append((diagnosis_id, id_card))
|
|
|
|
page = ConsultationsPage(
|
|
Repository(),
|
|
permissions=PermissionSet(["tcm.diagnosis/edit"]),
|
|
)
|
|
page.table.set_rows([_row()])
|
|
page.table.selectRow(0)
|
|
monkeypatch.setattr(
|
|
QInputDialog,
|
|
"getText",
|
|
lambda *_args, **_kwargs: ("11010519491231002X", True),
|
|
)
|
|
monkeypatch.setattr(page, "_run_mutation", lambda operation, _message: operation())
|
|
|
|
page._fill_selected_id_card()
|
|
|
|
assert calls == [(501, "11010519491231002X")]
|
|
page.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_multi_appointment_cancel_is_revalidated_before_repository_mutation(
|
|
application: QApplication,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
record = _row(
|
|
appointments=[
|
|
{"id": 101, "status": 1, "doctor_name": "陈医生"},
|
|
{"id": 102, "status": 4, "doctor_name": "李医生"},
|
|
]
|
|
)
|
|
page = ConsultationsPage(
|
|
SimpleNamespace(),
|
|
permissions=PermissionSet(["tcm.diagnosis/guahao"]),
|
|
)
|
|
page.table.set_rows([record])
|
|
page.table.selectRow(0)
|
|
mutations: list[Any] = []
|
|
questions: list[Any] = []
|
|
messages: list[str] = []
|
|
monkeypatch.setattr(page, "_run_mutation", lambda *args: mutations.append(args))
|
|
monkeypatch.setattr(
|
|
QMessageBox,
|
|
"question",
|
|
lambda *args, **kwargs: questions.append((args, kwargs)),
|
|
)
|
|
monkeypatch.setattr(
|
|
consultations_module,
|
|
"show_toast",
|
|
lambda _owner, message, *_args, **_kwargs: messages.append(message),
|
|
)
|
|
|
|
page._cancel_selected_appointment()
|
|
|
|
assert mutations == []
|
|
assert questions == []
|
|
assert messages == ["该诊单有多条挂号,请在具体挂号记录中取消。"]
|
|
page.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_single_appointment_cancel_uses_the_visible_nested_appointment_id(
|
|
application: QApplication,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
cancelled: list[int] = []
|
|
|
|
class Repository:
|
|
def cancel_diagnosis_appointment(self, appointment_id: int, **_kwargs: Any) -> None:
|
|
cancelled.append(appointment_id)
|
|
|
|
record = _row(
|
|
appointment_id=999,
|
|
appointments=[{"id": 101, "status": 1, "doctor_name": "陈医生"}],
|
|
)
|
|
page = ConsultationsPage(
|
|
Repository(),
|
|
permissions=PermissionSet(["tcm.diagnosis/guahao"]),
|
|
)
|
|
page.table.set_rows([record])
|
|
page.table.selectRow(0)
|
|
monkeypatch.setattr(
|
|
QMessageBox,
|
|
"question",
|
|
lambda *_args, **_kwargs: QMessageBox.StandardButton.Yes,
|
|
)
|
|
monkeypatch.setattr(
|
|
page,
|
|
"_run_mutation",
|
|
lambda operation, _message: operation(),
|
|
)
|
|
|
|
page._cancel_selected_appointment()
|
|
|
|
assert cancelled == [101]
|
|
page.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_multi_appointment_card_cancel_mutates_only_the_signalled_exact_id(
|
|
application: QApplication,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
cancelled: list[int] = []
|
|
|
|
class Repository:
|
|
def cancel_diagnosis_appointment(self, appointment_id: int) -> None:
|
|
cancelled.append(appointment_id)
|
|
|
|
record = _row(
|
|
appointment_id=999,
|
|
appointments=[
|
|
{"id": 101, "status": 1, "doctor_name": "陈医生", "time_text": "09:00"},
|
|
{"id": 102, "status": 4, "doctor_name": "李医生", "time_text": "10:00"},
|
|
],
|
|
)
|
|
page = ConsultationsPage(
|
|
Repository(),
|
|
permissions=PermissionSet(["tcm.diagnosis/guahao"]),
|
|
)
|
|
page.table.set_rows([record])
|
|
monkeypatch.setattr(
|
|
QMessageBox,
|
|
"question",
|
|
lambda *_args, **_kwargs: QMessageBox.StandardButton.Yes,
|
|
)
|
|
monkeypatch.setattr(page, "_run_mutation", lambda operation, _message: operation())
|
|
|
|
page._cancel_appointment_item(record, 102)
|
|
|
|
assert cancelled == [102]
|
|
page.close()
|
|
application.processEvents()
|
|
|
|
|
|
@pytest.mark.parametrize("appointment_id", [0, 999, 103])
|
|
def test_exact_cancel_rejects_unknown_or_non_cancellable_appointment_before_confirmation(
|
|
application: QApplication,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
appointment_id: int,
|
|
) -> None:
|
|
cancelled: list[int] = []
|
|
questions: list[Any] = []
|
|
|
|
class Repository:
|
|
def cancel_diagnosis_appointment(self, appointment_id: int) -> None:
|
|
cancelled.append(appointment_id)
|
|
|
|
record = _row(
|
|
appointments=[
|
|
{"id": 101, "status": 1, "doctor_name": "陈医生"},
|
|
{"id": 103, "status": 3, "doctor_name": "李医生"},
|
|
]
|
|
)
|
|
page = ConsultationsPage(
|
|
Repository(),
|
|
permissions=PermissionSet(["tcm.diagnosis/guahao"]),
|
|
)
|
|
page.table.set_rows([record])
|
|
monkeypatch.setattr(
|
|
QMessageBox,
|
|
"question",
|
|
lambda *args, **kwargs: questions.append((args, kwargs)),
|
|
)
|
|
|
|
page._cancel_appointment_item(record, appointment_id)
|
|
|
|
assert questions == []
|
|
assert cancelled == []
|
|
page.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_exact_cancel_revalidates_status_after_confirmation(
|
|
application: QApplication,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
cancelled: list[int] = []
|
|
messages: list[str] = []
|
|
|
|
class Repository:
|
|
def cancel_diagnosis_appointment(self, appointment_id: int) -> None:
|
|
cancelled.append(appointment_id)
|
|
|
|
record = _row(appointments=[{"id": 101, "status": 1, "doctor_name": "陈医生"}])
|
|
page = ConsultationsPage(
|
|
Repository(),
|
|
permissions=PermissionSet(["tcm.diagnosis/guahao"]),
|
|
)
|
|
page.table.set_rows([record])
|
|
|
|
def finish_while_confirming(*_args: Any, **_kwargs: Any) -> Any:
|
|
record["appointments"][0]["status"] = 3
|
|
return QMessageBox.StandardButton.Yes
|
|
|
|
monkeypatch.setattr(QMessageBox, "question", finish_while_confirming)
|
|
monkeypatch.setattr(
|
|
consultations_module,
|
|
"show_toast",
|
|
lambda _owner, message, *_args, **_kwargs: messages.append(message),
|
|
)
|
|
|
|
page._cancel_appointment_item(record, 101)
|
|
|
|
assert cancelled == []
|
|
assert messages == ["挂号状态已刷新,本次未执行取消。"]
|
|
page.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_exact_cancel_confirmation_cancel_never_starts_mutation(
|
|
application: QApplication,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
calls: list[Any] = []
|
|
|
|
class Repository:
|
|
def cancel_diagnosis_appointment(self, appointment_id: int) -> None:
|
|
calls.append(appointment_id)
|
|
|
|
record = _row(appointments=[{"id": 101, "status": 1, "doctor_name": "陈医生"}])
|
|
page = ConsultationsPage(
|
|
Repository(),
|
|
permissions=PermissionSet(["tcm.diagnosis/guahao"]),
|
|
)
|
|
page.table.set_rows([record])
|
|
monkeypatch.setattr(
|
|
QMessageBox,
|
|
"question",
|
|
lambda *_args, **_kwargs: QMessageBox.StandardButton.Cancel,
|
|
)
|
|
monkeypatch.setattr(page, "_run_mutation", lambda *args: calls.append(args))
|
|
|
|
page._cancel_appointment_item(record, 101)
|
|
|
|
assert calls == []
|
|
page.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_exact_cancel_direct_handler_rechecks_permission_before_confirmation(
|
|
application: QApplication,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
calls: list[Any] = []
|
|
|
|
class Repository:
|
|
def cancel_diagnosis_appointment(self, appointment_id: int) -> None:
|
|
calls.append(appointment_id)
|
|
|
|
record = _row(appointments=[{"id": 101, "status": 1}])
|
|
page = ConsultationsPage(Repository(), permissions=PermissionSet([]))
|
|
page.table.set_rows([record])
|
|
monkeypatch.setattr(
|
|
QMessageBox,
|
|
"question",
|
|
lambda *args, **kwargs: calls.append((args, kwargs)),
|
|
)
|
|
|
|
page._cancel_appointment_item(record, 101)
|
|
|
|
assert calls == []
|
|
page.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_menu_handlers_call_only_permission_gated_real_repository_methods(
|
|
application: QApplication,
|
|
immediate_async: None,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
calls: list[tuple[str, Any]] = []
|
|
|
|
class Repository:
|
|
def generate_video_qrcode(
|
|
self,
|
|
doctor_id: int,
|
|
patient_id: int,
|
|
share_user_id: int,
|
|
*,
|
|
diagnosis_id: int,
|
|
) -> dict[str, str]:
|
|
calls.append(
|
|
(
|
|
"video_qr",
|
|
{
|
|
"diagnosis_id": diagnosis_id,
|
|
"doctor_id": doctor_id,
|
|
"patient_id": patient_id,
|
|
"share_user_id": share_user_id,
|
|
},
|
|
)
|
|
)
|
|
return {"qrcode_url": "https://example.invalid/video.png"}
|
|
|
|
def generate_diagnosis_qrcode(
|
|
self,
|
|
diagnosis_id: int,
|
|
doctor_id: int,
|
|
patient_id: int,
|
|
share_user_id: int,
|
|
) -> dict[str, str]:
|
|
calls.append(
|
|
(
|
|
"confirm_qr",
|
|
{
|
|
"diagnosis_id": diagnosis_id,
|
|
"doctor_id": doctor_id,
|
|
"patient_id": patient_id,
|
|
"share_user_id": share_user_id,
|
|
},
|
|
)
|
|
)
|
|
return {"qrcode_url": "https://example.invalid/confirm.png"}
|
|
|
|
def list_appointment_logs(self, diagnosis_id: int) -> list[dict[str, Any]]:
|
|
calls.append(("logs", diagnosis_id))
|
|
return [{"id": 1, "action_desc": "挂号"}]
|
|
|
|
def create_diagnosis_order(
|
|
self,
|
|
patient_id: int,
|
|
order_type: int,
|
|
amount: float,
|
|
*,
|
|
remark: str = "",
|
|
) -> dict[str, Any]:
|
|
calls.append(
|
|
(
|
|
"order",
|
|
{
|
|
"patient_id": patient_id,
|
|
"order_type": order_type,
|
|
"amount": amount,
|
|
"remark": remark,
|
|
},
|
|
)
|
|
)
|
|
return {"order_no": "O-1"}
|
|
|
|
def generate_order_qrcode(self, order_no: str) -> dict[str, Any]:
|
|
calls.append(("order_qr", order_no))
|
|
return {"qrcode_url": "https://example.invalid/payment.png"}
|
|
|
|
record = _row(
|
|
appointment_doctor_id=77,
|
|
appointments=[
|
|
{
|
|
"id": 101,
|
|
"status": 1,
|
|
"doctor_id": 77,
|
|
"doctor_name": "陈医生",
|
|
}
|
|
],
|
|
)
|
|
page = ConsultationsPage(
|
|
Repository(),
|
|
permissions=PermissionSet(
|
|
[
|
|
"tcm.diagnosis/videoQr",
|
|
"tcm.diagnosis/guahao",
|
|
"tcm.diagnosis/guahaoLogList",
|
|
"tcm.diagnosis/order",
|
|
]
|
|
),
|
|
current_user={"id": 66},
|
|
)
|
|
page.table.set_rows([record])
|
|
page.table.selectRow(0)
|
|
shown_qr: list[tuple[str, Any]] = []
|
|
shown_logs: list[Any] = []
|
|
monkeypatch.setattr(
|
|
page,
|
|
"_show_qr_result",
|
|
lambda title, _record, result: shown_qr.append((title, result)),
|
|
)
|
|
monkeypatch.setattr(
|
|
page,
|
|
"_show_appointment_logs",
|
|
lambda _record, result: shown_logs.append(result),
|
|
)
|
|
|
|
class AcceptedOrderDialog:
|
|
def __init__(self, _record: Any, _parent: Any) -> None:
|
|
pass
|
|
|
|
def exec(self) -> Any:
|
|
return QDialog.DialogCode.Accepted
|
|
|
|
def payload(self) -> dict[str, Any]:
|
|
return {"patient_id": 301, "order_type": 2, "amount": 88.5, "remark": "复诊"}
|
|
|
|
monkeypatch.setattr(consultations_module, "_DiagnosisOrderDialog", AcceptedOrderDialog)
|
|
monkeypatch.setattr(consultations_module._QrImagePreview, "load_url", lambda *_args: None)
|
|
monkeypatch.setattr(page, "refresh", lambda *_args, **_kwargs: None)
|
|
|
|
page._request_video_qr()
|
|
page._request_confirm_qr()
|
|
page._request_appointment_logs()
|
|
page._create_diagnosis_order()
|
|
|
|
qr_payload = {
|
|
"diagnosis_id": 501,
|
|
"patient_id": 301,
|
|
"doctor_id": 77,
|
|
"share_user_id": 66,
|
|
}
|
|
assert calls == [
|
|
("video_qr", qr_payload),
|
|
("confirm_qr", qr_payload),
|
|
("logs", 501),
|
|
(
|
|
"order",
|
|
{"patient_id": 301, "order_type": 2, "amount": 88.5, "remark": "复诊"},
|
|
),
|
|
("order_qr", "O-1"),
|
|
]
|
|
assert [title for title, _result in shown_qr] == ["视频二维码", "诊单二维码"]
|
|
assert shown_logs == [[{"id": 1, "action_desc": "挂号"}]]
|
|
assert page._order_qr_dialog is not None
|
|
assert page._order_qr_dialog.order_no == "O-1"
|
|
assert page._order_qr_dialog.qrcode_url == "https://example.invalid/payment.png"
|
|
page._order_qr_dialog.reject()
|
|
page.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_menu_handler_direct_calls_fail_closed_without_permission_or_active_state(
|
|
application: QApplication,
|
|
immediate_async: None,
|
|
) -> None:
|
|
calls: list[str] = []
|
|
|
|
class Repository:
|
|
def generate_video_qrcode(self, payload: dict[str, Any]) -> dict[str, str]:
|
|
calls.append(str(payload))
|
|
return {"qrcode_url": "https://example.invalid/video.png"}
|
|
|
|
denied = ConsultationsPage(Repository(), permissions=PermissionSet([]))
|
|
denied.table.set_rows([_row(appointment_doctor_id=77)])
|
|
denied.table.selectRow(0)
|
|
denied._request_video_qr()
|
|
assert calls == []
|
|
denied.close()
|
|
|
|
inactive = ConsultationsPage(Repository(), permissions=PermissionSet(["tcm.diagnosis/videoQr"]))
|
|
inactive.table.set_rows(
|
|
[_row(appointment_status=4, appointments=[{"id": 101, "status": 4, "doctor_id": 77}])]
|
|
)
|
|
inactive.table.selectRow(0)
|
|
inactive._request_video_qr()
|
|
assert calls == []
|
|
inactive.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_menu_request_generation_ignores_result_after_row_selection_changes(
|
|
application: QApplication,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
class Repository:
|
|
def generate_video_qrcode(
|
|
self,
|
|
doctor_id: int,
|
|
patient_id: int,
|
|
share_user_id: int,
|
|
*,
|
|
diagnosis_id: int,
|
|
) -> dict[str, str]:
|
|
del diagnosis_id, doctor_id, patient_id, share_user_id
|
|
return {"qrcode_url": "https://example.invalid/video.png"}
|
|
|
|
callbacks: list[Any] = []
|
|
|
|
def hold_request(
|
|
_operation: Any,
|
|
*,
|
|
on_success: Any = None,
|
|
on_error: Any = None,
|
|
**_kwargs: Any,
|
|
) -> object:
|
|
callbacks.append((on_success, on_error))
|
|
return object()
|
|
|
|
monkeypatch.setattr(consultations_module, "run_async", hold_request)
|
|
page = ConsultationsPage(
|
|
Repository(),
|
|
permissions=PermissionSet(["tcm.diagnosis/videoQr"]),
|
|
current_user={"id": 66},
|
|
)
|
|
rows = [
|
|
_row(appointment_doctor_id=77),
|
|
_row(
|
|
id=502,
|
|
diagnosis_id=502,
|
|
patient_id=302,
|
|
appointment_id=102,
|
|
appointment_doctor_id=78,
|
|
appointments=[{"id": 102, "status": 1, "doctor_id": 78}],
|
|
),
|
|
]
|
|
page.table.set_rows(rows)
|
|
page.table.selectRow(0)
|
|
shown: list[Any] = []
|
|
monkeypatch.setattr(page, "_show_qr_result", lambda *args: shown.append(args))
|
|
|
|
page._request_video_qr()
|
|
assert len(callbacks) == 1
|
|
page.table.selectRow(1)
|
|
callbacks[0][0]({"qrcode_url": "https://example.invalid/video.png"})
|
|
|
|
assert shown == []
|
|
page.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_diagnosis_order_qr_failure_retries_without_creating_a_second_order(
|
|
application: QApplication,
|
|
immediate_async: None,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
calls: list[tuple[str, Any]] = []
|
|
|
|
class Repository:
|
|
def create_diagnosis_order(
|
|
self,
|
|
patient_id: int,
|
|
order_type: int,
|
|
amount: float,
|
|
*,
|
|
remark: str = "",
|
|
) -> dict[str, Any]:
|
|
calls.append(("create", (patient_id, order_type, amount, remark)))
|
|
return {"order_no": "PAY-RETRY-1"}
|
|
|
|
def generate_order_qrcode(self, order_no: str) -> dict[str, Any]:
|
|
calls.append(("qr", order_no))
|
|
if sum(kind == "qr" for kind, _payload in calls) == 1:
|
|
raise RuntimeError("二维码服务暂不可用")
|
|
return {"qrcode_url": "https://example.invalid/payment-retry.png"}
|
|
|
|
class AcceptedOrderDialog:
|
|
def __init__(self, _record: Any, _parent: Any) -> None:
|
|
pass
|
|
|
|
def exec(self) -> Any:
|
|
return QDialog.DialogCode.Accepted
|
|
|
|
def payload(self) -> dict[str, Any]:
|
|
return {"patient_id": 301, "order_type": 2, "amount": 88.5, "remark": "复诊"}
|
|
|
|
page = ConsultationsPage(
|
|
Repository(),
|
|
permissions=PermissionSet(["tcm.diagnosis/order"]),
|
|
)
|
|
page.table.set_rows([_row()])
|
|
page.table.selectRow(0)
|
|
monkeypatch.setattr(consultations_module, "_DiagnosisOrderDialog", AcceptedOrderDialog)
|
|
monkeypatch.setattr(consultations_module._QrImagePreview, "load_url", lambda *_args: None)
|
|
monkeypatch.setattr(page, "refresh", lambda *_args, **_kwargs: None)
|
|
|
|
page._create_diagnosis_order()
|
|
|
|
dialog = page._order_qr_dialog
|
|
assert dialog is not None
|
|
assert dialog.order_no == "PAY-RETRY-1"
|
|
assert dialog.retry_button.isEnabled()
|
|
assert "生成失败" in dialog.status_label.text()
|
|
assert calls == [
|
|
("create", (301, 2, 88.5, "复诊")),
|
|
("qr", "PAY-RETRY-1"),
|
|
]
|
|
|
|
dialog.retry_button.click()
|
|
|
|
assert calls == [
|
|
("create", (301, 2, 88.5, "复诊")),
|
|
("qr", "PAY-RETRY-1"),
|
|
("qr", "PAY-RETRY-1"),
|
|
]
|
|
assert dialog.qrcode_url == "https://example.invalid/payment-retry.png"
|
|
assert dialog.url_edit.text() == dialog.qrcode_url
|
|
dialog.reject()
|
|
assert not page._mutation_pending
|
|
page.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_diagnosis_order_without_order_no_never_requests_payment_qr(
|
|
application: QApplication,
|
|
immediate_async: None,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
qr_calls: list[str] = []
|
|
|
|
class Repository:
|
|
def create_diagnosis_order(
|
|
self,
|
|
patient_id: int,
|
|
order_type: int,
|
|
amount: float,
|
|
*,
|
|
remark: str = "",
|
|
) -> dict[str, Any]:
|
|
del patient_id, order_type, amount, remark
|
|
return {"id": 99}
|
|
|
|
def generate_order_qrcode(self, order_no: str) -> dict[str, Any]:
|
|
qr_calls.append(order_no)
|
|
return {"qrcode_url": "https://example.invalid/should-not-open.png"}
|
|
|
|
class AcceptedOrderDialog:
|
|
def __init__(self, _record: Any, _parent: Any) -> None:
|
|
pass
|
|
|
|
def exec(self) -> Any:
|
|
return QDialog.DialogCode.Accepted
|
|
|
|
def payload(self) -> dict[str, Any]:
|
|
return {"patient_id": 301, "order_type": 2, "amount": 10.0, "remark": ""}
|
|
|
|
page = ConsultationsPage(Repository(), permissions=PermissionSet(["tcm.diagnosis/order"]))
|
|
page.table.set_rows([_row()])
|
|
page.table.selectRow(0)
|
|
monkeypatch.setattr(consultations_module, "_DiagnosisOrderDialog", AcceptedOrderDialog)
|
|
monkeypatch.setattr(page, "refresh", lambda *_args, **_kwargs: None)
|
|
|
|
page._create_diagnosis_order()
|
|
|
|
assert qr_calls == []
|
|
assert page._order_qr_dialog is None
|
|
assert not page._mutation_pending
|
|
page.close()
|
|
application.processEvents()
|
|
|
|
|
|
@pytest.mark.parametrize("invalidate", ["selection", "permission"])
|
|
def test_diagnosis_order_create_result_is_rejected_when_active_context_changes(
|
|
application: QApplication,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
invalidate: str,
|
|
) -> None:
|
|
qr_calls: list[str] = []
|
|
|
|
class Repository:
|
|
def create_diagnosis_order(
|
|
self,
|
|
patient_id: int,
|
|
order_type: int,
|
|
amount: float,
|
|
*,
|
|
remark: str = "",
|
|
) -> dict[str, Any]:
|
|
del patient_id, order_type, amount, remark
|
|
return {"order_no": "STALE-1"}
|
|
|
|
def generate_order_qrcode(self, order_no: str) -> dict[str, Any]:
|
|
qr_calls.append(order_no)
|
|
return {"qrcode_url": "https://example.invalid/stale.png"}
|
|
|
|
callbacks: list[tuple[Any, Any, Any]] = []
|
|
|
|
def hold_request(
|
|
operation: Any,
|
|
*,
|
|
on_success: Any = None,
|
|
on_error: Any = None,
|
|
on_finished: Any = None,
|
|
**_kwargs: Any,
|
|
) -> object:
|
|
callbacks.append((operation, on_success, on_finished))
|
|
return object()
|
|
|
|
class AcceptedOrderDialog:
|
|
def __init__(self, _record: Any, _parent: Any) -> None:
|
|
pass
|
|
|
|
def exec(self) -> Any:
|
|
return QDialog.DialogCode.Accepted
|
|
|
|
def payload(self) -> dict[str, Any]:
|
|
return {"patient_id": 301, "order_type": 2, "amount": 10.0, "remark": ""}
|
|
|
|
page = ConsultationsPage(Repository(), permissions=PermissionSet(["tcm.diagnosis/order"]))
|
|
rows = [
|
|
_row(),
|
|
_row(id=502, diagnosis_id=502, patient_id=302, appointment_id=102),
|
|
]
|
|
page.table.set_rows(rows)
|
|
page.table.selectRow(0)
|
|
monkeypatch.setattr(consultations_module, "_DiagnosisOrderDialog", AcceptedOrderDialog)
|
|
monkeypatch.setattr(consultations_module, "run_async", hold_request)
|
|
monkeypatch.setattr(page, "refresh", lambda *_args, **_kwargs: None)
|
|
|
|
page._create_diagnosis_order()
|
|
assert len(callbacks) == 1
|
|
operation, on_success, _on_finished = callbacks[0]
|
|
result = operation()
|
|
if invalidate == "selection":
|
|
page.table.selectRow(1)
|
|
else:
|
|
page.permissions = PermissionSet([])
|
|
on_success(result)
|
|
|
|
assert qr_calls == []
|
|
assert page._order_qr_dialog is None
|
|
assert not page._mutation_pending
|
|
page.close()
|
|
application.processEvents()
|
|
|
|
|
|
def test_closing_payment_dialog_invalidates_inflight_qr_result(
|
|
application: QApplication,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
class Repository:
|
|
def create_diagnosis_order(
|
|
self,
|
|
patient_id: int,
|
|
order_type: int,
|
|
amount: float,
|
|
*,
|
|
remark: str = "",
|
|
) -> dict[str, Any]:
|
|
del patient_id, order_type, amount, remark
|
|
return {"order_no": "CLOSE-1"}
|
|
|
|
def generate_order_qrcode(self, order_no: str) -> dict[str, Any]:
|
|
return {"qrcode_url": f"https://example.invalid/{order_no}.png"}
|
|
|
|
staged: list[tuple[Any, Any, Any]] = []
|
|
call_count = 0
|
|
|
|
def stage_request(
|
|
operation: Any,
|
|
*,
|
|
on_success: Any = None,
|
|
on_error: Any = None,
|
|
on_finished: Any = None,
|
|
**_kwargs: Any,
|
|
) -> object:
|
|
nonlocal call_count
|
|
call_count += 1
|
|
if call_count == 1:
|
|
on_success(operation())
|
|
if on_finished:
|
|
on_finished()
|
|
else:
|
|
staged.append((operation, on_success, on_finished))
|
|
return object()
|
|
|
|
class AcceptedOrderDialog:
|
|
def __init__(self, _record: Any, _parent: Any) -> None:
|
|
pass
|
|
|
|
def exec(self) -> Any:
|
|
return QDialog.DialogCode.Accepted
|
|
|
|
def payload(self) -> dict[str, Any]:
|
|
return {"patient_id": 301, "order_type": 2, "amount": 10.0, "remark": ""}
|
|
|
|
page = ConsultationsPage(Repository(), permissions=PermissionSet(["tcm.diagnosis/order"]))
|
|
page.table.set_rows([_row()])
|
|
page.table.selectRow(0)
|
|
monkeypatch.setattr(consultations_module, "_DiagnosisOrderDialog", AcceptedOrderDialog)
|
|
monkeypatch.setattr(consultations_module, "run_async", stage_request)
|
|
monkeypatch.setattr(page, "refresh", lambda *_args, **_kwargs: None)
|
|
|
|
page._create_diagnosis_order()
|
|
dialog = page._order_qr_dialog
|
|
assert dialog is not None
|
|
assert len(staged) == 1
|
|
dialog.reject()
|
|
assert page._order_qr_dialog is None
|
|
assert not page._mutation_pending
|
|
|
|
operation, on_success, on_finished = staged[0]
|
|
on_success(operation())
|
|
if on_finished:
|
|
on_finished()
|
|
|
|
assert page._order_qr_dialog is None
|
|
assert not page._mutation_pending
|
|
page.close()
|
|
application.processEvents()
|