286 lines
10 KiB
Python
286 lines
10 KiB
Python
"""Patient workspaces keep resource identity while appending server pages."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Any
|
|
|
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
|
|
|
import pytest
|
|
from PySide6.QtCore import QDate, Qt
|
|
from PySide6.QtTest import QTest
|
|
from PySide6.QtWidgets import QApplication, QCheckBox, QPushButton
|
|
|
|
from doctor_workstation.core import PermissionSet
|
|
from doctor_workstation.ui.pages import patients as patients_module
|
|
from doctor_workstation.ui.pages.patients import (
|
|
PatientListWorkspace,
|
|
PatientOrdersWorkspace,
|
|
PatientProgressWorkspace,
|
|
PatientsPage,
|
|
)
|
|
|
|
|
|
class Repository:
|
|
def __init__(self) -> None:
|
|
self.calls: list[dict[str, Any]] = []
|
|
self.failed_pages: set[int] = set()
|
|
self.version = 0
|
|
|
|
def _list(self, **query: Any) -> dict[str, Any]:
|
|
self.calls.append(dict(query))
|
|
page = query["page_no"]
|
|
if page in self.failed_pages:
|
|
raise RuntimeError("连接失败,请重试。")
|
|
start = (page - 1) * query["page_size"]
|
|
rows = [
|
|
{
|
|
"id": index + 100,
|
|
"diagnosis_id": index + 1000,
|
|
"patient_id": index + 2000,
|
|
"appointment_id": index + 3000,
|
|
"order_id": index + 4000,
|
|
"patient_name": f"患者 {index:02d} · {query.get('keyword', '')}{self.version}",
|
|
"appointment_status": 1,
|
|
"prescription_audit_status": 0,
|
|
"payment_slip_audit_status": 0,
|
|
"fulfillment_status": 2,
|
|
"queue_no": index + 1,
|
|
"queue_status": "waiting",
|
|
"queue_status_text": "等待中",
|
|
}
|
|
for index in range(start, min(start + query["page_size"], 37))
|
|
]
|
|
return {
|
|
"lists": rows,
|
|
"count": 37,
|
|
"extend": {
|
|
"scope": {"label": "本人患者"},
|
|
"summary": {"orders": 37, "amount": 3700, "today": 37, "waiting": 37},
|
|
"schedule_mode": "ownership",
|
|
},
|
|
}
|
|
|
|
list_patients = _list
|
|
patient_orders = _list
|
|
patient_progress = _list
|
|
|
|
|
|
def run_immediately(function: Any, **callbacks: Any) -> object:
|
|
try:
|
|
result = function()
|
|
except Exception as error:
|
|
callbacks["on_error"](error)
|
|
else:
|
|
callbacks["on_success"](result)
|
|
finally:
|
|
if callbacks.get("on_finished"):
|
|
callbacks["on_finished"]()
|
|
return object()
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def application() -> QApplication:
|
|
return QApplication.instance() or QApplication([])
|
|
|
|
|
|
@pytest.fixture(params=["patients", "orders", "progress"])
|
|
def workspace(request: Any, application: QApplication, monkeypatch: pytest.MonkeyPatch):
|
|
monkeypatch.setattr(patients_module, "run_async", run_immediately)
|
|
repository = Repository()
|
|
permissions = PermissionSet(["*"])
|
|
constructors = {
|
|
"patients": lambda: PatientListWorkspace(repository, permissions),
|
|
"orders": lambda: PatientOrdersWorkspace(repository, permissions),
|
|
"progress": lambda: PatientProgressWorkspace(repository),
|
|
}
|
|
widget = constructors[request.param]()
|
|
widget.resize(1280, 900)
|
|
widget.show()
|
|
application.processEvents()
|
|
yield request.param, widget, repository
|
|
widget.close()
|
|
widget.deleteLater()
|
|
application.processEvents()
|
|
|
|
|
|
def table_for(workspace: Any) -> Any:
|
|
return getattr(workspace, "table", None) or workspace.queue_table
|
|
|
|
|
|
def settle(application: QApplication) -> None:
|
|
application.processEvents()
|
|
QTest.qWait(60)
|
|
application.processEvents()
|
|
|
|
|
|
def scroll_down(workspace: Any, application: QApplication) -> None:
|
|
table = table_for(workspace)
|
|
table.verticalScrollBar().setValue(table.verticalScrollBar().maximum())
|
|
settle(application)
|
|
|
|
|
|
def test_scrolling_appends_without_duplicates_or_selection_loss(workspace, application):
|
|
kind, widget, repository = workspace
|
|
widget.refresh()
|
|
settle(application)
|
|
table = table_for(widget)
|
|
assert table.rowCount() == 15
|
|
assert widget.pager.height() == 24
|
|
assert not hasattr(widget.pager, "page_changed")
|
|
assert not any(button.text().isdigit() for button in widget.pager.findChildren(QPushButton))
|
|
table.sortItems(0, Qt.SortOrder.DescendingOrder)
|
|
table.selectRow(5)
|
|
chosen = table.current_data()["id"]
|
|
if kind == "patients":
|
|
table.cellWidget(5, 0).findChild(QCheckBox).setChecked(True)
|
|
scroll_down(widget, application)
|
|
assert table.rowCount() == 30
|
|
assert table.current_data()["id"] == chosen
|
|
if kind == "patients":
|
|
selected_row = next(index for index in range(table.rowCount())
|
|
if table.item(index, 0).data(Qt.ItemDataRole.UserRole)["id"] == chosen)
|
|
assert table.cellWidget(selected_row, 0).findChild(QCheckBox).isChecked()
|
|
scroll_down(widget, application)
|
|
assert table.rowCount() == 37
|
|
assert len({table.item(index, 0).data(Qt.ItemDataRole.UserRole)["id"]
|
|
for index in range(table.rowCount())}) == 37
|
|
assert [query["page_no"] for query in repository.calls] == [1, 2, 3]
|
|
assert all(query["page_size"] == 15 and "page" not in query for query in repository.calls)
|
|
assert widget.pager.page == widget._page == 3
|
|
assert widget.pager.total == 37
|
|
assert not widget.pager.has_more
|
|
assert widget.scope == "本人患者"
|
|
scroll_down(widget, application)
|
|
assert len(repository.calls) == 3
|
|
|
|
|
|
def test_refresh_retains_loaded_prefix_scroll_and_distinct_action_ids(workspace, application):
|
|
kind, widget, repository = workspace
|
|
widget.refresh()
|
|
settle(application)
|
|
scroll_down(widget, application)
|
|
table = table_for(widget)
|
|
table.selectRow(20)
|
|
chosen = dict(table.current_data())
|
|
scroll = table.verticalScrollBar().value()
|
|
repository.version = 1
|
|
widget.refresh(silent=True)
|
|
assert [query["page_no"] for query in repository.calls] == [1, 2, 1, 2]
|
|
assert table.rowCount() == 30
|
|
assert table.current_data()["id"] == chosen["id"]
|
|
assert table.current_data()["patient_name"].endswith("1")
|
|
assert table.verticalScrollBar().value() == scroll
|
|
selected = []
|
|
if kind == "patients":
|
|
widget.diagnosis_requested.connect(lambda row, _edit: selected.append(row))
|
|
widget._open_selected_diagnosis()
|
|
elif kind == "orders":
|
|
widget.detail_requested.connect(selected.append)
|
|
widget._request_detail()
|
|
else:
|
|
widget.diagnosis_requested.connect(selected.append)
|
|
widget._open_selected()
|
|
assert selected[0]["id"] == chosen["id"]
|
|
assert selected[0]["diagnosis_id"] == chosen["diagnosis_id"]
|
|
assert selected[0]["diagnosis_id"] != selected[0]["patient_id"]
|
|
|
|
|
|
def test_failed_append_keeps_rows_and_retry_continues_same_page(workspace, application):
|
|
_kind, widget, repository = workspace
|
|
repository.failed_pages.add(2)
|
|
widget.refresh()
|
|
settle(application)
|
|
scroll_down(widget, application)
|
|
table = table_for(widget)
|
|
assert table.rowCount() == 15
|
|
assert widget.pager.page == 1
|
|
assert widget.pager.retry_button.isVisible()
|
|
settle(application)
|
|
assert [query["page_no"] for query in repository.calls] == [1, 2]
|
|
repository.failed_pages.clear()
|
|
widget.pager.retry_button.click()
|
|
assert table.rowCount() == 30
|
|
assert [query["page_no"] for query in repository.calls] == [1, 2, 2]
|
|
assert not widget.pager.retry_button.isVisible()
|
|
|
|
|
|
def test_refresh_preserves_horizontal_scroll_at_narrow_width(workspace, application):
|
|
_kind, widget, _repository = workspace
|
|
widget.resize(760, 600)
|
|
widget.refresh()
|
|
settle(application)
|
|
table = table_for(widget)
|
|
horizontal = table.horizontalScrollBar()
|
|
assert horizontal.maximum() > 0
|
|
horizontal.setValue(horizontal.maximum())
|
|
offset = horizontal.value()
|
|
widget.refresh(silent=True)
|
|
assert horizontal.value() == offset
|
|
|
|
|
|
def test_first_page_failure_keeps_compact_retry_accessible(workspace, application):
|
|
_kind, widget, repository = workspace
|
|
repository.failed_pages.add(1)
|
|
widget.refresh()
|
|
settle(application)
|
|
widget.scroll.ensureWidgetVisible(widget.pager)
|
|
settle(application)
|
|
assert table_for(widget).rowCount() == 0
|
|
assert widget.pager.isVisible()
|
|
assert widget.pager.retry_button.isVisible()
|
|
assert widget.pager.retry_button.height() <= widget.pager.height()
|
|
repository.failed_pages.clear()
|
|
widget.pager.retry_button.click()
|
|
assert table_for(widget).rowCount() == 15
|
|
|
|
|
|
def test_filter_change_restarts_and_ignores_old_append(workspace, application, monkeypatch):
|
|
kind, widget, repository = workspace
|
|
widget.refresh()
|
|
settle(application)
|
|
pending = []
|
|
monkeypatch.setattr(patients_module, "run_async",
|
|
lambda function, **callbacks: pending.append((function, callbacks)))
|
|
# Capture the new runner for the next scroll request without changing query.
|
|
widget.refresh(silent=True)
|
|
pending.pop()[1]["on_success"](repository._list(page_no=1, page_size=15))
|
|
scroll_down(widget, application)
|
|
assert len(pending) == 1
|
|
if kind == "progress":
|
|
class Tomorrow:
|
|
@staticmethod
|
|
def currentDate():
|
|
return QDate.currentDate().addDays(1)
|
|
|
|
monkeypatch.setattr(patients_module, "QDate", Tomorrow)
|
|
else:
|
|
widget.keyword_edit.setText("新条件")
|
|
widget.refresh()
|
|
assert len(pending) == 2
|
|
stale_function, stale_callbacks = pending[0]
|
|
current_function, current_callbacks = pending[1]
|
|
current_callbacks["on_success"](current_function())
|
|
stale_callbacks["on_success"](stale_function())
|
|
stale_callbacks["on_error"](RuntimeError("旧请求错误"))
|
|
assert table_for(widget).rowCount() == 15
|
|
assert widget.pager.page == 1
|
|
assert not widget.banner.isVisible()
|
|
latest = repository.calls[-2]
|
|
assert latest["page_no"] == 1
|
|
if kind == "progress":
|
|
assert latest["start_date"] == QDate.currentDate().addDays(1).toString("yyyy-MM-dd")
|
|
else:
|
|
assert latest["keyword"] == "新条件"
|
|
|
|
|
|
def test_patient_route_uses_eight_pixel_bottom_margin(application, monkeypatch):
|
|
monkeypatch.setattr(patients_module, "run_async", run_immediately)
|
|
page = PatientsPage(Repository(), PermissionSet(["*"]))
|
|
assert page.layout().contentsMargins().bottom() == 8
|
|
assert page.order_workspace.action_bar.height() == 54
|
|
page.close()
|
|
page.deleteLater()
|
|
application.processEvents()
|