"""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()