Files
zyt/app/tests/test_diagnosis_table_blue.py
2026-09-09 15:47:48 +08:00

271 lines
12 KiB
Python

from __future__ import annotations
import copy
import os
import subprocess
import sys
from pathlib import Path
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
import pytest
from PySide6.QtCore import QPoint, Qt
from PySide6.QtGui import QFont
from PySide6.QtTest import QTest
from PySide6.QtWidgets import QApplication, QLabel, QToolButton, QVBoxLayout, QWidget
from doctor_workstation.ui.diagnosis_index_widgets import (
DiagnosisTableHost,
_blue_appointment_layout,
_blue_appointment_text,
_blue_font,
)
from doctor_workstation.ui.reception_style import body_family, heading_family
@pytest.fixture(scope="module")
def application() -> QApplication:
return QApplication.instance() or QApplication([])
def _settle(application: QApplication) -> None:
for _ in range(12):
application.processEvents()
def _row(identifier: int = 501) -> dict:
return {
"id": identifier, "diagnosis_id": identifier, "patient_id": identifier + 1000,
"patient_name": "林晓岚", "gender": 2, "age": 46, "doctor_name": "陈医生(演示)",
"appointment_id": identifier + 2000, "has_appointment": 1, "appointment_status": 1,
"appointment_date": "2026-09-05", "appointment_time": "09:00-09:30",
"appointments": [
{"id": identifier + 2000, "status": 1, "appointment_date": "2026-09-05"},
{"id": identifier + 1900, "status": 3, "appointment_date": "2026-08-06"},
],
"diagnosis_confirmed": 1, "assistant_name": "周医助", "has_prescription": 0,
"unserved_days": 1,
}
def test_display_fallback_requires_exact_current_appointment_id(application: QApplication) -> None:
row = _row()
before = copy.deepcopy(row)
assert _blue_appointment_text(row, row["appointments"][0]) == (
"陈医生(演示)", "2026-09-05 09:00-09:30 · 视频问诊",
)
assert _blue_appointment_text(row, row["appointments"][1]) == (
"—", "2026-08-06 时间 — · 视频问诊",
)
nested = {"id": row["appointment_id"], "doctor_name": "原医生",
"appointment_date": "2026-09-07", "time_text": "2026-09-07 11:00-11:30"}
assert _blue_appointment_text(row, nested) == ("原医生", "2026-09-07 11:00-11:30 · 视频问诊")
assert _blue_appointment_text(row, {}) == ("—", "时间 —")
for missing_id in (0, "", None):
assert _blue_appointment_text({**row, "appointment_id": missing_id}, {}) == ("—", "时间 —")
assert row == before
legacy = DiagnosisTableHost()
blue = DiagnosisTableHost(tech_blue=True)
for host in (legacy, blue):
host.set_rows([row])
assert "陈医生" not in legacy.model.index(0, 4).data()
assert blue.model.index(0, 4).data().splitlines() == [
"陈医生(演示) · 2026-09-05 09:00-09:30 · 视频问诊", "— · 2026-08-06 时间 — · 视频问诊",
]
assert blue.model.index(0, 9).data() == "—"
blue.set_rows([{**row, "appointments": [], "appointment_id": None}])
assert blue.model.index(0, 4).data() == "— · 时间 —"
blue.set_rows([{**row, "appointments": []}])
assert blue.model.index(0, 4).data() == "陈医生(演示) · 2026-09-05 09:00-09:30 · 视频问诊"
legacy.close()
blue.close()
def test_blue_metrics_are_opt_in_and_expand_for_complete_content(application: QApplication) -> None:
blue = DiagnosisTableHost(tech_blue=True)
legacy = DiagnosisTableHost()
row = _row()
single = _row(502)
single["appointments"] = single["appointments"][:1]
long = copy.deepcopy(single)
long["id"] = long["diagnosis_id"] = 503
long["appointments"][0]["doctor_name"] = "超长完整医生姓名以及完整门诊部门需要换行" * 2
long["latest_appointment_channel_text"] = "完整保留的挂号渠道说明" * 4
long["has_prescription"] = 1
long["followup_time_text"] = "2026-10-15 09:00-09:30"
long["followup_doctor_name"] = "复诊医生姓名"
long["followup_rx_voided"] = True
blue.set_rows([row, single, long])
legacy.set_rows([row])
assert blue.model.columnCount() == 12
assert [blue.main.columnWidth(i) for i in range(10)] == [48, 70, 82, 102, 244, 90, 84, 90, 88, 122]
assert blue.main.horizontalHeader().height() == 41
assert legacy.main.horizontalHeader().height() == 39
assert blue.main.rowHeight(0) == 108
assert blue.main.rowHeight(1) == 72
assert blue.main.rowHeight(2) > 110
assert legacy.main.rowHeight(0) == 96
for index in range(3):
assert blue.main.rowHeight(index) == blue.fixed.rowHeight(index)
assert _blue_font(14).family() == body_family()
assert _blue_font(14, medium=True).family() == heading_family()
assert _blue_font(14, medium=True).weight() == QFont.Weight.Medium
assert _blue_font(13).pixelSize() == 13
blue.close()
legacy.close()
@pytest.mark.parametrize("tech_blue", [False, True])
def test_geometry_has_no_minimum_height_feedback_and_tracks_both_scroll_axes(
application: QApplication, tech_blue: bool,
) -> None:
window = QWidget()
layout = QVBoxLayout(window)
layout.setContentsMargins(7, 7, 7, 7)
host = DiagnosisTableHost(tech_blue=tech_blue)
layout.addWidget(host, 1)
footer = QLabel("始终可达的分页")
footer.setFixedHeight(42)
layout.addWidget(footer)
host.set_rows([_row(i) for i in range(40)])
window.resize(1280, 640)
window.show()
_settle(application)
minimum = window.minimumSizeHint().height()
for width, height in ((780, 480), (1440, 780), (800, 540), (1280, 640)) * 3:
window.resize(width, height)
_settle(application)
# The previous implementation wrote the available height into the
# fixed child's minimum and enlarged every ancestor on each relayout.
assert window.size().height() == height
assert window.minimumSizeHint().height() == minimum
assert host.fixed.minimumHeight() == 0
assert host.main.viewport().height() == host.fixed.viewport().height()
assert host.main.viewport().mapTo(host, QPoint()).y() == host.fixed.viewport().mapTo(host, QPoint()).y()
main_scroll, fixed_scroll = host.main.verticalScrollBar(), host.fixed.verticalScrollBar()
assert main_scroll.maximum() == fixed_scroll.maximum()
main_scroll.setValue(main_scroll.maximum() // 2)
_settle(application)
assert main_scroll.value() == fixed_scroll.value()
fixed_scroll.setValue(fixed_scroll.maximum())
_settle(application)
assert main_scroll.value() == fixed_scroll.value()
frozen_x = host.fixed.x()
host.main.horizontalScrollBar().setValue(host.main.horizontalScrollBar().maximum())
_settle(application)
assert host.fixed.x() == frozen_x
assert host.fixed.geometry().right() <= host.rect().right()
assert footer.mapTo(window, QPoint()).y() + footer.height() <= window.height()
window.close()
_settle(application)
def test_checkbox_toggle_and_select_all_do_not_change_current_patient(application: QApplication) -> None:
host = DiagnosisTableHost(tech_blue=True)
host.resize(1300, 420)
host.set_rows([_row(501), _row(502)])
host.show()
_settle(application)
host.main.selectRow(1)
box = host.main.visualRect(host.model.index(1, 0)).center()
QTest.mouseClick(host.main.viewport(), Qt.MouseButton.LeftButton, pos=box)
assert [row["id"] for row in host.selected_records()] == [502]
QTest.mouseClick(host.main.viewport(), Qt.MouseButton.LeftButton, pos=box)
assert host.selected_records() == []
assert host.main.currentRow() == 1
header = host.main.horizontalHeader()
QTest.mouseClick(header.viewport(), Qt.MouseButton.LeftButton, pos=QPoint(20, 20))
assert {row["id"] for row in host.selected_records()} == {501, 502}
assert host.main.currentRow() == 1
host.main.selectRow(0)
assert {row["id"] for row in host.selected_records()} == {501, 502}
QTest.mouseClick(header.viewport(), Qt.MouseButton.LeftButton, pos=QPoint(20, 20))
assert host.selected_records() == []
host.close()
def test_exact_appointment_cancel_hit_targets_and_video_gates(application: QApplication) -> None:
host = DiagnosisTableHost(tech_blue=True, action_policy={
"view": True, "edit": True, "appointment_cancel": True, "video_call": True,
})
row = _row()
# Make the second entry cancellable and the first doctor wrap: hit regions
# must follow measured content, not a legacy fixed 42-pixel multiplier.
row["appointments"][0]["doctor_name"] = "医生与门诊信息完整保留" * 3
row["appointments"][1]["status"] = 4
host.set_rows([row])
host.resize(1300, 500)
host.show()
_settle(application)
received = []
host.appointment_cancel_requested.connect(lambda record, identifier: received.append((record["id"], identifier)))
overlay = host.main.indexWidget(host.model.index(0, 4))
buttons = overlay.findChildren(QToolButton)
assert len(buttons) == 2
entries, total = _blue_appointment_layout(row, overlay.width())
for position, button in enumerate(buttons):
assert button.y() == max(9, (overlay.height() - total) // 2) + entries[position][0]
assert button.geometry().bottom() < overlay.height()
button.click()
assert received == [(501, 2501), (501, 2401)]
more = host.fixed.indexWidget(host.model.index(0, 11)).findChild(QToolButton, "DiagnosisRowMore")
assert "取消挂号" not in [action.text() for action in more.menu().actions()]
assert not host.fixed.indexWidget(host.model.index(0, 10)).findChildren(QToolButton)
row["video_call_hint"] = {"state": "live"}
host.set_rows([row])
video = host.fixed.indexWidget(host.model.index(0, 10)).findChild(QToolButton)
assert video is not None and video.isEnabled()
row["patient_id"] = 0
host.set_rows([row])
video = host.fixed.indexWidget(host.model.index(0, 10)).findChild(QToolButton)
assert video is not None and not video.isEnabled()
host.action_policy["appointment_cancel"] = False
host.set_rows([row])
assert host.main.indexWidget(host.model.index(0, 4)) is None
host.close()
def test_real_shell_navigation_has_no_fixed_height_stack_overflow(tmp_path: Path) -> None:
script = tmp_path / "shell_navigation.py"
script.write_text('''
import socket
from unittest.mock import patch
from PySide6.QtCore import Qt, QThreadPool
from PySide6.QtWidgets import QApplication
from doctor_workstation.services import DemoDoctorRepository
from doctor_workstation.ui import ShellWindow, apply_theme
app = QApplication([])
apply_theme(app)
def settle():
for _ in range(12):
QThreadPool.globalInstance().waitForDone(1000)
app.processEvents()
with patch.object(socket.socket, "connect", side_effect=RuntimeError("offline test")):
repo = DemoDoctorRepository()
session = repo.login(repo.DEMO_ACCOUNT, repo.DEMO_PASSWORD)
shell = ShellWindow(repo, {"session": session, "demo_mode": True}, permissions=session.permissions)
shell.setAttribute(Qt.WidgetAttribute.WA_DontShowOnScreen, True)
shell.resize(1536, 960)
shell.show()
settle()
for route in ("consultations", "appointments", "consultations"):
shell.navigate(route)
settle()
host = shell.pages["consultations"].table_host
for width, height in ((1024, 640), (1536, 960), (1366, 768)):
shell.resize(width, height)
settle()
assert host.main.viewport().height() == host.fixed.viewport().height()
assert host.fixed.minimumHeight() == 0
assert shell.height() == height
shell.close()
settle()
print("native navigation geometry stable")
''', encoding="utf-8")
env = {**os.environ, "QT_QPA_PLATFORM": "offscreen", "DOCTOR_SMOKE_TEST": "1",
"PYTHONPATH": str(Path(__file__).resolve().parents[1] / "src")}
result = subprocess.run([sys.executable, str(script)], capture_output=True, text=True,
encoding="utf-8", errors="replace", env=env, timeout=90)
assert result.returncode == 0, result.stdout + result.stderr
assert "native navigation geometry stable" in result.stdout