Files
zyt/app/tests/test_reception_workspace_redesign.py
T
2026-10-22 11:02:57 +08:00

325 lines
15 KiB
Python

"""Reachability and data preservation in the real reception shell."""
from __future__ import annotations
import os
import socket
from copy import deepcopy
from typing import Any
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
import pytest
from PySide6.QtCore import QEvent, QPoint, QRect, Qt, QTimer
from PySide6.QtTest import QTest
from PySide6.QtWidgets import QApplication, QFrame, QLabel, QPushButton
from doctor_workstation.ui import shell as shell_module
from doctor_workstation.ui.pages import reception as reception_module
from doctor_workstation.ui.pages.reception import QueueRow, ReceptionPage
from doctor_workstation.ui.shell import NavigationItem, ShellWindow
from doctor_workstation.ui.theme import apply_theme
def _settle(application: QApplication) -> None:
for _ in range(12):
application.processEvents()
QTest.qWait(5)
def _mapped_rect(widget, ancestor) -> QRect:
return QRect(widget.mapTo(ancestor, QPoint()), widget.size())
class _LocalRepository:
def __init__(self) -> None:
self.calls: list[str] = []
self.detail = {
"appointment": {
"id": 51, "patient_id": 151, "diagnosis_id": 251,
"patient_name": "本地测试患者", "status": 1,
"doctor_name": "测试医生", "assistant_name": "测试医助",
"appointment_time": "09:30", "appointment_type": "video",
},
"patient": {
"id": 151, "name": "本地测试患者", "gender": 2,
"age": 46, "height": 165, "weight": 60, "phone": "13800138000",
},
"diagnosis": {
"id": 251, "patient_id": 151, "chief_complaint": "反复口渴",
"present_illness": "近期口干,复诊时核对记录。",
"current_medications": "二甲双胍早晚服用,剂量待核对。",
"disease_course_text": "患病十年",
"clinical_diagnosis": "2 型糖尿病", "hba1c": 7.2,
},
}
def list_appointments(self, **_query: Any) -> dict[str, Any]:
self.calls.append("queue")
return {"lists": [deepcopy(self.detail["appointment"])], "count": 1}
def get_reception(self, appointment_id: int) -> dict[str, Any]:
assert appointment_id == 51
self.calls.append("detail")
return deepcopy(self.detail)
def get_diagnosis_ai_analysis(self, diagnosis_id: int, model: str) -> dict[str, Any]:
assert diagnosis_id == 251
self.calls.append(model)
return {
"diagnosis_advice": "结合当前病史复核血糖控制情况。",
"risk_assessment": [{"label": "血糖波动", "level": "medium"}],
"treatment_advice": "核对现有用药及近期检查。", "model_key": model,
}
@pytest.fixture(scope="module")
def application() -> QApplication:
app = QApplication.instance() or QApplication([])
apply_theme(app)
return app
@pytest.fixture
def shell_factory(application: QApplication, monkeypatch: pytest.MonkeyPatch):
def reject_network(*_args, **_kwargs):
pytest.fail("Reception workspace tests must remain local")
def immediate(function, *args, on_success=None, on_error=None, on_finished=None,
pool=None, priority=0, **kwargs):
del pool, priority
try:
result = function(*args, **kwargs)
except Exception as error:
if on_error:
on_error(error)
raise
else:
if on_success:
on_success(result)
finally:
if on_finished:
on_finished()
return object()
monkeypatch.setattr(socket.socket, "connect", reject_network)
monkeypatch.setattr(socket.socket, "connect_ex", reject_network)
monkeypatch.setattr(socket, "create_connection", reject_network)
monkeypatch.setattr(reception_module, "run_async", immediate)
monkeypatch.setattr(shell_module.motion, "reduced_motion", lambda: True)
item = NavigationItem("reception", "接诊台", "", ReceptionPage,
("doctor.appointment/lists",))
monkeypatch.setattr(shell_module, "_resolve_navigation",
lambda *_args, **_kwargs: [(item, item.title)])
opened = []
def create(width=1920, height=1080):
repository = _LocalRepository()
shell = ShellWindow(repository, {"user": {"id": 7, "name": "测试医生"}},
permissions={
"doctor.appointment/lists", "doctor.appointment/complete",
"doctor.appointment/addDoctorNote", "tcm.diagnosis/edit",
"tcm.diagnosis/aiAnalysis", "tcm.diagnosis/aiAssistant",
})
opened.append(shell)
shell.resize(width, height)
shell.show()
_settle(application)
page = shell.pages["reception"]
page.poll_timer.stop()
shell.chat_notifications.stop()
assert page._selected_appointment_id == 51
assert page.detail_stack.currentIndex() == 1
return shell, page, repository
yield create
for shell in opened:
for timer in shell.findChildren(QTimer):
timer.stop()
shell.close()
shell.deleteLater()
application.sendPostedEvents(None, QEvent.Type.DeferredDelete)
application.processEvents()
@pytest.mark.parametrize("size", [(1366, 768), (1920, 1080)])
def test_real_shell_prioritizes_patient_facts_while_long_history_scrolls(
application, shell_factory, size,
):
shell, page, repository = shell_factory(*size)
assert (shell.width(), shell.height()) == size
assert not page.workbench.mode_tabs.isVisible()
hero = page.findChild(QFrame, "ReceptionHero")
hero_before = _mapped_rect(hero, page)
actions = (page.notify_button, page.history_button, page.video_button,
page.more_button, page.complete_button)
actions_before = [_mapped_rect(button, page) for button in actions]
for button in actions:
assert button.isVisibleTo(page)
assert hero.rect().contains(_mapped_rect(button, hero))
assert button.width() >= button.minimumSizeHint().width()
insights_viewport = page.insights_scroll.viewport()
summary = page.patient_summary_card
vitals = page.findChild(QFrame, "ReceptionVitalsCard")
labs = page.findChild(QFrame, "ReceptionLabsCard")
assert insights_viewport.rect().contains(_mapped_rect(summary, insights_viewport))
assert insights_viewport.rect().contains(_mapped_rect(vitals, insights_viewport))
assert _mapped_rect(summary, insights_viewport).top() == 0
assert summary.mapTo(page, QPoint()).y() < vitals.mapTo(page, QPoint()).y()
assert vitals.mapTo(page, QPoint()).y() < labs.mapTo(page, QPoint()).y()
assert labs.mapTo(page, QPoint()).y() < page.ai_analysis_card.mapTo(page, QPoint()).y()
assert {key: label.text() for key, label in page.patient_summary_labels.items()} == {
"gender_age": "女 · 46岁", "phone": "138****8000",
"course": "患病十年", "diagnosis": "2 型糖尿病",
}
assistant_title = page.ai_assistant_card.findChild(QPushButton, "ReceptionAiTitle")
insight_scroll_value = page.insights_scroll.verticalScrollBar().value()
fact_positions = [_mapped_rect(card, page) for card in (summary, vitals, labs)]
requests = list(repository.calls)
history = "完整病史:口干持续,携带记录复诊,核对既往用药。\n" * 40
page.case_labels["present"].setText(history)
_settle(application)
assert page.case_labels["present"].text() == history
assert page.case_labels["present"].height() >= page.case_labels["present"].heightForWidth(
page.case_labels["present"].width()
)
scrollbar = page.clinical_scroll.verticalScrollBar()
assert scrollbar.maximum() > 0
scrollbar.setValue(scrollbar.maximum())
_settle(application)
assert scrollbar.value() == scrollbar.maximum()
assert _mapped_rect(hero, page) == hero_before
assert [_mapped_rect(button, page) for button in actions] == actions_before
assert [_mapped_rect(card, page) for card in (summary, vitals, labs)] == fact_positions
assert page.insights_scroll.verticalScrollBar().value() == insight_scroll_value
for title in (page.ai_analysis_title, assistant_title):
page.insights_scroll.ensureWidgetVisible(title)
_settle(application)
assert insights_viewport.rect().contains(_mapped_rect(title, insights_viewport))
assert repository.calls == requests
def test_narrow_workspace_reuses_scrolls_and_preserves_drafts_tabs_and_patient(
application, shell_factory,
):
shell, page, repository = shell_factory()
clinical, insights = page.clinical_scroll, page.insights_scroll
page.detail_tabs.setCurrentIndex(4)
page.note_edit.setPlainText("未保存的本地备注")
page._pending_tongue_images = ["draft-tongue.png"]
page._pending_report_files = ["draft-report.pdf"]
requests = list(repository.calls)
for width in (1024, 1920, 1024, 1366):
shell.resize(width, 768)
_settle(application)
assert page.clinical_scroll is clinical
assert page.insights_scroll is insights
assert page.detail_tabs.currentIndex() == page.clinical_pages.currentIndex() == 4
assert page._selected_appointment_id == 51
assert page.note_edit.toPlainText() == "未保存的本地备注"
assert page._pending_tongue_images == ["draft-tongue.png"]
assert page._pending_report_files == ["draft-report.pdf"]
if width == 1024:
assert page.workbench.mode_tabs.isVisible()
for index, scroll in ((1, insights), (0, clinical)):
page.workbench.mode_tabs.setCurrentIndex(index)
_settle(application)
assert scroll.isVisibleTo(page)
assert page.workbench._narrow_stack.currentWidget() is scroll
else:
assert not page.workbench.mode_tabs.isVisible()
assert clinical.isVisibleTo(page) and insights.isVisibleTo(page)
assert repository.calls == requests
def test_missing_labs_expand_and_collapse_without_losing_recorded_values(
application, shell_factory,
):
_shell, page, _repository = shell_factory()
before = {key: label.text() for key, label in page.lab_labels.items()}
assert before["hba1c"] == "7.2 %"
assert not page.lab_cells["hba1c"].isHidden()
assert all(cell.isHidden() for key, cell in page.lab_cells.items() if key != "hba1c")
assert page.lab_missing_button.text() == "显示 7 项未记录"
page.insights_scroll.ensureWidgetVisible(page.lab_missing_button)
_settle(application)
QTest.mouseClick(page.lab_missing_button, Qt.MouseButton.LeftButton)
_settle(application)
assert all(not cell.isHidden() for cell in page.lab_cells.values())
assert page.lab_missing_button.text() == "收起未记录项"
page.lab_missing_button.click()
_settle(application)
assert {key: label.text() for key, label in page.lab_labels.items()} == before
assert all(cell.isHidden() for key, cell in page.lab_cells.items() if key != "hba1c")
def test_free_text_glucose_keeps_source_without_invented_units_or_status(
application, shell_factory,
):
_shell, page, repository = shell_factory()
description = "近期空腹7至9之间,餐后约12,尚未携带原始记录"
diagnosis = {**repository.detail["diagnosis"], "fasting_blood_sugar": description}
page._render_case(repository.detail["appointment"], repository.detail["patient"], diagnosis)
_settle(application)
assert page.lab_labels["fasting"].text() == description
assert page.lab_labels["fasting"].textFormat() == Qt.TextFormat.PlainText
assert page.lab_cells["fasting"].property("description") is True
assert not page.lab_cells["fasting"].isHidden()
assert page.lab_state_labels["fasting"].isHidden()
assert page.case_metric_labels["fasting_glucose"].text() == description
assert f"空腹血糖:{description}\n" in page.diagnosis_text.text()
assert f"{description} mmol/L" not in page.health_summary_label.text()
row = QueueRow({**repository.detail["appointment"], "diagnosis": diagnosis})
try:
metric = row.findChild(QLabel, "ReceptionQueueMetric")
assert metric.text() == "血糖有描述"
assert metric.toolTip() == description
assert metric.property("tone") == "description"
assert row.glucose_drop.isHidden()
finally:
row.deleteLater()
def test_compact_ai_prompts_keep_full_questions_and_callbacks(shell_factory, monkeypatch):
_shell, page, _repository = shell_factory()
sent = []
monkeypatch.setattr(page, "_open_ai_report_for_prompt", sent.append)
questions = [
"基于当前病史,下一步检查建议?", "该患者的用药调整建议?",
"糖尿病教育要点有哪些?", "并发症筛查建议?",
]
labels = ["检查建议", "用药核对", "健康教育", "并发症筛查"]
assert len(page.ai_prompt_buttons) == len(questions)
for button, label, question in zip(page.ai_prompt_buttons, labels, questions, strict=True):
assert button.text() == label
assert button.toolTip() == button.accessibleName() == question
assert button.isEnabled()
button.click()
assert sent == questions
def test_patient_facts_stay_first_while_ai_is_loading_or_unavailable(application, shell_factory):
_shell, page, repository = shell_factory()
before = {key: label.text() for key, label in page.patient_summary_labels.items()}
requests = list(repository.calls)
for state in ("loading", "error", "permission"):
page._set_ai_analysis_state(state)
_settle(application)
assert {key: label.text() for key, label in page.patient_summary_labels.items()} == before
assert page.patient_summary_card.mapTo(page.insights_scroll.widget(), QPoint()).y() == 0
assert repository.calls == requests
def test_patient_change_returns_to_facts_and_clears_previous_summary(application, shell_factory, monkeypatch):
_shell, page, _repository = shell_factory()
page.insights_scroll.verticalScrollBar().setValue(page.insights_scroll.verticalScrollBar().maximum())
monkeypatch.setattr(page, "_load_detail", lambda record, **_kwargs: page._reset_detail_content(record))
page._select_record({"id": 52, "patient_name": "另一位患者"})
_settle(application)
assert page.insights_scroll.verticalScrollBar().value() == 0
assert all(label.text() == "正在加载…" for label in page.patient_summary_labels.values())
page._render_detail_load_failure("详情暂不可用")
assert all(label.text() == "未记录" for label in page.patient_summary_labels.values())
assert all(label.property("missing") is True for label in page.patient_summary_labels.values())