from __future__ import annotations import pytest from PySide6.QtCore import Qt from PySide6.QtGui import QPalette from PySide6.QtWidgets import QApplication, QFrame, QLabel, QPushButton from doctor_workstation.core import PermissionSet from doctor_workstation.ui.pages import reception as reception_module from doctor_workstation.ui.pages.reception import ReceptionPage from doctor_workstation.ui.theme import apply_theme @pytest.fixture(scope="module") def application(): app = QApplication.instance() or QApplication([]) apply_theme(app) return app def settle(app): for _ in range(8): app.processEvents() @pytest.fixture def page(application, monkeypatch): monkeypatch.setattr(reception_module, "run_async", lambda *_args, **_kwargs: None) widget = ReceptionPage(object(), PermissionSet(["*"])) widget.resize(1340, 900) widget.show() widget.poll_timer.stop() widget.detail_stack.setCurrentIndex(1) widget.patient_name_label.setText("林晓岚") widget.patient_meta_label.setText("女 · 46岁 · 138****1203 | 就诊号:101") settle(application) yield widget widget.close() widget.deleteLater() settle(application) def test_primary_action_and_reading_ink_use_separate_colors(page, application): page.video_button.setEnabled(True) settle(application) assert page.video_button.palette().color(QPalette.ColorRole.Button).name() == "#1769e8" assert page.video_button.palette().color(QPalette.ColorRole.ButtonText).name() == "#ffffff" assert page.case_labels["present"].palette().color(QPalette.ColorRole.WindowText).name() == "#273244" page.video_button.setEnabled(False) settle(application) assert page.video_button.palette().color(QPalette.ColorRole.ButtonText).name() == "#8e8f90" def test_typography_preserves_regular_body_and_distinct_values(page): body = page.case_labels["present"] assert body.font().pixelSize() == 14 assert body.font().weight() == 400 assert page.patient_name_label.font().pixelSize() == 22 assert page.patient_name_label.font().weight() == 600 assert page.vital_labels["height"].font().pixelSize() == 18 @pytest.mark.parametrize("width", [1340, 1080, 816]) def test_identity_and_actions_stay_inside_hero_at_both_layouts(page, application, width): page.resize(width, 900) settle(application) hero = page.findChild(QFrame, "ReceptionHero") for button in (page.notify_button, page.history_button, page.video_button, page.more_button): assert hero.rect().contains(button.geometry()) assert button.width() >= button.minimumSizeHint().width() if width == 1340: assert abs(page.video_button.geometry().center().y() - page.patient_avatar_label.geometry().center().y()) < 20 else: assert page.video_button.geometry().top() >= page.patient_meta_label.geometry().bottom() def test_body_growth_stays_readable_and_does_not_cover_actions(page, application): body = page.case_labels["present"] body.setText("患者自述近期口干,睡眠较浅,日常饮食与作息较规律。\n" * 12) settle(application) assert body.height() >= body.heightForWidth(body.width()) assert page.detail_scroll.verticalScrollBar().maximum() > 0 assert body.textFormat() == Qt.TextFormat.PlainText @pytest.mark.parametrize("value", ["6.8 mmol/L", "1234.56 mg/g", "≤0.25 mg/L", "—"]) def test_lab_value_keeps_full_plain_text_and_wraps_long_units(page, application, value): label = page.lab_labels["fasting"] label.setText(value) settle(application) assert label.text() == value assert label.textFormat() == Qt.TextFormat.PlainText for width in (55, 120): layout, size = label._value_layout(width) assert sum(layout.lineAt(i).textLength() for i in range(layout.lineCount())) == len(value) assert size.height() == label.heightForWidth(width) for i in range(layout.lineCount()): line = layout.lineAt(i) assert line.naturalTextWidth() <= width + 1 assert line.y() + line.height() <= size.height() def test_queue_summary_and_navigation_tabs_remain_available(page): summary = page.findChild(QLabel, "ReceptionQueueSummary") assert summary.isVisible() assert [page.detail_tabs.tabText(i) for i in range(page.detail_tabs.count())] == [ "问诊信息", "检查报告", "用药记录", "日常记录", "随访记录", "健康数据" ] def test_risks_remain_in_full_report_while_preview_matches_approved_image(page, application): page.ai_analysis_stack.setCurrentWidget(page.ai_analysis_content_page) settle(application) page._render_ai_risk_chips([ {"label": "血糖波动风险", "level": "medium"}, {"label": "睡眠质量下降", "level": "low"}, ]) settle(application) assert not page.ai_risk_chip_host.isVisible() dialog = reception_module._ReceptionAiAnalysisDialog({"qwen": { "diagnosis_advice": "需要结合病史复核。", "risk_assessment": [{"label": "血糖波动风险", "level": "medium"}], "treatment_advice": "完整治疗建议仍可阅读。", }}) dialog.show() settle(application) try: risk = next(label for label in dialog.findChildren(QLabel) if label.text() == "血糖波动风险") assert risk.isVisible() assert risk.width() >= risk.fontMetrics().horizontalAdvance(risk.text()) assert any(label.text() == "完整治疗建议仍可阅读。" for label in dialog.findChildren(QLabel)) finally: dialog.close() def test_compact_ai_keeps_full_treatment_data_without_pushing_controls_out_of_view(page, application): page.ai_analysis_stack.setCurrentWidget(page.ai_analysis_content_page) page.ai_summary_label.setText("需要结合完整病史与复诊记录核对。" * 25) page.ai_treatment_label.setText("请结合患者实际情况复核完整分析。" * 25) page._render_ai_risk_chips([{"label": "血糖波动风险", "level": "medium"}]) settle(application) assert page.ai_treatment_label.text() == "请结合患者实际情况复核完整分析。" * 25 assert not page.ai_treatment_label.isVisible() assert not page.ai_question_edit.isVisible() assert not page.ai_analysis_history_button.isVisible() actions = [action.text() for action in page.more_button.menu().actions()] assert "向 AI 提问" in actions assert "历史 AI 报告" in actions assert "重新分析 AI 报告" in actions assert page.ai_analysis_card.height() < 320 def test_patient_switch_clears_previous_condition_tooltip(page): page.patient_meta_label.setToolTip("上一位患者的病情提示") page._reset_detail_content({"id": 202, "patient_name": "新患者"}) assert page.patient_meta_label.toolTip() == "" page.patient_meta_label.setToolTip("已失效的病情提示") page._render_detail_load_failure("详情暂不可用") assert page.patient_meta_label.toolTip() == "" def test_moved_ai_controls_keep_actions_and_enabled_state(application, monkeypatch): calls = [] monkeypatch.setattr(reception_module, "run_async", lambda *_args, **_kwargs: None) monkeypatch.setattr(ReceptionPage, "_open_ai_analysis_dialog", lambda self: calls.append("history")) monkeypatch.setattr(ReceptionPage, "_regenerate_patient_ai_reports", lambda self: calls.append("regenerate")) monkeypatch.setattr(ReceptionPage, "_open_ai_assistant", lambda self, prompt: calls.append(("ask", prompt))) widget = ReceptionPage(object(), PermissionSet(["*"])) try: widget.poll_timer.stop() assert not widget.ai_history_action.isEnabled() assert not widget.ai_regenerate_action.isEnabled() widget._ai_analysis_histories["qwen"] = [{"diagnosis_advice": "示例分析"}] widget._ai_analysis_patient_flow = True widget._ai_analysis_patient_id = 101 widget._sync_ai_analysis_view() assert widget.ai_history_action.isEnabled() assert widget.ai_regenerate_action.isEnabled() widget.ai_history_action.trigger() widget.ai_regenerate_action.trigger() next(action for action in widget.more_button.menu().actions() if action.text() == "向 AI 提问").trigger() widget.ai_assistant_card.findChild(QPushButton, "ReceptionAiTitle").click() assert calls == ["history", "regenerate", ("ask", ""), ("ask", "")] widget._ai_analysis_regenerating = True widget._sync_ai_analysis_view() assert not widget.ai_regenerate_action.isEnabled() finally: widget.close() widget.deleteLater() settle(application)