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()