1361 lines
48 KiB
Python
1361 lines
48 KiB
Python
from __future__ import annotations
|
||
|
||
import os
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||
|
||
import pytest
|
||
from PySide6.QtCore import QPoint, Qt
|
||
from PySide6.QtGui import QImage
|
||
from PySide6.QtWidgets import (
|
||
QApplication,
|
||
QDialog,
|
||
QFrame,
|
||
QLabel,
|
||
QPushButton,
|
||
QRadioButton,
|
||
QScrollArea,
|
||
QToolButton,
|
||
QWidget,
|
||
)
|
||
|
||
from doctor_workstation.core import PermissionSet
|
||
from doctor_workstation.ui.diagnosis_drawer import (
|
||
DiagnosisChoiceButtons,
|
||
DiagnosisComboBox,
|
||
DiagnosisDateEdit,
|
||
DiagnosisLineEdit,
|
||
DiagnosisNumberEdit,
|
||
DiagnosisSwitch,
|
||
SaveStateButton,
|
||
)
|
||
from doctor_workstation.ui.dialogs import diagnosis as diagnosis_module
|
||
from doctor_workstation.ui.dialogs.diagnosis import DiagnosisDialog
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def application() -> QApplication:
|
||
return QApplication.instance() or QApplication([])
|
||
|
||
|
||
@pytest.fixture(autouse=True)
|
||
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)
|
||
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(diagnosis_module, "run_async", run_immediately)
|
||
|
||
|
||
def diagnosis_fixture(*, locked: bool = False) -> dict[str, Any]:
|
||
return {
|
||
"diagnosis": {
|
||
"id": 501,
|
||
"patient_id": 1501,
|
||
"patient_name": "林晓岚",
|
||
"phone": "13800138000",
|
||
"id_card": "110105199203071234",
|
||
"gender": 0,
|
||
"age": 34,
|
||
"marital_status": 1,
|
||
"height": 162,
|
||
"weight": 54.5,
|
||
"region": "浙江省杭州市",
|
||
"systolic_pressure": 146,
|
||
"diastolic_pressure": 92,
|
||
"fasting_blood_sugar": 8.2,
|
||
"diagnosis_type": "follow_up",
|
||
"diagnosis_date": "2026-08-10",
|
||
"status_desc": "诊疗中",
|
||
"channel_name": "健康顾问转介",
|
||
"statistical_visit_card": "第 4 次",
|
||
"current_medications": "二甲双胍",
|
||
"diabetes_discovery_year": "2019 年",
|
||
"local_hospital_diagnosis": ["2 型糖尿病", "高血压"],
|
||
"local_hospital_name": "杭州市第一人民医院",
|
||
"oral_condition": "口干",
|
||
"water_intake": "约 2200 ml",
|
||
"weight_change": "近月下降 1 kg",
|
||
"fatty_liver_degree": "轻度",
|
||
"diet_condition": ["偏甜", "夜宵"],
|
||
"body_feeling": ["乏力", "四肢沉重"],
|
||
"sleep_condition": "入睡稍慢",
|
||
"eye_condition": "偶有干涩",
|
||
"head_feeling": "午后头昏",
|
||
"sweat_condition": "易出汗",
|
||
"skin_condition": "皮肤偏干",
|
||
"urine_condition": "夜尿 2 次",
|
||
"stool_condition": "每日一次",
|
||
"kidney_condition": "腰酸",
|
||
"present_illness": "口渴、乏力反复半年,近期血糖波动。",
|
||
"past_history": "高血压病史 5 年。",
|
||
"trauma_history": 0,
|
||
"surgery_history": 0,
|
||
"allergy_history": 1,
|
||
"family_history": 1,
|
||
"pregnancy_history": 0,
|
||
"remark": "建议持续记录空腹及餐后血糖。",
|
||
"chief_complaint": "口渴乏力",
|
||
"clinical_diagnosis": "气阴两虚",
|
||
"prescription_opinion": "益气养阴,兼顾活血。",
|
||
"has_prescription": 1,
|
||
"unserved_days": 4,
|
||
"patient_basic_locked": locked,
|
||
"can_edit_patient_basic": not locked,
|
||
"latest_prescription_order": {
|
||
"id": "RX-240810-09",
|
||
"fulfillment_status_text": "待配药",
|
||
},
|
||
},
|
||
"patient": {
|
||
"id": 1501,
|
||
"patient_name": "林晓岚",
|
||
"phone": "13800138000",
|
||
"id_card": "110105199203071234",
|
||
"gender": 0,
|
||
"age": 34,
|
||
},
|
||
"appointment": {
|
||
"id": 2501,
|
||
"appointment_date": "2026-08-10",
|
||
"appointment_time": "09:00-09:30",
|
||
"period_text": "上午",
|
||
"doctor_name": "陈医生",
|
||
"assistant_name": "赵医助",
|
||
"status_desc": "已到诊",
|
||
"remark": "复诊评估",
|
||
},
|
||
}
|
||
|
||
|
||
class VisualRepository:
|
||
def __init__(self, *, locked: bool = False) -> None:
|
||
self.detail = diagnosis_fixture(locked=locked)
|
||
self.detail_calls: list[str] = []
|
||
self.dictionary_calls: list[str] = []
|
||
self.notes_calls = 0
|
||
self.tracking_calls: list[tuple[str, str]] = []
|
||
self.tracking_note_calls = 0
|
||
self.todo_calls: list[int | None] = []
|
||
self.prescription_calls = 0
|
||
self.order_queries: list[dict[str, Any]] = []
|
||
self.updates: list[dict[str, Any]] = []
|
||
|
||
def get_diagnosis_detail(self, diagnosis_id: int, **_kwargs: Any) -> dict[str, Any]:
|
||
assert diagnosis_id == 501
|
||
self.detail_calls.append("ordinary")
|
||
return self.detail
|
||
|
||
def diagnosis_readonly_detail(self, diagnosis_id: int, **_kwargs: Any) -> dict[str, Any]:
|
||
assert diagnosis_id == 501
|
||
self.detail_calls.append("readonly")
|
||
return self.detail
|
||
|
||
def patient_detail(self, diagnosis_id: int, **_kwargs: Any) -> dict[str, Any]:
|
||
assert diagnosis_id == 501
|
||
self.detail_calls.append("patient")
|
||
return self.detail
|
||
|
||
def get_dictionary(self, dictionary_type: str) -> list[dict[str, Any]]:
|
||
self.dictionary_calls.append(dictionary_type)
|
||
dictionaries = {
|
||
"diagnosis_type": [
|
||
{"name": "初诊", "value": "first_visit"},
|
||
{"name": "复诊", "value": "follow_up"},
|
||
{"name": "会诊", "value": "consultation"},
|
||
],
|
||
"appetite": [
|
||
{"name": "口干", "value": "口干"},
|
||
{"name": "口苦", "value": "口苦"},
|
||
],
|
||
}
|
||
return dictionaries.get(dictionary_type, [])
|
||
|
||
def get_doctor_notes(self, diagnosis_id: int) -> list[dict[str, Any]]:
|
||
assert diagnosis_id == 501
|
||
self.notes_calls += 1
|
||
return [
|
||
{
|
||
"id": 6101,
|
||
"diagnosis_id": 501,
|
||
"patient_id": 1501,
|
||
"create_time": "2026-08-10 09:20",
|
||
"doctor_name": "陈医生",
|
||
"content": "面色稍淡,舌淡红,苔薄白。",
|
||
"tongue_images": [{"url": "https://media.example.invalid/tongue.jpg"}],
|
||
"report_files": [
|
||
{
|
||
"name": "近期血糖趋势.pdf",
|
||
"url": "https://media.example.invalid/report.pdf",
|
||
}
|
||
],
|
||
}
|
||
]
|
||
|
||
def get_tracking_window(
|
||
self,
|
||
diagnosis_id: int,
|
||
*,
|
||
start_date: str = "",
|
||
end_date: str = "",
|
||
) -> dict[str, Any]:
|
||
assert diagnosis_id == 501
|
||
self.tracking_calls.append((start_date, end_date))
|
||
return {
|
||
"blood_records": [
|
||
{
|
||
"id": 6201,
|
||
"diagnosis_id": 501,
|
||
"patient_id": 1501,
|
||
"record_date": "2026-08-10",
|
||
"fasting_blood_sugar": 8.2,
|
||
"systolic_pressure": 146,
|
||
"source": 1,
|
||
},
|
||
{
|
||
"id": 6202,
|
||
"diagnosis_id": 501,
|
||
"patient_id": 1501,
|
||
"record_date": "2026-08-10",
|
||
"postprandial_blood_sugar": 12.4,
|
||
"diastolic_pressure": 92,
|
||
"western_medicine": "二甲双胍",
|
||
},
|
||
{
|
||
"id": 6203,
|
||
"diagnosis_id": 501,
|
||
"patient_id": 1501,
|
||
"record_date": "2026-08-09",
|
||
"fasting_blood_sugar": 7.6,
|
||
"postprandial_blood_sugar": 10.8,
|
||
},
|
||
],
|
||
"diet_records": [
|
||
{
|
||
"id": 6301,
|
||
"diagnosis_id": 501,
|
||
"patient_id": 1501,
|
||
"record_date": "2026-08-10",
|
||
"breakfast_foods": "燕麦、鸡蛋",
|
||
"lunch_foods": "杂粮饭",
|
||
}
|
||
],
|
||
"exercise_records": [
|
||
{
|
||
"id": 6401,
|
||
"diagnosis_id": 501,
|
||
"patient_id": 1501,
|
||
"record_date": "2026-08-09",
|
||
"exercise_type": "散步",
|
||
"duration": 35,
|
||
"intensity": 2,
|
||
}
|
||
],
|
||
}
|
||
|
||
def list_tracking_notes(self, diagnosis_id: int) -> list[dict[str, Any]]:
|
||
assert diagnosis_id == 501
|
||
self.tracking_note_calls += 1
|
||
return [{"note_date": "2026-08-10", "content": "饭后散步,继续观察。"}]
|
||
|
||
def list_diagnosis_todos(
|
||
self,
|
||
diagnosis_id: int,
|
||
*,
|
||
status: int | None = None,
|
||
**_kwargs: Any,
|
||
) -> dict[str, Any]:
|
||
assert diagnosis_id == 501
|
||
self.todo_calls.append(status)
|
||
return {
|
||
"lists": [
|
||
{
|
||
"remind_time_text": "2026-08-12 09:00",
|
||
"content": "回访复测餐后血糖",
|
||
"status": 0,
|
||
"status_text": "待执行",
|
||
"creator_name": "陈医生",
|
||
}
|
||
],
|
||
"count": 1,
|
||
}
|
||
|
||
def list_prescriptions_by_diagnosis(self, diagnosis_id: int) -> list[dict[str, Any]]:
|
||
assert diagnosis_id == 501
|
||
self.prescription_calls += 1
|
||
return [
|
||
{
|
||
"id": 901,
|
||
"diagnosis_id": 501,
|
||
"prescription_date": "2026-08-02",
|
||
"global_visit_sequence": 4,
|
||
"prescription_type_text": "中药",
|
||
"prescription_summary": "玉泉丸加减",
|
||
"doctor_name": "陈医生",
|
||
"status_text": "已审核",
|
||
}
|
||
]
|
||
|
||
def appointment_history(self, **_kwargs: Any) -> dict[str, Any]:
|
||
return {
|
||
"lists": [
|
||
{
|
||
"id": 2501,
|
||
"status_desc": "已到诊",
|
||
"patient_name": "林晓岚",
|
||
"patient_phone": "13800138000",
|
||
"doctor_name": "陈医生",
|
||
"assistant_name": "赵医助",
|
||
"appointment_date": "2026-08-10",
|
||
"appointment_time": "09:00-09:30",
|
||
"appointment_type_text": "复诊",
|
||
"channel_name": "健康顾问",
|
||
"is_confirmed": 1,
|
||
"has_prescription": 1,
|
||
"remark": "复诊评估",
|
||
"create_time": "2026-08-08 11:20",
|
||
}
|
||
]
|
||
}
|
||
|
||
def assign_history(self, **_kwargs: Any) -> dict[str, Any]:
|
||
return {
|
||
"lists": [
|
||
{
|
||
"create_time": "2026-08-08 11:22",
|
||
"from_assistant_name": "王医助",
|
||
"to_assistant_name": "赵医助",
|
||
"is_inherit": 1,
|
||
"snapshot_order_creator_name": "王医助",
|
||
"snapshot_order_create_time": "2026-08-01 10:00",
|
||
"operator_name": "陈医生",
|
||
"operator_account": "doctor.chen",
|
||
"ip": "10.1.2.8",
|
||
}
|
||
]
|
||
}
|
||
|
||
def list_prescription_orders(self, **kwargs: Any) -> dict[str, Any]:
|
||
self.order_queries.append(dict(kwargs))
|
||
return {
|
||
"lists": [
|
||
{
|
||
"id": 801,
|
||
"order_no": "RX-240810-09",
|
||
"global_visit_sequence": 4,
|
||
"statistics_count": 1,
|
||
"amount": "¥ 286.00",
|
||
"doctor_name": "陈医生",
|
||
"assistant_name": "赵医助",
|
||
"fulfillment_status_text": "待配药",
|
||
"create_time": "2026-08-10 09:24",
|
||
}
|
||
],
|
||
"count": 1,
|
||
}
|
||
|
||
def get_prescription_order(self, order_id: int) -> dict[str, Any]:
|
||
assert order_id == 801
|
||
return {
|
||
"id": order_id,
|
||
"order_no": "RX-240810-09",
|
||
"patient_name": "林晓岚",
|
||
"amount": 286,
|
||
"fulfillment_status_text": "待配药",
|
||
}
|
||
|
||
def update_diagnosis(
|
||
self, diagnosis: int, changes: dict[str, Any] | None = None
|
||
) -> dict[str, Any]:
|
||
assert diagnosis == 501
|
||
payload = dict(changes or {})
|
||
self.updates.append(payload)
|
||
return {"id": diagnosis, **payload}
|
||
|
||
def check_diagnosis_phone(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||
del payload
|
||
return {"exists": False}
|
||
|
||
def check_diagnosis_id_card(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||
del payload
|
||
return {"duplicate": False}
|
||
|
||
|
||
class ActionRepository(VisualRepository):
|
||
"""Repository double exposing every confirmed diagnosis-detail action."""
|
||
|
||
def __init__(self) -> None:
|
||
super().__init__()
|
||
self.action_calls: list[tuple[str, Any]] = []
|
||
|
||
def add_blood_record(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||
self.action_calls.append(("blood", dict(payload)))
|
||
return {"id": 41, **payload}
|
||
|
||
def add_diet_record(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||
self.action_calls.append(("diet", dict(payload)))
|
||
return {"id": 42, **payload}
|
||
|
||
def add_exercise_record(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||
self.action_calls.append(("exercise", dict(payload)))
|
||
return {"id": 43, **payload}
|
||
|
||
def update_blood_record(
|
||
self, record: dict[str, Any], changes: dict[str, Any] | None = None
|
||
) -> dict[str, Any]:
|
||
payload = {**record, **(changes or {})}
|
||
self.action_calls.append(("blood_edit", payload))
|
||
return payload
|
||
|
||
def update_diet_record(
|
||
self, record: dict[str, Any], changes: dict[str, Any] | None = None
|
||
) -> dict[str, Any]:
|
||
payload = {**record, **(changes or {})}
|
||
self.action_calls.append(("diet_edit", payload))
|
||
return payload
|
||
|
||
def update_exercise_record(
|
||
self, record: dict[str, Any], changes: dict[str, Any] | None = None
|
||
) -> dict[str, Any]:
|
||
payload = {**record, **(changes or {})}
|
||
self.action_calls.append(("exercise_edit", payload))
|
||
return payload
|
||
|
||
def add_tracking_note(self, diagnosis_id: int, content: str) -> dict[str, Any]:
|
||
self.action_calls.append(("tracking", (diagnosis_id, content)))
|
||
return {"id": 44}
|
||
|
||
def add_diagnosis_todo(
|
||
self, diagnosis_id: int, content: str, remind_time: int
|
||
) -> dict[str, Any]:
|
||
self.action_calls.append(("todo", (diagnosis_id, content, remind_time)))
|
||
return {"id": 45}
|
||
|
||
def cancel_diagnosis_todo(self, todo_id: int) -> dict[str, Any]:
|
||
self.action_calls.append(("cancel_todo", todo_id))
|
||
return {"id": todo_id, "status": 2}
|
||
|
||
def add_doctor_note(self, diagnosis_id: int, content: str = "", **kwargs: Any) -> Any:
|
||
self.action_calls.append(("note", (diagnosis_id, content, kwargs)))
|
||
return {"id": 46}
|
||
|
||
def delete_doctor_note_image(self, note_id: int, image_type: str, image_path: str) -> Any:
|
||
self.action_calls.append(("delete_note_media", (note_id, image_type, image_path)))
|
||
return {"ok": True}
|
||
|
||
def upload_material(self, path: str, material_type: str) -> str:
|
||
self.action_calls.append(("upload", (path, material_type)))
|
||
return "https://media.example.invalid/uploaded"
|
||
|
||
def create_prescription(self, prescription: dict[str, Any]) -> Any:
|
||
self.action_calls.append(("prescription", dict(prescription)))
|
||
return {"id": 47}
|
||
|
||
def set_revisit_slot_start_offset(self, diagnosis_id: int, offset: int) -> Any:
|
||
self.action_calls.append(("offset", (diagnosis_id, offset)))
|
||
return {"ok": True}
|
||
|
||
def list_call_records(self, diagnosis_id: int) -> list[dict[str, Any]]:
|
||
self.action_calls.append(("video_list", diagnosis_id))
|
||
return [
|
||
{
|
||
"id": 48,
|
||
"recording_urls_list": [
|
||
"https://media.example.invalid/replay.mp4",
|
||
"https://media.example.invalid/replay-backup.m3u8",
|
||
"https://media.example.invalid/replay.webm",
|
||
],
|
||
"status_text": "已结束",
|
||
"recording_status_text": "录制完成",
|
||
}
|
||
]
|
||
|
||
def upload_call_recording(
|
||
self, path: str, diagnosis_id: int, *, call_record_id: int | None = None
|
||
) -> dict[str, Any]:
|
||
self.action_calls.append(("video_upload", (path, diagnosis_id, call_record_id)))
|
||
return {"file_url": "https://media.example.invalid/replay.mp4"}
|
||
|
||
def list_im_chat_messages(
|
||
self, diagnosis_id: int, *, only_archived: bool = True
|
||
) -> dict[str, Any]:
|
||
self.action_calls.append(("chat_list", (diagnosis_id, only_archived)))
|
||
return {
|
||
"lists": [
|
||
{
|
||
"msg_id": "m1",
|
||
"msg_type": "image",
|
||
"image_url": "https://media.example.invalid/image.jpg",
|
||
"is_from_doctor": False,
|
||
},
|
||
{
|
||
"msg_id": "m2",
|
||
"msg_type": "file",
|
||
"file_url": "https://media.example.invalid/report.pdf",
|
||
"file_name": "报告.pdf",
|
||
"is_from_doctor": True,
|
||
},
|
||
]
|
||
}
|
||
|
||
def sync_im_chat_messages(self, diagnosis_id: int) -> dict[str, Any]:
|
||
self.action_calls.append(("chat_sync", diagnosis_id))
|
||
return {"queued": True}
|
||
|
||
|
||
def _open_dialog(
|
||
application: QApplication,
|
||
size: tuple[int, int],
|
||
*,
|
||
mode: str,
|
||
permissions: PermissionSet | None = None,
|
||
locked: bool = False,
|
||
repository: VisualRepository | None = None,
|
||
parent: QWidget | None = None,
|
||
) -> DiagnosisDialog:
|
||
dialog = DiagnosisDialog(
|
||
repository or VisualRepository(locked=locked),
|
||
parent,
|
||
permissions=permissions or PermissionSet(["*"]),
|
||
)
|
||
dialog.resize(*size)
|
||
if mode == "readonly":
|
||
dialog.open_for(501, editable=False)
|
||
elif mode == "edit":
|
||
dialog.open_for(501, editable=True)
|
||
elif mode == "viewOnly":
|
||
dialog.open_view_only(501)
|
||
else:
|
||
raise AssertionError(mode)
|
||
for _ in range(4):
|
||
application.processEvents()
|
||
return dialog
|
||
|
||
|
||
def _select_tab(dialog: DiagnosisDialog, key: str) -> None:
|
||
index = next(
|
||
index for index in range(dialog.tabs.count()) if dialog.tabs.tabBar().tabData(index) == key
|
||
)
|
||
dialog.tabs.setCurrentIndex(index)
|
||
QApplication.processEvents()
|
||
|
||
|
||
@pytest.mark.parametrize("size", [(1024, 640), (1440, 900)])
|
||
def test_readonly_is_an_independent_vertical_page_flow(
|
||
application: QApplication,
|
||
size: tuple[int, int],
|
||
) -> None:
|
||
dialog = _open_dialog(application, size, mode="readonly")
|
||
assert dialog.view_stack.currentWidget() is dialog.readonly_page
|
||
assert dialog.readonly_page.objectName() == "DiagnosisReadonlyPage"
|
||
assert dialog.readonly_scroll.horizontalScrollBar().maximum() == 0
|
||
assert dialog.readonly_hero.isVisibleTo(dialog)
|
||
assert dialog.readonly_patient_card.isVisibleTo(dialog)
|
||
assert dialog.case_grid.isVisibleTo(dialog)
|
||
assert not dialog.tabs.isVisibleTo(dialog)
|
||
assert dialog.findChild(QFrame, "DiagnosisReadonlySection_notes") is not None
|
||
assert dialog.readonly_status.text() == "未服务 4 天"
|
||
assert dialog.readonly_content_layout.spacing() == 16
|
||
dialog.close()
|
||
application.processEvents()
|
||
|
||
|
||
@pytest.mark.parametrize("size", [(1024, 640), (1440, 900)])
|
||
@pytest.mark.parametrize("mode", ["edit", "viewOnly"])
|
||
def test_drawer_is_full_height_rtl_and_sixty_percent_wide(
|
||
application: QApplication,
|
||
size: tuple[int, int],
|
||
mode: str,
|
||
) -> None:
|
||
dialog = _open_dialog(application, size, mode=mode, locked=mode == "viewOnly")
|
||
expected_width = round(size[0] * 0.60)
|
||
assert dialog.view_stack.currentWidget() is dialog.drawer_overlay
|
||
assert dialog.drawer_overlay.rect() == dialog.rect()
|
||
assert dialog.drawer_panel.width() == expected_width
|
||
assert dialog.drawer_panel.height() == size[1]
|
||
assert dialog.drawer_panel.x() + dialog.drawer_panel.width() == size[0]
|
||
assert dialog.findChild(QFrame, "DiagnosisDrawerHeader").geometry().top() == 0
|
||
footer = dialog.findChild(QFrame, "DiagnosisDrawerFooter")
|
||
assert footer.geometry().bottom() == dialog.drawer_panel.rect().bottom()
|
||
assert dialog.tabs.isVisibleTo(dialog)
|
||
for field in dialog.edit_fields.values():
|
||
if field.isVisibleTo(dialog):
|
||
right_edge = field.mapTo(dialog.drawer_panel, QPoint(field.width(), 0)).x()
|
||
assert right_edge <= dialog.drawer_panel.width() - 12
|
||
assert dialog.save_button.isVisibleTo(dialog) is (mode == "edit")
|
||
assert dialog.close_button.text() == ("取消" if mode == "edit" else "关闭")
|
||
assert dialog.mode_label.text() == ("编辑" if mode == "edit" else "只读")
|
||
dialog.close()
|
||
application.processEvents()
|
||
|
||
|
||
def test_tabs_permissions_locked_state_and_field_error_focus(
|
||
application: QApplication,
|
||
) -> None:
|
||
dialog = _open_dialog(application, (1024, 640), mode="edit", locked=True)
|
||
assert [dialog.tabs.tabText(index) for index in range(dialog.tabs.count())] == [
|
||
"病历",
|
||
"医生备注",
|
||
"日常记录",
|
||
"处方",
|
||
"业务订单",
|
||
"视频录制回放",
|
||
"聊天",
|
||
"指派医助记录",
|
||
"挂号记录",
|
||
]
|
||
assert dialog.privacy_banner.isVisibleTo(dialog)
|
||
assert dialog.edit_fields["patient_name"].isReadOnly()
|
||
assert not dialog.edit_fields["present_illness"].isReadOnly()
|
||
dialog.edit_fields["patient_name"].setReadOnly(False)
|
||
dialog.edit_fields["patient_name"].clear()
|
||
dialog._save()
|
||
application.processEvents()
|
||
assert dialog.edit_fields["patient_name"].property("invalid") is True
|
||
assert dialog.edit_fields["patient_name"].hasFocus()
|
||
assert dialog.drawer_banner.isVisibleTo(dialog)
|
||
dialog.close()
|
||
|
||
minimal = _open_dialog(
|
||
application,
|
||
(1024, 640),
|
||
mode="viewOnly",
|
||
permissions=PermissionSet(["tcm.diagnosis/readonlyDetail"]),
|
||
)
|
||
assert [minimal.tabs.tabText(index) for index in range(minimal.tabs.count())] == [
|
||
"病历",
|
||
"医生备注",
|
||
]
|
||
minimal.close()
|
||
application.processEvents()
|
||
|
||
|
||
def test_semantic_form_controls_keep_desktop_grid_and_canonical_diagnosis_type(
|
||
application: QApplication,
|
||
) -> None:
|
||
repository = VisualRepository()
|
||
dialog = _open_dialog(
|
||
application,
|
||
(1024, 640),
|
||
mode="edit",
|
||
repository=repository,
|
||
)
|
||
|
||
assert isinstance(dialog.edit_fields["gender"], DiagnosisChoiceButtons)
|
||
assert len(dialog.edit_fields["gender"].findChildren(QRadioButton)) == 2
|
||
assert isinstance(dialog.edit_fields["local_hospital_diagnosis"], DiagnosisChoiceButtons)
|
||
assert isinstance(dialog.edit_fields["age"], DiagnosisNumberEdit)
|
||
assert isinstance(dialog.edit_fields["height"], DiagnosisNumberEdit)
|
||
assert isinstance(dialog.edit_fields["weight"], DiagnosisNumberEdit)
|
||
assert isinstance(dialog.edit_fields["fasting_blood_sugar"], DiagnosisLineEdit)
|
||
assert isinstance(dialog.edit_fields["diagnosis_date"], DiagnosisDateEdit)
|
||
diagnosis_type = dialog.edit_fields["diagnosis_type"]
|
||
assert isinstance(diagnosis_type, DiagnosisComboBox)
|
||
assert [diagnosis_type.itemData(index) for index in range(1, diagnosis_type.count())] == [
|
||
"first_visit",
|
||
"follow_up",
|
||
"consultation",
|
||
]
|
||
assert diagnosis_type.toPlainText() == "follow_up"
|
||
assert "diagnosis_type" in repository.dictionary_calls
|
||
assert dialog._form_narrow is False
|
||
assert any(len(fields) >= 3 for _layout, fields, _remainder in dialog._form_rows)
|
||
|
||
_select_tab(dialog, "notes")
|
||
assert not dialog.save_button.isVisibleTo(dialog)
|
||
_select_tab(dialog, "basic")
|
||
assert dialog.save_button.isVisibleTo(dialog)
|
||
diagnosis_type.setFocus()
|
||
application.processEvents()
|
||
assert diagnosis_type.hasFocus()
|
||
dialog.close()
|
||
|
||
|
||
def test_view_only_uses_ordinary_detail_and_standalone_uses_readonly_detail(
|
||
application: QApplication,
|
||
) -> None:
|
||
view_repository = VisualRepository()
|
||
view = _open_dialog(
|
||
application,
|
||
(1024, 640),
|
||
mode="viewOnly",
|
||
repository=view_repository,
|
||
)
|
||
assert view_repository.detail_calls == ["ordinary"]
|
||
view.close()
|
||
|
||
readonly_repository = VisualRepository()
|
||
readonly = _open_dialog(
|
||
application,
|
||
(1024, 640),
|
||
mode="readonly",
|
||
repository=readonly_repository,
|
||
)
|
||
assert readonly_repository.detail_calls == ["readonly"]
|
||
readonly.close()
|
||
|
||
|
||
def test_tabs_lazy_load_real_repository_data_and_daily_matrix_structure(
|
||
application: QApplication,
|
||
) -> None:
|
||
repository = VisualRepository()
|
||
dialog = _open_dialog(
|
||
application,
|
||
(1024, 640),
|
||
mode="edit",
|
||
repository=repository,
|
||
)
|
||
assert repository.notes_calls == 0
|
||
assert repository.tracking_calls == []
|
||
assert repository.prescription_calls == 0
|
||
assert repository.order_queries == []
|
||
|
||
_select_tab(dialog, "notes")
|
||
assert repository.notes_calls == 1
|
||
_select_tab(dialog, "basic")
|
||
_select_tab(dialog, "notes")
|
||
assert repository.notes_calls == 1
|
||
|
||
_select_tab(dialog, "daily")
|
||
assert len(repository.tracking_calls) == 1
|
||
assert repository.tracking_note_calls == 1
|
||
assert repository.todo_calls == [None]
|
||
panel = dialog._daily_panels[1]
|
||
assert panel.matrix.rowCount() == 11
|
||
assert panel.matrix.columnCount() == 8
|
||
blood_column = next(
|
||
column
|
||
for column in range(1, panel.matrix.columnCount())
|
||
if panel.matrix.horizontalHeaderItem(column).text() == "08-10"
|
||
)
|
||
assert panel.matrix.item(0, blood_column).text() == "↑ 8.2 · 自录"
|
||
assert panel.matrix.item(3, blood_column).text() == "↑ 146/92 · 自录"
|
||
assert panel.todo_table.rowCount() == 1
|
||
assert panel.todo_summary.text() == "共 1 条"
|
||
dialog._daily_todo_filter_changed(0)
|
||
assert repository.todo_calls[-1] == 0
|
||
|
||
_select_tab(dialog, "prescription")
|
||
assert repository.prescription_calls == 1
|
||
assert dialog._table_registry["prescription"][0].rowCount() == 1
|
||
_select_tab(dialog, "orders")
|
||
assert len(repository.order_queries) == 1
|
||
assert dialog.orders_table.cellWidget(0, 8) is not None
|
||
|
||
_select_tab(dialog, "video")
|
||
video_table = dialog._table_registry["video"][1]
|
||
assert video_table.rowCount() == 0
|
||
assert not any(
|
||
token in button.text()
|
||
for button in video_table.findChildren(QPushButton)
|
||
for token in ("查看", "播放")
|
||
)
|
||
_select_tab(dialog, "chat")
|
||
assert "尚未接入" in dialog._chat_panels[1].empty.text()
|
||
dialog.close()
|
||
|
||
|
||
def test_appointment_history_phone_is_fail_closed_without_phone_plain(
|
||
application: QApplication,
|
||
) -> None:
|
||
dialog = _open_dialog(
|
||
application,
|
||
(1024, 640),
|
||
mode="readonly",
|
||
permissions=PermissionSet(["tcm.diagnosis/readonlyDetail", "doctor.appointment/lists"]),
|
||
)
|
||
patient_cell = dialog.appointment_table.item(0, 2)
|
||
assert patient_cell is not None
|
||
assert "138****8000" in patient_cell.text()
|
||
assert "13800138000" not in patient_cell.text()
|
||
dialog.close()
|
||
|
||
|
||
def test_order_detail_action_requires_exact_permission(application: QApplication) -> None:
|
||
repository = VisualRepository()
|
||
dialog = _open_dialog(
|
||
application,
|
||
(1024, 640),
|
||
mode="viewOnly",
|
||
permissions=PermissionSet(["tcm.diagnosis/patientOrders"]),
|
||
repository=repository,
|
||
)
|
||
_select_tab(dialog, "orders")
|
||
assert dialog.orders_table.rowCount() == 1
|
||
assert dialog.orders_table.cellWidget(0, 8) is None
|
||
assert dialog.orders_table.item(0, 8).text() == "无详情权限"
|
||
dialog.close()
|
||
|
||
|
||
def test_detail_failure_clears_seed_locks_save_and_supports_retry(
|
||
application: QApplication,
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
) -> None:
|
||
repository = VisualRepository()
|
||
callbacks: list[dict[str, Any]] = []
|
||
|
||
def queue_async(function: Any, **options: Any) -> object:
|
||
callbacks.append({"function": function, **options})
|
||
return object()
|
||
|
||
monkeypatch.setattr(diagnosis_module, "run_async", queue_async)
|
||
dialog = DiagnosisDialog(repository, permissions=PermissionSet(["*"]))
|
||
dialog.resize(1024, 640)
|
||
dialog.open_for(501, editable=True, seed=diagnosis_fixture())
|
||
application.processEvents()
|
||
assert dialog.edit_fields["patient_name"].toPlainText() == "林晓岚"
|
||
assert not dialog.save_button.isEnabled()
|
||
|
||
callbacks[0]["on_error"](RuntimeError("诊单详情读取失败"))
|
||
callbacks[0]["on_finished"]()
|
||
application.processEvents()
|
||
assert not dialog._authoritative_detail_loaded
|
||
assert dialog.edit_fields["patient_name"].toPlainText() == ""
|
||
assert dialog.edit_fields["patient_name"].isReadOnly()
|
||
assert not dialog.save_button.isEnabled()
|
||
assert dialog.drawer_banner.action_button.isVisibleTo(dialog)
|
||
dialog._save()
|
||
assert repository.updates == []
|
||
|
||
dialog.drawer_banner.action_button.click()
|
||
assert len(callbacks) == 2
|
||
result = callbacks[1]["function"]()
|
||
callbacks[1]["on_success"](result)
|
||
callbacks[1]["on_finished"]()
|
||
application.processEvents()
|
||
assert dialog._authoritative_detail_loaded
|
||
assert dialog.edit_fields["patient_name"].toPlainText() == "林晓岚"
|
||
assert dialog.save_button.isEnabled()
|
||
dialog.close()
|
||
|
||
|
||
def test_close_invalidates_pending_load_and_save_callbacks(
|
||
application: QApplication,
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
) -> None:
|
||
repository = VisualRepository()
|
||
queued: list[dict[str, Any]] = []
|
||
|
||
def queue_async(function: Any, **options: Any) -> object:
|
||
queued.append({"function": function, **options})
|
||
return object()
|
||
|
||
monkeypatch.setattr(diagnosis_module, "run_async", queue_async)
|
||
loading = DiagnosisDialog(repository, permissions=PermissionSet(["*"]))
|
||
loading.open_for(501, editable=True)
|
||
load_generation = loading._generation
|
||
loading.reject()
|
||
result = queued[0]["function"]()
|
||
queued[0]["on_success"](result)
|
||
queued[0]["on_finished"]()
|
||
assert loading._generation > load_generation
|
||
assert not loading._authoritative_detail_loaded
|
||
|
||
# Reuse the fixture's synchronous runner by manually applying a fresh authoritative bundle.
|
||
saving = DiagnosisDialog(repository, permissions=PermissionSet(["*"]))
|
||
saving._diagnosis_id = 501
|
||
saving._editable = True
|
||
saving._standalone_readonly = False
|
||
saving._view_only = False
|
||
saving._load_mode = "edit"
|
||
saving._apply_bundle(saving._load_bundle(501, "edit"), saving._generation)
|
||
saving.view_stack.setCurrentWidget(saving.drawer_overlay)
|
||
saving.show()
|
||
application.processEvents()
|
||
queued.clear()
|
||
monkeypatch.setattr(diagnosis_module, "run_async", queue_async)
|
||
saved: list[bool] = []
|
||
saving.saved.connect(lambda: saved.append(True))
|
||
saving._save()
|
||
assert len(queued) == 1
|
||
save_generation = saving._save_generation
|
||
saving.reject()
|
||
queued[0]["on_success"](None)
|
||
queued[0]["on_finished"]()
|
||
assert saving._save_generation > save_generation
|
||
assert saved == []
|
||
|
||
|
||
def test_owner_resize_move_and_close_syncs_geometry_and_invalidates_generation(
|
||
application: QApplication,
|
||
) -> None:
|
||
owner = QWidget()
|
||
owner.setGeometry(80, 60, 1200, 760)
|
||
owner.show()
|
||
repository = VisualRepository()
|
||
dialog = _open_dialog(
|
||
application,
|
||
(1200, 760),
|
||
mode="edit",
|
||
repository=repository,
|
||
parent=owner,
|
||
)
|
||
owner.setGeometry(110, 90, 1024, 650)
|
||
for _ in range(3):
|
||
application.processEvents()
|
||
assert dialog.size() == owner.size()
|
||
assert dialog.pos() == owner.mapToGlobal(QPoint(0, 0))
|
||
generation = dialog._generation
|
||
owner.close()
|
||
for _ in range(3):
|
||
application.processEvents()
|
||
assert dialog._generation > generation
|
||
assert not dialog.isVisible()
|
||
|
||
|
||
def test_body_scroll_does_not_move_fixed_footer(application: QApplication) -> None:
|
||
dialog = _open_dialog(application, (1024, 640), mode="edit")
|
||
body = dialog.findChild(QScrollArea, "DiagnosisDrawerBody")
|
||
footer = dialog.findChild(QFrame, "DiagnosisDrawerFooter")
|
||
before = footer.geometry()
|
||
assert body.verticalScrollBar().maximum() > 0
|
||
body.verticalScrollBar().setValue(body.verticalScrollBar().maximum())
|
||
application.processEvents()
|
||
assert footer.geometry() == before
|
||
assert footer.geometry().bottom() == dialog.drawer_panel.rect().bottom()
|
||
dialog.close()
|
||
|
||
|
||
def test_required_detail_reference_artifacts_exist() -> None:
|
||
root = Path(__file__).resolve().parents[1]
|
||
expected = {
|
||
root / "artifacts" / "diagnosis_visual" / f"diagnosis_{mode}_{width}x{height}.png": (
|
||
width,
|
||
height,
|
||
)
|
||
for mode in ("readonly", "edit", "viewonly")
|
||
for width, height in ((1024, 640), (1440, 900))
|
||
}
|
||
expected.update(
|
||
{
|
||
root / "artifacts" / "diagnosis_visual" / f"diagnosis_state_{state}_1024x640.png": (
|
||
1024,
|
||
640,
|
||
)
|
||
for state in (
|
||
"loading",
|
||
"error",
|
||
"empty",
|
||
"permission",
|
||
"daily",
|
||
"daily_lower",
|
||
"focus",
|
||
"save_loading",
|
||
"save_success",
|
||
"save_failure",
|
||
"notes_actions",
|
||
"video_replay",
|
||
"chat_archive",
|
||
"order_offset",
|
||
)
|
||
}
|
||
)
|
||
for path, dimensions in expected.items():
|
||
assert path.is_file(), (
|
||
f"run scripts/render_diagnosis_detail_visual.py to create {path.name}"
|
||
)
|
||
image = QImage(str(path))
|
||
assert not image.isNull()
|
||
assert (image.width(), image.height()) == dimensions
|
||
assert image.pixelColor(0, 0).alpha() > 0
|
||
assert image.sizeInBytes() > dimensions[0] * dimensions[1]
|
||
|
||
|
||
def test_drawer_tab_bar_is_horizontal_and_scrollable(application: QApplication) -> None:
|
||
dialog = _open_dialog(application, (1024, 640), mode="edit")
|
||
tab_bar = dialog.tabs.tabBar()
|
||
assert tab_bar.shape().name.startswith("RoundedNorth")
|
||
assert tab_bar.usesScrollButtons()
|
||
assert not tab_bar.expanding()
|
||
assert tab_bar.elideMode() == Qt.TextElideMode.ElideNone
|
||
assert dialog.tabs.tab_scrollbar.height() == 4
|
||
assert dialog.tabs.tab_scrollbar.isVisibleTo(dialog)
|
||
assert not any(
|
||
button.isVisibleTo(dialog) and button.width() > 0
|
||
for button in tab_bar.findChildren(QToolButton)
|
||
)
|
||
dialog.close()
|
||
|
||
|
||
def test_switch_actions_archive_and_offset_use_real_repository_contracts(
|
||
application: QApplication,
|
||
) -> None:
|
||
repository = ActionRepository()
|
||
dialog = _open_dialog(
|
||
application,
|
||
(1024, 640),
|
||
mode="edit",
|
||
repository=repository,
|
||
)
|
||
|
||
show_card = dialog.edit_fields["show_card"]
|
||
assert isinstance(show_card, DiagnosisSwitch)
|
||
show_card.setChecked(True)
|
||
dialog._save()
|
||
assert repository.updates[-1]["show_card"] == 1
|
||
|
||
_select_tab(dialog, "daily")
|
||
daily = dialog._daily_panels[1]
|
||
assert all(button.isVisibleTo(dialog) for button in daily.add_buttons.values())
|
||
assert daily.todo_add_button.isVisibleTo(dialog)
|
||
dialog._submit_daily_record(
|
||
"blood",
|
||
{
|
||
"diagnosis_id": 501,
|
||
"patient_id": 1501,
|
||
"record_date": "2026-08-10",
|
||
"fasting_blood_sugar": 5.9,
|
||
},
|
||
)
|
||
assert any(name == "blood" for name, _payload in repository.action_calls)
|
||
assert daily.todo_table.cellWidget(0, 2) is not None
|
||
|
||
_select_tab(dialog, "notes")
|
||
assert dialog.drawer_notes_timeline.toolbar.isVisibleTo(dialog)
|
||
assert not any(
|
||
button.text() in {"编辑笔记", "删除笔记"}
|
||
for button in dialog.drawer_notes_timeline.findChildren(QPushButton)
|
||
)
|
||
|
||
_select_tab(dialog, "video")
|
||
assert dialog.video_upload_button.isVisibleTo(dialog)
|
||
assert dialog._table_registry["video"][1].cellWidget(0, 0) is not None
|
||
|
||
_select_tab(dialog, "chat")
|
||
assert ("chat_list", (501, True)) in repository.action_calls
|
||
chat_buttons = {button.text() for button in dialog._chat_panels[1].findChildren(QPushButton)}
|
||
assert {"查看图片", "报告.pdf"} <= chat_buttons
|
||
|
||
_select_tab(dialog, "orders")
|
||
dialog.order_offset.setValue(6)
|
||
dialog._save_order_offset()
|
||
assert ("offset", (501, 6)) in repository.action_calls
|
||
dialog.close()
|
||
|
||
|
||
def test_mutations_fail_closed_and_save_button_paints_all_states(
|
||
application: QApplication,
|
||
) -> None:
|
||
dialog = _open_dialog(
|
||
application,
|
||
(1024, 640),
|
||
mode="edit",
|
||
repository=VisualRepository(),
|
||
)
|
||
assert isinstance(dialog.save_button, SaveStateButton)
|
||
assert dialog.save_button.state == "idle"
|
||
dialog._saving = True
|
||
dialog._sync_save_button()
|
||
assert dialog.save_button.state == "loading"
|
||
generation = dialog._save_generation
|
||
dialog._save_success(generation)
|
||
assert dialog.save_button.state == "success"
|
||
dialog._save_error(RuntimeError("保存失败"), generation)
|
||
assert dialog.save_button.state == "error"
|
||
|
||
_select_tab(dialog, "daily")
|
||
daily = dialog._daily_panels[1]
|
||
assert not any(button.isVisibleTo(dialog) for button in daily.add_buttons.values())
|
||
assert not daily.todo_add_button.isVisibleTo(dialog)
|
||
_select_tab(dialog, "video")
|
||
assert not dialog.video_upload_button.isVisibleTo(dialog)
|
||
_select_tab(dialog, "chat")
|
||
assert not dialog._chat_panels[1].sync_button.isVisibleTo(dialog)
|
||
assert not dialog.summary_fields["staff"].wordWrap()
|
||
dialog.close()
|
||
|
||
|
||
def test_daily_toolbar_wraps_and_button_variants_survive_scoped_qss(
|
||
application: QApplication,
|
||
) -> None:
|
||
dialog = _open_dialog(
|
||
application,
|
||
(1024, 640),
|
||
mode="edit",
|
||
repository=ActionRepository(),
|
||
)
|
||
_select_tab(dialog, "daily")
|
||
panel = dialog._daily_panels[1]
|
||
toolbar_layout = panel.toolbar_host.layout()
|
||
assert toolbar_layout.hasHeightForWidth()
|
||
assert toolbar_layout.heightForWidth(420) > 40
|
||
assert panel.add_buttons["blood"].property("variant") == "primary"
|
||
assert all(
|
||
panel.add_buttons[kind].property("variant") == "secondary"
|
||
for kind in ("diet", "exercise", "tracking")
|
||
)
|
||
assert panel.todo_add_button.property("variant") == "primary"
|
||
assert panel.refresh_button.geometry().right() <= panel.toolbar_host.rect().right()
|
||
assert panel.refresh_button.geometry().bottom() <= panel.toolbar_host.rect().bottom()
|
||
assert "QDialog#DiagnosisDialogRoot QPushButton {" not in diagnosis_module.DIAGNOSIS_QSS
|
||
assert 'QDialog QPushButton[variant="primary"]' in diagnosis_module.DIAGNOSIS_QSS
|
||
assert 'QDialog QPushButton[variant="dangerGhost"]' in diagnosis_module.DIAGNOSIS_QSS
|
||
dialog.close()
|
||
|
||
|
||
def test_existing_daily_cells_edit_real_records_and_reject_wrong_owner(
|
||
application: QApplication,
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
) -> None:
|
||
repository = ActionRepository()
|
||
dialog = _open_dialog(
|
||
application,
|
||
(1024, 640),
|
||
mode="edit",
|
||
repository=repository,
|
||
)
|
||
_select_tab(dialog, "daily")
|
||
panel = dialog._daily_panels[1]
|
||
|
||
class AcceptedEditor:
|
||
def __init__(self, kind: str, record: Any, **_kwargs: Any) -> None:
|
||
self.kind = kind
|
||
self.record = dict(record)
|
||
|
||
def exec(self) -> QDialog.DialogCode:
|
||
return QDialog.DialogCode.Accepted
|
||
|
||
def payload(self) -> dict[str, Any]:
|
||
payload = dict(self.record)
|
||
if self.kind == "blood":
|
||
payload["fasting_blood_sugar"] = 6.1
|
||
return payload
|
||
|
||
monkeypatch.setattr(diagnosis_module, "DailyRecordEditorDialog", AcceptedEditor)
|
||
blood_column = next(
|
||
column
|
||
for column in range(1, panel.matrix.columnCount())
|
||
if panel.matrix.horizontalHeaderItem(column).text() == "08-10"
|
||
)
|
||
exercise_column = next(
|
||
column
|
||
for column in range(1, panel.matrix.columnCount())
|
||
if panel.matrix.horizontalHeaderItem(column).text() == "08-09"
|
||
)
|
||
blood_role = panel.matrix.item(0, blood_column).data(Qt.ItemDataRole.UserRole)
|
||
diet_role = panel.matrix.item(6, blood_column).data(Qt.ItemDataRole.UserRole)
|
||
exercise_role = panel.matrix.item(9, exercise_column).data(Qt.ItemDataRole.UserRole)
|
||
assert blood_role[0] == "blood" and blood_role[1]["id"] == 6201
|
||
assert diet_role[0] == "diet" and diet_role[1]["id"] == 6301
|
||
assert exercise_role[0] == "exercise" and exercise_role[1]["id"] == 6401
|
||
|
||
panel._daily_cell_clicked(0, blood_column)
|
||
panel._daily_cell_clicked(6, blood_column)
|
||
panel._daily_cell_clicked(9, exercise_column)
|
||
assert {name for name, _payload in repository.action_calls} >= {
|
||
"blood_edit",
|
||
"diet_edit",
|
||
"exercise_edit",
|
||
}
|
||
edited_blood = next(
|
||
payload for name, payload in repository.action_calls if name == "blood_edit"
|
||
)
|
||
assert edited_blood["id"] == 6201
|
||
assert edited_blood["diagnosis_id"] == 501
|
||
assert edited_blood["patient_id"] == 1501
|
||
assert edited_blood["fasting_blood_sugar"] == 6.1
|
||
|
||
count = len(repository.action_calls)
|
||
dialog._edit_daily_record(
|
||
"blood",
|
||
{
|
||
"id": 9001,
|
||
"diagnosis_id": 999,
|
||
"patient_id": 1501,
|
||
"record_date": "2026-08-10",
|
||
"fasting_blood_sugar": 5.5,
|
||
},
|
||
)
|
||
assert len(repository.action_calls) == count
|
||
assert "归属与当前诊单不一致" in dialog.drawer_banner.label.text()
|
||
dialog.close()
|
||
|
||
|
||
def test_daily_edit_is_fail_closed_without_all_update_capabilities(
|
||
application: QApplication,
|
||
) -> None:
|
||
repository = ActionRepository()
|
||
repository.update_blood_record = None # type: ignore[method-assign]
|
||
dialog = _open_dialog(
|
||
application,
|
||
(1024, 640),
|
||
mode="edit",
|
||
repository=repository,
|
||
)
|
||
_select_tab(dialog, "daily")
|
||
panel = dialog._daily_panels[1]
|
||
assert panel.add_buttons["blood"].isVisibleTo(dialog)
|
||
assert panel.matrix.item(0, 1).data(Qt.ItemDataRole.UserRole) is None
|
||
dialog._edit_daily_record(
|
||
"blood",
|
||
{
|
||
"id": 6201,
|
||
"diagnosis_id": 501,
|
||
"patient_id": 1501,
|
||
"record_date": "2026-08-10",
|
||
"fasting_blood_sugar": 5.5,
|
||
},
|
||
)
|
||
assert not any(name == "blood_edit" for name, _payload in repository.action_calls)
|
||
dialog.close()
|
||
|
||
|
||
def test_stale_daily_edit_completion_cannot_reload_a_new_generation(
|
||
application: QApplication,
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
) -> None:
|
||
repository = ActionRepository()
|
||
dialog = _open_dialog(
|
||
application,
|
||
(1024, 640),
|
||
mode="edit",
|
||
repository=repository,
|
||
)
|
||
_select_tab(dialog, "daily")
|
||
callbacks: list[dict[str, Any]] = []
|
||
|
||
def queue_async(function: Any, **options: Any) -> object:
|
||
callbacks.append({"function": function, **options})
|
||
return object()
|
||
|
||
monkeypatch.setattr(diagnosis_module, "run_async", queue_async)
|
||
before_loads = len(repository.tracking_calls)
|
||
dialog._submit_daily_record(
|
||
"blood",
|
||
{
|
||
"id": 6201,
|
||
"diagnosis_id": 501,
|
||
"patient_id": 1501,
|
||
"record_date": "2026-08-10",
|
||
"fasting_blood_sugar": 6.2,
|
||
},
|
||
)
|
||
assert len(callbacks) == 1
|
||
result = callbacks[0]["function"]()
|
||
dialog._daily_mutation_generation += 1
|
||
callbacks[0]["on_success"](result)
|
||
assert len(repository.tracking_calls) == before_loads
|
||
dialog.close()
|
||
|
||
|
||
def test_note_attachment_hierarchy_uses_real_button_selectors(
|
||
application: QApplication,
|
||
) -> None:
|
||
dialog = _open_dialog(
|
||
application,
|
||
(1024, 640),
|
||
mode="edit",
|
||
repository=ActionRepository(),
|
||
)
|
||
_select_tab(dialog, "notes")
|
||
timeline = dialog.drawer_notes_timeline
|
||
assert timeline.findChild(QPushButton, "DiagnosisTongueThumb") is not None
|
||
assert timeline.findChild(QPushButton, "DiagnosisAttachmentChip") is not None
|
||
removes = timeline.findChildren(QPushButton, "DiagnosisAttachmentRemove")
|
||
assert len(removes) == 2
|
||
assert all(button.text() == "×" and button.size().width() == 22 for button in removes)
|
||
assert "QLabel#DiagnosisAttachmentChip" not in diagnosis_module.DIAGNOSIS_QSS
|
||
assert "QPushButton#DiagnosisAttachmentChip" in diagnosis_module.DIAGNOSIS_QSS
|
||
dialog.close()
|
||
|
||
|
||
def test_video_renders_every_url_and_row_upload_binds_exact_record(
|
||
application: QApplication,
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
) -> None:
|
||
repository = ActionRepository()
|
||
dialog = _open_dialog(
|
||
application,
|
||
(1024, 640),
|
||
mode="edit",
|
||
repository=repository,
|
||
)
|
||
_select_tab(dialog, "video")
|
||
table = dialog._table_registry["video"][1]
|
||
playback_host = table.cellWidget(0, 0)
|
||
assert playback_host is not None
|
||
inline = playback_host.findChild(QWidget, "DiagnosisInlineRecordingPlayer")
|
||
assert inline is not None
|
||
assert inline.maximumHeight() == 180
|
||
alternates = playback_host.findChildren(QPushButton, "DiagnosisRecordingAlternateLink")
|
||
assert [button.text() for button in alternates] == ["HLS 1", "链接 2"]
|
||
upload_host = table.cellWidget(0, 8)
|
||
assert upload_host is not None
|
||
upload = upload_host.findChild(QPushButton, "DiagnosisVideoRowUpload")
|
||
assert isinstance(upload, QPushButton)
|
||
assert upload.property("variant") == "secondary"
|
||
monkeypatch.setattr(
|
||
diagnosis_module.QFileDialog,
|
||
"getOpenFileName",
|
||
lambda *_args, **_kwargs: ("C:/safe/replay.mp4", "视频"),
|
||
)
|
||
upload.click()
|
||
assert (
|
||
"video_upload",
|
||
("C:/safe/replay.mp4", 501, 48),
|
||
) in repository.action_calls
|
||
dialog.close()
|
||
|
||
|
||
def test_embedded_player_chat_alert_prescription_and_order_visual_contracts(
|
||
application: QApplication,
|
||
) -> None:
|
||
repository = ActionRepository()
|
||
dialog = _open_dialog(
|
||
application,
|
||
(1024, 640),
|
||
mode="edit",
|
||
repository=repository,
|
||
)
|
||
assert dialog.prescribe_button.text() == "开方"
|
||
assert dialog.prescribe_button.property("variant") == "primary"
|
||
assert dialog.order_offset_save.text() == "保存"
|
||
assert dialog.order_offset_save.property("variant") == "primary"
|
||
|
||
_select_tab(dialog, "chat")
|
||
info = dialog._chat_panels[1].findChild(QFrame, "DiagnosisChatInfo")
|
||
assert info is not None
|
||
assert "所有医生 / 医助账号" in "".join(label.text() for label in info.findChildren(QLabel))
|
||
assert dialog._chat_panels[1].sync_button.text() == "同步最新(后台异步)"
|
||
|
||
dialog._open_recording_player("https://media.example.invalid/replay.mp4")
|
||
application.processEvents()
|
||
player = dialog._recording_players[-1]
|
||
assert player.url is not None
|
||
assert player.play_button.isEnabled()
|
||
player.close()
|
||
application.processEvents()
|
||
|
||
detail = dialog._build_order_detail_dialog(repository.get_prescription_order(801), 801)
|
||
detail.show()
|
||
application.processEvents()
|
||
drawer_panel = detail.findChild(QFrame, "DiagnosisOrderDetailDrawer")
|
||
assert drawer_panel is not None
|
||
assert abs(drawer_panel.width() - round(dialog.width() * 0.8)) <= 1
|
||
status = next(
|
||
label for label in detail.findChildren(QLabel) if label.property("diagnosisTag") is True
|
||
)
|
||
assert status.text() == "待配药"
|
||
assert status.property("kind") == "warning"
|
||
detail.close()
|
||
dialog.close()
|