191 lines
8.5 KiB
Python
191 lines
8.5 KiB
Python
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)
|