Files
zyt/app/tests/test_appointment_complete_ui.py
2026-09-01 10:42:16 +08:00

307 lines
12 KiB
Python

from __future__ import annotations
import os
from typing import Any
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
import pytest
from PySide6.QtWidgets import QApplication
from doctor_workstation.core import PermissionSet
from doctor_workstation.ui.dialogs.appointment_complete import (
COMPLETION_NOTE_LIMIT,
AppointmentCompleteDialog,
)
from doctor_workstation.ui.pages import reception as reception_module
from doctor_workstation.ui.pages.reception import ReceptionPage
@pytest.fixture(scope="module")
def application() -> QApplication:
return QApplication.instance() or QApplication([])
class CompletionRepository:
def __init__(self) -> None:
self.calls: list[tuple[Any, ...]] = []
self.fail_complete = False
self.fail_note = False
self.detail: dict[str, Any] = {
"appointment": {"id": 51, "patient_id": 251, "status": 1},
"diagnosis": {"id": 251, "patient_id": 151},
"patient": {"id": 151},
}
def get_reception(self, appointment_id: int) -> dict[str, Any]:
self.calls.append(("revalidate", appointment_id))
return self.detail
def complete_appointment(self, appointment_id: int) -> dict[str, bool]:
self.calls.append(("complete", appointment_id))
if self.fail_complete:
raise RuntimeError("完成接口失败")
return {"ok": True}
def add_doctor_note(self, diagnosis_id: int, content: str) -> dict[str, bool]:
self.calls.append(("note", diagnosis_id, content))
if self.fail_note:
raise RuntimeError("备注接口失败")
return {"ok": True}
@pytest.fixture
def harness(application: QApplication, monkeypatch: pytest.MonkeyPatch):
jobs: list[dict[str, Any]] = []
toasts: list[tuple[str, str]] = []
refreshes: list[bool] = []
def queue(function: Any, **options: Any) -> object:
jobs.append({"function": function, **options})
return object()
monkeypatch.setattr(reception_module, "run_async", queue)
monkeypatch.setattr(
reception_module,
"show_toast",
lambda _parent, text, kind, *_args: toasts.append((text, kind)),
)
repository = CompletionRepository()
page = ReceptionPage(
repository,
PermissionSet(["doctor.appointment/complete", "doctor.appointment/addDoctorNote"]),
)
page._selected_appointment_id = 51
page._selected_record = dict(repository.detail["appointment"])
page._selected_detail = repository.detail
page._update_action_state(repository.detail["appointment"], repository.detail["diagnosis"])
monkeypatch.setattr(page, "refresh", lambda *, silent=False: refreshes.append(silent))
yield page, repository, jobs, toasts, refreshes
if page._completion_dialog is not None:
page._completion_dialog.set_busy(False)
page._completion_dialog.reject()
page.close()
application.processEvents()
def finish_job(job: dict[str, Any]) -> None:
try:
result = job["function"]()
except Exception as error:
job["on_error"](error)
else:
job["on_success"](result)
finally:
job["on_finished"]()
@pytest.mark.parametrize("can_note", [True, False])
def test_completion_dialog_optional_note_limit_and_busy_state(
application: QApplication, can_note: bool
) -> None:
dialog = AppointmentCompleteDialog(can_note=can_note)
submitted: list[str] = []
dialog.submitted.connect(submitted.append)
dialog.show()
application.processEvents()
try:
assert dialog.windowTitle() == "完成问诊"
assert dialog.note_edit.isVisible() is can_note
assert dialog.note_counter.isVisible() is can_note
assert dialog.note_counter.text() == "0 / 500"
dialog.note_edit.setPlainText("字" * 501)
assert dialog.note_edit.toPlainText() == "字" * COMPLETION_NOTE_LIMIT
assert dialog.note_counter.text() == "500 / 500"
dialog.note_edit.insertPlainText("额外内容")
assert len(dialog.note_edit.toPlainText()) == COMPLETION_NOTE_LIMIT
dialog.note_edit.setPlainText(" 测试备注\n第二行 ")
dialog.confirm_button.click()
assert submitted == ["测试备注\n第二行" if can_note else ""]
dialog.set_busy(True)
assert dialog.note_edit.isReadOnly()
assert not dialog.cancel_button.isEnabled()
dialog.confirm_button.click()
dialog.reject()
dialog.close()
assert len(submitted) == 1
assert dialog.isVisible()
dialog.show_error("提交失败,请重试")
assert dialog.note_edit.toPlainText() == " 测试备注\n第二行 "
assert dialog.confirm_button.isEnabled()
dialog.cancel_button.click()
assert not dialog.isVisible()
finally:
dialog.set_busy(False)
dialog.close()
def test_cancel_completion_does_not_make_requests(harness: Any) -> None:
page, repository, jobs, _toasts, _refreshes = harness
page.complete_button.click()
dialog = page._completion_dialog
assert isinstance(dialog, AppointmentCompleteDialog)
page._complete_appointment()
assert page._completion_dialog is dialog
assert jobs == []
dialog.note_edit.setPlainText("取消后不应保存")
dialog.cancel_button.click()
assert page._completion_dialog is None
assert repository.calls == []
assert jobs == []
@pytest.mark.parametrize("note", ["", " \n ", " 测试备注\n补充内容 ", "字" * 500])
def test_complete_then_append_note_and_refresh_without_duplicate_submission(
harness: Any, note: str
) -> None:
page, repository, jobs, toasts, refreshes = harness
page.complete_button.click()
dialog = page._completion_dialog
dialog.note_edit.setPlainText(note)
dialog.confirm_button.click()
assert page._completion_pending
assert not page.complete_button.isEnabled()
assert len(jobs) == 1
# Polling and direct handler calls cannot enable/dispatch a second request.
page._update_action_state(repository.detail["appointment"], repository.detail["diagnosis"])
assert not page.complete_button.isEnabled()
page._complete_appointment()
dialog.confirm_button.click()
assert len(jobs) == 1
finish_job(jobs.pop())
expected = [("revalidate", 51), ("complete", 51)]
if note.strip():
expected.append(("note", 251, note.strip()))
assert repository.calls == expected
assert page._completion_dialog is None
assert not page._completion_pending
assert page._selected_appointment_id is None
assert toasts[-1] == ("接诊已完成。", "success")
assert refreshes == [True]
def test_completion_without_note_permission_does_not_submit_hidden_note(harness: Any) -> None:
page, repository, jobs, _toasts, _refreshes = harness
page._can_note = False
page._complete_appointment()
dialog = page._completion_dialog
assert dialog.note_edit.isHidden()
dialog.note_edit.setPlainText("不可提交")
dialog.confirm_button.click()
finish_job(jobs.pop())
assert repository.calls == [("revalidate", 51), ("complete", 51)]
def test_completion_failure_preserves_note_and_allows_explicit_retry(harness: Any) -> None:
page, repository, jobs, toasts, refreshes = harness
repository.fail_complete = True
page._complete_appointment()
dialog = page._completion_dialog
dialog.note_edit.setPlainText("需要保留的备注")
dialog.confirm_button.click()
finish_job(jobs.pop())
assert repository.calls == [("revalidate", 51), ("complete", 51)]
assert page._completion_dialog is dialog
assert dialog.isVisible()
assert dialog.note_edit.toPlainText() == "需要保留的备注"
assert dialog.confirm_button.isEnabled()
assert "完成接口失败" in dialog.banner.label.text()
assert not page._completion_pending
assert page.complete_button.isEnabled()
assert page._selected_appointment_id == 51
assert toasts == []
assert refreshes == []
repository.fail_complete = False
dialog.confirm_button.click()
finish_job(jobs.pop())
assert repository.calls[-1] == ("note", 251, "需要保留的备注")
assert page._completion_dialog is None
@pytest.mark.parametrize("missing_diagnosis", [True, False])
def test_note_failure_is_partial_success_and_keeps_note_for_copying(
harness: Any, missing_diagnosis: bool
) -> None:
page, repository, jobs, toasts, refreshes = harness
if missing_diagnosis:
repository.detail["diagnosis"] = {}
else:
repository.fail_note = True
page._complete_appointment()
dialog = page._completion_dialog
dialog.note_edit.setPlainText("备注不能丢失")
dialog.confirm_button.click()
finish_job(jobs.pop())
assert [call[0] for call in repository.calls] == (
["revalidate", "complete"] if missing_diagnosis else ["revalidate", "complete", "note"]
)
assert page._selected_appointment_id is None
assert refreshes == [True]
assert toasts[-1][1] == "warning"
assert "问诊已完成" in dialog.banner.label.text()
assert "备注未保存" in dialog.banner.label.text()
assert dialog.note_edit.toPlainText() == "备注不能丢失"
assert dialog.note_edit.isReadOnly()
assert dialog.confirm_button.isHidden()
assert not dialog.confirm_button.isEnabled()
assert dialog.cancel_button.text() == "关闭"
dialog.confirm_button.click()
assert jobs == []
@pytest.mark.parametrize("changed_id", [True, False])
def test_completion_revalidation_rejects_changed_record_before_any_write(
harness: Any, changed_id: bool
) -> None:
page, repository, _jobs, _toasts, _refreshes = harness
if changed_id:
repository.detail["appointment"]["id"] = 52
else:
repository.detail["appointment"]["status"] = 3
with pytest.raises(ValueError, match="不一致|状态已变化"):
page._complete_after_revalidation(51, "测试备注")
assert repository.calls == [("revalidate", 51)]
def test_completion_checks_permissions_and_note_length_before_requests(harness: Any) -> None:
page, repository, jobs, _toasts, _refreshes = harness
with pytest.raises(ValueError, match="500"):
page._complete_after_revalidation(51, "字" * 501)
page._can_note = False
with pytest.raises(ValueError, match="备注权限"):
page._complete_after_revalidation(51, "测试备注")
page._can_complete = False
page._complete_appointment()
assert page._completion_dialog is None
with pytest.raises(ValueError, match="完成接诊权限"):
page._complete_after_revalidation(51)
assert repository.calls == []
assert jobs == []
@pytest.mark.parametrize("switch_before_confirm", [True, False])
def test_completion_does_not_mutate_or_clear_a_new_selection(
harness: Any, switch_before_confirm: bool
) -> None:
page, repository, jobs, _toasts, _refreshes = harness
page._complete_appointment()
dialog = page._completion_dialog
dialog.note_edit.setPlainText("原患者的备注")
if not switch_before_confirm:
dialog.confirm_button.click()
page._selected_appointment_id = 52
page._selected_record = {"id": 52, "status": 1}
page._selected_detail = {"appointment": page._selected_record, "diagnosis": {"id": 252}}
page._detail_generation += 1
page._update_action_state(page._selected_record, {"id": 252})
if switch_before_confirm:
dialog.confirm_button.click()
assert jobs == []
assert repository.calls == []
assert "已切换" in dialog.banner.label.text()
else:
finish_job(jobs.pop())
assert repository.calls[-1] == ("note", 251, "原患者的备注")
assert page.complete_button.isEnabled()
assert page._selected_appointment_id == 52