Files
zyt/app/tests/test_appointment_infinite.py
2026-09-07 12:30:42 +08:00

243 lines
8.9 KiB
Python

"""Scrolling, query isolation and refresh contracts for the two clinic queues."""
from __future__ import annotations
import os
from copy import deepcopy
from datetime import date
from typing import Any
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
import pytest
from PySide6.QtCore import Qt, Signal
from PySide6.QtTest import QTest
from PySide6.QtWidgets import QApplication, QComboBox, QSpinBox, QWidget
from doctor_workstation.ui.infinite_list import InfiniteList
from doctor_workstation.ui.pages import appointments, consultations
class _DiagnosisDialog(QWidget):
saved = Signal()
def __init__(self, _repository: Any, parent: QWidget | None = None) -> None:
super().__init__(parent)
class _Repository:
def __init__(self) -> None:
self.calls: list[dict[str, Any]] = []
self.fail_page: int | None = None
self.revision = 0
def _list(self, **query: Any) -> dict[str, Any]:
self.calls.append(dict(query))
page = query["page_no"]
if page == self.fail_page:
raise RuntimeError("暂时无法加载")
second = bool(query.get("keyword") or query.get("patient_name"))
offset = 1000 if second else 0
total = 4 if second else 34
start = (page - 1) * query["page_size"]
rows = [
{
"id": offset + index,
"diagnosis_id": offset + index,
"source_patient_id": index + 2000,
"patient_id": index + 2000,
"patient_name": f"患者{offset + index} · {self.revision}",
"status": 1,
"status_desc": "待接诊",
"appointment_id": offset + index,
"appointment_status": 1,
"appointment_date": date.today().isoformat(),
"appointment_time": "09:00-09:30",
"patient_phone": "13800001234",
"doctor_name": "测试医生",
"doctor_id": 30,
"diagnosis_confirmed": True,
"appointments": [],
}
for index in range(start + 1, min(total, start + query["page_size"]) + 1)
]
result = {"lists": rows, "count": total}
if page == 1:
result["extend"] = {
"status_count": {"1": total, "3": 7},
"date_counts": {"today": total, "tomorrow": 9},
}
return result
list_appointments = _list
list_consultations = _list
def _inline(function: Any, *, on_success=None, on_error=None, on_finished=None) -> None:
try:
result = function()
except Exception as error:
if on_error is not None:
on_error(error)
else:
if on_success is not None:
on_success(result)
finally:
if on_finished is not None:
on_finished()
def _settle(application: QApplication) -> None:
for _ in range(3):
application.processEvents()
QTest.qWait(35)
@pytest.fixture(scope="module")
def application() -> QApplication:
return QApplication.instance() or QApplication([])
@pytest.fixture(params=["appointments", "consultations"])
def queue_page(request: Any, application: QApplication, monkeypatch: pytest.MonkeyPatch):
module = appointments if request.param == "appointments" else consultations
monkeypatch.setattr(module, "run_async", _inline)
monkeypatch.setattr(consultations, "DiagnosisDialog", _DiagnosisDialog)
monkeypatch.setattr(consultations.ConsultationsPage, "_load_filter_options", lambda self: None)
monkeypatch.setattr(consultations.ConsultationsPage, "_refresh_counts", lambda self: None)
monkeypatch.setattr(appointments.AppointmentsPage, "_load_departments", lambda self: None)
repository = _Repository()
page_class = appointments.AppointmentsPage if module is appointments else consultations.ConsultationsPage
page = page_class(repository, permissions={"*"}, current_user={"role_id": 1})
page.resize(1280, 800)
page.show()
page.poll_timer.stop()
_settle(application)
yield page, repository, module
page.close()
page.deleteLater()
_settle(application)
def _scroll_bottom(page: Any, application: QApplication) -> None:
scrollbar = page.table.verticalScrollBar()
assert scrollbar.maximum() > 0
scrollbar.setValue(scrollbar.maximum())
_settle(application)
def test_scroll_appends_and_preserves_selection(queue_page: Any, application: QApplication) -> None:
page, repository, _module = queue_page
assert page.table.rowCount() == 15
page.table.selectRow(6)
if hasattr(page, "table_host"):
model = page.table_host.model
model.setData(model.index(6, 0), Qt.CheckState.Checked, Qt.ItemDataRole.CheckStateRole)
_scroll_bottom(page, application)
assert page.table.rowCount() == 30
assert [call["page_no"] for call in repository.calls] == [1, 2]
assert page.table.current_data()["id"] == 7
assert page.table.verticalScrollBar().value() > 0
if hasattr(page, "table_host"):
assert [row["id"] for row in page.table_host.selected_records()] == [7]
else:
assert page._status_counts[3] == 7
assert page.date_buttons["tomorrow"].text().endswith(" 9")
_scroll_bottom(page, application)
assert page.table.rowCount() == 34
assert len({row["id"] for row in page.pager.rows}) == 34
assert not page.pager.has_more
assert "已全部加载" in page.pager.summary_label.text()
_scroll_bottom(page, application)
assert [call["page_no"] for call in repository.calls] == [1, 2, 3]
def test_silent_refresh_keeps_the_loaded_prefix(queue_page: Any, application: QApplication) -> None:
page, repository, _module = queue_page
_scroll_bottom(page, application)
page.table.selectRow(19)
old_scroll = page.table.verticalScrollBar().value()
repository.calls.clear()
repository.revision = 2
page.refresh(silent=True)
_settle(application)
assert [call["page_no"] for call in repository.calls] == [1, 2]
assert page.table.rowCount() == 30
assert page.table.current_data()["id"] == 20
assert page.table.current_data()["patient_name"].endswith(" · 2")
assert page.table.verticalScrollBar().value() == old_scroll
if isinstance(page, appointments.AppointmentsPage):
assert page._status_counts[3] == 7
assert page.date_buttons["tomorrow"].text().endswith(" 9")
def test_filter_change_supersedes_pending_append(
queue_page: Any, application: QApplication, monkeypatch: pytest.MonkeyPatch
) -> None:
page, _repository, module = queue_page
jobs: list[tuple[Any, dict[str, Any]]] = []
def deferred(function: Any, **callbacks: Any) -> None:
jobs.append((function, callbacks))
monkeypatch.setattr(module, "run_async", deferred)
# Reconfigure the shared controller to use the deferred runner, then finish
# that refresh before simulating a slow next page.
page.refresh(silent=True)
function, callbacks = jobs.pop()
callbacks["on_success"](function())
page.pager.load_more()
append_function, append_callbacks = jobs.pop()
stale_result = deepcopy(append_function())
search = page.patient_input if module is appointments else page.keyword_edit
search.setText("第二组")
page.refresh(silent=True)
assert len(jobs) == 1
function, callbacks = jobs.pop()
assert function()["lists"][0]["id"] == 1001
callbacks["on_success"](function())
append_callbacks["on_success"](stale_result)
_settle(application)
assert page.table.rowCount() == 4
assert [row["id"] for row in page.pager.rows] == [1001, 1002, 1003, 1004]
assert page.pager.page == 1
assert not page.pager.loading
assert page.table.verticalScrollBar().value() == 0
def test_failed_append_retries_without_losing_rows(queue_page: Any, application: QApplication) -> None:
page, repository, _module = queue_page
repository.fail_page = 2
_scroll_bottom(page, application)
assert page.table.rowCount() == 15
assert page.pager.page == 1
assert page.pager.retry_button.isVisible()
calls = len(repository.calls)
_settle(application)
assert len(repository.calls) == calls
repository.fail_page = None
page.pager.retry_button.click()
_settle(application)
assert page.table.rowCount() == 30
assert page.pager.page == 2
assert page.pager.retry_button.isHidden()
def test_list_footer_is_compact_without_page_controls(queue_page: Any, application: QApplication) -> None:
page, _repository, _module = queue_page
for height in (768, 960):
page.resize(1280, height)
_settle(application)
assert isinstance(page.pager, InfiniteList)
assert page.pager.height() == 24
assert not page.pager.findChildren(QComboBox)
assert not page.pager.findChildren(QSpinBox)
content = page.page_scroll.widget() if hasattr(page, "page_scroll") else page
assert content.layout().contentsMargins().bottom() == 8