Files
zyt/app/tests/test_diagnosis_drawer_visual.py
T
2026-09-07 10:07:47 +08:00

1695 lines
62 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import os
from datetime import date, timedelta
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, QTextCursor
from PySide6.QtWidgets import (
QApplication,
QDialog,
QFrame,
QLabel,
QPushButton,
QScrollArea,
QToolButton,
QWidget,
)
from doctor_workstation.core import PermissionSet
from doctor_workstation.ui.diagnosis_drawer import (
DiagnosisChoiceButtons,
DiagnosisComboBox,
DiagnosisDateEdit,
DiagnosisLineEdit,
DiagnosisNumberEdit,
DiagnosisSwitch,
ExpandableDiagnosisTextEdit,
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": "口苦"},
],
"water_intake": [
{"name": "偏少", "value": "偏少"},
{"name": "正常", "value": "正常"},
],
"weight_change": [
{"name": "下降", "value": "下降"},
{"name": "上升", "value": "上升"},
],
"fatty_liver_degree": [
{"name": "轻度", "value": "轻度"},
{"name": "中度", "value": "中度"},
],
"diet_condition": [
{"name": "纳差", "value": "纳差"},
{"name": "多食易饥", "value": "多食易饥"},
],
"body_feeling": [
{"name": "乏力", "value": "乏力"},
{"name": "肢体麻木", "value": "肢体麻木"},
],
"sleep_condition": [
{"name": "入睡困难", "value": "入睡困难"},
{"name": "多梦", "value": "多梦"},
{"name": "易醒", "value": "易醒"},
],
"eye_condition": [
{"name": "视物模糊", "value": "视物模糊"},
{"name": "眼干", "value": "眼干"},
],
"head_feeling": [
{"name": "头痛", "value": "头痛"},
{"name": "头晕", "value": "头晕"},
],
"sweat_condition": [
{"name": "自汗", "value": "自汗"},
{"name": "盗汗", "value": "盗汗"},
],
"skin_condition": [
{"name": "皮肤瘙痒", "value": "皮肤瘙痒"},
{"name": "皮肤干燥", "value": "皮肤干燥"},
],
"urine_condition": [
{"name": "尿频", "value": "尿频"},
{"name": "夜尿多", "value": "夜尿多"},
],
"stool_condition": [
{"name": "便秘", "value": "便秘"},
{"name": "便溏", "value": "便溏"},
],
"kidney_condition": [
{"name": "腰酸", "value": "腰酸"},
{"name": "腰痛", "value": "腰痛"},
],
"past_history": [
{"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))
newest_date = end_date or date.today().isoformat()
previous_date = (date.fromisoformat(newest_date) - timedelta(days=1)).isoformat()
return {
"blood_records": [
{
"id": 6201,
"diagnosis_id": 501,
"patient_id": 1501,
"record_date": newest_date,
"fasting_blood_sugar": 8.2,
"systolic_pressure": 146,
"source": 1,
},
{
"id": 6202,
"diagnosis_id": 501,
"patient_id": 1501,
"record_date": newest_date,
"postprandial_blood_sugar": 12.4,
"diastolic_pressure": 92,
"western_medicine": "二甲双胍",
},
{
"id": 6203,
"diagnosis_id": 501,
"patient_id": 1501,
"record_date": previous_date,
"fasting_blood_sugar": 7.6,
"postprandial_blood_sugar": 10.8,
},
],
"diet_records": [
{
"id": 6301,
"diagnosis_id": 501,
"patient_id": 1501,
"record_date": newest_date,
"breakfast_foods": "燕麦、鸡蛋",
"lunch_foods": "杂粮饭",
}
],
"exercise_records": [
{
"id": 6401,
"diagnosis_id": 501,
"patient_id": 1501,
"record_date": previous_date,
"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": date.today().isoformat(), "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()
def test_readonly_scroll_keeps_close_controls_reachable(application: QApplication) -> None:
dialog = _open_dialog(application, (760, 520), mode="readonly")
header_before = dialog.readonly_header.geometry()
scroll_bar = dialog.readonly_scroll.verticalScrollBar()
assert scroll_bar.maximum() > 0
assert dialog.readonly_close_button.isVisibleTo(dialog)
scroll_bar.setValue(scroll_bar.maximum())
application.processEvents()
assert dialog.readonly_header.geometry() == header_before
assert dialog.readonly_close_button.isVisibleTo(dialog)
dialog.readonly_close_button.click()
application.processEvents()
assert not dialog.isVisible()
@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"]._buttons) == 2
assert all(button.isCheckable() for button in dialog.edit_fields["gender"]._buttons)
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
# 1024×60% drawer keeps a two-column grid; only phone-narrow widths stack.
assert dialog._form_narrow is False
assert any(len(fields) >= 2 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 _settle_text_layout(application: QApplication) -> None:
# Editor measurement and the containing scroll area's layout are deferred.
for _ in range(12):
application.processEvents()
@pytest.mark.parametrize("key", ["symptoms", "current_medications"])
@pytest.mark.parametrize("size", [(1024, 640), (1440, 900)])
@pytest.mark.parametrize(
"text",
[
"这是一段用于验证自动换行的测试记录。" * 40,
"\n".join(f"第{index + 1}条:测试记录,保留原始换行。" for index in range(18)),
"LongUnbrokenTestValue" * 80,
],
ids=["wrapped-chinese", "multiline", "unbroken-text"],
)
def test_long_diagnosis_fields_expand_inline_without_inner_scrolling(
application: QApplication, key: str, size: tuple[int, int], text: str
) -> None:
repository = VisualRepository()
repository.detail["diagnosis"][key] = text
dialog = _open_dialog(application, size, mode="edit", repository=repository)
try:
_settle_text_layout(application)
editor = dialog.edit_fields[key]
assert isinstance(editor, ExpandableDiagnosisTextEdit)
assert editor.height() == 72
assert editor.expand_button.isVisibleTo(dialog)
assert not editor.expand_button.autoDefault()
assert editor.expand_button.text() == "展开全部"
body = dialog.findChild(QScrollArea, "DiagnosisDrawerBody")
footer = dialog.findChild(QFrame, "DiagnosisDrawerFooter")
footer_before = footer.geometry()
outer_range_before = body.verticalScrollBar().maximum()
horizontal_range_before = body.horizontalScrollBar().maximum()
editor.verticalScrollBar().setValue(editor.verticalScrollBar().maximum())
editor.expand_button.click()
_settle_text_layout(application)
assert editor.expand_button.text() == "收起"
assert editor.height() > 72
assert editor.sizeHint().height() == editor.height()
assert not editor._height_sync.isActive()
assert editor.verticalScrollBar().maximum() == 0
assert editor.horizontalScrollBar().maximum() == 0
assert not editor.verticalScrollBar().isVisibleTo(dialog)
last_block = editor.blockBoundingGeometry(editor.document().lastBlock())
assert last_block.translated(editor.contentOffset()).bottom() <= editor.viewport().height()
assert body.verticalScrollBar().maximum() > outer_range_before
# The existing narrow form has a small minimum-width overflow; expanding
# these fields must not increase it or clip their right edge.
assert body.horizontalScrollBar().maximum() == horizontal_range_before
assert editor.mapTo(body.viewport(), QPoint(editor.width(), 0)).x() <= body.viewport().width()
assert footer.geometry() == footer_before
assert editor.toPlainText() == text
assert not repository.updates
editor.expand_button.click()
_settle_text_layout(application)
assert editor.height() == 72
assert editor.toPlainText() == text
assert editor.expand_button.text() == "展开全部"
assert not editor._height_sync.isActive()
finally:
dialog.close()
_settle_text_layout(application)
@pytest.mark.parametrize("key", ["symptoms", "current_medications"])
def test_expanded_diagnosis_text_reflows_on_resize_and_keeps_edit_save_contract(
application: QApplication, key: str
) -> None:
repository = VisualRepository()
original = "自动换行测试内容,保持完整文本。" * 30
repository.detail["diagnosis"][key] = original
dialog = _open_dialog(application, (1440, 900), mode="edit", repository=repository)
try:
_settle_text_layout(application)
editor = dialog.edit_fields[key]
editor.expand_button.click()
_settle_text_layout(application)
wide_height = editor.height()
dialog.resize(1024, 640)
_settle_text_layout(application)
assert editor.height() > wide_height
assert editor.verticalScrollBar().maximum() == 0
dialog.resize(1440, 900)
_settle_text_layout(application)
assert editor.height() == wide_height
editor.moveCursor(QTextCursor.MoveOperation.End)
added = "\n新增测试记录,保存时不能截断。" * 12
editor.insertPlainText(added)
_settle_text_layout(application)
assert editor.height() > wide_height
assert editor.verticalScrollBar().maximum() == 0
cursor_position = editor.textCursor().position()
editor.expand_button.click()
editor.expand_button.click()
_settle_text_layout(application)
assert editor.textCursor().position() == cursor_position
editor.undo()
_settle_text_layout(application)
assert editor.toPlainText() == original
assert editor.height() == wide_height
editor.redo()
editor.expand_button.click()
_settle_text_layout(application)
dialog._save()
assert repository.updates[-1][key] == original + added
finally:
dialog.close()
_settle_text_layout(application)
@pytest.mark.parametrize("key", ["symptoms", "current_medications"])
def test_short_and_cleared_diagnosis_text_keep_compact_layout(
application: QApplication, key: str
) -> None:
dialog = _open_dialog(application, (1024, 640), mode="edit")
try:
editor = dialog.edit_fields[key]
for text in ("", "简短测试记录"):
editor.setPlainText(text)
_settle_text_layout(application)
assert editor.height() == 72
assert not editor.expand_button.isVisibleTo(dialog)
editor.insertPlainText("\n很长的测试记录" * 20)
_settle_text_layout(application)
assert editor.expand_button.isVisibleTo(dialog)
editor.expand_button.click()
_settle_text_layout(application)
editor.selectAll()
editor.insertPlainText("")
_settle_text_layout(application)
assert editor.toPlainText() == ""
assert editor.height() == 72
assert editor.expand_button.text() == "收起"
editor.expand_button.click()
_settle_text_layout(application)
assert not editor.expand_button.isVisibleTo(dialog)
assert not isinstance(dialog.edit_fields["remark"], ExpandableDiagnosisTextEdit)
assert not isinstance(dialog.edit_fields["present_illness"], ExpandableDiagnosisTextEdit)
finally:
dialog.close()
_settle_text_layout(application)
@pytest.mark.parametrize("key", ["symptoms", "current_medications"])
def test_readonly_diagnosis_text_can_expand_and_reopen_starts_collapsed(
application: QApplication, key: str
) -> None:
repository = VisualRepository()
text = "只读长文本测试\n" * 20
repository.detail["diagnosis"][key] = text
dialog = _open_dialog(application, (1024, 640), mode="viewOnly", repository=repository)
try:
_settle_text_layout(application)
editor = dialog.edit_fields[key]
assert editor.isReadOnly()
assert editor.expand_button.isEnabled()
editor.expand_button.click()
_settle_text_layout(application)
assert editor.height() > 72
assert editor.verticalScrollBar().maximum() == 0
assert editor.isReadOnly()
assert not dialog.save_button.isVisibleTo(dialog)
assert not repository.updates
dialog.close()
dialog.open_view_only(501)
_settle_text_layout(application)
assert editor.height() == 72
assert not editor.expand_button.isChecked()
assert editor.toPlainText() == text
finally:
dialog.close()
_settle_text_layout(application)
def test_hpi_choice_chips_are_visible_after_dictionary_load(
application: QApplication,
) -> None:
"""Regression: wrapped choice chips must not clip to blank white space."""
dialog = _open_dialog(application, (1024, 640), mode="edit")
diet = dialog.edit_fields["diet_condition"]
sleep = dialog.edit_fields["sleep_condition"]
assert isinstance(diet, DiagnosisChoiceButtons)
assert isinstance(sleep, DiagnosisChoiceButtons)
assert len(diet._buttons) >= 2
assert diet.height() >= 32
assert diet.minimumHeight() >= diet.heightForWidth(max(160, diet.width()))
visible_chips = [button for button in diet._buttons if button.isVisibleTo(dialog)]
assert visible_chips
assert all(button.height() >= 24 for button in visible_chips)
# Chips must sit inside the choice host, not below a clipped viewport.
host_bottom = diet.mapTo(dialog.drawer_panel, diet.rect().bottomLeft()).y()
chip_bottom = visible_chips[0].mapTo(dialog.drawer_panel, visible_chips[0].rect().bottomLeft()).y()
assert chip_bottom <= host_bottom + 2
dialog.close()
application.processEvents()
def test_choice_chips_keep_visible_checked_style_when_readonly(
application: QApplication,
) -> None:
"""Selected chips keep the blue-tinted fill when the form is view-only locked."""
dialog = _open_dialog(application, (1024, 640), mode="edit")
diet = dialog.edit_fields["diet_condition"]
assert isinstance(diet, DiagnosisChoiceButtons)
diet.setPlainText("纳差")
selected = next(button for button in diet._buttons if button.isChecked())
application.processEvents()
# Sample the pad (not glyph center) so white text does not hide the fill.
enabled_color = selected.grab().toImage().pixelColor(6, selected.height() // 2)
assert enabled_color.name().lower() == "#eef1fa"
diet.setReadOnly(True)
application.processEvents()
disabled_color = selected.grab().toImage().pixelColor(6, selected.height() // 2)
assert selected.isChecked()
assert selected.isEnabled()
assert diet.isReadOnly()
assert disabled_color.name().lower() == "#eef1fa"
dialog.close()
application.processEvents()
def test_view_only_drawer_shows_selected_choice_chips(
application: QApplication,
) -> None:
"""Admin openViewOnly keeps the 病历 form with selected chips visible."""
dialog = _open_dialog(application, (1024, 640), mode="viewOnly")
assert dialog.view_stack.currentWidget() is dialog.drawer_overlay
assert dialog._view_only is True
assert not dialog._editable
diet = dialog.edit_fields["diet_condition"]
assert isinstance(diet, DiagnosisChoiceButtons)
diet.setPlainText("纳差")
selected = next(button for button in diet._buttons if button.isChecked())
application.processEvents()
color = selected.grab().toImage().pixelColor(6, selected.height() // 2)
assert color.name().lower() == "#eef1fa"
assert diet.isReadOnly()
assert not dialog.save_button.isVisibleTo(dialog)
dialog.close()
application.processEvents()
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
newest_header = repository.tracking_calls[-1][1][5:]
blood_column = next(
column
for column in range(1, panel.matrix.columnCount())
if panel.matrix.horizontalHeaderItem(column).text() == newest_header
)
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)
newest_date = date.fromisoformat(repository.tracking_calls[-1][1])
newest_header = newest_date.strftime("%m-%d")
previous_header = (newest_date - timedelta(days=1)).strftime("%m-%d")
blood_column = next(
column
for column in range(1, panel.matrix.columnCount())
if panel.matrix.horizontalHeaderItem(column).text() == newest_header
)
exercise_column = next(
column
for column in range(1, panel.matrix.columnCount())
if panel.matrix.horizontalHeaderItem(column).text() == previous_header
)
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()